From ebb3a5d4bfc2b9d57134f73627f14910d8f7265d Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 11 Jan 2022 10:07:25 +0100 Subject: [PATCH 01/73] Let participant agent die on failed registration Co-authored-by: johanneshiry --- CHANGELOG.md | 3 +++ .../agent/participant/ParticipantAgentFundamentals.scala | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47d5ac600..6f5fb8042e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods +### Fixed +- Let `ParticipantAgent` die after failed registration with secondary services (prevents stuck simulation) + [Unreleased]: https://github.com/ie3-institute/simona diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala index 6fd0577072..ea91071b7f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala @@ -6,7 +6,7 @@ package edu.ie3.simona.agent.participant -import akka.actor.{ActorRef, FSM} +import akka.actor.{ActorRef, FSM, PoisonPill} import akka.event.LoggingAdapter import akka.util import akka.util.Timeout @@ -452,6 +452,7 @@ protected trait ParticipantAgentFundamentals[ ) } case RegistrationResponseMessage.RegistrationFailedMessage => + self ! PoisonPill throw new ActorNotRegisteredException( s"Registration of actor $actorName for ${sender()} failed." ) From 041b5f3c7025f275ae95ded1417cc5ad3ebb7fec Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Tue, 11 Jan 2022 10:20:30 +0100 Subject: [PATCH 02/73] Adapt default resolution of weather source Co-authored-by: johanneshiry --- CHANGELOG.md | 5 ++++- .../ie3/simona/service/weather/WeatherSourceWrapper.scala | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47d5ac600..21c9aab89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods -[Unreleased]: https://github.com/ie3-institute/simona +### Fixed +- Fix default resolution of weather source wrapper + +[Unreleased]: https://github.com/ie3-institute/simona/compare/a14a093239f58fca9b2b974712686b33e5e5f939...HEAD diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index baf41e6c9f..07c37562ed 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -227,7 +227,7 @@ private[weather] final case class WeatherSourceWrapper private ( } private[weather] object WeatherSourceWrapper extends LazyLogging { - private val DEFAULT_RESOLUTION = 360L + private val DEFAULT_RESOLUTION = 3600L def apply( csvSep: String, @@ -401,7 +401,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { ) } case object WeightSum { - val EMPTY_WEIGHT_SUM: WeightSum = WeightSum(0d, 0d, 0d, 0d) + val EMPTY_WEIGHT_SUM: WeightSum = WeightSum(1d, 1d, 1d, 1d) } } From 99fa1eea7b055e8280fe70196e4994874b9e1c7a Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Tue, 18 Jan 2022 18:30:53 +0100 Subject: [PATCH 03/73] Introducing primary data from sql --- build.gradle | 2 +- .../resources/config/config-template.conf | 7 +- .../edu/ie3/simona/config/SimonaConfig.scala | 31 ++-- .../primary/PrimaryServiceWorker.scala | 161 +++++++++++------- .../weather/WeatherSourceWrapper.scala | 2 +- .../edu/ie3/simona/util/ConfigUtil.scala | 6 +- .../primary/PrimaryServiceProxySpec.scala | 6 +- 7 files changed, 126 insertions(+), 89 deletions(-) diff --git a/build.gradle b/build.gradle index 1257ed2bf3..b9a125cefa 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ dependencies { /* Exclude our own nested dependencies */ exclude group: 'com.github.ie3-institute' } - implementation('com.github.ie3-institute:PowerSystemDataModel:2.1.0') { + implementation('com.github.ie3-institute:PowerSystemDataModel:3.0-SNAPSHOT') { exclude group: 'org.apache.logging.log4j' exclude group: 'org.slf4j' /* Exclude our own nested dependencies */ diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 8e6952e8f0..4848267d2c 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -101,9 +101,8 @@ simona.input.primary = { jdbcUrl: string userName: string password: string - weatherTableName: string + tableName: string schemaName: string | "public" - timeColumnName: string timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'" # default pattern from PSDM:TimeBasedSimpleValueFactory } #@optional @@ -150,9 +149,9 @@ simona.input.weather.datasource = { jdbcUrl: string userName: string password: string - weatherTableName: string + tableName: string schemaName: string | "public" - timeColumnName: string + timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'" # default pattern from PSDM:TimeBasedSimpleValueFactory } #@optional couchbaseParams = { diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 6283055bbc..f6eb5cee78 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -1,5 +1,5 @@ /* - * © 2021. TU Dortmund University, + * © 2022. TU Dortmund University, * Institute of Energy Systems, Energy Efficiency and Energy Economics, * Research group Distribution grid planning and operation */ @@ -908,10 +908,9 @@ object SimonaConfig { jdbcUrl: java.lang.String, password: java.lang.String, schemaName: java.lang.String, - timeColumnName: java.lang.String, + tableName: java.lang.String, timePattern: java.lang.String, - userName: java.lang.String, - weatherTableName: java.lang.String + userName: java.lang.String ) object SqlParams { def apply( @@ -925,14 +924,11 @@ object SimonaConfig { schemaName = if (c.hasPathOrNull("schemaName")) c.getString("schemaName") else "public", - timeColumnName = - $_reqStr(parentPath, c, "timeColumnName", $tsCfgValidator), + tableName = $_reqStr(parentPath, c, "tableName", $tsCfgValidator), timePattern = if (c.hasPathOrNull("timePattern")) c.getString("timePattern") else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'", - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - weatherTableName = - $_reqStr(parentPath, c, "weatherTableName", $tsCfgValidator) + userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator) ) } private def $_reqStr( @@ -1277,9 +1273,9 @@ object SimonaConfig { jdbcUrl: java.lang.String, password: java.lang.String, schemaName: java.lang.String, - timeColumnName: java.lang.String, - userName: java.lang.String, - weatherTableName: java.lang.String + tableName: java.lang.String, + timePattern: java.lang.String, + userName: java.lang.String ) object SqlParams { def apply( @@ -1293,11 +1289,12 @@ object SimonaConfig { schemaName = if (c.hasPathOrNull("schemaName")) c.getString("schemaName") else "public", - timeColumnName = - $_reqStr(parentPath, c, "timeColumnName", $tsCfgValidator), - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - weatherTableName = - $_reqStr(parentPath, c, "weatherTableName", $tsCfgValidator) + tableName = + $_reqStr(parentPath, c, "tableName", $tsCfgValidator), + timePattern = + if (c.hasPathOrNull("timePattern")) c.getString("timePattern") + else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'", + userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator) ) } private def $_reqStr( diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index 8553ef5533..ad59294518 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -7,14 +7,17 @@ package edu.ie3.simona.service.primary import akka.actor.{ActorRef, Props} +import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.csv.timeseries.ColumnScheme import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.source.TimeSeriesSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesSource +import edu.ie3.datamodel.io.source.sql.SqlTimeSeriesSource import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.agent.participant.data.Data.PrimaryData.RichValue +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams import edu.ie3.simona.exceptions.InitializationException import edu.ie3.simona.exceptions.WeatherServiceException.InvalidRegistrationRequestException import edu.ie3.simona.ontology.messages.SchedulerMessage @@ -24,11 +27,11 @@ import edu.ie3.simona.service.ServiceStateData.{ InitializeServiceStateData, ServiceActivationBaseStateData } -import edu.ie3.simona.service.{ServiceStateData, SimonaService} import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ PrimaryServiceInitializedStateData, ProvidePrimaryDataMessage } +import edu.ie3.simona.service.{ServiceStateData, SimonaService} import edu.ie3.simona.util.TickUtil.{RichZonedDateTime, TickLong} import edu.ie3.util.scala.collection.immutable.SortedDistinctSeq @@ -61,68 +64,94 @@ final case class PrimaryServiceWorker[V <: Value]( PrimaryServiceInitializedStateData[V], Option[Seq[SchedulerMessage.ScheduleTriggerMessage]] ) - ] = initServiceData match { - case PrimaryServiceWorker.CsvInitPrimaryServiceStateData( - timeSeriesUuid, - simulationStart, - csvSep, - directoryPath, - filePath, - fileNamingStrategy, - timePattern - ) => - /* Got the right data. Attempt to set up a source and acquire information */ - implicit val startDateTime: ZonedDateTime = simulationStart + ] = { + val trySource = initServiceData match { + case PrimaryServiceWorker.CsvInitPrimaryServiceStateData( + timeSeriesUuid, + simulationStart, + csvSep, + directoryPath, + filePath, + fileNamingStrategy, + timePattern + ) => + Try { + /* Set up source and acquire information */ + val factory = new TimeBasedSimpleValueFactory(valueClass, timePattern) + val source = new CsvTimeSeriesSource( + csvSep, + directoryPath, + fileNamingStrategy, + timeSeriesUuid, + filePath, + valueClass, + factory + ) + (source, simulationStart) + } + case PrimaryServiceWorker.SqlInitPrimaryServiceStateData( + sqlParams: SqlParams, + timeSeriesUuid: UUID, + simulationStart: ZonedDateTime + ) => + Try { + val valueFactory = + new TimeBasedSimpleValueFactory(valueClass, sqlParams.timePattern) - Try { - /* Set up source and acquire information */ - val factory = new TimeBasedSimpleValueFactory(valueClass, timePattern) - val source = new CsvTimeSeriesSource( - csvSep, - directoryPath, - fileNamingStrategy, - timeSeriesUuid, - filePath, - valueClass, - factory - ) - /* This seems not to be very efficient, but it is as efficient as possible. The getter method points to a - * final attribute within the source implementation. */ - val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( - source.getTimeSeries.getEntries.asScala - .filter { timeBasedValue => - val dateTime = timeBasedValue.getTime - dateTime.isEqual(simulationStart) || dateTime.isAfter( - simulationStart - ) - } - .map(timeBasedValue => timeBasedValue.getTime.toTick) - .toSeq - .sorted - ).pop + val sqlConnector = new SqlConnector( + sqlParams.jdbcUrl, + sqlParams.userName, + sqlParams.password + ) - /* Set up the state data and determine the next activation tick. */ - val initializedStateData = - PrimaryServiceInitializedStateData( - maybeNextTick, - furtherActivationTicks, - simulationStart, - source + val source = new SqlTimeSeriesSource( + sqlConnector, + sqlParams.schemaName, + sqlParams.tableName, + timeSeriesUuid, + valueClass, + valueFactory ) - val triggerMessage = - ServiceActivationBaseStateData.tickToScheduleTriggerMessages( - maybeNextTick, - self + + (source, simulationStart) + } + case unsupported => + /* Got the wrong init data */ + Failure( + new InitializationException( + s"Provided init data '${unsupported.getClass.getSimpleName}' for primary service are invalid!" ) - (initializedStateData, triggerMessage) - } - case unsupported => - /* Got the wrong init data */ - Failure( - new InitializationException( - s"Provided init data '${unsupported.getClass.getSimpleName}' for primary service are invalid!" ) - ) + } + trySource.map { case (source, simulationStart) => + val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( + source.getTimeSeries.getEntries.asScala + .filter { timeBasedValue => + val dateTime = timeBasedValue.getTime + dateTime.isEqual(simulationStart) || dateTime.isAfter( + simulationStart + ) + } + .map(timeBasedValue => timeBasedValue.getTime.toTick) + .toSeq + .sorted + ).pop + + /* Set up the state data and determine the next activation tick. */ + val initializedStateData = + PrimaryServiceInitializedStateData( + maybeNextTick, + furtherActivationTicks, + simulationStart, + source + ) + val triggerMessage = + ServiceActivationBaseStateData.tickToScheduleTriggerMessages( + maybeNextTick, + self + ) + (initializedStateData, triggerMessage) + } } /** Handle a request to register for information from this service @@ -348,6 +377,22 @@ case object PrimaryServiceWorker { timePattern: String ) extends InitPrimaryServiceStateData + /** Specific implementation of [[InitPrimaryServiceStateData]], if the source + * to use utilizes csv files. + * + * TODO + * + * @param timeSeriesUuid + * Unique identifier of the time series to read + * @param simulationStart + * Wall clock time of the beginning of simulation time + */ + final case class SqlInitPrimaryServiceStateData( + sqlParams: SqlParams, + override val timeSeriesUuid: UUID, + override val simulationStart: ZonedDateTime + ) extends InitPrimaryServiceStateData + /** Class carrying the state of a fully initialized [[PrimaryServiceWorker]] * * @param maybeNextActivationTick diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index baf41e6c9f..a1224055f0 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -328,7 +328,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { sqlConnector, idCoordinateSource, sqlParams.schemaName, - sqlParams.weatherTableName, + sqlParams.tableName, buildFactory(timestampPattern, scheme) ) logger.info( diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index b4703c2873..d4ee521750 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -321,11 +321,7 @@ object ConfigUtil { logger.info( "Password for SQL weather source is empty. This is allowed, but not common. Please check if this an intended setting." ) - if (sql.timeColumnName.isEmpty) - throw new InvalidConfigParameterException( - "Time column for SQL weather source cannot be empty" - ) - if (sql.weatherTableName.isEmpty) + if (sql.tableName.isEmpty) throw new InvalidConfigParameterException( "Weather table name for SQL weather source cannot be empty" ) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index b72aea17f3..a80ccd0608 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -127,7 +127,7 @@ class PrimaryServiceProxySpec mappingSource ) - val scheduler = TestProbe("scheduler") + private val scheduler = TestProbe("scheduler") "Testing a primary service config" should { "lead to complaining about too much source definitions" in { @@ -204,7 +204,7 @@ class PrimaryServiceProxySpec None, None, None, - Some(SqlParams("", "", "", "", "", "", "")) + Some(SqlParams("", "", "", "", "", "")) ) val exception = intercept[InvalidConfigParameterException]( @@ -276,7 +276,7 @@ class PrimaryServiceProxySpec None, None, None, - Some(SqlParams("", "", "", "", "", "", "")) + Some(SqlParams("", "", "", "", "", "")) ) proxy invokePrivate prepareStateData( From ff30158497178d43377a9281eb46bd7d08022624 Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Tue, 18 Jan 2022 18:59:08 +0100 Subject: [PATCH 04/73] Fixing test --- .../ie3/simona/service/primary/PrimaryServiceProxySpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index a80ccd0608..769115381e 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -210,7 +210,7 @@ class PrimaryServiceProxySpec val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,,,)' for a time series source.\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,,)' for a time series source.\nAvailable types:\n\tcsv" } "fails on invalid time pattern" in { @@ -287,7 +287,7 @@ class PrimaryServiceProxySpec fail("Building state data with missing config should fail") case Failure(exception) => exception.getClass shouldBe classOf[IllegalArgumentException] - exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,,,)'" + exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,,)'" } } From 7c6f6f49fbe5a6aeb84f8784be4fb1e8b15ca3f7 Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Tue, 18 Jan 2022 19:35:20 +0100 Subject: [PATCH 05/73] Making PrimaryService tests work on Windows --- .../primary/PrimaryServiceProxySpec.scala | 14 ++++++++++---- .../primary/PrimaryServiceWorkerSpec.scala | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index 769115381e..1f6f4a5f17 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -54,10 +54,10 @@ import edu.ie3.util.TimeUtil import org.scalatest.PartialFunctionValues import org.scalatest.prop.TableDrivenPropertyChecks +import java.nio.file.Paths import java.time.ZonedDateTime import java.util.concurrent.TimeUnit import java.util.{Objects, UUID} -import scala.reflect.io.File import scala.util.{Failure, Success, Try} import scala.concurrent.ExecutionContext.Implicits.global @@ -74,9 +74,15 @@ class PrimaryServiceProxySpec ) with TableDrivenPropertyChecks with PartialFunctionValues { - val baseDirectoryPath: String = this.getClass - .getResource(File.separator + "it-data" + File.separator + "primaryService") - .getPath + val baseDirectoryPath: String = Paths + .get( + this.getClass + .getResource( + "/it-data/primaryService" + ) + .toURI + ) + .toString val csvSep = ";" val fileNamingStrategy = new FileNamingStrategy() val validPrimaryConfig: PrimaryConfig = diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala index 640497cf3f..b23eeff6c9 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala @@ -42,9 +42,9 @@ import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.collection.immutable.SortedDistinctSeq import tech.units.indriya.quantity.Quantities +import java.nio.file.Paths import java.time.ZonedDateTime import java.util.UUID -import scala.reflect.io.File import scala.util.{Failure, Success} class PrimaryServiceWorkerSpec @@ -57,14 +57,15 @@ class PrimaryServiceWorkerSpec """.stripMargin) ) ) { - val baseDirectoryPath: String = "^file:".r.replaceFirstIn( - this.getClass - .getResource( - File.separator + "it-data" + File.separator + "primaryService" - ) - .toString, - "" - ) + val baseDirectoryPath: String = Paths + .get( + this.getClass + .getResource( + "/it-data/primaryService" + ) + .toURI + ) + .toString private val simulationStart = TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00") From 4ec37aa1529d65036ade9a2a3c07f5a194c279d9 Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Tue, 18 Jan 2022 21:54:57 +0100 Subject: [PATCH 06/73] Added comments --- .../edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala | 1 + .../ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala | 1 + 2 files changed, 2 insertions(+) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index 1f6f4a5f17..7f17045fe3 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -74,6 +74,7 @@ class PrimaryServiceProxySpec ) with TableDrivenPropertyChecks with PartialFunctionValues { + // this works both on Windows and Unix systems val baseDirectoryPath: String = Paths .get( this.getClass diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala index b23eeff6c9..05992f39b2 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala @@ -57,6 +57,7 @@ class PrimaryServiceWorkerSpec """.stripMargin) ) ) { + // this works both on Windows and Unix systems val baseDirectoryPath: String = Paths .get( this.getClass From 1212567f636a5e5ef9be65267d93b4dc358eca8e Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Thu, 20 Jan 2022 16:13:08 +0100 Subject: [PATCH 07/73] Implementing test for SQL primary data with postgresql testcontainer --- build.gradle | 6 + .../primary/PrimaryServiceWorker.scala | 9 +- ...p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql | 17 ++ ...h_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql | 19 ++ .../primary/PrimaryServiceWorkerSqlIT.scala | 215 ++++++++++++++++++ 5 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql create mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql create mode 100644 src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala diff --git a/build.gradle b/build.gradle index b9a125cefa..ef2c7e566f 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,8 @@ ext { akkaVersion = '2.6.18' tscfgVersion = '0.9.996' + testContainerVersion = '0.39.12' + scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins } @@ -100,6 +102,10 @@ dependencies { testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' testImplementation "com.typesafe.akka:akka-testkit_${scalaVersion}:${akkaVersion}" // akka testkit + // testcontainers + testImplementation "com.dimafeng:testcontainers-scala-scalatest_${scalaVersion}:${testContainerVersion}" + testImplementation "com.dimafeng:testcontainers-scala-postgresql_${scalaVersion}:${testContainerVersion}" + /* --- Scala libs --- */ /* CORE Scala */ implementation "org.scala-lang:scala-library:${scalaBinaryVersion}" diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index ad59294518..ccad2ef790 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -65,7 +65,7 @@ final case class PrimaryServiceWorker[V <: Value]( Option[Seq[SchedulerMessage.ScheduleTriggerMessage]] ) ] = { - val trySource = initServiceData match { + (initServiceData match { case PrimaryServiceWorker.CsvInitPrimaryServiceStateData( timeSeriesUuid, simulationStart, @@ -95,7 +95,7 @@ final case class PrimaryServiceWorker[V <: Value]( simulationStart: ZonedDateTime ) => Try { - val valueFactory = + val factory = new TimeBasedSimpleValueFactory(valueClass, sqlParams.timePattern) val sqlConnector = new SqlConnector( @@ -110,7 +110,7 @@ final case class PrimaryServiceWorker[V <: Value]( sqlParams.tableName, timeSeriesUuid, valueClass, - valueFactory + factory ) (source, simulationStart) @@ -122,8 +122,7 @@ final case class PrimaryServiceWorker[V <: Value]( s"Provided init data '${unsupported.getClass.getSimpleName}' for primary service are invalid!" ) ) - } - trySource.map { case (source, simulationStart) => + }).map { case (source, simulationStart) => val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( source.getTimeSeries.getEntries.asScala .filter { timeBasedValue => diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql new file mode 100644 index 0000000000..1f956e06a7 --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql @@ -0,0 +1,17 @@ +CREATE TABLE public."its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5" +( + time timestamp with time zone, + p double precision, + uuid uuid, + CONSTRAINT its_p_pkey PRIMARY KEY (uuid) +) + WITH ( + OIDS = FALSE + ) + TABLESPACE pg_default; + +INSERT INTO + public."its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5" (uuid, time, p) +VALUES +('0245d599-9a5c-4c32-9613-5b755fac8ca0', '2020-01-01 00:00:00+0', 1000.0), +('a5e27652-9024-4a93-9d2a-590fbc3ab5a1', '2020-01-01 00:15:00+0', 1250.0); diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql new file mode 100644 index 0000000000..230393eb5b --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql @@ -0,0 +1,19 @@ +CREATE TABLE public."its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047" +( + time timestamp with time zone, + p double precision, + q double precision, + heat_demand double precision, + uuid uuid, + CONSTRAINT its_pqh_pkey PRIMARY KEY (uuid) +) + WITH ( + OIDS = FALSE + ) + TABLESPACE pg_default; + +INSERT INTO + public."its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047" (uuid, time, p, q, heat_demand) +VALUES +('661ac594-47f0-4442-8d82-bbeede5661f7', '2020-01-01 00:00:00+0', 1000.0, 329.0, 8.0), +('5adcd6c5-a903-433f-b7b5-5fe669a3ed30', '2020-01-01 00:15:00+0', 1250.0, 411.0, 12.0); diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala new file mode 100644 index 0000000000..88eddf6f5a --- /dev/null +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -0,0 +1,215 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.service.primary + +import akka.actor.{ActorRef, ActorSystem} +import akka.testkit.{TestActorRef, TestProbe} +import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} +import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.models.value.{HeatAndSValue, PValue, Value} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ActivePower, + ApparentPowerAndHeat +} +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + CompletionMessage, + ScheduleTriggerMessage, + TriggerWithIdMessage +} +import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.WorkerRegistrationMessage +import edu.ie3.simona.ontology.trigger.Trigger.{ + ActivityStartTrigger, + InitializeServiceTrigger +} +import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ + ProvidePrimaryDataMessage, + SqlInitPrimaryServiceStateData +} +import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.util.TimeUtil +import org.scalatest.BeforeAndAfterAll +import org.scalatest.prop.TableDrivenPropertyChecks +import org.testcontainers.utility.MountableFile + +import java.nio.file.Paths +import java.util.UUID +import scala.language.postfixOps +import scala.reflect.ClassTag + +class PrimaryServiceWorkerSqlIT + extends AgentSpec( + ActorSystem( + "PrimaryServiceWorkerSqlIT", + ConfigFactory + .parseString(""" + |akka.loglevel="OFF" + """.stripMargin) + ) + ) + with ForAllTestContainer + with BeforeAndAfterAll + with TableDrivenPropertyChecks { + + override val container: PostgreSQLContainer = PostgreSQLContainer( + "postgres:11.14" + ) + + private val simulationStart = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00") + + private val schemaName = "public" + + private val uuidP = UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") + private val uuidPhq = UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047") + + private val tableNameP = s"its_p_$uuidP" + private val tableNamePhq = s"its_pqh_$uuidPhq" + + override protected def beforeAll(): Unit = { + val url = getClass.getResource("timeseries/") + url shouldNot be(null) + val path = Paths.get(url.toURI) + + // Copy sql import scripts into docker + val sqlImportFile = MountableFile.forHostPath(path) + container.copyFileToContainer(sqlImportFile, "/home/") + + Iterable(s"$tableNameP.sql", s"$tableNamePhq.sql") + .foreach { file => + val res = container.execInContainer("psql", "-Utest", "-f/home/" + file) + res.getStderr shouldBe empty + } + } + + override protected def afterAll(): Unit = { + container.stop() + container.close() + } + + private def getServiceActor[T <: Value]( + scheduler: ActorRef + )(implicit tag: ClassTag[T]): PrimaryServiceWorker[T] = { + new PrimaryServiceWorker[T]( + scheduler, + tag.runtimeClass.asInstanceOf[Class[T]], + simulationStart + ) + } + + "A primary service actor with SQL source" should { + "initialize and send out data when activated" in { + + val cases = Table( + ( + "getService", + "uuid", + "tableName", + "firstTick", + "dataValueClass", + "maybeNextTick" + ), + ( + getServiceActor[HeatAndSValue](_), + uuidPhq, + tableNamePhq, + 0L, + classOf[ApparentPowerAndHeat], + Some(900L) + ), + ( + getServiceActor[PValue](_), + uuidP, + tableNameP, + 0L, + classOf[ActivePower], + Some(900L) + ) + ) + + forAll(cases) { + ( + getService, + uuid, + tableName, + firstTick, + dataValueClass, + maybeNextTick + ) => + val scheduler = TestProbe("scheduler") + + val serviceRef = + TestActorRef( + getService(scheduler.ref) + ) + + val initData = SqlInitPrimaryServiceStateData( + SqlParams( + jdbcUrl = container.jdbcUrl, + userName = container.username, + password = container.password, + schemaName = schemaName, + tableName = tableName, + timePattern = "yyyy-MM-dd HH:mm:ss" + ), + uuid, + simulationStart + ) + + val triggerId1 = 1L + + scheduler.send( + serviceRef, + TriggerWithIdMessage( + InitializeServiceTrigger(initData), + triggerId1, + serviceRef + ) + ) + + scheduler.expectMsg( + CompletionMessage( + triggerId1, + Some( + List( + ScheduleTriggerMessage( + ActivityStartTrigger(firstTick), + serviceRef + ) + ) + ) + ) + ) + + val participant = TestProbe() + + participant.send( + serviceRef, + WorkerRegistrationMessage(participant.ref) + ) + participant.expectMsg(RegistrationSuccessfulMessage(Some(firstTick))) + + val triggerId2 = 2L + + scheduler.send( + serviceRef, + TriggerWithIdMessage( + ActivityStartTrigger(firstTick), + triggerId2, + serviceRef + ) + ) + + val dataMsg = participant.expectMsgType[ProvidePrimaryDataMessage] + dataMsg.tick shouldBe firstTick + dataMsg.data.getClass shouldBe dataValueClass + dataMsg.nextDataTick shouldBe maybeNextTick + } + } + } +} From c2b2cfb4c831a424cf8fb5e2a5a2d1d4a82583d1 Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Thu, 20 Jan 2022 16:50:38 +0100 Subject: [PATCH 08/73] Small improvements --- .../ie3/simona/service/primary/PrimaryServiceWorker.scala | 6 +++--- .../simona/service/primary/PrimaryServiceWorkerSqlIT.scala | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index ccad2ef790..68ba913443 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -377,10 +377,10 @@ case object PrimaryServiceWorker { ) extends InitPrimaryServiceStateData /** Specific implementation of [[InitPrimaryServiceStateData]], if the source - * to use utilizes csv files. - * - * TODO + * to use utilizes an SQL database. * + * @param sqlParams + * Parameters regarding SQL connection and table selection * @param timeSeriesUuid * Unique identifier of the time series to read * @param simulationStart diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala index 88eddf6f5a..6d81f05d3e 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -205,6 +205,8 @@ class PrimaryServiceWorkerSqlIT ) ) + scheduler.expectMsgType[CompletionMessage] + val dataMsg = participant.expectMsgType[ProvidePrimaryDataMessage] dataMsg.tick shouldBe firstTick dataMsg.data.getClass shouldBe dataValueClass From 8d41e16dfa060ea622bffbb45fc61d8faa06e6f9 Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Thu, 20 Jan 2022 16:50:50 +0100 Subject: [PATCH 09/73] Adding to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47d5ac600..3ca43d56b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,5 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods +- Implement SQL source for primary data [#34](https://github.com/ie3-institute/simona/issues/34) [Unreleased]: https://github.com/ie3-institute/simona From 882baa68298501a0cae2d0d9e9d0e06fc076432c Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Mon, 24 Jan 2022 14:24:19 +0100 Subject: [PATCH 10/73] Addressing Thomas' comments --- .../edu/ie3/simona/service/primary/PrimaryServiceWorker.scala | 1 + .../ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index 68ba913443..3ccb45cac3 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -124,6 +124,7 @@ final case class PrimaryServiceWorker[V <: Value]( ) }).map { case (source, simulationStart) => val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( + // Note: The whole data set is used here, which might be inefficient depending on the source implementation. source.getTimeSeries.getEntries.asScala .filter { timeBasedValue => val dateTime = timeBasedValue.getTime diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala index 6d81f05d3e..77536c1442 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -92,6 +92,8 @@ class PrimaryServiceWorkerSqlIT container.close() } + // asInstanceOf throws ClassCastException if cast fails, thus this is safe here + @SuppressWarnings(Array("AsInstanceOf")) private def getServiceActor[T <: Value]( scheduler: ActorRef )(implicit tag: ClassTag[T]): PrimaryServiceWorker[T] = { From bf8535722df79170b47986c6d2e949b95eca14e2 Mon Sep 17 00:00:00 2001 From: Sebastian Peter <14994800+sebastian-peter@users.noreply.github.com> Date: Thu, 27 Jan 2022 10:28:53 +0100 Subject: [PATCH 11/73] Improved changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca43d56b5..e6f4029eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- Implement SQL source for primary data [#34](https://github.com/ie3-institute/simona/issues/34) + ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods -- Implement SQL source for primary data [#34](https://github.com/ie3-institute/simona/issues/34) [Unreleased]: https://github.com/ie3-institute/simona From b44a36bc552272cb5c8c9682a832744a25ed9b79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 04:27:36 +0000 Subject: [PATCH 12/73] Bump com.diffplug.spotless from 6.2.2 to 6.3.0 Bumps com.diffplug.spotless from 6.2.2 to 6.3.0. --- updated-dependencies: - dependency-name: com.diffplug.spotless dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2739d63836..06843ae78c 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'signing' id 'maven-publish' // publish to a maven repo (local or mvn central, has to be defined) id 'pmd' // code check, working on source code - id 'com.diffplug.spotless' version '6.2.2'// code format + id 'com.diffplug.spotless' version '6.3.0'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' id "de.undercouch.download" version "5.0.1" // downloads plugin From 29a2a035a5ab6f1bba5ee7c09707c9da9c3f07ba Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 25 Feb 2022 11:20:25 +0100 Subject: [PATCH 13/73] fix variable height to elevationAngle --- src/main/scala/edu/ie3/simona/model/participant/PVModel.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/PVModel.scala b/src/main/scala/edu/ie3/simona/model/participant/PVModel.scala index 235f8a3981..aa3f746cc7 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/PVModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/PVModel.scala @@ -817,7 +817,7 @@ case object PVModel { inputModel.getAlbedo, inputModel.getEtaConv, inputModel.getAzimuth, - inputModel.getHeight + inputModel.getElevationAngle ) model.enable() From b35c4b14a3264fa15d3a9b254df158bfff1a3136 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 25 Feb 2022 17:12:38 +0100 Subject: [PATCH 14/73] removed multiplier (-1) to comply with changes in PowerSystemDataModel >3.0 --- .../scala/edu/ie3/simona/model/grid/Transformer3wModel.scala | 2 +- src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala b/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala index cb54dde44e..c9bfa8c95c 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/Transformer3wModel.scala @@ -327,7 +327,7 @@ case object Transformer3wModel { transformerRefSystem.rInPu(transformerType.getrScA), transformerRefSystem.xInPu(transformerType.getxScA), transformerRefSystem.gInPu(transformerType.getgM), - transformerRefSystem.gInPu(transformerType.getbM).multiply(-1) + transformerRefSystem.gInPu(transformerType.getbM) ) case PowerFlowCaseB => ( diff --git a/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala b/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala index 63074a3138..18f573547f 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/TransformerModel.scala @@ -147,7 +147,7 @@ case object TransformerModel { trafoType.getrSc.divide(squaredNominalVoltRatio), trafoType.getxSc.divide(squaredNominalVoltRatio), trafoType.getgM.multiply(squaredNominalVoltRatio), - trafoType.getbM.multiply(-1).multiply(squaredNominalVoltRatio) + trafoType.getbM.multiply(squaredNominalVoltRatio) ) /* Transfer the dimensionless parameters into the grid reference system */ From bd62a305920e5c15a8b2655f6f89134528c663c9 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:58:21 +0100 Subject: [PATCH 15/73] Upload figures (#153) --- .../_static/figures/uml/InitializationPhase.png | Bin 0 -> 70782 bytes .../uml/ParticipantTriggeredByItself.png | Bin 0 -> 18259 bytes .../uml/ParticipantTriggeredByPrimaryData.png | Bin 0 -> 21901 bytes .../uml/ParticipantTriggeredBySecondaryData.png | Bin 0 -> 30772 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/readthedocs/_static/figures/uml/InitializationPhase.png create mode 100644 docs/readthedocs/_static/figures/uml/ParticipantTriggeredByItself.png create mode 100644 docs/readthedocs/_static/figures/uml/ParticipantTriggeredByPrimaryData.png create mode 100644 docs/readthedocs/_static/figures/uml/ParticipantTriggeredBySecondaryData.png diff --git a/docs/readthedocs/_static/figures/uml/InitializationPhase.png b/docs/readthedocs/_static/figures/uml/InitializationPhase.png new file mode 100644 index 0000000000000000000000000000000000000000..2e82ab5e49ea00644bf7222645de0e3456be29be GIT binary patch literal 70782 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�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 Date: Mon, 28 Feb 2022 11:51:49 +0100 Subject: [PATCH 16/73] Updating Documenation (#142) * Update usersguide.rst Update Git https-Link * Update usersguide.rst Update Link SimonaAPI * Typo --- docs/readthedocs/usersguide.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/readthedocs/usersguide.rst b/docs/readthedocs/usersguide.rst index 4cf3a867f0..cd9399eef4 100644 --- a/docs/readthedocs/usersguide.rst +++ b/docs/readthedocs/usersguide.rst @@ -17,11 +17,11 @@ To run and customize the project you need a Java Development Kit (JDK) installat Installation ============ -You can find and download the source code of the latest stable SIMONA version `here `_. Go ahead and clone the repository using git: +You can find and download the source code of the latest stable SIMONA version `here `_. Go ahead and clone the repository using git: .. code-block:: none - $ git clone https://git.ie3.e-technik.tu-dortmund.de/SIMONACrew/SIMONA.git + $ git clone https://github.com/ie3-institute/simona.git Running a Standalone Simulation @@ -170,7 +170,7 @@ SIMONA is capable of running an external sub-simulation by integration within th The information flow between SIMONA and the external simulation is partitioned into a control stream (see ``edu.ie3.simona.api.ExtSimAdapter``) and a number of optional data streams. Currently, only a data stream transporting electric vehicle movement information is implemented (see ``edu.ie3.simona.service.ev.ExtEvDataService``). -An external simulation has to depend on `SimonaAPI `_ and make use of some of its interfaces (see below). +An external simulation has to depend on `SimonaAPI `_ and make use of some of its interfaces (see below). In order to run an external simulation, several requirements have to be fulfilled and a bunch of preparation steps have to be followed. .. note:: @@ -200,3 +200,4 @@ These steps have to be performed each time updates to the external simulation ne - Copy the resulting *jar* (usually placed inside /build/libs) to SIMONA/inputData/ext_sim. Now, when a simulation with SIMONA is started (see `above <#running-a-standalone-simulation>`_), the external simulation is triggered at each tick that it requested. + From d2caa6cfb9a706382a3bd3c7ee1c5dfffc455ca6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:53:00 +0100 Subject: [PATCH 17/73] Bump indriya from 2.1.2 to 2.1.3 (#152) Bumps [indriya](https://github.com/unitsofmeasurement/indriya) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/unitsofmeasurement/indriya/releases) - [Commits](https://github.com/unitsofmeasurement/indriya/compare/2.1.2...2.1.3) --- updated-dependencies: - dependency-name: tech.units:indriya dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 90cd1a25b7..ef5ac7e105 100644 --- a/build.gradle +++ b/build.gradle @@ -138,7 +138,7 @@ dependencies { implementation 'org.apache.commons:commons-math3:3.6.1' // apache commons math3 implementation 'org.apache.poi:poi-ooxml:5.2.0' // used for FilenameUtils implementation 'javax.measure:unit-api:2.1.3' - implementation 'tech.units:indriya:2.1.2' // quantities + implementation 'tech.units:indriya:2.1.3' // quantities implementation 'org.apache.commons:commons-csv:1.9.0' implementation 'org.scalanlp:breeze_2.13:1.3' // scientific calculations (http://www.scalanlp.org/) implementation 'de.lmu.ifi.dbs.elki:elki:0.7.5' // Statistics (for random load model) From 35af0e31185d9aeebecfdfb089bdd6894d463d6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 04:29:44 +0000 Subject: [PATCH 18/73] Bump guava from 31.0.1-jre to 31.1-jre Bumps [guava](https://github.com/google/guava) from 31.0.1-jre to 31.1-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ef5ac7e105..a4355a41a1 100644 --- a/build.gradle +++ b/build.gradle @@ -142,7 +142,7 @@ dependencies { implementation 'org.apache.commons:commons-csv:1.9.0' implementation 'org.scalanlp:breeze_2.13:1.3' // scientific calculations (http://www.scalanlp.org/) implementation 'de.lmu.ifi.dbs.elki:elki:0.7.5' // Statistics (for random load model) - implementation 'com.google.guava:guava:31.0.1-jre' // Building threads + implementation 'com.google.guava:guava:31.1-jre' // Building threads implementation 'org.jgrapht:jgrapht-core:1.5.1' } From 2735eb7bb0e272075edd2bfe29f844efd7f36145 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 1 Mar 2022 10:28:19 +0100 Subject: [PATCH 19/73] Updating dependabot.yml --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf5ca1f334..685991b817 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,11 +12,16 @@ updates: - johanneshiry - t-ober - sensarmad + - sebastian-peter + - danielfeismann ignore: - dependency-name: org.spockframework:spock-core versions: - 2.1-groovy-3.0-SNAPSHOT - 2.1-groovy-2.5-SNAPSHOT + - 2.2-groovy-4.0-SNAPSHOT + - 2.2-groovy-3.0-SNAPSHOT + - 2.2-groovy-2.5-SNAPSHOT - dependency-name: org.scalatest:scalatest_2.13 versions: - 3.3.0-SNAP+ From 1234f44fd401cca5bcd4f574bf196592ad5f21dd Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 1 Mar 2022 14:01:44 +0100 Subject: [PATCH 20/73] Ignoring milestone versions as well --- .github/dependabot.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 685991b817..66ae07e78b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,11 +17,17 @@ updates: ignore: - dependency-name: org.spockframework:spock-core versions: - - 2.1-groovy-3.0-SNAPSHOT - 2.1-groovy-2.5-SNAPSHOT + - 2.1-groovy-3.0-SNAPSHOT - 2.2-groovy-4.0-SNAPSHOT - - 2.2-groovy-3.0-SNAPSHOT - 2.2-groovy-2.5-SNAPSHOT + - 2.2-groovy-3.0-SNAPSHOT + - 2.2-M1-groovy-2.5 + - 2.2-M1-groovy-3.0 + - 2.2-M1-groovy-4.0 + - 2.2-M2-groovy-2.5 + - 2.2-M2-groovy-3.0 + - 2.2-M2-groovy-4.0 - dependency-name: org.scalatest:scalatest_2.13 versions: - 3.3.0-SNAP+ From 539848e437e6acc761b2fcabc14222e571f5b5cd Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 1 Mar 2022 16:18:57 +0100 Subject: [PATCH 21/73] powerflow isn't converging troubleshooting --- docs/readthedocs/usersguide.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/readthedocs/usersguide.rst b/docs/readthedocs/usersguide.rst index cd9399eef4..4d211d945a 100644 --- a/docs/readthedocs/usersguide.rst +++ b/docs/readthedocs/usersguide.rst @@ -201,3 +201,24 @@ These steps have to be performed each time updates to the external simulation ne Now, when a simulation with SIMONA is started (see `above <#running-a-standalone-simulation>`_), the external simulation is triggered at each tick that it requested. +Troubleshooting +=============== + +My power flow calculation isn't converging - why is that? +--------------------------------------------------------- + +When your power flow is not converging it means that the load situation in the grid during the time of the power flow calculation is not physically feasible. + +This can have basically one of the following two reasons: + +#. + There is more load in the grid than it can physically handle. + +#. + There is more generation in the grid than it can physically handle. + +One of the main reasons is a misconfiguration of the grid and its assets. +Assess the power of the load and generation units and check if the values make sense. +Keep in mind the metric prefixes that are assumed for the models, which are listed in the `PSDM docs `_. +If everything seems to be configured correctly it could also be the case that the grid itself is incorrectly configured. +Do a similar sanity check for the grids assets. From 4423ab247aba7e348422d0a461a92e24eb61b99a Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Tue, 1 Mar 2022 16:36:33 +0100 Subject: [PATCH 22/73] Adapt Transformer Test Data --- .../simona/test/common/model/grid/TransformerTestGrid.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestGrid.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestGrid.scala index a2a3b4d6b1..0e63d23174 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestGrid.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestGrid.scala @@ -84,7 +84,7 @@ trait TransformerTestGrid { Quantities.getQuantity(10d, KILOVOLT), Quantities.getQuantity(0.4d, KILOVOLT), Quantities.getQuantity(0d, SIEMENS), - Quantities.getQuantity(15e-6, SIEMENS), + Quantities.getQuantity(-15e-6, SIEMENS), Quantities.getQuantity(2.5d, PERCENT), Quantities.getQuantity(0d, DEGREE_GEOM), false, @@ -102,7 +102,7 @@ trait TransformerTestGrid { Quantities.getQuantity(10d, KILOVOLT), Quantities.getQuantity(0.4d, KILOVOLT), Quantities.getQuantity(0d, SIEMENS), - Quantities.getQuantity(15e-6, SIEMENS), + Quantities.getQuantity(-15e-6, SIEMENS), Quantities.getQuantity(2.5d, PERCENT), Quantities.getQuantity(0d, DEGREE_GEOM), true, From bc132feacb6b102d0b191ce2d3f3345d99ff6485 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:42:25 +0100 Subject: [PATCH 23/73] Adapt Transformer Test Data in Transformer3wTestData --- .../ie3/simona/test/common/input/Transformer3wTestData.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/test/common/input/Transformer3wTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/Transformer3wTestData.scala index d686e49f81..cbc439fb99 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/Transformer3wTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/Transformer3wTestData.scala @@ -127,7 +127,7 @@ trait Transformer3wTestData extends DefaultTestData { Quantities.getQuantity(0.954711d, OHM), Quantities.getQuantity(1.083000d, OHM), Quantities.getQuantity(40d, MetricPrefix.NANO(SIEMENS)), - Quantities.getQuantity(1d, MetricPrefix.NANO(SIEMENS)), + Quantities.getQuantity(-1d, MetricPrefix.NANO(SIEMENS)), Quantities.getQuantity(1.5, PERCENT), Quantities.getQuantity(0d, DEGREE_GEOM), 0, From b283429b807029a33660f1d1da983694577dd8e7 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:42:43 +0100 Subject: [PATCH 24/73] Adapt Transformer Test Data in ThreeWindingTestData --- .../scala/edu/ie3/simona/test/common/ThreeWindingTestData.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/test/common/ThreeWindingTestData.scala b/src/test/scala/edu/ie3/simona/test/common/ThreeWindingTestData.scala index c92a83f43c..e5142e4401 100644 --- a/src/test/scala/edu/ie3/simona/test/common/ThreeWindingTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/ThreeWindingTestData.scala @@ -93,7 +93,7 @@ trait ThreeWindingTestData extends DefaultTestData { Quantities.getQuantity(0.08, OHM), Quantities.getQuantity(0.003, OHM), Quantities.getQuantity(40d, MetricPrefix.NANO(SIEMENS)), - Quantities.getQuantity(1d, MetricPrefix.NANO(SIEMENS)), + Quantities.getQuantity(-1d, MetricPrefix.NANO(SIEMENS)), Quantities.getQuantity(1.5, PERCENT), Quantities.getQuantity(0d, DEGREE_GEOM), 0, From dfe6de6305dcb9e05bdea4807571de3f952320d9 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Tue, 1 Mar 2022 18:15:28 +0100 Subject: [PATCH 25/73] Adapt Transformer Test Data in DbfsTestGrid --- .../edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala index e8523d1600..15d1bca092 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/DbfsTestGrid.scala @@ -235,7 +235,7 @@ trait DbfsTestGrid extends SubGridGateMokka { Quantities.getQuantity(380.0, KILOVOLT), Quantities.getQuantity(110.0, KILOVOLT), Quantities.getQuantity(555.5, MetricPrefix.NANO(SIEMENS)), - Quantities.getQuantity(1.27, MetricPrefix.NANO(SIEMENS)), + Quantities.getQuantity(-1.27, MetricPrefix.NANO(SIEMENS)), Quantities.getQuantity(1.5, PERCENT), Quantities.getQuantity(0, RADIAN), false, From a1ad70dc85a353da68373f8729dee887ab1862c8 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Wed, 2 Mar 2022 10:39:39 +0100 Subject: [PATCH 26/73] Change Solar_Height to SOLAR_ELEVATION_ANGLE in PvInputTestData --- .../edu/ie3/simona/test/common/input/PvInputTestData.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala index 9d1fcbed43..415941a886 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala @@ -52,7 +52,7 @@ trait PvInputTestData 1, Quantities.getQuantity(12, StandardUnits.AZIMUTH), Quantities.getQuantity(10, StandardUnits.EFFICIENCY), - Quantities.getQuantity(100, StandardUnits.SOLAR_HEIGHT), + Quantities.getQuantity(100, StandardUnits.SOLAR_ELEVATION_ANGLE), 12, 11, false, From 9cf52c43cb345cc20f31f5e5a6c9245a0702c104 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Thu, 3 Mar 2022 10:52:14 +0100 Subject: [PATCH 27/73] height to elevationAngle in pv_input.csv --- input/samples/vn_simona/fullGrid/pv_input.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/samples/vn_simona/fullGrid/pv_input.csv b/input/samples/vn_simona/fullGrid/pv_input.csv index 011dde4eca..41986cdd09 100644 --- a/input/samples/vn_simona/fullGrid/pv_input.csv +++ b/input/samples/vn_simona/fullGrid/pv_input.csv @@ -1,4 +1,4 @@ -"uuid","albedo","azimuth","cos_phi_rated","eta_conv","height","id","k_g","k_t","market_reaction","node","operates_from","operates_until","operator","q_characteristics","s_rated" +"uuid","albedo","azimuth","cos_phi_rated","eta_conv","elevationAngle","id","k_g","k_t","market_reaction","node","operates_from","operates_until","operator","q_characteristics","s_rated" 5b38af42-1ee4-4a41-b666-ea141187df37,0.20000000298023224,-11.463644027709961,0.8999999761581421,96.0,33.62879943847656,NS_NET146_F2_(3)_PV,0.8999999761581421,1.0,false,0170837a-1876-45f9-a613-666f9991964d,,,,cosPhiFixed:{(0.00,0.90)},10.0 e447506e-3d43-4bce-8aab-a7ca8b7fbc45,0.20000000298023224,3.8914573192596436,0.8999999761581421,98.0,42.77021408081055,NS_NET146_F4_(9)_PV,0.8999999761581421,1.0,false,9b889b73-c108-4b38-b6eb-3377841e0c83,,,,cosPhiFixed:{(0.00,0.90)},10.0 6cac0624-6336-4418-bcf0-990abcdb824b,0.20000000298023224,-8.097375869750977,0.8999999761581421,98.0,44.90728759765625,NS_NET146_F4_(16)_PV,0.8999999761581421,1.0,false,9f7599de-c488-46c5-b053-1279a511f7b9,,,,cosPhiFixed:{(0.00,0.90)},30.0 From 29175ae46881b87d3f8d10de7f769d3bee11199a Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Thu, 3 Mar 2022 14:05:26 +0100 Subject: [PATCH 28/73] Adapted b also here --- .../ie3/simona/test/common/input/TransformerInputTestData.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/test/common/input/TransformerInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/TransformerInputTestData.scala index 006711c268..06ccc6a8e8 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/TransformerInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/TransformerInputTestData.scala @@ -91,7 +91,7 @@ trait TransformerInputTestData extends DefaultTestData { Quantities.getQuantity(110d, KILOVOLT), Quantities.getQuantity(10d, KILOVOLT), Quantities.getQuantity(0d, MetricPrefix.NANO(SIEMENS)), - Quantities.getQuantity(1.1, MetricPrefix.NANO(SIEMENS)), + Quantities.getQuantity(-1.1, MetricPrefix.NANO(SIEMENS)), Quantities.getQuantity(1.5, PERCENT), Quantities.getQuantity(0d, DEGREE_GEOM), false, From fb5cceb4c1779c36f2928dd36abedac2e16b3c2b Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 10:39:36 +0100 Subject: [PATCH 29/73] snake case for csv file --- input/samples/vn_simona/fullGrid/pv_input.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/samples/vn_simona/fullGrid/pv_input.csv b/input/samples/vn_simona/fullGrid/pv_input.csv index 41986cdd09..a7c99726d2 100644 --- a/input/samples/vn_simona/fullGrid/pv_input.csv +++ b/input/samples/vn_simona/fullGrid/pv_input.csv @@ -1,4 +1,4 @@ -"uuid","albedo","azimuth","cos_phi_rated","eta_conv","elevationAngle","id","k_g","k_t","market_reaction","node","operates_from","operates_until","operator","q_characteristics","s_rated" +"uuid","albedo","azimuth","cos_phi_rated","eta_conv","elevation_angle","id","k_g","k_t","market_reaction","node","operates_from","operates_until","operator","q_characteristics","s_rated" 5b38af42-1ee4-4a41-b666-ea141187df37,0.20000000298023224,-11.463644027709961,0.8999999761581421,96.0,33.62879943847656,NS_NET146_F2_(3)_PV,0.8999999761581421,1.0,false,0170837a-1876-45f9-a613-666f9991964d,,,,cosPhiFixed:{(0.00,0.90)},10.0 e447506e-3d43-4bce-8aab-a7ca8b7fbc45,0.20000000298023224,3.8914573192596436,0.8999999761581421,98.0,42.77021408081055,NS_NET146_F4_(9)_PV,0.8999999761581421,1.0,false,9b889b73-c108-4b38-b6eb-3377841e0c83,,,,cosPhiFixed:{(0.00,0.90)},10.0 6cac0624-6336-4418-bcf0-990abcdb824b,0.20000000298023224,-8.097375869750977,0.8999999761581421,98.0,44.90728759765625,NS_NET146_F4_(16)_PV,0.8999999761581421,1.0,false,9f7599de-c488-46c5-b053-1279a511f7b9,,,,cosPhiFixed:{(0.00,0.90)},30.0 From a978077a6b72a657608efbccf5bde3854e17843f Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 10:42:38 +0100 Subject: [PATCH 30/73] snake case for csv file --- .../ie3/simona/model/participant/pv/it/grid_data/pv_input.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/edu/ie3/simona/model/participant/pv/it/grid_data/pv_input.csv b/src/test/resources/edu/ie3/simona/model/participant/pv/it/grid_data/pv_input.csv index a594598bfc..b4ff778eaf 100644 --- a/src/test/resources/edu/ie3/simona/model/participant/pv/it/grid_data/pv_input.csv +++ b/src/test/resources/edu/ie3/simona/model/participant/pv/it/grid_data/pv_input.csv @@ -1,4 +1,4 @@ -"uuid";"albedo";"azimuth";"cosphi_rated";"eta_conv";"height";"id";"k_g";"k_t";"market_reaction";"operates_from";"operates_until";"s_rated";"q_characteristics";"node";"operator" +"uuid";"albedo";"azimuth";"cosphi_rated";"eta_conv";"elevation_angle";"id";"k_g";"k_t";"market_reaction";"operates_from";"operates_until";"s_rated";"q_characteristics";"node";"operator" 7ac5bb15-36ee-42b0-902b-9cd520e241b3;0.2;16.09490984119475;0.95;91.23978812713176;51.75144341774285;pv_south_1;0.9;1;false;;;100;cosPhiFixed:{(0.00,1.0)};022a94c6-2d60-4400-875c-ab9db1ae2736; 939d254a-98b9-43d9-939d-dac9d91e7d73;0.2;-11.883286549709737;0.95;93.55452200165019;50.710754711180925;pv_south_2;0.9;1;false;;;100;cosPhiFixed:{(0.00,1.0)};9a2524f1-3639-4e90-a547-81a259712f8c; e3b34366-9a4b-4e8f-b46d-fccdd3c318b3;0.2;-3.6445723846554756;0.95;90.07983175106347;50.727743320167065;pv_south_3;0.9;1;false;;;100;cosPhiFixed:{(0.00,1.0)};9354b02c-a4a9-4e9d-905a-e48110b04d88; From 0f417f9469c1a80c78d1ff0c27c94be18be0e8e3 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 10:43:37 +0100 Subject: [PATCH 31/73] height to elevationAngle --- .../groovy/edu/ie3/simona/model/participant/PVModelIT.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy b/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy index 90002b66e6..d0dbdc8048 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy @@ -153,7 +153,7 @@ trait PVModelITHelper { inputModel.getAlbedo(), inputModel.getEtaConv(), inputModel.getAzimuth(), - inputModel.getHeight(), + inputModel.getElevationAngle(), getQuantity(1d, SQUARE_METRE) ) From a7c49e98c1e1f390baff7223218fef839d50a31e Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 10:44:19 +0100 Subject: [PATCH 32/73] height to elevationAngle --- .../groovy/edu/ie3/simona/model/participant/PVModelTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/PVModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/PVModelTest.groovy index 6cb8535c2c..6aac83d271 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/PVModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/PVModelTest.groovy @@ -108,7 +108,7 @@ class PVModelTest extends Specification { pvInput.getAlbedo(), pvInput.getEtaConv() as ComparableQuantity, getQuantity(Math.toRadians(pvInput.getAzimuth().getValue().doubleValue()), RADIAN), - getQuantity(Math.toRadians(pvInput.getHeight().getValue().doubleValue()), RADIAN), + getQuantity(Math.toRadians(pvInput.getElevationAngle().getValue().doubleValue()), RADIAN), getQuantity(1d, SQUARE_METRE) ) } From 230a2e7dedf70fdb7bd25e1eb31c09f24e909916 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 11:11:31 +0100 Subject: [PATCH 33/73] Change PSDM Weather to Cosmo --- .../edu/ie3/simona/service/weather/WeatherSource.scala | 2 +- .../ie3/simona/service/weather/WeatherSourceWrapper.scala | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala index 6da1016db2..62c4856b41 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -559,7 +559,7 @@ object WeatherSource { */ object WeatherScheme extends ParsableEnumeration { val ICON: Value = Value("icon") - val PSDM: Value = Value("psdm") + val Cosmo: Value = Value("psdm") } } diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index baf41e6c9f..46e567b30c 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -14,7 +14,7 @@ import edu.ie3.datamodel.io.connectors.{ } import edu.ie3.datamodel.io.factory.timeseries.{ IconTimeBasedWeatherValueFactory, - PsdmTimeBasedWeatherValueFactory + CosmoTimeBasedWeatherValueFactory } import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.source.couchbase.CouchbaseWeatherSource @@ -357,7 +357,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { .mkString("\n\t")}'" ) case Success(WeatherScheme.ICON) => new IconTimeBasedWeatherValueFactory() - case Success(WeatherScheme.PSDM) => new PsdmTimeBasedWeatherValueFactory() + case Success(WeatherScheme.Cosmo) => new CosmoTimeBasedWeatherValueFactory() case Success(unknownScheme) => throw new InitializationException( s"Error while initializing WeatherFactory for weather source wrapper: weather scheme '$unknownScheme' is not an expected input." @@ -373,8 +373,8 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { ) case Success(WeatherScheme.ICON) => new IconTimeBasedWeatherValueFactory(timeStampPattern) - case Success(WeatherScheme.PSDM) => - new PsdmTimeBasedWeatherValueFactory(timeStampPattern) + case Success(WeatherScheme.Cosmo) => + new CosmoTimeBasedWeatherValueFactory(timeStampPattern) case Success(unknownScheme) => throw new InitializationException( s"Error while initializing WeatherFactory for weather source wrapper: weather scheme '$unknownScheme' is not an expected input." From b861a999cf938c4b331a2010b7f7b48e615ab8b2 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 11:20:31 +0100 Subject: [PATCH 34/73] GSA --- .../edu/ie3/simona/service/weather/WeatherSourceWrapper.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index 46e567b30c..736d00773a 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -357,7 +357,8 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { .mkString("\n\t")}'" ) case Success(WeatherScheme.ICON) => new IconTimeBasedWeatherValueFactory() - case Success(WeatherScheme.Cosmo) => new CosmoTimeBasedWeatherValueFactory() + case Success(WeatherScheme.Cosmo) => + new CosmoTimeBasedWeatherValueFactory() case Success(unknownScheme) => throw new InitializationException( s"Error while initializing WeatherFactory for weather source wrapper: weather scheme '$unknownScheme' is not an expected input." From c35b7e426d2908c5cfb6473d98aa86f8cd2b6957 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 11:28:58 +0100 Subject: [PATCH 35/73] COSMO to uppercase --- .../scala/edu/ie3/simona/service/weather/WeatherSource.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala index 62c4856b41..9e8b7c7065 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -559,7 +559,7 @@ object WeatherSource { */ object WeatherScheme extends ParsableEnumeration { val ICON: Value = Value("icon") - val Cosmo: Value = Value("psdm") + val COSMO: Value = Value("cosmo") } } From e3fd1f92e6835cb429dd53f977ae5f21823a087b Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 11:38:25 +0100 Subject: [PATCH 36/73] COSMO to uppercase WeatherSourceWrapper --- .../edu/ie3/simona/service/weather/WeatherSourceWrapper.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index 736d00773a..ded598932e 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -357,7 +357,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { .mkString("\n\t")}'" ) case Success(WeatherScheme.ICON) => new IconTimeBasedWeatherValueFactory() - case Success(WeatherScheme.Cosmo) => + case Success(WeatherScheme.COSMO) => new CosmoTimeBasedWeatherValueFactory() case Success(unknownScheme) => throw new InitializationException( @@ -374,7 +374,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { ) case Success(WeatherScheme.ICON) => new IconTimeBasedWeatherValueFactory(timeStampPattern) - case Success(WeatherScheme.Cosmo) => + case Success(WeatherScheme.COSMO) => new CosmoTimeBasedWeatherValueFactory(timeStampPattern) case Success(unknownScheme) => throw new InitializationException( From ffc27875d486cfa47932e4e82442a3746ab9e281 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 4 Mar 2022 14:37:57 +0100 Subject: [PATCH 37/73] Change Schemes --- .../ie3/simona/service/primary/PrimaryServiceProxy.scala | 8 ++++---- .../ie3/simona/service/primary/PrimaryServiceWorker.scala | 2 +- .../scala/edu/ie3/simona/config/ConfigFailFastSpec.scala | 2 +- .../simona/service/primary/PrimaryServiceProxySpec.scala | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index 27c9808db9..010c0525dd 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -7,9 +7,9 @@ package edu.ie3.simona.service.primary import akka.actor.{Actor, ActorRef, PoisonPill, Props} -import edu.ie3.datamodel.io.connectors.CsvFileConnector.CsvIndividualTimeSeriesMetaInformation -import edu.ie3.datamodel.io.csv.timeseries.ColumnScheme +import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.naming.FileNamingStrategy +import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme import edu.ie3.datamodel.io.source.TimeSeriesMappingSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource import edu.ie3.datamodel.models.value.Value @@ -148,7 +148,7 @@ case class PrimaryServiceProxy( .distinct .flatMap { timeSeriesUuid => mappingSource - .getTimeSeriesMetaInformation(timeSeriesUuid) + .timeSeriesMetaInformation(timeSeriesUuid) .toScala match { case Some(metaInformation) => val columnScheme = metaInformation.getColumnScheme @@ -386,7 +386,7 @@ case class PrimaryServiceProxy( None ) => /* The mapping and actual data sources are from csv. At first, get the file name of the file to read. */ - Try(mappingSource.getTimeSeriesMetaInformation(timeSeriesUuid).get) + Try(mappingSource.timeSeriesMetaInformation(timeSeriesUuid).get) .flatMap { /* Time series meta information could be successfully obtained */ case csvMetaData: CsvIndividualTimeSeriesMetaInformation => diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index 8553ef5533..ec50abf716 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -7,9 +7,9 @@ package edu.ie3.simona.service.primary import akka.actor.{ActorRef, Props} -import edu.ie3.datamodel.io.csv.timeseries.ColumnScheme import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory import edu.ie3.datamodel.io.naming.FileNamingStrategy +import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme import edu.ie3.datamodel.io.source.TimeSeriesSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesSource import edu.ie3.datamodel.models.value.Value diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index decc6d59cd..57a4144cc0 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -875,7 +875,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ConfigFailFast invokePrivate checkWeatherDataSource( weatherDataSource ) - }.getMessage shouldBe "The weather data scheme 'this won't work' is not supported. Supported schemes:\n\ticon\n\tpsdm" + }.getMessage shouldBe "The weather data scheme 'this won't work' is not supported. Supported schemes:\n\ticon\n\tcosmo" } } diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index dae2702292..94a27323b9 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -10,8 +10,8 @@ import akka.actor.{ActorRef, ActorSystem, PoisonPill} import akka.testkit.{TestActorRef, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.io.csv.timeseries.ColumnScheme import edu.ie3.datamodel.io.naming.FileNamingStrategy +import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme import edu.ie3.datamodel.io.source.TimeSeriesMappingSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource import edu.ie3.datamodel.models.value.{SValue, Value} From 6834b6993b8db1f8ac200eaf967a2cc4b627b259 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Mar 2022 17:53:32 +0000 Subject: [PATCH 38/73] Bump poi-ooxml from 5.2.0 to 5.2.1 Bumps poi-ooxml from 5.2.0 to 5.2.1. --- updated-dependencies: - dependency-name: org.apache.poi:poi-ooxml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a4355a41a1..68e6e254cb 100644 --- a/build.gradle +++ b/build.gradle @@ -136,7 +136,7 @@ dependencies { scalaCompilerPlugin "com.sksamuel.scapegoat:scalac-scapegoat-plugin_${scalaBinaryVersion}:${scapegoatVersion}" implementation 'org.apache.commons:commons-math3:3.6.1' // apache commons math3 - implementation 'org.apache.poi:poi-ooxml:5.2.0' // used for FilenameUtils + implementation 'org.apache.poi:poi-ooxml:5.2.1' // used for FilenameUtils implementation 'javax.measure:unit-api:2.1.3' implementation 'tech.units:indriya:2.1.3' // quantities implementation 'org.apache.commons:commons-csv:1.9.0' From 07a52c11390d2e8022b0cd905bb80fba853ab4d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 10:29:01 +0000 Subject: [PATCH 39/73] Bump logback-classic from 1.2.10 to 1.2.11 (#165) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 68e6e254cb..4a80fb20ac 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ dependencies { /* logging */ implementation "com.typesafe.scala-logging:scala-logging_${scalaVersion}:3.9.4" // akka scala logging - implementation "ch.qos.logback:logback-classic:1.2.10" + implementation "ch.qos.logback:logback-classic:1.2.11" /* testing */ testImplementation 'org.spockframework:spock-core:2.1-M2-groovy-3.0' From 388d51297fb2ba671d9c7bb9ff5cfdfbc000afd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:26:16 +0000 Subject: [PATCH 40/73] Bump spock-core from 2.1-M2-groovy-3.0 to 2.1-groovy-3.0 Bumps spock-core from 2.1-M2-groovy-3.0 to 2.1-groovy-3.0. --- updated-dependencies: - dependency-name: org.spockframework:spock-core dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4a80fb20ac..c477b645b4 100644 --- a/build.gradle +++ b/build.gradle @@ -99,7 +99,7 @@ dependencies { implementation "ch.qos.logback:logback-classic:1.2.11" /* testing */ - testImplementation 'org.spockframework:spock-core:2.1-M2-groovy-3.0' + testImplementation 'org.spockframework:spock-core:2.1-groovy-3.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' implementation 'org.mockito:mockito-core:4.3.1' // mocking framework testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.11" From 18b6e11236fe7f05b1313b00b5f4b028190cb1d9 Mon Sep 17 00:00:00 2001 From: danielfeismann <98817556+danielfeismann@users.noreply.github.com> Date: Tue, 8 Mar 2022 12:10:26 +0100 Subject: [PATCH 41/73] input files comes with negative susceptance --- .../vn_146_lv_small/fullGrid/transformer_2_w_type_input.csv | 2 +- input/samples/vn_simona/fullGrid/transformer_2_w_type_input.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/input/samples/vn_146_lv_small/fullGrid/transformer_2_w_type_input.csv b/input/samples/vn_146_lv_small/fullGrid/transformer_2_w_type_input.csv index f0c3702e2f..987777ff65 100644 --- a/input/samples/vn_146_lv_small/fullGrid/transformer_2_w_type_input.csv +++ b/input/samples/vn_146_lv_small/fullGrid/transformer_2_w_type_input.csv @@ -1,6 +1,6 @@ "uuid","b_m","d_phi","d_v","g_m","id","r_sc","s_rated","tap_max","tap_min","tap_neutr","tap_side","v_rated_a","v_rated_b","x_sc" 14b1798a-6903-49d6-8578-ad2a7d399341,0.0,0.0,1.5,0.0,HS-MS_1,45.375,20000.0,10,-10,0,false,110.0,20.0,102.759 -97735722-05cc-4ca8-8a8d-c08ac3ded19a,1.27,0.0,1.5,555.5,HöS-HS_1,5.415,200000.0,5,-5,0,false,380.0,110.0,108.165 +97735722-05cc-4ca8-8a8d-c08ac3ded19a,-1.27,0.0,1.5,555.5,HöS-HS_1,5.415,200000.0,5,-5,0,false,380.0,110.0,108.165 f88989c7-9812-4b3e-9bc0-3df29f1e5ae1,0.0,0.0,0.5,0.0,MS-NS_1,10.078,630.0,10,-10,0,false,20.0,0.4,23.312 cf7b1102-8dbd-4da2-a469-90800b3394b6,0.0,0.0,1.5,0.0,HS-MS_1,45.375,20000.0,10,-10,0,false,110.0,20.0,102.759 1214c366-826e-4aeb-88f5-af8f40acaa04,0.0,0.0,1.5,0.0,HS-MS_1,45.375,20000.0,10,-10,0,false,110.0,20.0,102.759 diff --git a/input/samples/vn_simona/fullGrid/transformer_2_w_type_input.csv b/input/samples/vn_simona/fullGrid/transformer_2_w_type_input.csv index f0c3702e2f..987777ff65 100644 --- a/input/samples/vn_simona/fullGrid/transformer_2_w_type_input.csv +++ b/input/samples/vn_simona/fullGrid/transformer_2_w_type_input.csv @@ -1,6 +1,6 @@ "uuid","b_m","d_phi","d_v","g_m","id","r_sc","s_rated","tap_max","tap_min","tap_neutr","tap_side","v_rated_a","v_rated_b","x_sc" 14b1798a-6903-49d6-8578-ad2a7d399341,0.0,0.0,1.5,0.0,HS-MS_1,45.375,20000.0,10,-10,0,false,110.0,20.0,102.759 -97735722-05cc-4ca8-8a8d-c08ac3ded19a,1.27,0.0,1.5,555.5,HöS-HS_1,5.415,200000.0,5,-5,0,false,380.0,110.0,108.165 +97735722-05cc-4ca8-8a8d-c08ac3ded19a,-1.27,0.0,1.5,555.5,HöS-HS_1,5.415,200000.0,5,-5,0,false,380.0,110.0,108.165 f88989c7-9812-4b3e-9bc0-3df29f1e5ae1,0.0,0.0,0.5,0.0,MS-NS_1,10.078,630.0,10,-10,0,false,20.0,0.4,23.312 cf7b1102-8dbd-4da2-a469-90800b3394b6,0.0,0.0,1.5,0.0,HS-MS_1,45.375,20000.0,10,-10,0,false,110.0,20.0,102.759 1214c366-826e-4aeb-88f5-af8f40acaa04,0.0,0.0,1.5,0.0,HS-MS_1,45.375,20000.0,10,-10,0,false,110.0,20.0,102.759 From eb986c5691c9f7d5bcd79f4ca0bda2111ed22b75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 10:12:38 +0000 Subject: [PATCH 42/73] Bump de.undercouch.download from 5.0.1 to 5.0.2 (#168) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4a80fb20ac..da23018f97 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { id 'com.diffplug.spotless' version '6.3.0'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' - id "de.undercouch.download" version "5.0.1" // downloads plugin + id "de.undercouch.download" version "5.0.2" // downloads plugin id "kr.motd.sphinx" version "2.10.1" // documentation generation id "com.github.johnrengelman.shadow" version "7.1.2" // fat jar id "org.sonarqube" version "3.3" // sonarqube From 2dcd83dc17100e32426876af17b3685bb84bcde7 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Mon, 14 Mar 2022 11:42:42 +0100 Subject: [PATCH 43/73] Addressing reviewer's comment --- .../weather/WeatherSourceWrapper.scala | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index 324e185171..b29c9657a3 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -198,15 +198,13 @@ private[weather] final case class WeatherSourceWrapper private ( ) } match { case (weatherData: WeatherData, weightSum: WeightSum) => - /* Divide by weight sum to correctly account for missing data. Change temperature scale back to absolute*/ WeatherData( - weatherData.diffRad.divide(weightSum.diffRad), - weatherData.dirRad.divide(weightSum.dirRad), + weatherData.diffRad.divide(weightSum.diffIrr), + weatherData.dirRad.divide(weightSum.dirIrr), weatherData.temp.divide(weightSum.temp), weatherData.windVel.divide(weightSum.windVel) ) } - } /** Determine an Array with all ticks between the request frame's start and @@ -382,9 +380,22 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { ) } + /** Simple container class to allow for accumulating determination of the sum + * of weights for different weather properties for different locations + * surrounding a given coordinate of interest + * + * @param diffIrr + * Sum of weight for diffuse irradiance + * @param dirIrr + * Sum of weight for direct irradiance + * @param temp + * Sum of weight for temperature + * @param windVel + * Sum of weight for wind velocity + */ final case class WeightSum( - diffRad: Double, - dirRad: Double, + diffIrr: Double, + dirIrr: Double, temp: Double, windVel: Double ) { @@ -395,13 +406,13 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { windVel: Double ): WeightSum = WeightSum( - this.diffRad + diffRad, - this.dirRad + dirRad, + this.diffIrr + diffRad, + this.dirIrr + dirRad, this.temp + temp, this.windVel + windVel ) } - case object WeightSum { + object WeightSum { val EMPTY_WEIGHT_SUM: WeightSum = WeightSum(1d, 1d, 1d, 1d) } From d5fc1ce586af3b11105919e1c1222b8095e0fe21 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Mon, 14 Mar 2022 13:35:18 +0100 Subject: [PATCH 44/73] Improve handling of weight sum --- .../service/weather/WeatherSource.scala | 3 +- .../weather/WeatherSourceWrapper.scala | 42 +++-- .../edu/ie3/util/scala/DoubleUtils.scala | 15 ++ .../weather/WeatherSourceWrapperSpec.scala | 144 +++++++++++++++++- 4 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 src/main/scala/edu/ie3/util/scala/DoubleUtils.scala diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala index 9e8b7c7065..ea263c07cf 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -164,7 +164,8 @@ trait WeatherSource { } } - /** Determine the weights of each coordinate + /** Determine the weights of each coordinate. It is ensured, that the entirety + * of weights sum up to 1.0 * * @param nearestCoordinates * Collection of nearest coordinates with their distances diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index b29c9657a3..43bb9418f2 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -13,8 +13,8 @@ import edu.ie3.datamodel.io.connectors.{ SqlConnector } import edu.ie3.datamodel.io.factory.timeseries.{ - IconTimeBasedWeatherValueFactory, - CosmoTimeBasedWeatherValueFactory + CosmoTimeBasedWeatherValueFactory, + IconTimeBasedWeatherValueFactory } import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.source.couchbase.CouchbaseWeatherSource @@ -45,7 +45,9 @@ import edu.ie3.simona.util.TickUtil import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.exceptions.EmptyQuantityException import edu.ie3.util.interval.ClosedInterval +import edu.ie3.util.scala.DoubleUtils.ImplicitDouble import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units import java.time.ZonedDateTime import javax.measure.Quantity @@ -164,7 +166,7 @@ private[weather] final case class WeatherSourceWrapper private ( case EMPTY_WEATHER_DATA.temp => (EMPTY_WEATHER_DATA.temp, 0d) case nonEmptyTemp => calculateContrib( - nonEmptyTemp, + nonEmptyTemp.to(Units.KELVIN), weight, StandardUnits.TEMPERATURE, s"Temperature not available at $point." @@ -198,12 +200,7 @@ private[weather] final case class WeatherSourceWrapper private ( ) } match { case (weatherData: WeatherData, weightSum: WeightSum) => - WeatherData( - weatherData.diffRad.divide(weightSum.diffIrr), - weatherData.dirRad.divide(weightSum.dirIrr), - weatherData.temp.divide(weightSum.temp), - weatherData.windVel.divide(weightSum.windVel) - ) + weightSum.scale(weatherData) } } @@ -411,9 +408,34 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { this.temp + temp, this.windVel + windVel ) + + /** Scale the given [[WeatherData]] by dividing by the sum of weights per + * attribute of the weather data. If one of the weight sums is empty (and + * thus a division by zero would happen) the defined "empty" information + * for this attribute a returned. + * + * @param weatherData + * Weighted and accumulated weather information + * @return + * Weighted weather information, which are divided by the sum of weights + */ + def scale(weatherData: WeatherData): WeatherData = weatherData match { + case WeatherData(diffRad, dirRad, temp, windVel) => + implicit val precision: Double = 1e-3 + WeatherData( + if (this.diffIrr !~= 0d) diffRad.multiply(this.diffIrr) + else EMPTY_WEATHER_DATA.diffRad, + if (this.dirIrr !~= 0d) dirRad.divide(this.dirIrr) + else EMPTY_WEATHER_DATA.dirRad, + if (this.temp !~= 0d) temp.divide(this.temp) + else EMPTY_WEATHER_DATA.temp, + if (this.windVel !~= 0d) windVel.divide(this.windVel) + else EMPTY_WEATHER_DATA.windVel + ) + } } object WeightSum { - val EMPTY_WEIGHT_SUM: WeightSum = WeightSum(1d, 1d, 1d, 1d) + val EMPTY_WEIGHT_SUM: WeightSum = WeightSum(0d, 0d, 0d, 0d) } } diff --git a/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala b/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala new file mode 100644 index 0000000000..33d295c43e --- /dev/null +++ b/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala @@ -0,0 +1,15 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.util.scala + +object DoubleUtils { + implicit class ImplicitDouble(d: Double) { + def ~=(other: Double)(implicit precision: Double): Boolean = + (d - other).abs < precision + def !~=(other: Double)(implicit precision: Double): Boolean = ! ~=(other) + } +} diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala index e619deb026..09c5ac140b 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -16,14 +16,20 @@ import edu.ie3.datamodel.models.timeseries.individual.{ TimeBasedValue } import edu.ie3.datamodel.models.value.WeatherValue -import edu.ie3.simona.service.weather.WeatherSource.WeightedCoordinates +import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData +import edu.ie3.simona.service.weather.WeatherSource.{ + EMPTY_WEATHER_DATA, + WeightedCoordinates +} import edu.ie3.simona.service.weather.WeatherSourceSpec.DummyIdCoordinateSource +import edu.ie3.simona.service.weather.WeatherSourceWrapper.WeightSum import edu.ie3.simona.service.weather.WeatherSourceWrapperSpec._ import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.geo.GeoUtils import edu.ie3.util.interval.ClosedInterval import org.locationtech.jts.geom.Point import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units import java.time.{ZoneId, ZonedDateTime} import java.util @@ -147,6 +153,100 @@ class WeatherSourceWrapperSpec extends UnitSpec { result.temp.getScale shouldBe Scale.ABSOLUTE } } + + "Handling the weighted weather" when { + "scaling the weighted attributes with the sum of weights" should { + "calculate proper information on proper input" in { + val weatherSeq = Seq( + (0.5, 0.75, 291d, 10d), + (12.3, 1.2, 293d, 12d), + (25.0, 5.7, 290d, 9d), + (26.3, 1.7, 289d, 11d) + ) + val weights = Seq( + (0.1, 0.2, 0.3, 0.4), + (0.25, 0.2, 0.25, 0.1), + (0.3, 0.4, 0.15, 0.05), + (0.35, 0.2, 0.3, 0.45) + ) + + val (_, weightedWeather, weightSum) = + prepareWeightTestData(weatherSeq, weights) + + weightSum.scale(weightedWeather) match { + case WeatherData(diffRad, dirRad, temp, windVel) => + diffRad should equalWithTolerance( + Quantities.getQuantity(19.83, StandardUnits.SOLAR_IRRADIANCE), + 1e-6 + ) + dirRad should equalWithTolerance( + Quantities.getQuantity(3.01, StandardUnits.SOLAR_IRRADIANCE), + 1e-6 + ) + temp should equalWithTolerance( + Quantities + .getQuantity(290.75, Units.KELVIN) + .to(StandardUnits.TEMPERATURE), + 1e-6 + ) + windVel should equalWithTolerance( + Quantities.getQuantity(10.6, StandardUnits.WIND_VELOCITY), + 1e-6 + ) + } + } + } + + "calculate proper input, if data is missing in one coordinate" in { + val weatherSeq = Seq( + (0.5, 0.75, 291d, 10d), + (12.3, 1.2, 293d, 12d), + (25.0, 5.7, 290d, 9d), + (26.3, 1.7, 289d, 11d) + ) + val weights = Seq( + (0.1, 0.2, 0d, 0.4), + (0.25, 0.2, 0d, 0.1), + (0.3, 0.4, 0d, 0.05), + (0.35, 0.2, 0d, 0.45) + ) + + val (_, weightedWeather, weightSum) = + prepareWeightTestData(weatherSeq, weights) + + weightSum.scale(weightedWeather) match { + case WeatherData(_, _, temp, _) => + temp shouldBe EMPTY_WEATHER_DATA.temp + } + } + + "return empty value for an attribute, if weight sum is zero" in { + val weatherSeq = Seq( + (0.5, 0.75, 291d, 10d), + (12.3, 1.2, 0d, 12d), + (25.0, 5.7, 290d, 9d), + (26.3, 1.7, 289d, 11d) + ) + val weights = Seq( + (0.1, 0.2, 0.3, 0.4), + (0.25, 0.2, 0d, 0.1), + (0.3, 0.4, 0.15, 0.05), + (0.35, 0.2, 0.3, 0.45) + ) + + val (_, weightedWeather, weightSum) = + prepareWeightTestData(weatherSeq, weights) + + weightSum.scale(weightedWeather) match { + case WeatherData(_, _, temp, _) => + temp should equalWithTolerance( + Quantities + .getQuantity(290d, Units.KELVIN) + .to(StandardUnits.TEMPERATURE) + ) + } + } + } } case object WeatherSourceWrapperSpec { @@ -271,4 +371,46 @@ case object WeatherSourceWrapperSpec { } } + def prepareWeightTestData( + weatherSeq: Seq[(Double, Double, Double, Double)], + weights: Seq[(Double, Double, Double, Double)] + ): (Seq[WeatherData], WeatherData, WeightSum) = { + val weatherData = weatherSeq.map { case (diff, dir, temp, wVel) => + WeatherData( + Quantities.getQuantity(diff, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(dir, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(temp, Units.KELVIN), + Quantities.getQuantity(wVel, StandardUnits.WIND_VELOCITY) + ) + } + + val weightedWeather = + weatherData.zip(weights).foldLeft(EMPTY_WEATHER_DATA) { + case ( + currentSum, + ( + WeatherData(diffRad, dirRad, temp, windVel), + (diffWeight, dirWeight, tempWeight, wVelWeight) + ) + ) => + currentSum.copy( + diffRad = currentSum.diffRad.add(diffRad.multiply(diffWeight)), + dirRad = currentSum.dirRad.add(dirRad.multiply(dirWeight)), + temp = currentSum.temp.add(temp.multiply(tempWeight)), + windVel = currentSum.windVel.add(windVel.multiply(wVelWeight)) + ) + } + val weightSum = weights.foldLeft(WeightSum.EMPTY_WEIGHT_SUM) { + case (currentSum, currentWeight) => + currentSum.add( + currentWeight._1, + currentWeight._2, + currentWeight._3, + currentWeight._1 + ) + } + + (weatherData, weightedWeather, weightSum) + } + } From 2f1d31bfd9068ebc914185faa35e33bcb00af249 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Mon, 14 Mar 2022 13:39:45 +0100 Subject: [PATCH 45/73] Adapt CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aacfbd5ab..211e5e6863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Re-organizing test resources into their respective packages [#105](https://github.com/ie3-institute/simona/issues/105) - BREAKING: Using snapshot version of PSDM - Simplified PrimaryServiceProxy due to changes in PSDM [#120](https://github.com/ie3-institute/simona/issues/120) +- Improved handling of weights and their sum in determination of weather data [#173](https://github.com/ie3-institute/simona/issues/173) ### Fixed - Location of `vn_simona` test grid (was partially in Berlin and Dortmund) - Let `ParticipantAgent` die after failed registration with secondary services (prevents stuck simulation) -- Fix default resolution of weather source wrapper +- Fix default resolution of weather source wrapper [#78](https://github.com/ie3-institute/simona/issues/78) [Unreleased]: https://github.com/ie3-institute/simona/compare/a14a093239f58fca9b2b974712686b33e5e5f939...HEAD From 9cfd7becd1cd45434ea1f307490ef4a74c6674e0 Mon Sep 17 00:00:00 2001 From: "Kittl, Chris" Date: Mon, 14 Mar 2022 14:11:38 +0100 Subject: [PATCH 46/73] Fix broken calculation --- .../edu/ie3/simona/service/weather/WeatherSourceWrapper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index 43bb9418f2..d2ec4a26ce 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -423,7 +423,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { case WeatherData(diffRad, dirRad, temp, windVel) => implicit val precision: Double = 1e-3 WeatherData( - if (this.diffIrr !~= 0d) diffRad.multiply(this.diffIrr) + if (this.diffIrr !~= 0d) diffRad.divide(this.diffIrr) else EMPTY_WEATHER_DATA.diffRad, if (this.dirIrr !~= 0d) dirRad.divide(this.dirIrr) else EMPTY_WEATHER_DATA.dirRad, From 03d6d1060c96ac2c4e59e986ffba3e8cbffbed05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 13:29:43 +0100 Subject: [PATCH 47/73] Bump mockito-core from 4.3.1 to 4.4.0 (#169) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.3.1 to 4.4.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.3.1...v4.4.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ae7fe99231..52f34f8ff7 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,7 @@ dependencies { /* testing */ testImplementation 'org.spockframework:spock-core:2.1-groovy-3.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' - implementation 'org.mockito:mockito-core:4.3.1' // mocking framework + implementation 'org.mockito:mockito-core:4.4.0' // mocking framework testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.11" testRuntimeClasspath 'com.vladsch.flexmark:flexmark-all:0.64.0' testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' From 8dcbc8c39deb9be514f0f51d6bc375cc2f92b7a6 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 16 Mar 2022 19:19:23 +0100 Subject: [PATCH 48/73] Adapting to changes in PSDM and addressing some comments --- gradle/scripts/tscfg.gradle | 2 +- .../resources/config/config-template.conf | 1 - .../edu/ie3/simona/config/SimonaConfig.scala | 2 - .../service/primary/PrimaryServiceProxy.scala | 136 +++++++-------- .../primary/PrimaryServiceWorker.scala | 34 ++-- ...p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql | 17 -- ...h_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql | 19 --- .../primary/timeseries/time_series_p.sql | 21 +++ .../primary/timeseries/time_series_pqh.sql | 21 +++ .../primary/PrimaryServiceProxySpec.scala | 161 +++++++----------- .../primary/PrimaryServiceWorkerSpec.scala | 23 ++- .../primary/PrimaryServiceWorkerSqlIT.scala | 79 +++------ .../common/input/TimeSeriesTestData.scala | 39 +++++ .../test/helper/TestContainerHelper.scala | 36 ++++ 14 files changed, 305 insertions(+), 286 deletions(-) delete mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql delete mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql create mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql create mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql create mode 100644 src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala create mode 100644 src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala diff --git a/gradle/scripts/tscfg.gradle b/gradle/scripts/tscfg.gradle index 7f430ff1f7..ce1fe4dbe9 100644 --- a/gradle/scripts/tscfg.gradle +++ b/gradle/scripts/tscfg.gradle @@ -15,7 +15,7 @@ task genConfigClass { args = [ "build/tscfg-${tscfgVersion}.jar", "--spec", - "src/main/resources/config/simona-config-template.conf", + "src/main/resources/config/config-template.conf", "--scala", "--durations", "--pn", diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 4848267d2c..9c36b25cf5 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -101,7 +101,6 @@ simona.input.primary = { jdbcUrl: string userName: string password: string - tableName: string schemaName: string | "public" timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'" # default pattern from PSDM:TimeBasedSimpleValueFactory } diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index f6eb5cee78..93e9b4931b 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -908,7 +908,6 @@ object SimonaConfig { jdbcUrl: java.lang.String, password: java.lang.String, schemaName: java.lang.String, - tableName: java.lang.String, timePattern: java.lang.String, userName: java.lang.String ) @@ -924,7 +923,6 @@ object SimonaConfig { schemaName = if (c.hasPathOrNull("schemaName")) c.getString("schemaName") else "public", - tableName = $_reqStr(parentPath, c, "tableName", $tsCfgValidator), timePattern = if (c.hasPathOrNull("timePattern")) c.getString("timePattern") else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'", diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index 010c0525dd..3e6d213b07 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -9,9 +9,12 @@ package edu.ie3.simona.service.primary import akka.actor.{Actor, ActorRef, PoisonPill, Props} import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.naming.FileNamingStrategy -import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme +import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.source.TimeSeriesMappingSource -import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource +import edu.ie3.datamodel.io.source.csv.{ + CsvTimeSeriesMappingSource, + CsvTimeSeriesTypeSource +} import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.CsvParams @@ -51,7 +54,6 @@ import java.time.ZonedDateTime import java.util.UUID import scala.Option.when import scala.jdk.CollectionConverters._ -import scala.jdk.OptionConverters._ import scala.util.{Failure, Success, Try} /** This actor has information on which models can be replaced by precalculated @@ -137,19 +139,27 @@ case class PrimaryServiceProxy( ).filter(_.isDefined).flatten.headOption match { case Some(CsvParams(csvSep, folderPath, _)) => // TODO: Configurable file naming strategy + val fileNamingStrategy = new FileNamingStrategy() val mappingSource = new CsvTimeSeriesMappingSource( csvSep, folderPath, - new FileNamingStrategy() + fileNamingStrategy + ) + val typeSource = new CsvTimeSeriesTypeSource( + csvSep, + folderPath, + fileNamingStrategy ) val modelToTimeSeries = mappingSource.getMapping.asScala.toMap + val timeSeriesMetaInformation = + typeSource.getTimeSeriesMetaInformation.asScala.toMap + val timeSeriesToSourceRef = modelToTimeSeries.values .to(LazyList) .distinct .flatMap { timeSeriesUuid => - mappingSource - .timeSeriesMetaInformation(timeSeriesUuid) - .toScala match { + timeSeriesMetaInformation + .get(timeSeriesUuid) match { case Some(metaInformation) => val columnScheme = metaInformation.getColumnScheme /* Only register those entries, that meet the supported column schemes */ @@ -157,7 +167,7 @@ case class PrimaryServiceProxy( PrimaryServiceWorker.supportedColumnSchemes .contains(columnScheme) ) { - timeSeriesUuid -> SourceRef(columnScheme, None) + timeSeriesUuid -> SourceRef(metaInformation, None) } case None => log.warning( @@ -251,14 +261,12 @@ case class PrimaryServiceProxy( /* There is yet a worker apparent. Register the requesting actor. The worker will reply to the original * requesting actor. */ worker ! WorkerRegistrationMessage(requestingActor) - case Some(SourceRef(columnScheme, None)) => + case Some(SourceRef(metaInformation, None)) => /* There is NO worker apparent, yet. Spin one off. */ initializeWorker( - columnScheme, - timeSeriesUuid, + metaInformation, stateData.simulationStart, - stateData.primaryConfig, - stateData.mappingSource + stateData.primaryConfig ) match { case Success(workerRef) => /* Forward the registration request. The worker will reply about successful registration or not. */ @@ -289,33 +297,28 @@ case class PrimaryServiceProxy( /** Instantiate a new [[PrimaryServiceWorker]] and send initialization * information * - * @param columnScheme - * Scheme of the data to expect + * @param metaInformation + * Meta information (including column scheme) of the time series + * @param simulationStart + * The time of the simulation start * @param primaryConfig * Configuration for the primary config - * @param mappingSource - * Source for time series mapping, that might deliver additional - * information for the source initialization * @return * The [[ActorRef]] to the worker */ protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = { val workerRef = classToWorkerRef( - columnScheme.getValueClass, - timeSeriesUuid.toString, - simulationStart + metaInformation.getColumnScheme.getValueClass, + metaInformation.getUuid.toString ) toInitData( - primaryConfig, - mappingSource, - timeSeriesUuid, - simulationStart + metaInformation, + simulationStart, + primaryConfig ) match { case Success(initData) => scheduler ! ScheduleTriggerMessage( @@ -341,8 +344,6 @@ case class PrimaryServiceProxy( * Class of the values to provide later on * @param timeSeriesUuid * uuid of the time series the actor processes - * @param simulationStart - * Wall clock time of first instant in simulation * @tparam V * Type of the class to provide * @return @@ -350,33 +351,29 @@ case class PrimaryServiceProxy( */ protected def classToWorkerRef[V <: Value]( valueClass: Class[V], - timeSeriesUuid: String, - simulationStart: ZonedDateTime + timeSeriesUuid: String ): ActorRef = { import edu.ie3.simona.actor.SimonaActorNaming._ context.system.simonaActorOf( - PrimaryServiceWorker.props(scheduler, valueClass, simulationStart), + PrimaryServiceWorker.props(scheduler, valueClass), timeSeriesUuid ) } /** Building proper init data for the worker * - * @param primaryConfig - * Configuration for primary sources - * @param mappingSource - * Source to get mapping information about time series - * @param timeSeriesUuid - * Unique identifier for the time series + * @param metaInformation + * Meta information (including column scheme) of the time series * @param simulationStart - * Wall clock time of the first instant in simulation + * The time of the simulation start + * @param primaryConfig + * Configuration for the primary config * @return */ private def toInitData( - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource, - timeSeriesUuid: UUID, - simulationStart: ZonedDateTime + metaInformation: IndividualTimeSeriesMetaInformation, + simulationStart: ZonedDateTime, + primaryConfig: PrimaryConfig ): Try[InitPrimaryServiceStateData] = primaryConfig match { case PrimaryConfig( @@ -386,28 +383,27 @@ case class PrimaryServiceProxy( None ) => /* The mapping and actual data sources are from csv. At first, get the file name of the file to read. */ - Try(mappingSource.timeSeriesMetaInformation(timeSeriesUuid).get) - .flatMap { - /* Time series meta information could be successfully obtained */ - case csvMetaData: CsvIndividualTimeSeriesMetaInformation => - Success( - CsvInitPrimaryServiceStateData( - timeSeriesUuid, - simulationStart, - csvSep, - directoryPath, - csvMetaData.getFullFilePath, - new FileNamingStrategy(), - timePattern - ) + metaInformation match { + /* Time series meta information could be successfully obtained */ + case csvMetaData: CsvIndividualTimeSeriesMetaInformation => + Success( + CsvInitPrimaryServiceStateData( + csvMetaData.getUuid, + simulationStart, + csvSep, + directoryPath, + csvMetaData.getFullFilePath, + new FileNamingStrategy(), + timePattern ) - case invalidMetaData => - Failure( - new InitializationException( - s"Expected '${classOf[CsvIndividualTimeSeriesMetaInformation]}', but got '$invalidMetaData'." - ) + ) + case invalidMetaData => + Failure( + new InitializationException( + s"Expected '${classOf[CsvIndividualTimeSeriesMetaInformation]}', but got '$invalidMetaData'." ) - } + ) + } case unsupported => Failure( new InitializationException( @@ -489,14 +485,14 @@ object PrimaryServiceProxy { /** Giving reference to the target time series and source worker. * - * @param columnScheme - * Column scheme of the time series to get + * @param metaInformation + * Meta information (including column scheme) of the time series * @param worker - * Optional reference to a yet existing worker providing information on - * that time series + * Optional reference to an already existing worker providing information + * on that time series */ final case class SourceRef( - columnScheme: ColumnScheme, + metaInformation: IndividualTimeSeriesMetaInformation, worker: Option[ActorRef] ) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index 17ff2405bc..7d25630cac 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -9,8 +9,8 @@ package edu.ie3.simona.service.primary import akka.actor.{ActorRef, Props} import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory -import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme +import edu.ie3.datamodel.io.naming.{DatabaseNamingStrategy, FileNamingStrategy} import edu.ie3.datamodel.io.source.TimeSeriesSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesSource import edu.ie3.datamodel.io.source.sql.SqlTimeSeriesSource @@ -43,8 +43,7 @@ import scala.util.{Failure, Success, Try} final case class PrimaryServiceWorker[V <: Value]( override protected val scheduler: ActorRef, - valueClass: Class[V], - private implicit val startDateTime: ZonedDateTime + valueClass: Class[V] ) extends SimonaService[PrimaryServiceInitializedStateData[V]](scheduler) { /** Initialize the actor with the given information. Try to figure out the @@ -89,10 +88,12 @@ final case class PrimaryServiceWorker[V <: Value]( ) (source, simulationStart) } + case PrimaryServiceWorker.SqlInitPrimaryServiceStateData( - sqlParams: SqlParams, timeSeriesUuid: UUID, - simulationStart: ZonedDateTime + simulationStart: ZonedDateTime, + sqlParams: SqlParams, + namingStrategy: DatabaseNamingStrategy ) => Try { val factory = @@ -107,7 +108,7 @@ final case class PrimaryServiceWorker[V <: Value]( val source = new SqlTimeSeriesSource( sqlConnector, sqlParams.schemaName, - sqlParams.tableName, + namingStrategy, timeSeriesUuid, valueClass, factory @@ -115,6 +116,7 @@ final case class PrimaryServiceWorker[V <: Value]( (source, simulationStart) } + case unsupported => /* Got the wrong init data */ Failure( @@ -123,6 +125,8 @@ final case class PrimaryServiceWorker[V <: Value]( ) ) }).map { case (source, simulationStart) => + implicit val startDateTime: ZonedDateTime = simulationStart + val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( // Note: The whole data set is used here, which might be inefficient depending on the source implementation. source.getTimeSeries.getEntries.asScala @@ -318,7 +322,7 @@ final case class PrimaryServiceWorker[V <: Value]( } } -case object PrimaryServiceWorker { +object PrimaryServiceWorker { /** List of supported column schemes aka. column schemes, that belong to * primary data @@ -332,10 +336,9 @@ case object PrimaryServiceWorker { def props[V <: Value]( scheduler: ActorRef, - valueClass: Class[V], - simulationStart: ZonedDateTime + valueClass: Class[V] ): Props = - Props(new PrimaryServiceWorker(scheduler, valueClass, simulationStart)) + Props(new PrimaryServiceWorker(scheduler, valueClass)) /** Abstract class pattern for specific [[InitializeServiceStateData]]. * Different implementations are needed, because the [[PrimaryServiceProxy]] @@ -380,17 +383,20 @@ case object PrimaryServiceWorker { /** Specific implementation of [[InitPrimaryServiceStateData]], if the source * to use utilizes an SQL database. * - * @param sqlParams - * Parameters regarding SQL connection and table selection * @param timeSeriesUuid * Unique identifier of the time series to read * @param simulationStart * Wall clock time of the beginning of simulation time + * @param sqlParams + * Parameters regarding SQL connection and table selection + * @param databaseNamingStrategy + * Strategy of naming database entities, such as tables */ final case class SqlInitPrimaryServiceStateData( - sqlParams: SqlParams, override val timeSeriesUuid: UUID, - override val simulationStart: ZonedDateTime + override val simulationStart: ZonedDateTime, + sqlParams: SqlParams, + databaseNamingStrategy: DatabaseNamingStrategy ) extends InitPrimaryServiceStateData /** Class carrying the state of a fully initialized [[PrimaryServiceWorker]] diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql deleted file mode 100644 index 1f956e06a7..0000000000 --- a/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE public."its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5" -( - time timestamp with time zone, - p double precision, - uuid uuid, - CONSTRAINT its_p_pkey PRIMARY KEY (uuid) -) - WITH ( - OIDS = FALSE - ) - TABLESPACE pg_default; - -INSERT INTO - public."its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5" (uuid, time, p) -VALUES -('0245d599-9a5c-4c32-9613-5b755fac8ca0', '2020-01-01 00:00:00+0', 1000.0), -('a5e27652-9024-4a93-9d2a-590fbc3ab5a1', '2020-01-01 00:15:00+0', 1250.0); diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql deleted file mode 100644 index 230393eb5b..0000000000 --- a/src/test/resources/edu/ie3/simona/service/primary/timeseries/its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047.sql +++ /dev/null @@ -1,19 +0,0 @@ -CREATE TABLE public."its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047" -( - time timestamp with time zone, - p double precision, - q double precision, - heat_demand double precision, - uuid uuid, - CONSTRAINT its_pqh_pkey PRIMARY KEY (uuid) -) - WITH ( - OIDS = FALSE - ) - TABLESPACE pg_default; - -INSERT INTO - public."its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047" (uuid, time, p, q, heat_demand) -VALUES -('661ac594-47f0-4442-8d82-bbeede5661f7', '2020-01-01 00:00:00+0', 1000.0, 329.0, 8.0), -('5adcd6c5-a903-433f-b7b5-5fe669a3ed30', '2020-01-01 00:15:00+0', 1250.0, 411.0, 12.0); diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql new file mode 100644 index 0000000000..79beaf5e70 --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql @@ -0,0 +1,21 @@ +CREATE TABLE public.time_series_p +( + uuid uuid PRIMARY KEY, + time_series uuid NOT NULL, + time timestamp with time zone NOT NULL, + p double precision NOT NULL +) + WITHOUT OIDS + TABLESPACE pg_default; + +CREATE INDEX time_series_p_series_id ON time_series_p USING hash (time_series); + +CREATE UNIQUE INDEX time_series_p_series_time ON time_series_p USING btree (time_series, time); + +INSERT INTO + public.time_series_p (uuid, time_series, time, p) +VALUES +('0245d599-9a5c-4c32-9613-5b755fac8ca0', '9185b8c1-86ba-4a16-8dea-5ac898e8caa5', '2020-01-01 00:00:00+0', 1000.0), +('a5e27652-9024-4a93-9d2a-590fbc3ab5a1', '9185b8c1-86ba-4a16-8dea-5ac898e8caa5', '2020-01-01 00:15:00+0', 1250.0), +('b4a2b3e0-7215-431b-976e-d8b41c7bc71b', 'b669e4bf-a351-4067-860d-d5f224b62247', '2020-01-01 00:00:00+0', 50.0), +('1c8f072c-c833-47da-a3e9-5f4d305ab926', 'b669e4bf-a351-4067-860d-d5f224b62247', '2020-01-01 00:15:00+0', 100.0); diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql new file mode 100644 index 0000000000..8bd3a48908 --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql @@ -0,0 +1,21 @@ +CREATE TABLE public.time_series_pqh +( + uuid uuid PRIMARY KEY, + time_series uuid NOT NULL, + time timestamp with time zone NOT NULL, + p double precision NOT NULL, + q double precision NOT NULL, + heat_demand double precision NOT NULL +) + WITHOUT OIDS + TABLESPACE pg_default; + +CREATE INDEX time_series_pqh_series_id ON time_series_pqh USING hash (time_series); + +CREATE UNIQUE INDEX time_series_pqh_series_time ON time_series_pqh USING btree (time_series, time); + +INSERT INTO + public.time_series_pqh (uuid, time_series, time, p, q, heat_demand) +VALUES +('661ac594-47f0-4442-8d82-bbeede5661f7', '46be1e57-e4ed-4ef7-95f1-b2b321cb2047', '2020-01-01 00:00:00+0', 1000.0, 329.0, 8.0), +('5adcd6c5-a903-433f-b7b5-5fe669a3ed30', '46be1e57-e4ed-4ef7-95f1-b2b321cb2047', '2020-01-01 00:15:00+0', 1250.0, 411.0, 12.0); diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index 1a31e3bae2..8ab191fc3b 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -10,8 +10,9 @@ import akka.actor.{ActorRef, ActorSystem, PoisonPill} import akka.testkit.{TestActorRef, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.naming.FileNamingStrategy -import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme +import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.source.TimeSeriesMappingSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource import edu.ie3.datamodel.models.value.{SValue, Value} @@ -33,22 +34,23 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ ScheduleTriggerMessage, TriggerWithIdMessage } +import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ PrimaryServiceRegistrationMessage, WorkerRegistrationMessage } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.trigger.Trigger.InitializeServiceTrigger -import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ - CsvInitPrimaryServiceStateData, - InitPrimaryServiceStateData -} import edu.ie3.simona.service.primary.PrimaryServiceProxy.{ InitPrimaryServiceProxyStateData, PrimaryServiceStateData, SourceRef } +import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ + CsvInitPrimaryServiceStateData, + InitPrimaryServiceStateData +} import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.common.input.TimeSeriesTestData import edu.ie3.util.TimeUtil import org.scalatest.PartialFunctionValues import org.scalatest.prop.TableDrivenPropertyChecks @@ -57,8 +59,8 @@ import java.nio.file.Paths import java.time.ZonedDateTime import java.util.concurrent.TimeUnit import java.util.{Objects, UUID} -import scala.util.{Failure, Success, Try} import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.{Failure, Success, Try} class PrimaryServiceProxySpec extends AgentSpec( @@ -72,7 +74,8 @@ class PrimaryServiceProxySpec ) ) with TableDrivenPropertyChecks - with PartialFunctionValues { + with PartialFunctionValues + with TimeSeriesTestData { // this works both on Windows and Unix systems val baseDirectoryPath: String = Paths .get( @@ -103,30 +106,19 @@ class PrimaryServiceProxySpec baseDirectoryPath, fileNamingStrategy ) - val workerId: String = - "PrimaryService_3fbfaa97-cff4-46d4-95ba-a95665e87c26" + val workerId: String = "PrimaryService_" + uuidPq val modelUuid: UUID = UUID.fromString("c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8") - val timeSeriesUuid: UUID = - UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") val simulationStart: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2021-03-17 13:14:00") val proxyStateData: PrimaryServiceStateData = PrimaryServiceStateData( Map( - UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), - modelUuid -> UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), - UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") + UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> uuidP, + modelUuid -> uuidPq, + UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> uuidPq ), Map( - UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") -> SourceRef( - ColumnScheme.ACTIVE_POWER, - None - ), - UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") -> SourceRef( - ColumnScheme.APPARENT_POWER, - None - ) + uuidP -> SourceRef(metaP, None), + uuidPq -> SourceRef(metaPq, None) ), simulationStart, validPrimaryConfig, @@ -210,13 +202,13 @@ class PrimaryServiceProxySpec None, None, None, - Some(SqlParams("", "", "", "", "", "")) + Some(SqlParams("", "", "", "", "")) ) val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,,)' for a time series source.\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,)' for a time series source.\nAvailable types:\n\tcsv" } "fails on invalid time pattern" in { @@ -282,7 +274,7 @@ class PrimaryServiceProxySpec None, None, None, - Some(SqlParams("", "", "", "", "", "")) + Some(SqlParams("", "", "", "", "")) ) proxy invokePrivate prepareStateData( @@ -293,7 +285,7 @@ class PrimaryServiceProxySpec fail("Building state data with missing config should fail") case Failure(exception) => exception.getClass shouldBe classOf[IllegalArgumentException] - exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,,)'" + exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,)'" } } @@ -312,24 +304,13 @@ class PrimaryServiceProxySpec ) ) => modelToTimeSeries shouldBe Map( - UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), - UUID.fromString("c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8") -> UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), - UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") + UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> uuidP, + UUID.fromString("c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8") -> uuidPq, + UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> uuidPq ) timeSeriesToSourceRef shouldBe Map( - UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") -> SourceRef( - ColumnScheme.ACTIVE_POWER, - None - ), - UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") -> SourceRef( - ColumnScheme.APPARENT_POWER, - None - ) + uuidP -> SourceRef(metaP, None), + uuidPq -> SourceRef(metaPq, None) ) simulationStart shouldBe this.simulationStart primaryConfig shouldBe validPrimaryConfig @@ -372,8 +353,7 @@ class PrimaryServiceProxySpec val workerRef = proxy invokePrivate classToWorkerRef( testClass, - workerId, - simulationStart + workerId ) Objects.nonNull(workerRef) shouldBe true @@ -385,12 +365,15 @@ class PrimaryServiceProxySpec val toInitData = PrivateMethod[Try[InitPrimaryServiceStateData]]( Symbol("toInitData") ) + val metaInformation = new CsvIndividualTimeSeriesMetaInformation( + metaPq, + "its_pq_" + uuidPq + ) proxy invokePrivate toInitData( - validPrimaryConfig, - mappingSource, - timeSeriesUuid, - simulationStart + metaInformation, + simulationStart, + validPrimaryConfig ) match { case Success( CsvInitPrimaryServiceStateData( @@ -403,11 +386,11 @@ class PrimaryServiceProxySpec timePattern ) ) => - actualTimeSeriesUuid shouldBe timeSeriesUuid + actualTimeSeriesUuid shouldBe uuidPq actualSimulationStart shouldBe simulationStart actualCsvSep shouldBe csvSep directoryPath shouldBe baseDirectoryPath - filePath shouldBe "its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26" + filePath shouldBe metaInformation.getFullFilePath classOf[FileNamingStrategy].isAssignableFrom( fileNamingStrategy.getClass ) shouldBe true @@ -429,13 +412,10 @@ class PrimaryServiceProxySpec None, None ) - proxy invokePrivate initializeWorker( - ColumnScheme.APPARENT_POWER, - timeSeriesUuid, + metaPq, simulationStart, - maliciousPrimaryConfig, - mappingSource + maliciousPrimaryConfig ) match { case Failure(exception) => /* Check the exception */ @@ -468,35 +448,32 @@ class PrimaryServiceProxySpec TestActorRef(new PrimaryServiceProxy(scheduler.ref, simulationStart) { override protected def classToWorkerRef[V <: Value]( valueClass: Class[V], - timeSeriesUuid: String, - simulationStart: ZonedDateTime + timeSeriesUuid: String ): ActorRef = testProbe.ref // needs to be overwritten as to make it available to the private method tester @SuppressWarnings(Array("NoOpOverride")) override protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = super.initializeWorker( - columnScheme, - timeSeriesUuid, + metaInformation, simulationStart, - primaryConfig, - mappingSource + primaryConfig ) }) val fakeProxy: PrimaryServiceProxy = fakeProxyRef.underlyingActor + val metaInformation = new CsvIndividualTimeSeriesMetaInformation( + metaPq, + "its_pq_" + uuidPq + ) fakeProxy invokePrivate initializeWorker( - ColumnScheme.APPARENT_POWER, - timeSeriesUuid, + metaInformation, simulationStart, - validPrimaryConfig, - mappingSource + validPrimaryConfig ) match { case Success(workerRef) => /* Check, if expected init message has been sent */ @@ -515,11 +492,11 @@ class PrimaryServiceProxySpec ), actorToBeScheduled ) => - actualTimeSeriesUuid shouldBe timeSeriesUuid + actualTimeSeriesUuid shouldBe uuidPq actualSimulationStart shouldBe simulationStart actualCsvSep shouldBe csvSep directoryPath shouldBe baseDirectoryPath - filePath shouldBe "its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26" + filePath shouldBe metaInformation.getFullFilePath classOf[FileNamingStrategy].isAssignableFrom( fileNamingStrategy.getClass ) shouldBe true @@ -556,7 +533,7 @@ class PrimaryServiceProxySpec "work otherwise" in { proxy invokePrivate updateStateData( proxyStateData, - timeSeriesUuid, + uuidPq, self ) match { case PrimaryServiceStateData( @@ -568,16 +545,8 @@ class PrimaryServiceProxySpec ) => modelToTimeSeries shouldBe proxyStateData.modelToTimeSeries timeSeriesToSourceRef shouldBe Map( - UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") -> SourceRef( - ColumnScheme.ACTIVE_POWER, - None - ), - UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") -> SourceRef( - ColumnScheme.APPARENT_POWER, - Some(self) - ) + uuidP -> SourceRef(metaP, None), + uuidPq -> SourceRef(metaPq, Some(self)) ) simulationStart shouldBe proxyStateData.simulationStart primaryConfig shouldBe proxyStateData.primaryConfig @@ -595,7 +564,7 @@ class PrimaryServiceProxySpec proxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, maliciousStateData, self ) @@ -605,13 +574,13 @@ class PrimaryServiceProxySpec "forward the registration request, if worker is already known" in { val adaptedStateData = proxyStateData.copy( timeSeriesToSourceRef = Map( - timeSeriesUuid -> SourceRef(ColumnScheme.APPARENT_POWER, Some(self)) + uuidPq -> SourceRef(metaPq, Some(self)) ) ) proxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, adaptedStateData, self ) @@ -630,7 +599,7 @@ class PrimaryServiceProxySpec proxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, maliciousStateData, self ) @@ -643,11 +612,9 @@ class PrimaryServiceProxySpec val fakeProxyRef = TestActorRef(new PrimaryServiceProxy(self, simulationStart) { override protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = Success(probe.ref) // needs to be overwritten as to make it available to the private method tester @@ -669,7 +636,7 @@ class PrimaryServiceProxySpec fakeProxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, proxyStateData, self ) @@ -693,11 +660,9 @@ class PrimaryServiceProxySpec val fakeProxyRef = TestActorRef(new PrimaryServiceProxy(self, simulationStart) { override protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = Success(probe.ref) }) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala index 53556678fa..2a7e0d816c 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala @@ -37,6 +37,7 @@ import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ } import edu.ie3.simona.service.primary.PrimaryServiceWorkerSpec.WrongInitPrimaryServiceStateData import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.common.input.TimeSeriesTestData import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.collection.immutable.SortedDistinctSeq @@ -56,7 +57,8 @@ class PrimaryServiceWorkerSpec |akka.loglevel="OFF" """.stripMargin) ) - ) { + ) + with TimeSeriesTestData { // this works both on Windows and Unix systems val baseDirectoryPath: String = Paths .get( @@ -68,15 +70,12 @@ class PrimaryServiceWorkerSpec ) .toString - private val simulationStart = - TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00") - val validInitData: CsvInitPrimaryServiceStateData = CsvInitPrimaryServiceStateData( - timeSeriesUuid = UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), + timeSeriesUuid = uuidP, csvSep = ";", directoryPath = baseDirectoryPath, - filePath = "its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5", + filePath = "its_p_" + uuidP, fileNamingStrategy = new FileNamingStrategy(), simulationStart = TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00"), @@ -88,8 +87,7 @@ class PrimaryServiceWorkerSpec TestActorRef( new PrimaryServiceWorker[PValue]( self, - classOf[PValue], - simulationStart + classOf[PValue] ) ) val service = serviceRef.underlyingActor @@ -106,13 +104,12 @@ class PrimaryServiceWorkerSpec "fail, if pointed to the wrong file" in { val maliciousInitData = CsvInitPrimaryServiceStateData( - timeSeriesUuid = - UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), + timeSeriesUuid = uuidPq, simulationStart = TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00"), csvSep = ";", directoryPath = baseDirectoryPath, - filePath = "its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26", + filePath = "its_pq_" + uuidPq, fileNamingStrategy = new FileNamingStrategy(), timePattern = TimeUtil.withDefaults.getDtfPattern ) @@ -200,8 +197,8 @@ class PrimaryServiceWorkerSpec ";", baseDirectoryPath, new FileNamingStrategy(), - UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), - "its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5", + uuidP, + "its_p_" + uuidP, classOf[PValue], new TimeBasedSimpleValueFactory[PValue](classOf[PValue]) ), diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala index 77536c1442..d171975881 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -6,11 +6,12 @@ package edu.ie3.simona.service.primary -import akka.actor.{ActorRef, ActorSystem} +import akka.actor.ActorSystem import akka.testkit.{TestActorRef, TestProbe} import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.models.value.{HeatAndSValue, PValue, Value} +import edu.ie3.datamodel.io.naming.DatabaseNamingStrategy +import edu.ie3.datamodel.models.value.{HeatAndSValue, PValue} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ActivePower, ApparentPowerAndHeat @@ -32,15 +33,11 @@ import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ SqlInitPrimaryServiceStateData } import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.common.input.TimeSeriesTestData +import edu.ie3.simona.test.helper.TestContainerHelper import edu.ie3.util.TimeUtil import org.scalatest.BeforeAndAfterAll import org.scalatest.prop.TableDrivenPropertyChecks -import org.testcontainers.utility.MountableFile - -import java.nio.file.Paths -import java.util.UUID -import scala.language.postfixOps -import scala.reflect.ClassTag class PrimaryServiceWorkerSqlIT extends AgentSpec( @@ -54,7 +51,9 @@ class PrimaryServiceWorkerSqlIT ) with ForAllTestContainer with BeforeAndAfterAll - with TableDrivenPropertyChecks { + with TableDrivenPropertyChecks + with TimeSeriesTestData + with TestContainerHelper { override val container: PostgreSQLContainer = PostgreSQLContainer( "postgres:11.14" @@ -65,22 +64,12 @@ class PrimaryServiceWorkerSqlIT private val schemaName = "public" - private val uuidP = UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") - private val uuidPhq = UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047") - - private val tableNameP = s"its_p_$uuidP" - private val tableNamePhq = s"its_pqh_$uuidPhq" - override protected def beforeAll(): Unit = { - val url = getClass.getResource("timeseries/") - url shouldNot be(null) - val path = Paths.get(url.toURI) - // Copy sql import scripts into docker - val sqlImportFile = MountableFile.forHostPath(path) + val sqlImportFile = getMountableFile("timeseries/") container.copyFileToContainer(sqlImportFile, "/home/") - Iterable(s"$tableNameP.sql", s"$tableNamePhq.sql") + Iterable("time_series_p.sql", "time_series_pqh.sql") .foreach { file => val res = container.execInContainer("psql", "-Utest", "-f/home/" + file) res.getStderr shouldBe empty @@ -92,42 +81,34 @@ class PrimaryServiceWorkerSqlIT container.close() } - // asInstanceOf throws ClassCastException if cast fails, thus this is safe here - @SuppressWarnings(Array("AsInstanceOf")) - private def getServiceActor[T <: Value]( - scheduler: ActorRef - )(implicit tag: ClassTag[T]): PrimaryServiceWorker[T] = { - new PrimaryServiceWorker[T]( - scheduler, - tag.runtimeClass.asInstanceOf[Class[T]], - simulationStart - ) - } - "A primary service actor with SQL source" should { "initialize and send out data when activated" in { + val scheduler = TestProbe("scheduler") val cases = Table( ( - "getService", + "service", "uuid", - "tableName", "firstTick", "dataValueClass", "maybeNextTick" ), ( - getServiceActor[HeatAndSValue](_), - uuidPhq, - tableNamePhq, + PrimaryServiceWorker.props( + scheduler.ref, + classOf[HeatAndSValue] + ), + uuidPqh, 0L, classOf[ApparentPowerAndHeat], Some(900L) ), ( - getServiceActor[PValue](_), + PrimaryServiceWorker.props( + scheduler.ref, + classOf[PValue] + ), uuidP, - tableNameP, 0L, classOf[ActivePower], Some(900L) @@ -136,31 +117,25 @@ class PrimaryServiceWorkerSqlIT forAll(cases) { ( - getService, + service, uuid, - tableName, firstTick, dataValueClass, maybeNextTick ) => - val scheduler = TestProbe("scheduler") - - val serviceRef = - TestActorRef( - getService(scheduler.ref) - ) + val serviceRef = TestActorRef(service) val initData = SqlInitPrimaryServiceStateData( + uuid, + simulationStart, SqlParams( jdbcUrl = container.jdbcUrl, userName = container.username, password = container.password, schemaName = schemaName, - tableName = tableName, timePattern = "yyyy-MM-dd HH:mm:ss" ), - uuid, - simulationStart + new DatabaseNamingStrategy() ) val triggerId1 = 1L @@ -213,6 +188,8 @@ class PrimaryServiceWorkerSqlIT dataMsg.tick shouldBe firstTick dataMsg.data.getClass shouldBe dataValueClass dataMsg.nextDataTick shouldBe maybeNextTick + + scheduler.expectNoMessage() } } } diff --git a/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala new file mode 100644 index 0000000000..64d674e3ad --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala @@ -0,0 +1,39 @@ +/* + * © 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.input + +import edu.ie3.datamodel.io.naming.timeseries.{ + ColumnScheme, + IndividualTimeSeriesMetaInformation +} + +import java.util.UUID + +trait TimeSeriesTestData { + protected val uuidP: UUID = + UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") + protected val uuidPq: UUID = + UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") + protected val uuidPqh: UUID = + UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047") + + protected val metaP: IndividualTimeSeriesMetaInformation = + new IndividualTimeSeriesMetaInformation( + uuidP, + ColumnScheme.ACTIVE_POWER + ) + protected val metaPq: IndividualTimeSeriesMetaInformation = + new IndividualTimeSeriesMetaInformation( + uuidPq, + ColumnScheme.APPARENT_POWER + ) + protected val metaPqh: IndividualTimeSeriesMetaInformation = + new IndividualTimeSeriesMetaInformation( + uuidPqh, + ColumnScheme.APPARENT_POWER_AND_HEAT_DEMAND + ) +} diff --git a/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala b/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala new file mode 100644 index 0000000000..8bb7ff7ad6 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala @@ -0,0 +1,36 @@ +/* + * © 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.helper + +import akka.testkit.TestException +import org.testcontainers.utility.MountableFile + +import java.nio.file.Paths + +trait TestContainerHelper { + + /** Retrieve resource with the class' resource loader. In contrast to + * [[org.testcontainers.utility.MountableFile#forClasspathResource(java.lang.String, java.lang.Integer)]], + * this also works with paths relative to the current class (i.e. without + * leading '/'). + * @param resource + * the resource directory or file path + * @return + * a MountableFile to use with test containers + */ + def getMountableFile(resource: String): MountableFile = { + def url = getClass.getResource(resource) + if (url == null) { + throw TestException( + "Resource '" + resource + "' was not found from " + getClass.toString + ) + } + def path = Paths.get(url.toURI) + + MountableFile.forHostPath(path) + } +} From 0e885d5ee2cfea3f21e26505adbf061e21f9f545 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 16 Mar 2022 19:25:45 +0100 Subject: [PATCH 49/73] Updating testcontainers version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4f6c24d20a..a5045c941f 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext { tscfgVersion = '0.9.997' scapegoatVersion = '1.4.12' - testContainerVersion = '0.39.12' + testContainerVersion = '0.40.3' scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins } From 880697fdec0feb0c41addd141be6089839d96f4f Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 16 Mar 2022 19:44:29 +0100 Subject: [PATCH 50/73] Improving getMountableFile as stream of Option --- .../simona/test/helper/TestContainerHelper.scala | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala b/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala index 8bb7ff7ad6..f58786aa6d 100644 --- a/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala +++ b/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala @@ -23,14 +23,13 @@ trait TestContainerHelper { * a MountableFile to use with test containers */ def getMountableFile(resource: String): MountableFile = { - def url = getClass.getResource(resource) - if (url == null) { - throw TestException( - "Resource '" + resource + "' was not found from " + getClass.toString + Option(getClass.getResource(resource)) + .map(url => Paths.get(url.toURI)) + .map(MountableFile.forHostPath) + .getOrElse( + throw TestException( + "Resource '" + resource + "' was not found from " + getClass.toString + ) ) - } - def path = Paths.get(url.toURI) - - MountableFile.forHostPath(path) } } From 962e87dd65154afc473c723cc2ca26fd204e3834 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 21 Mar 2022 17:26:30 +0100 Subject: [PATCH 51/73] Adapted/removed comments --- .../edu/ie3/simona/service/primary/PrimaryServiceProxy.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index 3e6d213b07..f46af75f53 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -382,9 +382,8 @@ case class PrimaryServiceProxy( None, None ) => - /* The mapping and actual data sources are from csv. At first, get the file name of the file to read. */ + /* The actual data sources are from csv. Meta information have to match */ metaInformation match { - /* Time series meta information could be successfully obtained */ case csvMetaData: CsvIndividualTimeSeriesMetaInformation => Success( CsvInitPrimaryServiceStateData( From dbc797d29e9928edb4171268bef5a6b8be4aab68 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 21 Mar 2022 17:26:52 +0100 Subject: [PATCH 52/73] Testing first data values with IT --- .../primary/PrimaryServiceWorkerSqlIT.scala | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala index d171975881..1609809225 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -11,6 +11,7 @@ import akka.testkit.{TestActorRef, TestProbe} import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.io.naming.DatabaseNamingStrategy +import edu.ie3.datamodel.models.StandardUnits import edu.ie3.datamodel.models.value.{HeatAndSValue, PValue} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ActivePower, @@ -38,6 +39,7 @@ import edu.ie3.simona.test.helper.TestContainerHelper import edu.ie3.util.TimeUtil import org.scalatest.BeforeAndAfterAll import org.scalatest.prop.TableDrivenPropertyChecks +import tech.units.indriya.quantity.Quantities class PrimaryServiceWorkerSqlIT extends AgentSpec( @@ -56,7 +58,7 @@ class PrimaryServiceWorkerSqlIT with TestContainerHelper { override val container: PostgreSQLContainer = PostgreSQLContainer( - "postgres:11.14" + "postgres:14.2" ) private val simulationStart = @@ -90,7 +92,7 @@ class PrimaryServiceWorkerSqlIT "service", "uuid", "firstTick", - "dataValueClass", + "firstData", "maybeNextTick" ), ( @@ -100,7 +102,11 @@ class PrimaryServiceWorkerSqlIT ), uuidPqh, 0L, - classOf[ApparentPowerAndHeat], + ApparentPowerAndHeat( + Quantities.getQuantity(1000.0d, StandardUnits.ACTIVE_POWER_IN), + Quantities.getQuantity(329.0d, StandardUnits.REACTIVE_POWER_IN), + Quantities.getQuantity(8000.0, StandardUnits.HEAT_DEMAND_PROFILE) + ), Some(900L) ), ( @@ -110,7 +116,9 @@ class PrimaryServiceWorkerSqlIT ), uuidP, 0L, - classOf[ActivePower], + ActivePower( + Quantities.getQuantity(1000.0d, StandardUnits.ACTIVE_POWER_IN) + ), Some(900L) ) ) @@ -120,7 +128,7 @@ class PrimaryServiceWorkerSqlIT service, uuid, firstTick, - dataValueClass, + firstData, maybeNextTick ) => val serviceRef = TestActorRef(service) @@ -186,7 +194,7 @@ class PrimaryServiceWorkerSqlIT val dataMsg = participant.expectMsgType[ProvidePrimaryDataMessage] dataMsg.tick shouldBe firstTick - dataMsg.data.getClass shouldBe dataValueClass + dataMsg.data shouldBe firstData dataMsg.nextDataTick shouldBe maybeNextTick scheduler.expectNoMessage() From 0cc29d92bb1bca9a6bde0b717edd85755755fcba Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 21 Mar 2022 17:57:42 +0100 Subject: [PATCH 53/73] Introducing SQL sources to PrimaryServiceProxy --- CHANGELOG.md | 2 +- .../service/primary/PrimaryServiceProxy.scala | 110 ++++++++++++------ .../primary/PrimaryServiceProxySpec.scala | 18 +-- 3 files changed, 86 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb3726570..3e1f6b57c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Implement SQL source for primary data [#34](https://github.com/ie3-institute/simona/issues/34) +- Implement SQL source for primary data [#34](https://github.com/ie3-institute/simona/issues/34), [#101](https://github.com/ie3-institute/simona/issues/101) ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index f46af75f53..f3acde1e13 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -7,17 +7,32 @@ package edu.ie3.simona.service.primary import akka.actor.{Actor, ActorRef, PoisonPill, Props} +import edu.ie3.datamodel.io.connectors.SqlConnector +import edu.ie3.datamodel.io.naming.{ + DatabaseNamingStrategy, + EntityPersistenceNamingStrategy, + FileNamingStrategy +} import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation -import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation -import edu.ie3.datamodel.io.source.TimeSeriesMappingSource +import edu.ie3.datamodel.io.source.{ + TimeSeriesMappingSource, + TimeSeriesTypeSource +} import edu.ie3.datamodel.io.source.csv.{ CsvTimeSeriesMappingSource, CsvTimeSeriesTypeSource } +import edu.ie3.datamodel.io.source.sql.{ + SqlTimeSeriesMappingSource, + SqlTimeSeriesTypeSource +} import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.CsvParams +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.{ + CsvParams, + SqlParams +} import edu.ie3.simona.config.SimonaConfig.Simona.Input.{ Primary => PrimaryConfig } @@ -130,29 +145,12 @@ case class PrimaryServiceProxy( private def prepareStateData( primaryConfig: PrimaryConfig, simulationStart: ZonedDateTime - ): Try[PrimaryServiceStateData] = - Seq( - primaryConfig.sqlParams, - primaryConfig.influxDb1xParams, - primaryConfig.csvParams, - primaryConfig.couchbaseParams - ).filter(_.isDefined).flatten.headOption match { - case Some(CsvParams(csvSep, folderPath, _)) => - // TODO: Configurable file naming strategy - val fileNamingStrategy = new FileNamingStrategy() - val mappingSource = new CsvTimeSeriesMappingSource( - csvSep, - folderPath, - fileNamingStrategy - ) - val typeSource = new CsvTimeSeriesTypeSource( - csvSep, - folderPath, - fileNamingStrategy - ) + ): Try[PrimaryServiceStateData] = { + createSources(primaryConfig).map { + case (mappingSource, metaInformationSource) => val modelToTimeSeries = mappingSource.getMapping.asScala.toMap val timeSeriesMetaInformation = - typeSource.getTimeSeriesMetaInformation.asScala.toMap + metaInformationSource.getTimeSeriesMetaInformation.asScala.toMap val timeSeriesToSourceRef = modelToTimeSeries.values .to(LazyList) @@ -161,11 +159,10 @@ case class PrimaryServiceProxy( timeSeriesMetaInformation .get(timeSeriesUuid) match { case Some(metaInformation) => - val columnScheme = metaInformation.getColumnScheme /* Only register those entries, that meet the supported column schemes */ when( PrimaryServiceWorker.supportedColumnSchemes - .contains(columnScheme) + .contains(metaInformation.getColumnScheme) ) { timeSeriesUuid -> SourceRef(metaInformation, None) } @@ -178,16 +175,58 @@ case class PrimaryServiceProxy( } } .toMap + PrimaryServiceStateData( + modelToTimeSeries, + timeSeriesToSourceRef, + simulationStart, + primaryConfig, + mappingSource + ) + } + } + + private def createSources( + primaryConfig: PrimaryConfig + ): Try[(TimeSeriesMappingSource, TimeSeriesTypeSource)] = { + Seq( + primaryConfig.sqlParams, + primaryConfig.influxDb1xParams, + primaryConfig.csvParams, + primaryConfig.couchbaseParams + ).filter(_.isDefined).flatten.headOption match { + case Some(CsvParams(csvSep, folderPath, _)) => + // TODO: Configurable file naming strategy + val fileNamingStrategy = new FileNamingStrategy() Success( - PrimaryServiceStateData( - modelToTimeSeries, - timeSeriesToSourceRef, - simulationStart, - primaryConfig, - mappingSource + new CsvTimeSeriesMappingSource( + csvSep, + folderPath, + fileNamingStrategy + ), + new CsvTimeSeriesTypeSource( + csvSep, + folderPath, + fileNamingStrategy + ) + ) + case Some(sqlParams: SqlParams) => + val sqlConnector = new SqlConnector( + sqlParams.jdbcUrl, + sqlParams.userName, + sqlParams.password + ) + Success( + new SqlTimeSeriesMappingSource( + sqlConnector, + sqlParams.schemaName, + new EntityPersistenceNamingStrategy() + ), + new SqlTimeSeriesTypeSource( + sqlConnector, + sqlParams.schemaName, + new DatabaseNamingStrategy() ) ) - case Some(x) => Failure( new IllegalArgumentException( @@ -201,6 +240,7 @@ case class PrimaryServiceProxy( ) ) } + } /** Message handling, if the actor has been initialized already. This method * basically handles registration requests, checks, if pre-calculated, @@ -517,7 +557,7 @@ object PrimaryServiceProxy { } val supportedSources = - Set("csv") + Set("csv", "sql") val sourceConfigs = Seq( primaryConfig.couchbaseParams, @@ -541,6 +581,8 @@ object PrimaryServiceProxy { // note: if inheritance is supported by tscfg, // the following method should be called for all different supported sources! checkTimePattern(csvParams.timePattern) + case Some(sqlParams: SimonaConfig.Simona.Input.Primary.SqlParams) => + checkTimePattern(sqlParams.timePattern) case Some(x) => throw new InvalidConfigParameterException( s"Invalid configuration '$x' for a time series source.\nAvailable types:\n\t${supportedSources diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index 8ab191fc3b..f6ddcc4589 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -139,7 +139,7 @@ class PrimaryServiceProxySpec val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "2 time series source types defined. Please define only one type!\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "2 time series source types defined. Please define only one type!\nAvailable types:\n\tcsv\n\tsql" } "lead to complaining about too few source definitions" in { @@ -153,7 +153,7 @@ class PrimaryServiceProxySpec val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "No time series source type defined. Please define exactly one type!\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "No time series source type defined. Please define exactly one type!\nAvailable types:\n\tcsv\n\tsql" } "not let couchbase parameters pass for mapping configuration" in { @@ -167,7 +167,7 @@ class PrimaryServiceProxySpec val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "Invalid configuration 'CouchbaseParams(,,,,,,)' for a time series source.\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "Invalid configuration 'CouchbaseParams(,,,,,,)' for a time series source.\nAvailable types:\n\tcsv\n\tsql" } "let csv parameters pass for mapping configuration" in { @@ -194,7 +194,7 @@ class PrimaryServiceProxySpec val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "Invalid configuration 'InfluxDb1xParams(,0,,)' for a time series source.\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "Invalid configuration 'InfluxDb1xParams(,0,,)' for a time series source.\nAvailable types:\n\tcsv\n\tsql" } "not let sql parameters pass for mapping configuration" in { @@ -211,7 +211,7 @@ class PrimaryServiceProxySpec exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,)' for a time series source.\nAvailable types:\n\tcsv" } - "fails on invalid time pattern" in { + "fails on invalid time pattern with csv" in { val invalidTimePatternConfig = PrimaryConfig( None, Some(CsvParams("", "", "xYz")), @@ -226,7 +226,7 @@ class PrimaryServiceProxySpec } - "succeeds on valid time pattern" in { + "succeeds on valid time pattern with csv" in { val validTimePatternConfig = PrimaryConfig( None, Some(CsvParams("", "", "yyyy-MM-dd'T'HH:mm'Z[UTC]'")), @@ -273,8 +273,8 @@ class PrimaryServiceProxySpec val maliciousConfig = PrimaryConfig( None, None, - None, - Some(SqlParams("", "", "", "", "")) + Some(InfluxDb1xParams("", -1, "", "")), + None ) proxy invokePrivate prepareStateData( @@ -285,7 +285,7 @@ class PrimaryServiceProxySpec fail("Building state data with missing config should fail") case Failure(exception) => exception.getClass shouldBe classOf[IllegalArgumentException] - exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,)'" + exception.getMessage shouldBe "Unsupported config for mapping source: 'InfluxDb1xParams(,-1,,)'" } } From 3a84d1a805a717dc5a9f4668c0caa60befaaba65 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 21 Mar 2022 18:30:37 +0100 Subject: [PATCH 54/73] Removing obsolete test --- .../service/primary/PrimaryServiceProxySpec.scala | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index f6ddcc4589..c8a708af7a 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -197,20 +197,6 @@ class PrimaryServiceProxySpec exception.getMessage shouldBe "Invalid configuration 'InfluxDb1xParams(,0,,)' for a time series source.\nAvailable types:\n\tcsv\n\tsql" } - "not let sql parameters pass for mapping configuration" in { - val maliciousConfig = PrimaryConfig( - None, - None, - None, - Some(SqlParams("", "", "", "", "")) - ) - - val exception = intercept[InvalidConfigParameterException]( - PrimaryServiceProxy.checkConfig(maliciousConfig) - ) - exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,)' for a time series source.\nAvailable types:\n\tcsv" - } - "fails on invalid time pattern with csv" in { val invalidTimePatternConfig = PrimaryConfig( None, From 78c174dc307973527fd5ad58fe6e78efd6c6219e Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Mar 2022 18:04:10 +0100 Subject: [PATCH 55/73] Added init data creation for worker --- .../service/primary/PrimaryServiceProxy.scala | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index f3acde1e13..de9c367f45 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -61,7 +61,8 @@ import edu.ie3.simona.service.primary.PrimaryServiceProxy.{ } import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ CsvInitPrimaryServiceStateData, - InitPrimaryServiceStateData + InitPrimaryServiceStateData, + SqlInitPrimaryServiceStateData } import java.text.SimpleDateFormat @@ -195,7 +196,6 @@ case class PrimaryServiceProxy( primaryConfig.couchbaseParams ).filter(_.isDefined).flatten.headOption match { case Some(CsvParams(csvSep, folderPath, _)) => - // TODO: Configurable file naming strategy val fileNamingStrategy = new FileNamingStrategy() Success( new CsvTimeSeriesMappingSource( @@ -443,6 +443,22 @@ case class PrimaryServiceProxy( ) ) } + + case PrimaryConfig( + None, + None, + None, + Some(sqlParams: SqlParams) + ) => + Success( + SqlInitPrimaryServiceStateData( + metaInformation.getUuid, + simulationStart, + sqlParams, + new DatabaseNamingStrategy() + ) + ) + case unsupported => Failure( new InitializationException( From 5467b7ff5ef7e3404906aba429b7d60a8c93f9d0 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Mar 2022 18:04:49 +0100 Subject: [PATCH 56/73] Updating SQLs from PSDM --- .../ie3/simona/service/primary/timeseries/time_series_p.sql | 4 +++- .../ie3/simona/service/primary/timeseries/time_series_pqh.sql | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql index 79beaf5e70..8f0c48c294 100644 --- a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql @@ -10,7 +10,9 @@ CREATE TABLE public.time_series_p CREATE INDEX time_series_p_series_id ON time_series_p USING hash (time_series); -CREATE UNIQUE INDEX time_series_p_series_time ON time_series_p USING btree (time_series, time); +-- Order of columns is important when using btree: https://www.postgresql.org/docs/14/indexes-multicolumn.html +-- time_series at first since we at most use an equality constraint on time_series and a range query on time +CREATE UNIQUE INDEX time_series_p_series_time ON time_series_p USING btree (time_series, time); INSERT INTO public.time_series_p (uuid, time_series, time, p) diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql index 8bd3a48908..54f6513579 100644 --- a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql @@ -12,7 +12,9 @@ CREATE TABLE public.time_series_pqh CREATE INDEX time_series_pqh_series_id ON time_series_pqh USING hash (time_series); -CREATE UNIQUE INDEX time_series_pqh_series_time ON time_series_pqh USING btree (time_series, time); +-- Order of columns is important when using btree: https://www.postgresql.org/docs/14/indexes-multicolumn.html +-- time_series at first since we at most use an equality constraint on time_series and a range query on time +CREATE UNIQUE INDEX time_series_pqh_series_time ON time_series_pqh USING btree (time_series, time); INSERT INTO public.time_series_pqh (uuid, time_series, time, p, q, heat_demand) From f8aa8c6b16c2e9c6d9a299730f1ad8a1521f9b1a Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Mar 2022 18:06:48 +0100 Subject: [PATCH 57/73] Introducing test for PrimaryServiceProxy with SQL source --- .../timeseries/time_series_mapping.sql | 15 ++ .../primary/PrimaryServiceProxySqlIT.scala | 199 ++++++++++++++++++ .../primary/PrimaryServiceWorkerSqlIT.scala | 4 +- 3 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_mapping.sql create mode 100644 src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_mapping.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_mapping.sql new file mode 100644 index 0000000000..b3921f442c --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_mapping.sql @@ -0,0 +1,15 @@ +CREATE TABLE public.time_series_mapping +( + uuid uuid PRIMARY KEY, + participant uuid, + time_series uuid +) + WITHOUT OIDS + TABLESPACE pg_default; + +INSERT INTO + public.time_series_mapping (uuid, participant, time_series) +VALUES +('58167015-d760-4f90-8109-f2ebd94cda91', 'b86e95b0-e579-4a80-a534-37c7a470a409', '9185b8c1-86ba-4a16-8dea-5ac898e8caa5'), +('9a9ebfda-dc26-4a40-b9ca-25cd42f6cc3f', 'c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8', '3fbfaa97-cff4-46d4-95ba-a95665e87c26'), +('9c1c53ea-e575-41a2-a373-a8b2d3ed2c39', '90a96daa-012b-4fea-82dc-24ba7a7ab81c', '3fbfaa97-cff4-46d4-95ba-a95665e87c26'); diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala new file mode 100644 index 0000000000..9a3f97e46f --- /dev/null +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala @@ -0,0 +1,199 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.service.primary + +import akka.actor.ActorSystem +import akka.testkit.{TestActorRef, TestProbe} +import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} +import com.typesafe.config.ConfigFactory +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + CompletionMessage, + ScheduleTriggerMessage, + TriggerWithIdMessage +} +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ + RegistrationFailedMessage, + RegistrationSuccessfulMessage +} +import edu.ie3.simona.ontology.trigger.Trigger.{ + ActivityStartTrigger, + InitializeServiceTrigger +} +import edu.ie3.simona.service.primary.PrimaryServiceProxy.InitPrimaryServiceProxyStateData +import edu.ie3.simona.service.primary.PrimaryServiceWorker.SqlInitPrimaryServiceStateData +import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.helper.TestContainerHelper +import edu.ie3.util.TimeUtil +import org.scalatest.BeforeAndAfterAll + +import java.util.UUID + +class PrimaryServiceProxySqlIT + extends AgentSpec( + ActorSystem( + "PrimaryServiceWorkerSqlIT", + ConfigFactory + .parseString(""" + |akka.loglevel="OFF" + """.stripMargin) + ) + ) + with ForAllTestContainer + with BeforeAndAfterAll + with TestContainerHelper { + + override val container: PostgreSQLContainer = PostgreSQLContainer( + "postgres:14.2" + ) + + private val simulationStart = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00") + + private val schemaName = "public" + + override protected def beforeAll(): Unit = { + // Copy sql import scripts into docker + val sqlImportFile = getMountableFile("timeseries/") + container.copyFileToContainer(sqlImportFile, "/home/") + + Iterable( + "time_series_p.sql", + "time_series_pqh.sql", + "time_series_mapping.sql" + ).foreach { file => + val res = container.execInContainer("psql", "-Utest", "-f/home/" + file) + res.getStderr shouldBe empty + } + } + + override protected def afterAll(): Unit = { + container.stop() + container.close() + } + + // function definition because postgres parameters are only available after initialization + private def sqlParams: SqlParams = SqlParams( + jdbcUrl = container.jdbcUrl, + userName = container.username, + password = container.password, + schemaName = schemaName, + timePattern = "yyyy-MM-dd HH:mm:ss" + ) + + "A primary service proxy with SQL source" should { + val scheduler = TestProbe("Scheduler") + + val proxyRef = TestActorRef( + PrimaryServiceProxy.props( + scheduler.ref, + simulationStart + ) + ) + + "initialize when given proper SQL input configs" in { + val initData = InitPrimaryServiceProxyStateData( + SimonaConfig.Simona.Input.Primary( + None, + None, + None, + sqlParams = Some(sqlParams) + ), + simulationStart + ) + + val triggerIdInit1 = 1L + + scheduler.send( + proxyRef, + TriggerWithIdMessage( + InitializeServiceTrigger(initData), + triggerIdInit1, + proxyRef + ) + ) + + scheduler.expectMsg( + CompletionMessage( + triggerIdInit1, + None + ) + ) + } + + "handle participant request correctly if participant has primary data" in { + val systemParticipantProbe = TestProbe("SystemParticipant") + + systemParticipantProbe.send( + proxyRef, + PrimaryServiceRegistrationMessage( + UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") + ) + ) + + val initTriggerMsg = scheduler.expectMsgType[ScheduleTriggerMessage] + + initTriggerMsg.trigger match { + case InitializeServiceTrigger( + sqlInit: SqlInitPrimaryServiceStateData + ) => + sqlInit.sqlParams shouldBe sqlParams + sqlInit.simulationStart shouldBe simulationStart + sqlInit.timeSeriesUuid shouldBe UUID.fromString( + "9185b8c1-86ba-4a16-8dea-5ac898e8caa5" + ) + case unexpected => fail(s"Received unexpected trigger $unexpected") + } + + val triggerIdInit2 = 2L + + // extract ref to the worker that the proxy created + val workerRef = initTriggerMsg.actorToBeScheduled + scheduler.send( + workerRef, + TriggerWithIdMessage( + initTriggerMsg.trigger, + triggerIdInit2, + workerRef + ) + ) + + scheduler.expectMsg( + CompletionMessage( + triggerIdInit2, + Some( + Seq( + ScheduleTriggerMessage( + ActivityStartTrigger(0L), + workerRef + ) + ) + ) + ) + ) + + systemParticipantProbe.expectMsg(RegistrationSuccessfulMessage(Some(0L))) + } + + "handle participant request correctly if participant does not have primary data" in { + val systemParticipantProbe = TestProbe("SystemParticipant") + + systemParticipantProbe.send( + proxyRef, + PrimaryServiceRegistrationMessage( + UUID.fromString("db958617-e49d-44d3-b546-5f7b62776afd") + ) + ) + + scheduler.expectNoMessage() + + systemParticipantProbe.expectMsg(RegistrationFailedMessage) + } + } +} diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala index 1609809225..a11ec6ae73 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -85,7 +85,7 @@ class PrimaryServiceWorkerSqlIT "A primary service actor with SQL source" should { "initialize and send out data when activated" in { - val scheduler = TestProbe("scheduler") + val scheduler = TestProbe("Scheduler") val cases = Table( ( @@ -161,7 +161,7 @@ class PrimaryServiceWorkerSqlIT CompletionMessage( triggerId1, Some( - List( + Seq( ScheduleTriggerMessage( ActivityStartTrigger(firstTick), serviceRef From d80c00b32bf2e1cbe534e8995d05af8768080317 Mon Sep 17 00:00:00 2001 From: Daniel Feismann <98817556+danielfeismann@users.noreply.github.com> Date: Thu, 24 Mar 2022 12:51:24 +0100 Subject: [PATCH 58/73] Fix unreachable code (#171) * Adapt ResultEventListener Failure on Init Introduce ServiceInitFailed and ServiceInitResponse @SimonaSim Introduce Test checking for termination of actor * Removed Supervisorstrategy since its obsolete here * gradle spotlessapply * Pipeto myself * removed unnecessary imports * . * Improving test a tiny bit * error log * undo extra error logging Co-authored-by: Sebastian Peter --- .../event/listener/ResultEventListener.scala | 80 ++++++++++--------- .../scala/edu/ie3/simona/sim/SimonaSim.scala | 3 +- .../listener/ResultEventListenerSpec.scala | 32 +++++--- 3 files changed, 67 insertions(+), 48 deletions(-) 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 7c7b14013d..84feebd2e2 100644 --- a/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala +++ b/src/main/scala/edu/ie3/simona/event/listener/ResultEventListener.scala @@ -6,7 +6,8 @@ package edu.ie3.simona.event.listener -import akka.actor.{ActorRef, FSM, PoisonPill, Props, Stash} +import akka.actor.{ActorRef, FSM, Props, Stash, Status} +import akka.pattern.pipe import akka.stream.Materializer import edu.ie3.datamodel.io.processor.result.ResultEntityProcessor import edu.ie3.datamodel.models.result.ResultEntity @@ -23,6 +24,7 @@ import edu.ie3.simona.event.listener.ResultEventListener.{ BaseData, Init, ResultEventListenerData, + SinkResponse, Transformer3wKey, UninitializedData } @@ -59,6 +61,10 @@ object ResultEventListener extends Transformer3wResultSupport { private final case object Init + private final case class SinkResponse( + response: Map[Class[_], ResultEntitySink] + ) + /** [[ResultEventListener]] base data containing all information the listener * needs * @@ -106,29 +112,33 @@ object ResultEventListener extends Transformer3wResultSupport { case _: ResultSinkType.Csv => eventClassesToConsider .map(resultClass => { - val fileName = - resultFileHierarchy.rawOutputDataFilePaths.getOrElse( - resultClass, - throw 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}" - ) - ) - if (fileName.endsWith(".csv") || fileName.endsWith(".csv.gz")) { - val sink = - ResultEntityCsvSink( - fileName.replace(".gz", ""), - new ResultEntityProcessor(resultClass), - fileName.endsWith(".gz") + 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}" + ) ) - sink.map((resultClass, _)) - } else { - throw new ProcessResultEventException( - s"Invalid output file format for file $fileName provided. Currently only '.csv' or '.csv.gz' is supported!" ) - } + .flatMap { fileName => + if (fileName.endsWith(".csv") || fileName.endsWith(".csv.gz")) { + ResultEntityCsvSink( + fileName.replace(".gz", ""), + new ResultEntityProcessor(resultClass), + fileName.endsWith(".gz") + ).map((resultClass, _)) + } else { + Future( + throw new ProcessResultEventException( + s"Invalid output file format for file $fileName provided. Currently only '.csv' or '.csv.gz' is supported!" + ) + ) + } + } }) - case ResultSinkType.InfluxDb1x(url, database, scenario) => // creates one connection per result entity that should be processed eventClassesToConsider @@ -287,10 +297,6 @@ class ResultEventListener( stash() stay() - case Event(baseData: BaseData, UninitializedData) => - unstashAll() - goto(Idle) using baseData - case Event(Init, _) => Future .sequence( @@ -299,18 +305,20 @@ class ResultEventListener( resultFileHierarchy ) ) - .onComplete { - case Failure(exception) => - throw new InitializationException( - "Cannot initialize result sinks!" - ).initCause(exception) - self ! PoisonPill - case Success(classToSink) => - log.debug("Initialization complete!") - supervisor ! ServiceInitComplete - self ! BaseData(classToSink.toMap) - } + .map(result => SinkResponse(result.toMap)) + .pipeTo(self) stay() + + case Event(SinkResponse(classToSink), _) => + // Sink Initialization succeeded + log.debug("Initialization complete!") + supervisor ! ServiceInitComplete + + unstashAll() + goto(Idle) using BaseData(classToSink) + + case Event(Status.Failure(ex), _) => + throw new InitializationException("Unable to setup SimonaSim.", ex) } when(Idle) { diff --git a/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala b/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala index 6626241d66..d8d06d53c2 100644 --- a/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala +++ b/src/main/scala/edu/ie3/simona/sim/SimonaSim.scala @@ -11,7 +11,6 @@ import akka.actor.{ Actor, ActorRef, AllForOneStrategy, - PoisonPill, Props, Stash, SupervisorStrategy, @@ -37,8 +36,8 @@ import edu.ie3.simona.sim.SimonaSim.{ import edu.ie3.simona.sim.setup.{ExtSimSetupData, SimonaSetup} import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Await, Future} import scala.concurrent.duration.{DurationInt, FiniteDuration} +import scala.concurrent.{Await, Future} import scala.language.postfixOps /** Main entrance point to a simona simulation as top level actor. This actors 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 60daa65d82..8a95b4fa64 100644 --- a/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala @@ -10,7 +10,7 @@ import java.io.{File, FileInputStream} import java.util.zip.GZIPInputStream import akka.actor.ActorSystem import akka.stream.Materializer -import akka.testkit.{ImplicitSender, TestFSMRef, TestKit, TestProbe} +import akka.testkit.{TestFSMRef, TestProbe} import com.typesafe.config.ConfigFactory import edu.ie3.datamodel.models.result.connector.{ LineResult, @@ -25,11 +25,7 @@ import edu.ie3.simona.event.ResultEvent.{ ParticipantResultEvent, PowerFlowResultEvent } -import edu.ie3.simona.io.result.{ - ResultEntityCsvSink, - ResultEntitySink, - ResultSinkType -} +import edu.ie3.simona.io.result.{ResultEntitySink, ResultSinkType} import edu.ie3.simona.test.common.result.PowerFlowResultData import edu.ie3.simona.test.common.{AgentSpec, IOTestCommons, UnitSpec} import edu.ie3.simona.util.ResultFileHierarchy @@ -141,11 +137,27 @@ class ResultEventListenerSpec assert(outputFile.exists) assert(outputFile.isFile) } + + "check if actor dies when it should die" in { + val fileHierarchy = resultFileHierarchy(2, ".ttt") + val testProbe = TestProbe() + val listener = testProbe.childActorOf( + ResultEventListener.props( + Set(classOf[Transformer3WResult]), + fileHierarchy, + testProbe.ref + ) + ) + + testProbe watch listener + testProbe expectTerminated (listener, 2 seconds) + + } } "handling ordinary results" should { "process a valid participants result correctly" in { - val specificOutputFileHierarchy = resultFileHierarchy(2, ".csv") + val specificOutputFileHierarchy = resultFileHierarchy(3, ".csv") val listenerRef = system.actorOf( ResultEventListener @@ -192,7 +204,7 @@ class ResultEventListenerSpec } "process a valid power flow result correctly" in { - val specificOutputFileHierarchy = resultFileHierarchy(3, ".csv") + val specificOutputFileHierarchy = resultFileHierarchy(4, ".csv") val listenerRef = system.actorOf( ResultEventListener .props( @@ -280,7 +292,7 @@ class ResultEventListenerSpec PrivateMethod[Map[Transformer3wKey, AggregatedTransformer3wResult]]( Symbol("registerPartialTransformer3wResult") ) - val fileHierarchy = resultFileHierarchy(4, ".csv") + val fileHierarchy = resultFileHierarchy(5, ".csv") val listener = TestFSMRef( new ResultEventListener( Set(classOf[Transformer3WResult]), @@ -510,7 +522,7 @@ class ResultEventListenerSpec "shutting down" should { "shutdown and compress the data when requested to do so without any errors" in { - val specificOutputFileHierarchy = resultFileHierarchy(5, ".csv.gz") + val specificOutputFileHierarchy = resultFileHierarchy(6, ".csv.gz") val listenerRef = system.actorOf( ResultEventListener .props( From b39fd31ba8670bba197ffb6a8592f45368850978 Mon Sep 17 00:00:00 2001 From: Kittl Date: Thu, 24 Mar 2022 14:53:25 +0100 Subject: [PATCH 59/73] Addressing reviewer's comments --- .../participant/pv/PVAgentFundamentals.scala | 4 +-- .../messages/services/WeatherMessage.scala | 16 +++++++++--- .../service/weather/WeatherSource.scala | 4 +-- .../weather/WeatherSourceWrapper.scala | 26 +++++++++---------- .../edu/ie3/util/scala/DoubleUtils.scala | 6 +++-- .../PVAgentModelCalculationSpec.scala | 8 +++--- .../weather/SampleWeatherSourceSpec.scala | 8 +++--- .../weather/WeatherSourceWrapperSpec.scala | 24 ++++++++--------- 8 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala index 16547f8693..f42d8aa8c9 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala @@ -230,8 +230,8 @@ protected trait PVAgentFundamentals PVRelevantData( dateTime, tickInterval, - weatherData.diffRad, - weatherData.dirRad + weatherData.diffIrr, + weatherData.dirIrr ) val power = pvModel.calculatePower( diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala index 140def4563..27a9e88e7f 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala @@ -56,11 +56,21 @@ object WeatherMessage { ) extends WeatherMessage with ProvisionMessage[WeatherData] - /** Hold entire weather result together + /** Container class for the entirety of weather information at a certain point + * in time and at a certain coordinate + * + * @param diffIrr + * Diffuse irradiance on the horizontal pane + * @param dirIrr + * Direct irradiance on the horizontal pane + * @param temp + * Temperature + * @param windVel + * Wind velocity */ final case class WeatherData( - diffRad: ComparableQuantity[Irradiance], - dirRad: ComparableQuantity[Irradiance], + diffIrr: ComparableQuantity[Irradiance], + dirIrr: ComparableQuantity[Irradiance], temp: ComparableQuantity[Temperature], windVel: ComparableQuantity[Speed] ) extends SecondaryData diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala index ea263c07cf..488ecd4e5e 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -523,9 +523,9 @@ object WeatherSource { ): WeatherData = { WeatherData( weatherValue.getSolarIrradiance.getDiffuseIrradiance - .orElse(EMPTY_WEATHER_DATA.diffRad), + .orElse(EMPTY_WEATHER_DATA.diffIrr), weatherValue.getSolarIrradiance.getDirectIrradiance - .orElse(EMPTY_WEATHER_DATA.dirRad), + .orElse(EMPTY_WEATHER_DATA.dirIrr), weatherValue.getTemperature.getTemperature .orElse(EMPTY_WEATHER_DATA.temp), weatherValue.getWind.getVelocity.orElse(EMPTY_WEATHER_DATA.windVel) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index d2ec4a26ce..bff6f2544c 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -142,8 +142,8 @@ private[weather] final case class WeatherSourceWrapper private ( ) /* Determine actual weights and contributions */ - val (diffRadContrib, diffRadWeight) = currentWeather.diffRad match { - case EMPTY_WEATHER_DATA.diffRad => (EMPTY_WEATHER_DATA.diffRad, 0d) + val (diffRadContrib, diffRadWeight) = currentWeather.diffIrr match { + case EMPTY_WEATHER_DATA.`diffIrr` => (EMPTY_WEATHER_DATA.diffIrr, 0d) case nonEmptyDiffRad => calculateContrib( nonEmptyDiffRad, @@ -152,8 +152,8 @@ private[weather] final case class WeatherSourceWrapper private ( s"Diffuse solar irradiance not available at $point." ) } - val (dirRadContrib, dirRadWeight) = currentWeather.dirRad match { - case EMPTY_WEATHER_DATA.dirRad => (EMPTY_WEATHER_DATA.dirRad, 0d) + val (dirRadContrib, dirRadWeight) = currentWeather.dirIrr match { + case EMPTY_WEATHER_DATA.`dirIrr` => (EMPTY_WEATHER_DATA.dirIrr, 0d) case nonEmptyDirRad => calculateContrib( nonEmptyDirRad, @@ -186,8 +186,8 @@ private[weather] final case class WeatherSourceWrapper private ( /* Sum up weight and contributions */ ( WeatherData( - averagedWeather.diffRad.add(diffRadContrib), - averagedWeather.dirRad.add(dirRadContrib), + averagedWeather.diffIrr.add(diffRadContrib), + averagedWeather.dirIrr.add(dirRadContrib), averagedWeather.temp.add(tempContrib), averagedWeather.windVel.add(windVelContrib) ), @@ -397,14 +397,14 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { windVel: Double ) { def add( - diffRad: Double, - dirRad: Double, + diffIrr: Double, + dirIrr: Double, temp: Double, windVel: Double ): WeightSum = WeightSum( - this.diffIrr + diffRad, - this.dirIrr + dirRad, + this.diffIrr + diffIrr, + this.dirIrr + dirIrr, this.temp + temp, this.windVel + windVel ) @@ -412,7 +412,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { /** Scale the given [[WeatherData]] by dividing by the sum of weights per * attribute of the weather data. If one of the weight sums is empty (and * thus a division by zero would happen) the defined "empty" information - * for this attribute a returned. + * for this attribute is returned. * * @param weatherData * Weighted and accumulated weather information @@ -424,9 +424,9 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { implicit val precision: Double = 1e-3 WeatherData( if (this.diffIrr !~= 0d) diffRad.divide(this.diffIrr) - else EMPTY_WEATHER_DATA.diffRad, + else EMPTY_WEATHER_DATA.diffIrr, if (this.dirIrr !~= 0d) dirRad.divide(this.dirIrr) - else EMPTY_WEATHER_DATA.dirRad, + else EMPTY_WEATHER_DATA.dirIrr, if (this.temp !~= 0d) temp.divide(this.temp) else EMPTY_WEATHER_DATA.temp, if (this.windVel !~= 0d) windVel.divide(this.windVel) diff --git a/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala b/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala index 33d295c43e..67bc7cf7cf 100644 --- a/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala +++ b/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala @@ -6,10 +6,12 @@ package edu.ie3.util.scala +@deprecated("Use implementation in power system utils package") object DoubleUtils { implicit class ImplicitDouble(d: Double) { def ~=(other: Double)(implicit precision: Double): Boolean = - (d - other).abs < precision - def !~=(other: Double)(implicit precision: Double): Boolean = ! ~=(other) + (d - other).abs <= precision + def !~=(other: Double)(implicit precision: Double): Boolean = + (d - other).abs > precision } } diff --git a/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala index cc54e60372..39edbf6921 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala @@ -585,8 +585,8 @@ class PVAgentModelCalculationSpec 0L -> PVRelevantData( 0L.toDateTime, 3600L, - weatherData.diffRad, - weatherData.dirRad + weatherData.diffIrr, + weatherData.dirIrr ) ) } @@ -737,8 +737,8 @@ class PVAgentModelCalculationSpec 0L -> PVRelevantData( 0L.toDateTime, 3600L, - weatherData.diffRad, - weatherData.dirRad + weatherData.diffIrr, + weatherData.dirIrr ) ) } diff --git a/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala index 0b84a7bda7..9a57251f21 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala @@ -83,16 +83,16 @@ class SampleWeatherSourceSpec val actual = source invokePrivate getWeatherPrivate(tick) /* Units meet expectation */ - actual.diffRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE - actual.dirRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + actual.diffIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + actual.dirIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE actual.temp.getUnit shouldBe StandardUnits.TEMPERATURE actual.windVel.getUnit shouldBe StandardUnits.WIND_VELOCITY /* Values meet expectations */ - actual.diffRad should equalWithTolerance( + actual.diffIrr should equalWithTolerance( Quantities.getQuantity(72.7656, StandardUnits.SOLAR_IRRADIANCE) ) - actual.dirRad should equalWithTolerance( + actual.dirIrr should equalWithTolerance( Quantities.getQuantity(80.1172, StandardUnits.SOLAR_IRRADIANCE) ) actual.windVel should equalWithTolerance( diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala index 09c5ac140b..b7689630b5 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -66,10 +66,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) val sumOfAll = 1 + 1 + 1 + 13 - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -91,10 +91,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) val sumOfAll = 1 + 1 + 1 + 13 - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -116,10 +116,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) val sumOfAll = 1 + 1 + 1 - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 3, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 3, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -133,10 +133,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { "calculate the correct weighted value for 1 coordinate with a weight of 1" in { val weightedCoordinates = WeightedCoordinates(Map(coordinate13 -> 1d)) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(13, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(13, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -249,7 +249,7 @@ class WeatherSourceWrapperSpec extends UnitSpec { } } -case object WeatherSourceWrapperSpec { +object WeatherSourceWrapperSpec { // lat/lon are irrelevant, we will manually create weights later on private val coordinate1a = GeoUtils.xyToPoint(6, 51) private val coordinate1b = GeoUtils.xyToPoint(7, 51) @@ -394,8 +394,8 @@ case object WeatherSourceWrapperSpec { ) ) => currentSum.copy( - diffRad = currentSum.diffRad.add(diffRad.multiply(diffWeight)), - dirRad = currentSum.dirRad.add(dirRad.multiply(dirWeight)), + diffIrr = currentSum.diffIrr.add(diffRad.multiply(diffWeight)), + dirIrr = currentSum.dirIrr.add(dirRad.multiply(dirWeight)), temp = currentSum.temp.add(temp.multiply(tempWeight)), windVel = currentSum.windVel.add(windVel.multiply(wVelWeight)) ) @@ -406,7 +406,7 @@ case object WeatherSourceWrapperSpec { currentWeight._1, currentWeight._2, currentWeight._3, - currentWeight._1 + currentWeight._4 ) } From 103d3303c9b84cfff6d3323e88ce6d6aa6ca880d Mon Sep 17 00:00:00 2001 From: Kittl Date: Thu, 24 Mar 2022 15:12:22 +0100 Subject: [PATCH 60/73] Fix wrong method reference --- .../groovy/edu/ie3/simona/model/participant/PVModelIT.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy b/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy index d0dbdc8048..2dd49af64d 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy @@ -88,7 +88,7 @@ class PVModelIT extends Specification implements PVModelITHelper { "build the needed data" WeatherMessage.WeatherData weather = modelToWeatherMap.get(modelId) - PVModel.PVRelevantData neededData = new PVModel.PVRelevantData(dateTime,3600L, weather.diffRad() as ComparableQuantity, weather.dirRad() as ComparableQuantity) + PVModel.PVRelevantData neededData = new PVModel.PVRelevantData(dateTime,3600L, weather.diffIrr() as ComparableQuantity, weather.dirIrr() as ComparableQuantity) ComparableQuantity voltage = getQuantity(1.414213562, PU) "collect the results and calculate the difference between the provided results and the calculated ones" From 9dddd1fe0f9151d42c9f6f1a7d980c199b6d95bd Mon Sep 17 00:00:00 2001 From: Kittl Date: Thu, 24 Mar 2022 15:17:45 +0100 Subject: [PATCH 61/73] Further renaming --- docs/uml/main/ParticipantModelling.puml | 6 ++-- .../weather/WeatherSourceWrapper.scala | 28 +++++++++---------- .../weather/SampleWeatherSourceSpec.scala | 10 +++---- .../weather/WeatherSourceWrapperSpec.scala | 12 ++++---- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/uml/main/ParticipantModelling.puml b/docs/uml/main/ParticipantModelling.puml index 2d1d1ec79c..ec44203170 100644 --- a/docs/uml/main/ParticipantModelling.puml +++ b/docs/uml/main/ParticipantModelling.puml @@ -41,9 +41,9 @@ package edu.ie3.edu.ie3.simona { } DateTime --|> SecondaryData - Class Weather{ - + diffRad: Quantity[Irradiation] - + dirRad: Quantity[Irradiation] + Class WeatherData{ + + diffIrr: Quantity[Irradiation] + + dirIrr: Quantity[Irradiation] + temp: Quantity[Temperature] + windVel: Quantity[Speed] } diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index 3e918994f4..b3159d5963 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -142,21 +142,21 @@ private[weather] final case class WeatherSourceWrapper private ( ) /* Determine actual weights and contributions */ - val (diffRadContrib, diffRadWeight) = currentWeather.diffIrr match { - case EMPTY_WEATHER_DATA.`diffIrr` => (EMPTY_WEATHER_DATA.diffIrr, 0d) - case nonEmptyDiffRad => + val (diffIrrContrib, diffIrrWeight) = currentWeather.diffIrr match { + case EMPTY_WEATHER_DATA.diffIrr => (EMPTY_WEATHER_DATA.diffIrr, 0d) + case nonEmptyDiffIrr => calculateContrib( - nonEmptyDiffRad, + nonEmptyDiffIrr, weight, StandardUnits.SOLAR_IRRADIANCE, s"Diffuse solar irradiance not available at $point." ) } - val (dirRadContrib, dirRadWeight) = currentWeather.dirIrr match { + val (dirIrrContrib, dirIrrWeight) = currentWeather.dirIrr match { case EMPTY_WEATHER_DATA.`dirIrr` => (EMPTY_WEATHER_DATA.dirIrr, 0d) - case nonEmptyDirRad => + case nonEmptyDirIrr => calculateContrib( - nonEmptyDirRad, + nonEmptyDirIrr, weight, StandardUnits.SOLAR_IRRADIANCE, s"Direct solar irradiance not available at $point." @@ -186,14 +186,14 @@ private[weather] final case class WeatherSourceWrapper private ( /* Sum up weight and contributions */ ( WeatherData( - averagedWeather.diffIrr.add(diffRadContrib), - averagedWeather.dirIrr.add(dirRadContrib), + averagedWeather.diffIrr.add(diffIrrContrib), + averagedWeather.dirIrr.add(dirIrrContrib), averagedWeather.temp.add(tempContrib), averagedWeather.windVel.add(windVelContrib) ), currentWeightSum.add( - diffRadWeight, - dirRadWeight, + diffIrrWeight, + dirIrrWeight, tempWeight, windVelWeight ) @@ -420,12 +420,12 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { * Weighted weather information, which are divided by the sum of weights */ def scale(weatherData: WeatherData): WeatherData = weatherData match { - case WeatherData(diffRad, dirRad, temp, windVel) => + case WeatherData(diffIrr, dirIrr, temp, windVel) => implicit val precision: Double = 1e-3 WeatherData( - if (this.diffIrr !~= 0d) diffRad.divide(this.diffIrr) + if (this.diffIrr !~= 0d) diffIrr.divide(this.diffIrr) else EMPTY_WEATHER_DATA.diffIrr, - if (this.dirIrr !~= 0d) dirRad.divide(this.dirIrr) + if (this.dirIrr !~= 0d) dirIrr.divide(this.dirIrr) else EMPTY_WEATHER_DATA.dirIrr, if (this.temp !~= 0d) temp.divide(this.temp) else EMPTY_WEATHER_DATA.temp, diff --git a/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala index 9a57251f21..ef9987a3fd 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala @@ -108,14 +108,14 @@ class SampleWeatherSourceSpec WeightedCoordinates(Map(NodeInput.DEFAULT_GEO_POSITION -> 1d)) source.getWeather(tick, weightedCoordinates) match { - case WeatherData(diffRad, dirRad, temp, windVel) => - diffRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE - diffRad should equalWithTolerance( + case WeatherData(diffIrr, dirIrr, temp, windVel) => + diffIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + diffIrr should equalWithTolerance( Quantities.getQuantity(72.7656, StandardUnits.SOLAR_IRRADIANCE) ) - dirRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE - dirRad should equalWithTolerance( + dirIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + dirIrr should equalWithTolerance( Quantities.getQuantity(80.1172, StandardUnits.SOLAR_IRRADIANCE) ) diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala index b7689630b5..644b86135b 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -174,12 +174,12 @@ class WeatherSourceWrapperSpec extends UnitSpec { prepareWeightTestData(weatherSeq, weights) weightSum.scale(weightedWeather) match { - case WeatherData(diffRad, dirRad, temp, windVel) => - diffRad should equalWithTolerance( + case WeatherData(diffIrr, dirIrr, temp, windVel) => + diffIrr should equalWithTolerance( Quantities.getQuantity(19.83, StandardUnits.SOLAR_IRRADIANCE), 1e-6 ) - dirRad should equalWithTolerance( + dirIrr should equalWithTolerance( Quantities.getQuantity(3.01, StandardUnits.SOLAR_IRRADIANCE), 1e-6 ) @@ -389,13 +389,13 @@ object WeatherSourceWrapperSpec { case ( currentSum, ( - WeatherData(diffRad, dirRad, temp, windVel), + WeatherData(diffIrr, dirIrr, temp, windVel), (diffWeight, dirWeight, tempWeight, wVelWeight) ) ) => currentSum.copy( - diffIrr = currentSum.diffIrr.add(diffRad.multiply(diffWeight)), - dirIrr = currentSum.dirIrr.add(dirRad.multiply(dirWeight)), + diffIrr = currentSum.diffIrr.add(diffIrr.multiply(diffWeight)), + dirIrr = currentSum.dirIrr.add(dirIrr.multiply(dirWeight)), temp = currentSum.temp.add(temp.multiply(tempWeight)), windVel = currentSum.windVel.add(windVel.multiply(wVelWeight)) ) From 65c3ad5c04c43d3bc0f1d70a60f430a4e3e3acee Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Mar 2022 20:21:15 +0100 Subject: [PATCH 62/73] Simplifying factory creation in WeatherSourceWrapper --- .../weather/WeatherSourceWrapper.scala | 47 ++++++------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index 5f0947bbae..e1fba71f73 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -243,7 +243,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { folderPath, new FileNamingStrategy(), idCoordinateSource, - buildFactory(timestampPattern, scheme) + buildFactory(scheme, timestampPattern) ) logger.info( "Successfully initiated CsvWeatherSource as source for WeatherSourceWrapper." @@ -274,7 +274,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { idCoordinateSourceFunction(), couchbaseParams.coordinateColumnName, couchbaseParams.keyPrefix, - buildFactory(timestampPattern, scheme) + buildFactory(scheme, timestampPattern) ) logger.info( "Successfully initiated CouchbaseWeatherSource as source for WeatherSourceWrapper." @@ -299,7 +299,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { val source = new InfluxDbWeatherSource( influxDb1xConnector, idCoordinateSource, - buildFactory(timestampPattern, scheme) + buildFactory(scheme, timestampPattern) ) logger.info( "Successfully initiated InfluxDbWeatherSource as source for WeatherSourceWrapper." @@ -329,7 +329,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { idCoordinateSource, sqlParams.schemaName, sqlParams.tableName, - buildFactory(timestampPattern, scheme) + buildFactory(scheme, timestampPattern) ) logger.info( "Successfully initiated SqlWeatherSource as source for WeatherSourceWrapper." @@ -341,41 +341,22 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { ) } - private def buildFactory(timestampPattern: Option[String], scheme: String) = { - timestampPattern match { - case None => initWeatherFactory(scheme) - case Some(timeStampPattern) => - initWeatherFactory(scheme, timeStampPattern) - } - } - - private def initWeatherFactory(scheme: String) = - Try(WeatherScheme(scheme)) match { - case Failure(_) => - throw new InitializationException( - s"Error while initializing WeatherFactory for weather source wrapper: '$scheme' is not a weather scheme. Supported schemes:\n\t${WeatherScheme.values - .mkString("\n\t")}'" - ) - case Success(WeatherScheme.ICON) => new IconTimeBasedWeatherValueFactory() - case Success(WeatherScheme.COSMO) => - new CosmoTimeBasedWeatherValueFactory() - case Success(unknownScheme) => - throw new InitializationException( - s"Error while initializing WeatherFactory for weather source wrapper: weather scheme '$unknownScheme' is not an expected input." - ) - } - - private def initWeatherFactory(scheme: String, timeStampPattern: String) = + private def buildFactory(scheme: String, timestampPattern: Option[String]) = Try(WeatherScheme(scheme)) match { - case Failure(_) => + case Failure(exception) => throw new InitializationException( s"Error while initializing WeatherFactory for weather source wrapper: '$scheme' is not a weather scheme. Supported schemes:\n\t${WeatherScheme.values - .mkString("\n\t")}'" + .mkString("\n\t")}'", + exception ) case Success(WeatherScheme.ICON) => - new IconTimeBasedWeatherValueFactory(timeStampPattern) + timestampPattern + .map(new IconTimeBasedWeatherValueFactory(_)) + .getOrElse(new IconTimeBasedWeatherValueFactory()) case Success(WeatherScheme.COSMO) => - new CosmoTimeBasedWeatherValueFactory(timeStampPattern) + timestampPattern + .map(new CosmoTimeBasedWeatherValueFactory(_)) + .getOrElse(new CosmoTimeBasedWeatherValueFactory()) case Success(unknownScheme) => throw new InitializationException( s"Error while initializing WeatherFactory for weather source wrapper: weather scheme '$unknownScheme' is not an expected input." From 51b69637339cd2254f7b6aa55aa71dc81ed0c3e2 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Mar 2022 20:22:41 +0100 Subject: [PATCH 63/73] Removing duplicated timestamp pattern in sql primary data config --- src/main/resources/config/config-template.conf | 1 - src/main/scala/edu/ie3/simona/config/SimonaConfig.scala | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 9c36b25cf5..086469b577 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -150,7 +150,6 @@ simona.input.weather.datasource = { password: string tableName: string schemaName: string | "public" - timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'" # default pattern from PSDM:TimeBasedSimpleValueFactory } #@optional couchbaseParams = { diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 93e9b4931b..e8fa1c018c 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -1272,7 +1272,6 @@ object SimonaConfig { password: java.lang.String, schemaName: java.lang.String, tableName: java.lang.String, - timePattern: java.lang.String, userName: java.lang.String ) object SqlParams { @@ -1289,9 +1288,6 @@ object SimonaConfig { else "public", tableName = $_reqStr(parentPath, c, "tableName", $tsCfgValidator), - timePattern = - if (c.hasPathOrNull("timePattern")) c.getString("timePattern") - else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'", userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator) ) } From 822180a04a2dc2c38592b710c5b4bd633adff4ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:52:06 +0000 Subject: [PATCH 64/73] Bump akkaVersion from 2.6.18 to 2.6.19 (#179) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a5045c941f..665acf0f30 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { scalaVersion = '2.13' scalaBinaryVersion = '2.13.8' - akkaVersion = '2.6.18' + akkaVersion = '2.6.19' tscfgVersion = '0.9.997' scapegoatVersion = '1.4.12' From ef0562e619ad53ca1d2a0635377ec7dd384a3482 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:52:48 +0000 Subject: [PATCH 65/73] Bump poi-ooxml from 5.2.1 to 5.2.2 (#177) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 665acf0f30..c9dcf4ccfd 100644 --- a/build.gradle +++ b/build.gradle @@ -142,7 +142,7 @@ dependencies { scalaCompilerPlugin "com.sksamuel.scapegoat:scalac-scapegoat-plugin_${scalaBinaryVersion}:${scapegoatVersion}" implementation 'org.apache.commons:commons-math3:3.6.1' // apache commons math3 - implementation 'org.apache.poi:poi-ooxml:5.2.1' // used for FilenameUtils + implementation 'org.apache.poi:poi-ooxml:5.2.2' // used for FilenameUtils implementation 'javax.measure:unit-api:2.1.3' implementation 'tech.units:indriya:2.1.3' // quantities implementation 'org.apache.commons:commons-csv:1.9.0' From 757ca7b4aafea8fb9be31c05660f78876a7aece1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 12:58:34 +0200 Subject: [PATCH 66/73] Bump com.diffplug.spotless from 6.3.0 to 6.4.0 (#181) Bumps com.diffplug.spotless from 6.3.0 to 6.4.0. --- updated-dependencies: - dependency-name: com.diffplug.spotless dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c9dcf4ccfd..32297f6d3e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'signing' id 'maven-publish' // publish to a maven repo (local or mvn central, has to be defined) id 'pmd' // code check, working on source code - id 'com.diffplug.spotless' version '6.3.0'// code format + id 'com.diffplug.spotless' version '6.4.0'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' id "de.undercouch.download" version "5.0.2" // downloads plugin From 69c01a68af5f644dad106ed1d5bcc1e96d19bfa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 04:30:13 +0000 Subject: [PATCH 67/73] Bump testContainerVersion from 0.40.3 to 0.40.4 Bumps `testContainerVersion` from 0.40.3 to 0.40.4. Updates `testcontainers-scala-scalatest_2.13` from 0.40.3 to 0.40.4 - [Release notes](https://github.com/testcontainers/testcontainers-scala/releases) - [Commits](https://github.com/testcontainers/testcontainers-scala/commits) Updates `testcontainers-scala-postgresql_2.13` from 0.40.3 to 0.40.4 - [Release notes](https://github.com/testcontainers/testcontainers-scala/releases) - [Commits](https://github.com/testcontainers/testcontainers-scala/commits) --- updated-dependencies: - dependency-name: com.dimafeng:testcontainers-scala-scalatest_2.13 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.dimafeng:testcontainers-scala-postgresql_2.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 32297f6d3e..451a7ce280 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext { tscfgVersion = '0.9.997' scapegoatVersion = '1.4.12' - testContainerVersion = '0.40.3' + testContainerVersion = '0.40.4' scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins } From 9f2ab346a851e4883faa6e49e239346481aca6bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Mar 2022 04:27:18 +0000 Subject: [PATCH 68/73] Bump com.diffplug.spotless from 6.4.0 to 6.4.1 Bumps com.diffplug.spotless from 6.4.0 to 6.4.1. --- updated-dependencies: - dependency-name: com.diffplug.spotless dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 32297f6d3e..6bc8189743 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'signing' id 'maven-publish' // publish to a maven repo (local or mvn central, has to be defined) id 'pmd' // code check, working on source code - id 'com.diffplug.spotless' version '6.4.0'// code format + id 'com.diffplug.spotless' version '6.4.1'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' id "de.undercouch.download" version "5.0.2" // downloads plugin From 81a7f5c2d16b3d7eb9d75c11f29598cb4690dc49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Apr 2022 08:41:15 +0000 Subject: [PATCH 69/73] Bump de.undercouch.download from 5.0.2 to 5.0.4 (#184) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 33e7366bb9..5ed5e87868 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { id 'com.diffplug.spotless' version '6.4.1'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' - id "de.undercouch.download" version "5.0.2" // downloads plugin + id "de.undercouch.download" version "5.0.4" // downloads plugin id "kr.motd.sphinx" version "2.10.1" // documentation generation id "com.github.johnrengelman.shadow" version "7.1.2" // fat jar id "org.sonarqube" version "3.3" // sonarqube From a50e6b85d5b7f2956f33d19223aa39ab66cb820b Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 1 Apr 2022 15:15:17 +0200 Subject: [PATCH 70/73] Improving comments in SQL test files --- .../ie3/simona/service/primary/timeseries/time_series_p.sql | 3 ++- .../ie3/simona/service/primary/timeseries/time_series_pqh.sql | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql index 8f0c48c294..b17b091eac 100644 --- a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql @@ -11,7 +11,8 @@ CREATE TABLE public.time_series_p CREATE INDEX time_series_p_series_id ON time_series_p USING hash (time_series); -- Order of columns is important when using btree: https://www.postgresql.org/docs/14/indexes-multicolumn.html --- time_series at first since we at most use an equality constraint on time_series and a range query on time +-- Column time_series needs to placed as the first argument since we at most use an equality constraint on +-- time_series and a range query on time. CREATE UNIQUE INDEX time_series_p_series_time ON time_series_p USING btree (time_series, time); INSERT INTO diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql index 54f6513579..fa23010ce0 100644 --- a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql @@ -13,7 +13,8 @@ CREATE TABLE public.time_series_pqh CREATE INDEX time_series_pqh_series_id ON time_series_pqh USING hash (time_series); -- Order of columns is important when using btree: https://www.postgresql.org/docs/14/indexes-multicolumn.html --- time_series at first since we at most use an equality constraint on time_series and a range query on time +-- Column time_series needs to placed as the first argument since we at most use an equality constraint on +-- time_series and a range query on time. CREATE UNIQUE INDEX time_series_pqh_series_time ON time_series_pqh USING btree (time_series, time); INSERT INTO From f33520720781a266abd447e51b7e317976d84d47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 04:29:20 +0000 Subject: [PATCH 71/73] Bump testContainerVersion from 0.40.4 to 0.40.5 Bumps `testContainerVersion` from 0.40.4 to 0.40.5. Updates `testcontainers-scala-scalatest_2.13` from 0.40.4 to 0.40.5 - [Release notes](https://github.com/testcontainers/testcontainers-scala/releases) - [Commits](https://github.com/testcontainers/testcontainers-scala/compare/v0.40.4...v0.40.5) Updates `testcontainers-scala-postgresql_2.13` from 0.40.4 to 0.40.5 - [Release notes](https://github.com/testcontainers/testcontainers-scala/releases) - [Commits](https://github.com/testcontainers/testcontainers-scala/compare/v0.40.4...v0.40.5) --- updated-dependencies: - dependency-name: com.dimafeng:testcontainers-scala-scalatest_2.13 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.dimafeng:testcontainers-scala-postgresql_2.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5ed5e87868..323a7a2feb 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext { tscfgVersion = '0.9.997' scapegoatVersion = '1.4.12' - testContainerVersion = '0.40.4' + testContainerVersion = '0.40.5' scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins } From 47530b3c82664595c898b7ffd6a92e6f0b44e32a Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 6 Apr 2022 12:54:34 +0200 Subject: [PATCH 72/73] Enhanced WeatherSourceWrapperSpec and added a bit of ScalaDoc --- .../weather/WeatherSourceWrapperSpec.scala | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala index 644b86135b..8b0502c6cf 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -155,6 +155,18 @@ class WeatherSourceWrapperSpec extends UnitSpec { } "Handling the weighted weather" when { + "adding to the weight sum" should { + "produce correct results" in { + val weightSum = WeightSum(0.1d, 0.2d, 0.3d, 0.4d) + val weightSumAdded = weightSum.add(0.2d, 0.3d, 0.4d, 0.5d) + + weightSumAdded.diffIrr should ===(0.3d +- 1e-10) + weightSumAdded.dirIrr should ===(0.5d +- 1e-10) + weightSumAdded.temp should ===(0.7d +- 1e-10) + weightSumAdded.windVel should ===(0.9d +- 1e-10) + } + } + "scaling the weighted attributes with the sum of weights" should { "calculate proper information on proper input" in { val weatherSeq = Seq( @@ -170,7 +182,7 @@ class WeatherSourceWrapperSpec extends UnitSpec { (0.35, 0.2, 0.3, 0.45) ) - val (_, weightedWeather, weightSum) = + val (weightedWeather, weightSum) = prepareWeightTestData(weatherSeq, weights) weightSum.scale(weightedWeather) match { @@ -211,7 +223,7 @@ class WeatherSourceWrapperSpec extends UnitSpec { (0.35, 0.2, 0d, 0.45) ) - val (_, weightedWeather, weightSum) = + val (weightedWeather, weightSum) = prepareWeightTestData(weatherSeq, weights) weightSum.scale(weightedWeather) match { @@ -234,7 +246,7 @@ class WeatherSourceWrapperSpec extends UnitSpec { (0.35, 0.2, 0.3, 0.45) ) - val (_, weightedWeather, weightSum) = + val (weightedWeather, weightSum) = prepareWeightTestData(weatherSeq, weights) weightSum.scale(weightedWeather) match { @@ -246,6 +258,33 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) } } + + "correctly calculate scaled properties if provided with varying weight components" in { + val weatherData = WeatherData( + Quantities.getQuantity(1.0, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(1.0, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(1.0, Units.KELVIN), + Quantities.getQuantity(1.0, StandardUnits.WIND_VELOCITY) + ) + val weightSum = WeightSum(0.25, 0.5, 0.8, 1.0) + + weightSum.scale(weatherData) match { + case WeatherData(diffIrr, dirIrr, temp, windVel) => + diffIrr should equalWithTolerance( + Quantities.getQuantity(4.0, StandardUnits.SOLAR_IRRADIANCE) + ) + dirIrr should equalWithTolerance( + Quantities.getQuantity(2.0, StandardUnits.SOLAR_IRRADIANCE) + ) + temp should equalWithTolerance( + Quantities + .getQuantity(1.25, Units.KELVIN) + ) + windVel should equalWithTolerance( + Quantities.getQuantity(1.0, StandardUnits.WIND_VELOCITY) + ) + } + } } } @@ -371,10 +410,20 @@ object WeatherSourceWrapperSpec { } } - def prepareWeightTestData( + /** Prepare test data for WeightSum-related tests + * + * @param weatherSeq + * sequence of raw weather data + * @param weights + * the weights to use for averaging the weather data, with rows equivalent + * to the rows in weatherSeq + * @return + * A tuple of 1. the weighted average weather data and 2. the weight sum + */ + private def prepareWeightTestData( weatherSeq: Seq[(Double, Double, Double, Double)], weights: Seq[(Double, Double, Double, Double)] - ): (Seq[WeatherData], WeatherData, WeightSum) = { + ): (WeatherData, WeightSum) = { val weatherData = weatherSeq.map { case (diff, dir, temp, wVel) => WeatherData( Quantities.getQuantity(diff, StandardUnits.SOLAR_IRRADIANCE), @@ -410,7 +459,7 @@ object WeatherSourceWrapperSpec { ) } - (weatherData, weightedWeather, weightSum) + (weightedWeather, weightSum) } } From ac43abf605f4879472422ef41ab5932d8f0279bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 12:24:59 +0000 Subject: [PATCH 73/73] Bump com.diffplug.spotless from 6.4.1 to 6.4.2 (#190) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 323a7a2feb..0a739102af 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'signing' id 'maven-publish' // publish to a maven repo (local or mvn central, has to be defined) id 'pmd' // code check, working on source code - id 'com.diffplug.spotless' version '6.4.1'// code format + id 'com.diffplug.spotless' version '6.4.2'// code format id 'com.github.onslip.gradle-one-jar' version '1.0.6' // pack a self contained jar id "com.github.ben-manes.versions" version '0.42.0' id "de.undercouch.download" version "5.0.4" // downloads plugin