From be68e40b778bd5f13e24e5130c1f2847a6f2b516 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Tue, 6 Feb 2024 14:39:27 +0100 Subject: [PATCH 01/67] Refactor WeatherSource and WeatherSourceWrapper. --- CHANGELOG.md | 1 + .../ie3/simona/config/ConfigFailFast.scala | 130 +++++++- .../service/weather/WeatherService.scala | 3 +- .../service/weather/WeatherSource.scala | 283 +++++------------- .../weather/WeatherSourceWrapper.scala | 39 +-- .../simona/config/ConfigFailFastSpec.scala | 151 ++++++++-- .../service/weather/WeatherSourceSpec.scala | 35 --- 7 files changed, 343 insertions(+), 299 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea6909c12..33c26705f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Replaced akka with pekko [#641](https://github.com/ie3-institute/simona/issues/641) - Use `ThermalGrid` to calculate thermal environment of a heat pump [#315](https://github.com/ie3-institute/simona/issues/315) - Enable windows path as config parameters [#549](https://github.com/ie3-institute/simona/issues/549) +- Refactor `WeatherSource` and `WeatherSourceWrapper` [#180](https://github.com/ie3-institute/simona/issues/180) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index ab3b3895a9..0e7bfd6502 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -8,8 +8,15 @@ package edu.ie3.simona.config import com.typesafe.config.{Config, ConfigException} import com.typesafe.scalalogging.LazyLogging +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ + CouchbaseParams, + InfluxDb1xParams, + SampleParams, + SqlParams +} import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.InfluxDb1x import edu.ie3.simona.config.SimonaConfig.{ + BaseCsvParams, BaseOutputConfig, RefSystemConfig, ResultKafkaParams @@ -18,11 +25,14 @@ import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.io.result.ResultSinkType import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.service.primary.PrimaryServiceProxy -import edu.ie3.simona.service.weather.WeatherSource +import edu.ie3.simona.service.weather.WeatherSource.WeatherScheme import edu.ie3.simona.util.CollectionUtils +import edu.ie3.simona.util.ConfigUtil.CsvConfigUtil.checkBaseCsvParams import edu.ie3.simona.util.ConfigUtil.DatabaseConfigUtil.{ + checkCouchbaseParams, checkInfluxDb1xParams, - checkKafkaParams + checkKafkaParams, + checkSqlParams } import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} import edu.ie3.util.scala.ReflectionTools @@ -527,8 +537,120 @@ case object ConfigFailFast extends LazyLogging { PrimaryServiceProxy.checkConfig(primary) private def checkWeatherDataSource( - dataSourceConfig: SimonaConfig.Simona.Input.Weather.Datasource - ): Unit = WeatherSource.checkConfig(dataSourceConfig) + weatherDataSourceCfg: SimonaConfig.Simona.Input.Weather.Datasource + ): Unit = { + // check coordinate source + val definedCoordinateSource: String = checkCoordinateSource( + weatherDataSourceCfg.coordinateSource + ) + + /* Check, if the column scheme is supported */ + if (!WeatherScheme.isEligibleInput(weatherDataSourceCfg.scheme)) + throw new InvalidConfigParameterException( + s"The weather data scheme '${weatherDataSourceCfg.scheme}' is not supported. Supported schemes:\n\t${WeatherScheme.values + .mkString("\n\t")}" + ) + + // check weather source parameters + val supportedWeatherSources = + Set("influxdb1x", "csv", "sql", "couchbase", "sample") + val definedWeatherSources = Vector( + weatherDataSourceCfg.sampleParams, + weatherDataSourceCfg.csvParams, + weatherDataSourceCfg.influxDb1xParams, + weatherDataSourceCfg.couchbaseParams, + weatherDataSourceCfg.sqlParams + ).filter(_.isDefined) + + // check that only one source is defined + if (definedWeatherSources.size > 1) + throw new InvalidConfigParameterException( + s"Multiple weather sources defined: '${definedWeatherSources.map(_.getClass.getSimpleName).mkString("\n\t")}'." + + s"Please define only one source!\nAvailable sources:\n\t${supportedWeatherSources.mkString("\n\t")}" + ) + + definedWeatherSources.headOption.flatten match { + case Some(baseCsvParams: BaseCsvParams) => + checkBaseCsvParams(baseCsvParams, "WeatherSource") + case Some(params: CouchbaseParams) => + checkCouchbaseParams(params) + case Some(InfluxDb1xParams(database, _, url)) => + checkInfluxDb1xParams("WeatherSource", url, database) + case Some(params: SqlParams) => + checkSqlParams(params) + case Some(_: SampleParams) => + // sample weather, no check required + // coordinate source must be sample coordinate source + if (weatherDataSourceCfg.coordinateSource.sampleParams.isEmpty) { + // cannot use sample weather source with other combination of weather source than sample weather source + throw new InvalidConfigParameterException( + s"Invalid coordinate source " + + s"'$definedCoordinateSource' defined for SampleWeatherSource. " + + "Please adapt the configuration to use sample coordinate source for weather data!" + ) + } + case None | Some(_) => + throw new InvalidConfigParameterException( + s"No weather source defined! This is currently not supported! Please provide the config parameters for one " + + s"of the following weather sources:\n\t${supportedWeatherSources.mkString("\n\t")}" + ) + } + } + + /** Check the provided coordinate id data source configuration to ensure its + * validity. For any invalid configuration parameters exceptions are thrown. + * + * @param coordinateSourceConfig + * the config to be checked + * @return + * the name of the defined + * [[edu.ie3.datamodel.io.source.IdCoordinateSource]] + */ + private def checkCoordinateSource( + coordinateSourceConfig: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource + ): String = { + val supportedCoordinateSources = Set("csv", "sql", "sample") + val definedCoordSources = Vector( + coordinateSourceConfig.sampleParams, + coordinateSourceConfig.csvParams, + coordinateSourceConfig.sqlParams + ).filter(_.isDefined) + + // check that only one source is defined + if (definedCoordSources.size > 1) + throw new InvalidConfigParameterException( + s"Multiple coordinate sources defined: '${definedCoordSources.map(_.getClass.getSimpleName).mkString("\n\t")}'." + + s"Please define only one source!\nAvailable sources:\n\t${supportedCoordinateSources.mkString("\n\t")}" + ) + + definedCoordSources.headOption.flatten match { + case Some(baseCsvParams: BaseCsvParams) => + checkBaseCsvParams(baseCsvParams, "CoordinateSource") + + // check the grid model configuration + val gridModel = coordinateSourceConfig.gridModel.toLowerCase + if (gridModel != "icon" && gridModel != "cosmo") { + throw new InvalidConfigParameterException( + s"Grid model '$gridModel' is not supported!" + ) + } + + "csv" + case Some(sqlParams: SqlParams) => + checkSqlParams(sqlParams) + "sql" + case Some( + _: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams + ) => + "sample" + case None | Some(_) => + throw new InvalidConfigParameterException( + s"No coordinate source defined! This is currently not supported! Please provide the config parameters for one " + + s"of the following coordinate sources:\n\t${supportedCoordinateSources.mkString("\n\t")}" + ) + } + + } /** Check the config sub tree for output parameterization * diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala index 12b2e6e2cb..0aae5e9675 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala @@ -123,8 +123,7 @@ final case class WeatherService( ): Try[(WeatherInitializedStateData, Option[Long])] = initServiceData match { case InitWeatherServiceStateData(sourceDefinition) => - val weatherSource = - WeatherSource(sourceDefinition, simulationStart) + val weatherSource = WeatherSource(sourceDefinition) /* What is the first tick to be triggered for? And what are further activation ticks */ val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( 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 e054b01715..4f8c0127eb 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -6,6 +6,7 @@ package edu.ie3.simona.service.weather +import edu.ie3.datamodel.exceptions.SourceException import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.factory.timeseries.{ CosmoIdCoordinateFactory, @@ -30,17 +31,11 @@ import edu.ie3.simona.service.weather.WeatherSource.{ AgentCoordinates, WeightedCoordinates } -import edu.ie3.simona.util.ConfigUtil.CsvConfigUtil.checkBaseCsvParams -import edu.ie3.simona.util.ConfigUtil.DatabaseConfigUtil.{ - checkCouchbaseParams, - checkInfluxDb1xParams, - checkSqlParams -} import edu.ie3.simona.util.ParsableEnumeration import edu.ie3.util.geo.{CoordinateDistance, GeoUtils} import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.quantities.WattsPerSquareMeter -import org.locationtech.jts.geom.{Coordinate, Point} +import org.locationtech.jts.geom.{Coordinate, Point, Polygon} import squants.motion.MetersPerSecond import squants.thermal.Kelvin import tech.units.indriya.ComparableQuantity @@ -123,23 +118,13 @@ trait WeatherSource { case Some(exactHit) => /* The queried coordinate hit one of the weather coordinates. Don't average and take it directly */ Success(Vector(exactHit)) - case None if nearestCoords.size < amountOfInterpolationCoords => - Failure( - ServiceException( - s"There are not enough coordinates for averaging. Found ${nearestCoords.size} within the given distance of " + - s"$maxCoordinateDistance but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." - ) - ) case None => - /* Check if enough coordinates are within the coordinate distance limit */ - val nearestCoordsInMaxDistance = nearestCoords.filter(coordDistance => - coordDistance.getDistance - .isLessThan(maxCoordinateDistance) - ) - if (nearestCoordsInMaxDistance.size < amountOfInterpolationCoords) { + /* There aren't enough coordinates inside the given distance */ + if (nearestCoords.size < amountOfInterpolationCoords) { Failure( ServiceException( - s"There are not enough coordinates within the max coordinate distance of $maxCoordinateDistance. Found ${nearestCoordsInMaxDistance.size} but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." + s"There are not enough coordinates for averaging. Found ${nearestCoords.size} within the given distance of " + + s"$maxCoordinateDistance but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." ) ) } else { @@ -293,136 +278,48 @@ trait WeatherSource { object WeatherSource { def apply( - dataSourceConfig: SimonaConfig.Simona.Input.Weather.Datasource, - simulationStart: ZonedDateTime - ): WeatherSource = - checkConfig(dataSourceConfig)(simulationStart) - - /** Check the provided weather data source configuration to ensure its - * validity. If the configuration is valid, a function to build the - * corresponding [[WeatherSource]] instance is returned. For any invalid - * configuration parameters exceptions are thrown. - * - * @param weatherDataSourceCfg - * the config to be checked - * @return - * a function that can be used to actually build the configured weather - * data source - */ - def checkConfig( weatherDataSourceCfg: SimonaConfig.Simona.Input.Weather.Datasource - ): ZonedDateTime => WeatherSource = { + )(implicit simulationStart: ZonedDateTime): WeatherSource = { + // get coordinate source + implicit val coordinateSourceFunction: IdCoordinateSource = + buildCoordinateSource(weatherDataSourceCfg.coordinateSource) - // check and get coordinate source - val coordinateSourceFunction: () => IdCoordinateSource = - checkCoordinateSource( - weatherDataSourceCfg.coordinateSource - ) - - /* Check, if the column scheme is supported */ - if (!WeatherScheme.isEligibleInput(weatherDataSourceCfg.scheme)) - throw new InvalidConfigParameterException( - s"The weather data scheme '${weatherDataSourceCfg.scheme}' is not supported. Supported schemes:\n\t${WeatherScheme.values - .mkString("\n\t")}" - ) - - // check weather source parameters - val supportedWeatherSources = - Set("influxdb1x", "csv", "sql", "couchbase", "sample") val definedWeatherSources = Vector( weatherDataSourceCfg.sampleParams, weatherDataSourceCfg.csvParams, weatherDataSourceCfg.influxDb1xParams, weatherDataSourceCfg.couchbaseParams, weatherDataSourceCfg.sqlParams - ).filter(_.isDefined) + ).find(_.isDefined).flatten - val timestampPattern: Option[String] = weatherDataSourceCfg.timestampPattern - val scheme: String = weatherDataSourceCfg.scheme - val resolution: Option[Long] = weatherDataSourceCfg.resolution - val distance: ComparableQuantity[Length] = + implicit val timestampPattern: Option[String] = + weatherDataSourceCfg.timestampPattern + implicit val scheme: String = weatherDataSourceCfg.scheme + implicit val resolution: Option[Long] = weatherDataSourceCfg.resolution + implicit val distance: ComparableQuantity[Length] = Quantities.getQuantity( weatherDataSourceCfg.maxCoordinateDistance, Units.METRE ) - // check that only one source is defined - if (definedWeatherSources.size > 1) - throw new InvalidConfigParameterException( - s"Multiple weather sources defined: '${definedWeatherSources.map(_.getClass.getSimpleName).mkString("\n\t")}'." + - s"Please define only one source!\nAvailable sources:\n\t${supportedWeatherSources.mkString("\n\t")}" - ) - definedWeatherSources.headOption match { - case Some( - Some(baseCsvParams @ BaseCsvParams(csvSep, directoryPath, _)) - ) => - checkBaseCsvParams(baseCsvParams, "WeatherSource") - (simulationStart: ZonedDateTime) => - WeatherSourceWrapper( - csvSep, - Paths.get(directoryPath), - coordinateSourceFunction, - timestampPattern, - scheme, - resolution, - distance - )(simulationStart) - case Some(Some(params: CouchbaseParams)) => - checkCouchbaseParams(params) - (simulationStart: ZonedDateTime) => - WeatherSourceWrapper( - params, - coordinateSourceFunction, - timestampPattern, - scheme, - resolution, - distance - )(simulationStart) - case Some(Some(params @ InfluxDb1xParams(database, _, url))) => - checkInfluxDb1xParams("WeatherSource", url, database) - (simulationStart: ZonedDateTime) => - WeatherSourceWrapper( - params, - coordinateSourceFunction, - timestampPattern, - scheme, - resolution, - distance - )(simulationStart) - case Some(Some(params: SqlParams)) => - checkSqlParams(params) - (simulationStart: ZonedDateTime) => - WeatherSourceWrapper( - params, - coordinateSourceFunction, - timestampPattern, - scheme, - resolution, - distance - )(simulationStart) - case Some(Some(_: SampleParams)) => - // sample weather, no check required - // coordinate source must be sample coordinate source - // calling the function here is not an issue as the sample coordinate source is already - // an object (= no overhead costs) - coordinateSourceFunction() match { - case _: SampleWeatherSource.SampleIdCoordinateSource.type => - // all fine - (simulationStart: ZonedDateTime) => - new SampleWeatherSource()(simulationStart) - case coordinateSource => - // cannot use sample weather source with other combination of weather source than sample weather source - throw new InvalidConfigParameterException( - s"Invalid coordinate source " + - s"'${coordinateSource.getClass.getSimpleName}' defined for SampleWeatherSource. " + - "Please adapt the configuration to use sample coordinate source for weather data!" - ) - } - case None | Some(_) => - throw new InvalidConfigParameterException( - s"No weather source defined! This is currently not supported! Please provide the config parameters for one " + - s"of the following weather sources:\n\t${supportedWeatherSources.mkString("\n\t")}" + definedWeatherSources match { + case Some(BaseCsvParams(csvSep, directoryPath, _)) => + WeatherSourceWrapper( + csvSep, + Paths.get(directoryPath) ) + case Some(params: CouchbaseParams) => + WeatherSourceWrapper(params) + case Some(params: InfluxDb1xParams) => + WeatherSourceWrapper(params) + case Some(params: SqlParams) => + WeatherSourceWrapper(params) + case Some(_: SampleParams) => + new SampleWeatherSource()(simulationStart) + case None => + throw new SourceException( + s"Expected a WeatherSource, but no source where defined in $weatherDataSourceCfg." + ); } } @@ -437,99 +334,57 @@ object WeatherSource { * a function that can be used to actually build the configured coordinate * id data source */ - private def checkCoordinateSource( + private def buildCoordinateSource( coordinateSourceConfig: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource - ): () => IdCoordinateSource = { - val supportedCoordinateSources = Set("csv", "sql", "sample") + ): IdCoordinateSource = { val definedCoordSources = Vector( coordinateSourceConfig.sampleParams, coordinateSourceConfig.csvParams, coordinateSourceConfig.sqlParams - ).filter(_.isDefined) + ).find(_.isDefined).flatten - // check that only one source is defined - if (definedCoordSources.size > 1) - throw new InvalidConfigParameterException( - s"Multiple coordinate sources defined: '${definedCoordSources.map(_.getClass.getSimpleName).mkString("\n\t")}'." + - s"Please define only one source!\nAvailable sources:\n\t${supportedCoordinateSources.mkString("\n\t")}" - ) - - // check source parameters - definedCoordSources.headOption match { + definedCoordSources match { case Some( - Some(baseCsvParams @ BaseCsvParams(csvSep, directoryPath, _)) + BaseCsvParams(csvSep, directoryPath, _) ) => - checkBaseCsvParams(baseCsvParams, "CoordinateSource") - val idCoordinateFactory = checkCoordinateFactory( - coordinateSourceConfig.gridModel - ) - () => - new CsvIdCoordinateSource( - idCoordinateFactory, - new CsvDataSource( - csvSep, - Paths.get(directoryPath), - new FileNamingStrategy() - ) + val idCoordinateFactory = + coordinateSourceConfig.gridModel.toLowerCase match { + case "icon" => new IconIdCoordinateFactory() + case "cosmo" => new CosmoIdCoordinateFactory() + } + + new CsvIdCoordinateSource( + idCoordinateFactory, + new CsvDataSource( + csvSep, + Paths.get(directoryPath), + new FileNamingStrategy() ) + ) case Some( - Some( - sqlParams @ SqlParams( - jdbcUrl, - userName, - password, - schemaName, - tableName - ) + SqlParams( + jdbcUrl, + userName, + password, + schemaName, + tableName ) ) => - checkSqlParams(sqlParams) - - () => - new SqlIdCoordinateSource( - new SqlConnector(jdbcUrl, userName, password), - schemaName, - tableName, - new SqlIdCoordinateFactory() - ) + new SqlIdCoordinateSource( + new SqlConnector(jdbcUrl, userName, password), + schemaName, + tableName, + new SqlIdCoordinateFactory() + ) case Some( - Some( - _: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams - ) + _: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams ) => // sample coordinates, no check required - () => SampleWeatherSource.SampleIdCoordinateSource - case None | Some(_) => - throw new InvalidConfigParameterException( - s"No coordinate source defined! This is currently not supported! Please provide the config parameters for one " + - s"of the following coordinate sources:\n\t${supportedCoordinateSources.mkString("\n\t")}" - ) - } - } - - /** Check the provided coordinate grid model configuration to ensure its - * validity. If the configuration is valid, the corresponding - * IdCoordinateSource is returned. For any invalid configuration parameters - * exceptions are thrown. - * - * @param gridModel - * the grid model string to be checked - * @return - * a function that can be used to actually build the id coordinate factory - * for the grid model - */ - private def checkCoordinateFactory( - gridModel: String - ): IdCoordinateFactory = { - if (gridModel.isEmpty) - throw new InvalidConfigParameterException("No grid model defined!") - gridModel.toLowerCase() match { - case "icon" => new IconIdCoordinateFactory() - case "cosmo" => new CosmoIdCoordinateFactory() - case _ => - throw new InvalidConfigParameterException( - s"Grid model '$gridModel' is not supported!" - ) + SampleWeatherSource.SampleIdCoordinateSource + case None => + throw new SourceException( + s"Expected an IdCoordinateSource, but no source where defined in $coordinateSourceConfig." + ); } } 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 2fc48cd5f9..46bad45414 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -49,7 +49,6 @@ import tech.units.indriya.ComparableQuantity import java.nio.file.Path import java.time.ZonedDateTime import javax.measure.quantity.Length - import scala.jdk.CollectionConverters.{IterableHasAsJava, MapHasAsScala} import scala.jdk.OptionConverters.RichOptional import scala.util.{Failure, Success, Try} @@ -200,14 +199,15 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { def apply( csvSep: String, - directoryPath: Path, - idCoordinateSourceFunction: () => IdCoordinateSource, + directoryPath: Path + )(implicit + simulationStart: ZonedDateTime, + idCoordinateSource: IdCoordinateSource, timestampPattern: Option[String], scheme: String, resolution: Option[Long], maxCoordinateDistance: ComparableQuantity[Length] - )(implicit simulationStart: ZonedDateTime): WeatherSourceWrapper = { - val idCoordinateSource = idCoordinateSourceFunction() + ): WeatherSourceWrapper = { val source = new CsvWeatherSource( csvSep, directoryPath, @@ -227,23 +227,24 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { } def apply( - couchbaseParams: CouchbaseParams, - idCoordinateSourceFunction: () => IdCoordinateSource, + couchbaseParams: CouchbaseParams + )(implicit + simulationStart: ZonedDateTime, + idCoordinateSource: IdCoordinateSource, timestampPattern: Option[String], scheme: String, resolution: Option[Long], maxCoordinateDistance: ComparableQuantity[Length] - )(implicit simulationStart: ZonedDateTime): WeatherSourceWrapper = { + ): WeatherSourceWrapper = { val couchbaseConnector = new CouchbaseConnector( couchbaseParams.url, couchbaseParams.bucketName, couchbaseParams.userName, couchbaseParams.password ) - val idCoordinateSource = idCoordinateSourceFunction() val source = new CouchbaseWeatherSource( couchbaseConnector, - idCoordinateSourceFunction(), + idCoordinateSource, couchbaseParams.coordinateColumnName, couchbaseParams.keyPrefix, buildFactory(scheme, timestampPattern), @@ -261,16 +262,17 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { } def apply( - influxDbParams: InfluxDb1xParams, - idCoordinateSourceFunction: () => IdCoordinateSource, + influxDbParams: InfluxDb1xParams + )(implicit + simulationStart: ZonedDateTime, + idCoordinateSource: IdCoordinateSource, timestampPattern: Option[String], scheme: String, resolution: Option[Long], maxCoordinateDistance: ComparableQuantity[Length] - )(implicit simulationStart: ZonedDateTime): WeatherSourceWrapper = { + ): WeatherSourceWrapper = { val influxDb1xConnector = new InfluxDbConnector(influxDbParams.url, influxDbParams.database) - val idCoordinateSource = idCoordinateSourceFunction() val source = new InfluxDbWeatherSource( influxDb1xConnector, idCoordinateSource, @@ -288,19 +290,20 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { } def apply( - sqlParams: SqlParams, - idCoordinateSourceFunction: () => IdCoordinateSource, + sqlParams: SqlParams + )(implicit + simulationStart: ZonedDateTime, + idCoordinateSource: IdCoordinateSource, timestampPattern: Option[String], scheme: String, resolution: Option[Long], maxCoordinateDistance: ComparableQuantity[Length] - )(implicit simulationStart: ZonedDateTime): WeatherSourceWrapper = { + ): WeatherSourceWrapper = { val sqlConnector = new SqlConnector( sqlParams.jdbcUrl, sqlParams.userName, sqlParams.password ) - val idCoordinateSource = idCoordinateSourceFunction() val source = new SqlWeatherSource( sqlConnector, idCoordinateSource, diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index 33f9914a0f..1680535cd6 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -7,7 +7,12 @@ package edu.ie3.simona.config import com.typesafe.config.ConfigFactory -import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource +import edu.ie3.datamodel.io.source.csv.CsvWeatherSource +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ + CoordinateSource, + SampleParams +} import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.{Csv, InfluxDb1x} import edu.ie3.simona.config.SimonaConfig.Simona.Powerflow.Newtonraphson @@ -18,8 +23,8 @@ import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} import edu.ie3.util.TimeUtil -import java.time.{Duration, ZonedDateTime} import java.time.temporal.ChronoUnit +import java.time.{Duration, ZonedDateTime} class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "Validating the configs" when { @@ -909,44 +914,138 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { /* Checking of primary source configuration is delegated to the specific actor. Tests are placed there */ "Checking weather data sources" should { - val checkWeatherDataSource = PrivateMethod[Unit](Symbol("checkWeatherDataSource")) + val csv: BaseCsvParams = + BaseCsvParams(",", "input", isHierarchic = false) + val sample = new SampleParams(true) + + val weatherDataSource = Datasource( + CoordinateSource( + None, + "icon", + Some( + SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource + .SampleParams(true) + ), + None + ), + None, + None, + None, + 50000d, + Some(360L), + None, + "icon", + None, + Some("yyyy-MM-dd HH:mm") + ) + "detects invalid weather data scheme" in { - val weatherDataSource = - new SimonaConfig.Simona.Input.Weather.Datasource( - CoordinateSource( - None, - "icon", - Some( - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource - .SampleParams(true) - ), - None - ), - None, - None, - None, - 50000d, - Some(360L), - Some( - SimonaConfig.Simona.Input.Weather.Datasource.SampleParams(true) - ), - "this won't work", - None, - Some("yyyy-MM-dd HH:mm") + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkWeatherDataSource( + weatherDataSource.copy(scheme = "this won't work") ) + }.getMessage shouldBe "The weather data scheme 'this won't work' is not supported. " + + "Supported schemes:\n\ticon\n\tcosmo" + } + + "detect missing source" in { intercept[InvalidConfigParameterException] { ConfigFailFast invokePrivate checkWeatherDataSource( weatherDataSource ) - }.getMessage shouldBe "The weather data scheme 'this won't work' is not supported. Supported schemes:\n\ticon\n\tcosmo" + }.getMessage should startWith( + "No weather source defined! This is currently not supported! Please provide the config parameters for " + + "one of the following weather sources:" + ) + } + + "detect too many sources" in { + val tooManySources = weatherDataSource.copy( + csvParams = Some(csv), + sampleParams = Some(sample) + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkWeatherDataSource(tooManySources) + }.getMessage should startWith("Multiple weather sources defined:") + } + + "detects sample source mismatch" in { + val csvCoordinateSource = new CoordinateSource( + csvParams = Some(csv), + gridModel = "icon", + sampleParams = None, + sqlParams = None + ) + + val sampleMismatch = weatherDataSource.copy( + coordinateSource = csvCoordinateSource, + sampleParams = Some(sample) + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkWeatherDataSource(sampleMismatch) + }.getMessage shouldBe "Invalid coordinate source 'csv' defined for SampleWeatherSource. Please adapt the configuration to use sample coordinate source for weather data!" } } } + "Checking coordinate sources" should { + val checkCoordinateSource = + PrivateMethod[Unit](Symbol("checkCoordinateSource")) + val csvParams: BaseCsvParams = BaseCsvParams( + ",", + "input", + isHierarchic = false + ) + val sampleParams = + new SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams( + true + ) + + val coordinateSource = new CoordinateSource( + csvParams = None, + gridModel = "icon", + sampleParams = None, + sqlParams = None + ) + + "detect missing source" in { + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkCoordinateSource(coordinateSource) + }.getMessage should startWith( + "No coordinate source defined! This is currently not supported! Please provide the config parameters for one of the following coordinate sources" + ) + } + + "detect too many sources" in { + val tooManySources = coordinateSource.copy( + csvParams = Some(csvParams), + sampleParams = Some(sampleParams) + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkCoordinateSource(tooManySources) + }.getMessage should startWith("Multiple coordinate sources defined:") + } + + "detect invalid grid model" in { + val invalidGridModel = coordinateSource.copy( + csvParams = Some(csvParams), + gridModel = "invalid" + ) + + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkCoordinateSource(invalidGridModel) + }.getMessage should startWith("Grid model 'invalid' is not supported!") + } + + } + "validating the typesafe config" when { "checking the availability of pekko logger parameterization" should { val checkPekkoLoggers = PrivateMethod[Unit](Symbol("checkPekkoLoggers")) diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala index 063e46d1f7..da2f1a2de4 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala @@ -299,41 +299,6 @@ class WeatherSourceSpec extends UnitSpec { ) } } - - "return correct coordinate factory" in { - val checkCoordinateFactory = - PrivateMethod[IdCoordinateFactory](Symbol("checkCoordinateFactory")) - - val cases = Table( - ("gridModel", "expectedClass", "failureMessage"), - ( - "", - classOf[InvalidConfigParameterException], - "No grid model defined!" - ), - ("icon", classOf[IconIdCoordinateFactory], ""), - ("cosmo", classOf[CosmoIdCoordinateFactory], ""), - ( - "else", - classOf[InvalidConfigParameterException], - "Grid model 'else' is not supported!" - ) - ) - - forAll(cases) { (gridModel, expectedClass, failureMessage) => - val actual = - Try(WeatherSource invokePrivate checkCoordinateFactory(gridModel)) - - actual match { - case Success(factory) => - factory.getClass shouldBe expectedClass - - case Failure(exception) => - exception.getClass shouldBe expectedClass - exception.getMessage shouldBe failureMessage - } - } - } } } From 9fd9a212416730eedc4bd326ab010af48a73d26a Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Fri, 8 Mar 2024 10:54:00 +0100 Subject: [PATCH 02/67] Some improvements. --- .../service/weather/WeatherSource.scala | 33 +-- .../weather/WeatherSourceWrapper.scala | 192 ++++++++---------- 2 files changed, 95 insertions(+), 130 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 4f8c0127eb..118ff91834 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -31,6 +31,7 @@ import edu.ie3.simona.service.weather.WeatherSource.{ AgentCoordinates, WeightedCoordinates } +import edu.ie3.simona.service.weather.WeatherSourceWrapper.buildPSDMSource import edu.ie3.simona.util.ParsableEnumeration import edu.ie3.util.geo.{CoordinateDistance, GeoUtils} import edu.ie3.util.quantities.PowerSystemUnits @@ -292,9 +293,13 @@ object WeatherSource { weatherDataSourceCfg.sqlParams ).find(_.isDefined).flatten - implicit val timestampPattern: Option[String] = - weatherDataSourceCfg.timestampPattern - implicit val scheme: String = weatherDataSourceCfg.scheme + if (definedWeatherSources.isEmpty) { + // should not happen, due to the config fail fast check + throw new SourceException( + s"Expected a WeatherSource, but no source where defined in $weatherDataSourceCfg." + ) + } + implicit val resolution: Option[Long] = weatherDataSourceCfg.resolution implicit val distance: ComparableQuantity[Length] = Quantities.getQuantity( @@ -302,25 +307,9 @@ object WeatherSource { Units.METRE ) - definedWeatherSources match { - case Some(BaseCsvParams(csvSep, directoryPath, _)) => - WeatherSourceWrapper( - csvSep, - Paths.get(directoryPath) - ) - case Some(params: CouchbaseParams) => - WeatherSourceWrapper(params) - case Some(params: InfluxDb1xParams) => - WeatherSourceWrapper(params) - case Some(params: SqlParams) => - WeatherSourceWrapper(params) - case Some(_: SampleParams) => - new SampleWeatherSource()(simulationStart) - case None => - throw new SourceException( - s"Expected a WeatherSource, but no source where defined in $weatherDataSourceCfg." - ); - } + buildPSDMSource(weatherDataSourceCfg, definedWeatherSources) + .map(WeatherSourceWrapper.apply) + .getOrElse(new SampleWeatherSource()) } /** Check the provided coordinate id data source configuration to ensure its 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 46bad45414..6ae5f52022 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -25,6 +25,8 @@ import edu.ie3.datamodel.io.source.{ IdCoordinateSource, WeatherSource => PsdmWeatherSource } +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.SimonaConfig.BaseCsvParams import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ CouchbaseParams, InfluxDb1xParams, @@ -46,7 +48,7 @@ import edu.ie3.util.DoubleUtils.ImplicitDouble import edu.ie3.util.interval.ClosedInterval import tech.units.indriya.ComparableQuantity -import java.nio.file.Path +import java.nio.file.Paths import java.time.ZonedDateTime import javax.measure.quantity.Length import scala.jdk.CollectionConverters.{IterableHasAsJava, MapHasAsScala} @@ -198,128 +200,102 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { private val DEFAULT_RESOLUTION = 3600L def apply( - csvSep: String, - directoryPath: Path + source: PsdmWeatherSource )(implicit simulationStart: ZonedDateTime, idCoordinateSource: IdCoordinateSource, - timestampPattern: Option[String], - scheme: String, resolution: Option[Long], - maxCoordinateDistance: ComparableQuantity[Length] + distance: ComparableQuantity[Length] ): WeatherSourceWrapper = { - val source = new CsvWeatherSource( - csvSep, - directoryPath, - new FileNamingStrategy(), - idCoordinateSource, - buildFactory(scheme, timestampPattern) - ) - logger.info( - "Successfully initiated CsvWeatherSource as source for WeatherSourceWrapper." - ) WeatherSourceWrapper( source, idCoordinateSource, resolution.getOrElse(DEFAULT_RESOLUTION), - maxCoordinateDistance + distance ) } - def apply( - couchbaseParams: CouchbaseParams + private[weather] def buildPSDMSource( + cfgParams: SimonaConfig.Simona.Input.Weather.Datasource, + definedWeatherSources: Option[Serializable] )(implicit - simulationStart: ZonedDateTime, - idCoordinateSource: IdCoordinateSource, - timestampPattern: Option[String], - scheme: String, - resolution: Option[Long], - maxCoordinateDistance: ComparableQuantity[Length] - ): WeatherSourceWrapper = { - val couchbaseConnector = new CouchbaseConnector( - couchbaseParams.url, - couchbaseParams.bucketName, - couchbaseParams.userName, - couchbaseParams.password - ) - val source = new CouchbaseWeatherSource( - couchbaseConnector, - idCoordinateSource, - couchbaseParams.coordinateColumnName, - couchbaseParams.keyPrefix, - buildFactory(scheme, timestampPattern), - "yyyy-MM-dd'T'HH:mm:ssxxx" - ) - logger.info( - "Successfully initiated CouchbaseWeatherSource as source for WeatherSourceWrapper." - ) - WeatherSourceWrapper( - source, - idCoordinateSource, - resolution.getOrElse(DEFAULT_RESOLUTION), - maxCoordinateDistance - ) - } + idCoordinateSource: IdCoordinateSource + ): Option[PsdmWeatherSource] = { + implicit val timestampPattern: Option[String] = + cfgParams.timestampPattern + implicit val scheme: String = cfgParams.scheme - def apply( - influxDbParams: InfluxDb1xParams - )(implicit - simulationStart: ZonedDateTime, - idCoordinateSource: IdCoordinateSource, - timestampPattern: Option[String], - scheme: String, - resolution: Option[Long], - maxCoordinateDistance: ComparableQuantity[Length] - ): WeatherSourceWrapper = { - val influxDb1xConnector = - new InfluxDbConnector(influxDbParams.url, influxDbParams.database) - val source = new InfluxDbWeatherSource( - influxDb1xConnector, - idCoordinateSource, - buildFactory(scheme, timestampPattern) - ) - logger.info( - "Successfully initiated InfluxDbWeatherSource as source for WeatherSourceWrapper." - ) - WeatherSourceWrapper( - source, - idCoordinateSource, - resolution.getOrElse(DEFAULT_RESOLUTION), - maxCoordinateDistance - ) - } + val factory = buildFactory(scheme, timestampPattern) - def apply( - sqlParams: SqlParams - )(implicit - simulationStart: ZonedDateTime, - idCoordinateSource: IdCoordinateSource, - timestampPattern: Option[String], - scheme: String, - resolution: Option[Long], - maxCoordinateDistance: ComparableQuantity[Length] - ): WeatherSourceWrapper = { - val sqlConnector = new SqlConnector( - sqlParams.jdbcUrl, - sqlParams.userName, - sqlParams.password - ) - val source = new SqlWeatherSource( - sqlConnector, - idCoordinateSource, - sqlParams.schemaName, - sqlParams.tableName, - buildFactory(scheme, timestampPattern) - ) - logger.info( - "Successfully initiated SqlWeatherSource as source for WeatherSourceWrapper." - ) - WeatherSourceWrapper( - source, - idCoordinateSource, - resolution.getOrElse(DEFAULT_RESOLUTION), - maxCoordinateDistance - ) + val source = definedWeatherSources.flatMap { + case BaseCsvParams(csvSep, directoryPath, _) => + // initializing a csv weather source + Some( + new CsvWeatherSource( + csvSep, + Paths.get(directoryPath), + new FileNamingStrategy(), + idCoordinateSource, + factory + ) + ) + case couchbaseParams: CouchbaseParams => + // initializing a couchbase weather source + val couchbaseConnector = new CouchbaseConnector( + couchbaseParams.url, + couchbaseParams.bucketName, + couchbaseParams.userName, + couchbaseParams.password + ) + Some( + new CouchbaseWeatherSource( + couchbaseConnector, + idCoordinateSource, + couchbaseParams.coordinateColumnName, + couchbaseParams.keyPrefix, + factory, + "yyyy-MM-dd'T'HH:mm:ssxxx" + ) + ) + case InfluxDb1xParams(database, _, url) => + // initializing an influxDb weather source + val influxDb1xConnector = + new InfluxDbConnector(url, database) + Some( + new InfluxDbWeatherSource( + influxDb1xConnector, + idCoordinateSource, + factory + ) + ) + case sqlParams: SqlParams => + // initializing a sql weather source + val sqlConnector = new SqlConnector( + sqlParams.jdbcUrl, + sqlParams.userName, + sqlParams.password + ) + Some( + new SqlWeatherSource( + sqlConnector, + idCoordinateSource, + sqlParams.schemaName, + sqlParams.tableName, + factory + ) + ) + case _ => + // no weather source is initialized + None + } + + source.foreach { source => + logger.info( + s"Successfully initialized ${source.getClass.getSimpleName} as source for WeatherSourceWrapper." + ) + } + + source } private def buildFactory(scheme: String, timestampPattern: Option[String]) = From d37031e2500bd06d3c69a26537756146fb80b62b Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Fri, 8 Mar 2024 11:10:40 +0100 Subject: [PATCH 03/67] fmt --- .../ie3/simona/config/ConfigFailFast.scala | 8 +++--- .../service/weather/WeatherSource.scala | 22 ++++----------- .../weather/WeatherSourceWrapper.scala | 28 +++++++++---------- .../simona/config/ConfigFailFastSpec.scala | 19 ++++++------- 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index b2eb2eac34..8687556b90 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -12,7 +12,7 @@ import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ CouchbaseParams, InfluxDb1xParams, SampleParams, - SqlParams + SqlParams, } import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.InfluxDb1x import edu.ie3.simona.config.SimonaConfig.{ @@ -34,7 +34,7 @@ import edu.ie3.simona.util.ConfigUtil.DatabaseConfigUtil.{ checkCouchbaseParams, checkInfluxDb1xParams, checkKafkaParams, - checkSqlParams + checkSqlParams, } import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} import edu.ie3.util.scala.ReflectionTools @@ -564,7 +564,7 @@ case object ConfigFailFast extends LazyLogging { weatherDataSourceCfg.csvParams, weatherDataSourceCfg.influxDb1xParams, weatherDataSourceCfg.couchbaseParams, - weatherDataSourceCfg.sqlParams + weatherDataSourceCfg.sqlParams, ).filter(_.isDefined) // check that only one source is defined @@ -618,7 +618,7 @@ case object ConfigFailFast extends LazyLogging { val definedCoordSources = Vector( coordinateSourceConfig.sampleParams, coordinateSourceConfig.csvParams, - coordinateSourceConfig.sqlParams + coordinateSourceConfig.sqlParams, ).filter(_.isDefined) // check that only one source is defined 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 86518854b6..cb8fec8db8 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -11,7 +11,6 @@ import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.factory.timeseries.{ CosmoIdCoordinateFactory, IconIdCoordinateFactory, - IdCoordinateFactory, SqlIdCoordinateFactory, } import edu.ie3.datamodel.io.naming.FileNamingStrategy @@ -22,27 +21,18 @@ import edu.ie3.datamodel.models.value.WeatherValue import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig.BaseCsvParams import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource._ -import edu.ie3.simona.exceptions.{ - InvalidConfigParameterException, - ServiceException, -} +import edu.ie3.simona.exceptions.ServiceException import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData import edu.ie3.simona.service.weather.WeatherSource.{ AgentCoordinates, WeightedCoordinates, } -import edu.ie3.simona.util.ConfigUtil.CsvConfigUtil.checkBaseCsvParams -import edu.ie3.simona.util.ConfigUtil.DatabaseConfigUtil.{ - checkCouchbaseParams, - checkInfluxDb1xParams, - checkSqlParams, -} import edu.ie3.simona.service.weather.WeatherSourceWrapper.buildPSDMSource import edu.ie3.simona.util.ParsableEnumeration import edu.ie3.util.geo.{CoordinateDistance, GeoUtils} import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.quantities.WattsPerSquareMeter -import org.locationtech.jts.geom.{Coordinate, Point, Polygon} +import org.locationtech.jts.geom.{Coordinate, Point} import squants.motion.MetersPerSecond import squants.thermal.Kelvin import tech.units.indriya.ComparableQuantity @@ -353,8 +343,8 @@ object WeatherSource { new CsvDataSource( csvSep, Paths.get(directoryPath), - new FileNamingStrategy() - ) + new FileNamingStrategy(), + ), ) case Some( SqlParams( @@ -362,14 +352,14 @@ object WeatherSource { userName, password, schemaName, - tableName + tableName, ) ) => new SqlIdCoordinateSource( new SqlConnector(jdbcUrl, userName, password), schemaName, tableName, - new SqlIdCoordinateFactory() + new SqlIdCoordinateFactory(), ) case Some( _: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams 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 16f7a4b84e..6ccc0942bd 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -74,7 +74,7 @@ private[weather] final case class WeatherSourceWrapper private ( source: PsdmWeatherSource, override val idCoordinateSource: IdCoordinateSource, resolution: Long, - maxCoordinateDistance: ComparableQuantity[Length] + maxCoordinateDistance: ComparableQuantity[Length], )( private implicit val simulationStart: ZonedDateTime ) extends SimonaWeatherSource @@ -92,7 +92,7 @@ private[weather] final case class WeatherSourceWrapper private ( */ override def getWeather( tick: Long, - weightedCoordinates: WeatherSource.WeightedCoordinates + weightedCoordinates: WeatherSource.WeightedCoordinates, ): WeatherMessage.WeatherData = { val dateTime = tick.toDateTime val interval = new ClosedInterval(dateTime, dateTime) @@ -100,7 +100,7 @@ private[weather] final case class WeatherSourceWrapper private ( val results = source .getWeather( interval, - coordinates + coordinates, ) .asScala .toMap @@ -126,7 +126,7 @@ private[weather] final case class WeatherSourceWrapper private ( point, { logger.warn(s"Received an unexpected point: $point") 0d - } + }, ) /* Sum up weight and contributions */ @@ -171,8 +171,8 @@ private[weather] final case class WeatherSourceWrapper private ( diffIrrWeight, dirIrrWeight, tempWeight, - windVelWeight - ) + windVelWeight, + ), ) } match { case (weatherData: WeatherData, weightSum: WeightSum) => @@ -206,7 +206,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { simulationStart: ZonedDateTime, idCoordinateSource: IdCoordinateSource, resolution: Option[Long], - distance: ComparableQuantity[Length] + distance: ComparableQuantity[Length], ): WeatherSourceWrapper = { WeatherSourceWrapper( source, @@ -218,7 +218,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { private[weather] def buildPSDMSource( cfgParams: SimonaConfig.Simona.Input.Weather.Datasource, - definedWeatherSources: Option[Serializable] + definedWeatherSources: Option[Serializable], )(implicit idCoordinateSource: IdCoordinateSource ): Option[PsdmWeatherSource] = { @@ -237,7 +237,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { Paths.get(directoryPath), new FileNamingStrategy(), idCoordinateSource, - factory + factory, ) ) case couchbaseParams: CouchbaseParams => @@ -246,7 +246,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { couchbaseParams.url, couchbaseParams.bucketName, couchbaseParams.userName, - couchbaseParams.password + couchbaseParams.password, ) Some( new CouchbaseWeatherSource( @@ -255,7 +255,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { couchbaseParams.coordinateColumnName, couchbaseParams.keyPrefix, factory, - "yyyy-MM-dd'T'HH:mm:ssxxx" + "yyyy-MM-dd'T'HH:mm:ssxxx", ) ) case InfluxDb1xParams(database, _, url) => @@ -266,7 +266,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { new InfluxDbWeatherSource( influxDb1xConnector, idCoordinateSource, - factory + factory, ) ) case sqlParams: SqlParams => @@ -274,7 +274,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { val sqlConnector = new SqlConnector( sqlParams.jdbcUrl, sqlParams.userName, - sqlParams.password + sqlParams.password, ) Some( new SqlWeatherSource( @@ -282,7 +282,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { idCoordinateSource, sqlParams.schemaName, sqlParams.tableName, - factory + factory, ) ) case _ => diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index bb6a1fbc16..01d137019e 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -7,11 +7,10 @@ package edu.ie3.simona.config import com.typesafe.config.ConfigFactory -import edu.ie3.datamodel.io.source.csv.CsvWeatherSource import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ CoordinateSource, - SampleParams + SampleParams, } import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.{Csv, InfluxDb1x} @@ -939,7 +938,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource .SampleParams(true) ), - None + None, ), None, None, @@ -975,7 +974,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "detect too many sources" in { val tooManySources = weatherDataSource.copy( csvParams = Some(csv), - sampleParams = Some(sample) + sampleParams = Some(sample), ) intercept[InvalidConfigParameterException] { @@ -988,12 +987,12 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { csvParams = Some(csv), gridModel = "icon", sampleParams = None, - sqlParams = None + sqlParams = None, ) val sampleMismatch = weatherDataSource.copy( coordinateSource = csvCoordinateSource, - sampleParams = Some(sample) + sampleParams = Some(sample), ) intercept[InvalidConfigParameterException] { @@ -1081,7 +1080,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { val csvParams: BaseCsvParams = BaseCsvParams( ",", "input", - isHierarchic = false + isHierarchic = false, ) val sampleParams = new SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams( @@ -1092,7 +1091,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { csvParams = None, gridModel = "icon", sampleParams = None, - sqlParams = None + sqlParams = None, ) "detect missing source" in { @@ -1106,7 +1105,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "detect too many sources" in { val tooManySources = coordinateSource.copy( csvParams = Some(csvParams), - sampleParams = Some(sampleParams) + sampleParams = Some(sampleParams), ) intercept[InvalidConfigParameterException] { @@ -1117,7 +1116,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "detect invalid grid model" in { val invalidGridModel = coordinateSource.copy( csvParams = Some(csvParams), - gridModel = "invalid" + gridModel = "invalid", ) intercept[InvalidConfigParameterException] { From 734b9b0bff5932085434da7e3193dc04ce7ac939 Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Sun, 10 Mar 2024 17:53:40 +0100 Subject: [PATCH 04/67] Remove genindex and search --- docs/readthedocs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index 68cc508bcb..533fcd0699 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -22,6 +22,5 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu Indices and tables ================== -* :ref:`genindex` * :ref:`modindex` -* :ref:`search` + From 0cedbf90a589efb29767f432220fe113ffbc9322 Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 13 Mar 2024 08:33:21 +0100 Subject: [PATCH 05/67] test --- docs/readthedocs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index 533fcd0699..336e31fd02 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -22,5 +22,5 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu Indices and tables ================== -* :ref:`modindex` +* :ref:`testindex` From 3efc5e6b407d2f705bfb9c7ef313ca9668d6d91f Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 13 Mar 2024 09:51:37 +0100 Subject: [PATCH 06/67] add All Pages page --- docs/readthedocs/all_pages.rst | 10 ++++++++++ docs/readthedocs/index.rst | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/readthedocs/all_pages.rst diff --git a/docs/readthedocs/all_pages.rst b/docs/readthedocs/all_pages.rst new file mode 100644 index 0000000000..4d006cff2d --- /dev/null +++ b/docs/readthedocs/all_pages.rst @@ -0,0 +1,10 @@ +All Pages +========= + +Here are all available pages listed: + +.. toctree:: + :glob: + + * + diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index 336e31fd02..47b7d6d834 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -22,5 +22,6 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu Indices and tables ================== -* :ref:`testindex` +For more information, please visit the :doc:`All Pages ` page. + From a5aaded890cdf5c76b54178ad4d606bd509c13a7 Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 13 Mar 2024 09:54:16 +0100 Subject: [PATCH 07/67] CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83520da4e..6b901013aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Config possibility for transformer control groups [#90](https://github.com/ie3-institute/simona/issues/90) - Implemented scaling of all relevant input parameters [#764](https://github.com/ie3-institute/simona/issues/764) - Consider scaling factor with flex options [#734](https://github.com/ie3-institute/simona/issues/734) +- New documentation index page [#375](https://github.com/ie3-institute/simona/issues/375) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) From 7ad0bf3b84148c9c93a7be6e01a4848798f6c45f Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 13 Mar 2024 10:55:45 +0100 Subject: [PATCH 08/67] fix warnings --- docs/readthedocs/all_pages.rst | 10 ---------- docs/readthedocs/allpages.md | 17 +++++++++++++++++ docs/readthedocs/conf.py | 1 - docs/readthedocs/index.rst | 3 ++- 4 files changed, 19 insertions(+), 12 deletions(-) delete mode 100644 docs/readthedocs/all_pages.rst create mode 100644 docs/readthedocs/allpages.md diff --git a/docs/readthedocs/all_pages.rst b/docs/readthedocs/all_pages.rst deleted file mode 100644 index 4d006cff2d..0000000000 --- a/docs/readthedocs/all_pages.rst +++ /dev/null @@ -1,10 +0,0 @@ -All Pages -========= - -Here are all available pages listed: - -.. toctree:: - :glob: - - * - diff --git a/docs/readthedocs/allpages.md b/docs/readthedocs/allpages.md new file mode 100644 index 0000000000..b97e268aa3 --- /dev/null +++ b/docs/readthedocs/allpages.md @@ -0,0 +1,17 @@ +(allpages)= + +# All Pages + +Here are all available pages listed: + +```{toctree} +--- +maxdepth: 1 +--- +.. toctree:: + :maxdepth: 2 + :glob: + + * + !allpages +``` \ No newline at end of file diff --git a/docs/readthedocs/conf.py b/docs/readthedocs/conf.py index a97c81859f..e78a262a87 100644 --- a/docs/readthedocs/conf.py +++ b/docs/readthedocs/conf.py @@ -44,7 +44,6 @@ # PlantUML options plantuml = 'plantuml' - # Intersphinx for references to external ReadTheDocs intersphinx_mapping = { 'psdm': ('https://powersystemdatamodel.readthedocs.io/en/latest/', None), diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index 47b7d6d834..1360094243 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -18,10 +18,11 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu models developersguide references + allpages Indices and tables ================== -For more information, please visit the :doc:`All Pages ` page. +For more information, please visit the :doc:`All Pages ` page. From 0c1e1566f0907a75d6b5d41d23d74edafeba098f Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Wed, 13 Mar 2024 12:38:19 +0100 Subject: [PATCH 09/67] Print log directory to terminal upon simulation failure --- CHANGELOG.md | 1 + .../scala/edu/ie3/simona/main/RunSimona.scala | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83520da4e..17f204d16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Config possibility for transformer control groups [#90](https://github.com/ie3-institute/simona/issues/90) - Implemented scaling of all relevant input parameters [#764](https://github.com/ie3-institute/simona/issues/764) - Consider scaling factor with flex options [#734](https://github.com/ie3-institute/simona/issues/734) +- Printing the directory of log to terminal upon simulation failure [#626](https://github.com/ie3-institute/simona/issues/626) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) diff --git a/src/main/scala/edu/ie3/simona/main/RunSimona.scala b/src/main/scala/edu/ie3/simona/main/RunSimona.scala index 707da41338..3ba84bbef0 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimona.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimona.scala @@ -11,6 +11,8 @@ import edu.ie3.simona.sim.setup.SimonaSetup import edu.ie3.util.scala.quantities.QuantityUtil import org.apache.pekko.util.Timeout +import java.io.File +import java.nio.file.Path import java.util.Locale import scala.concurrent.duration.FiniteDuration import scala.util.Random @@ -39,7 +41,7 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { val successful = run(simonaSetup) - printGoodbye() + printGoodbye(successful) // prevents cutting of the log when having a fast simulation Thread.sleep(1000) @@ -54,7 +56,7 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { ) } - private def printGoodbye(): Unit = { + private def printGoodbye(successful: Boolean): Unit = { val myWords = Array( "\"Vielleicht ist heute ein besonders guter Tag zum Sterben.\" - Worf (in Star Trek: Der erste Kontakt)", "\"Assimiliert das!\" - Worf (in Star Trek: Der erste Kontakt)", @@ -68,6 +70,21 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { val randIdx = rand.nextInt(myWords.length) logger.info(myWords(randIdx)) logger.info("Goodbye!") + + if (!successful) { + // to ensure that the link to the log is printed last + Thread.sleep(1000) + + val root: Path = Path.of( + new File(".").getAbsoluteFile.getParent, + "logs", + "simona", + "simona.log", + ) + logger.error( + s"Simulation stopped due to the occurrence of an error! The full log can be found here: $root" + ) + } } /** Method to be implemented to setup everything that is necessary for a From bf1ecf1e6657f0cec93e96b6683dbe8ab1b1d1b3 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 20 Mar 2024 09:55:47 +0100 Subject: [PATCH 10/67] Taking changes from all/#tbw-wHp-squants-2023-12 --- .../vn_simona/fullGrid/storage_input.csv | 2 - input/samples/vn_simona/vn_simona.conf | 9 + .../resources/config/config-template.conf | 13 + .../agent/grid/GridAgentController.scala | 75 +- .../participant/storage/StorageAgent.scala | 68 ++ .../storage/StorageAgentFundamentals.scala | 374 ++++++++++ .../edu/ie3/simona/config/SimonaConfig.scala | 119 ++++ .../model/participant/StorageModel.scala | 380 +++++++++++ .../edu/ie3/simona/util/ConfigUtil.scala | 2 + .../model/participant/StorageModelTest.groovy | 381 +++++++++++ .../edu/ie3/simona/agent/em/EmAgentIT.scala | 286 ++++++++ .../StorageAgentModelCalculationSpec.scala | 646 ++++++++++++++++++ .../simona/test/common/ConfigTestData.scala | 9 + .../simona/test/common/DefaultTestData.scala | 15 + .../common/input/StorageInputTestData.scala | 50 ++ .../edu/ie3/simona/util/ConfigUtilSpec.scala | 6 +- 16 files changed, 2428 insertions(+), 7 deletions(-) delete mode 100644 input/samples/vn_simona/fullGrid/storage_input.csv create mode 100644 src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgent.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala create mode 100644 src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy create mode 100644 src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala diff --git a/input/samples/vn_simona/fullGrid/storage_input.csv b/input/samples/vn_simona/fullGrid/storage_input.csv deleted file mode 100644 index 7d68ff5120..0000000000 --- a/input/samples/vn_simona/fullGrid/storage_input.csv +++ /dev/null @@ -1,2 +0,0 @@ -uuid,id,node,operates_from,operates_until,operator,q_characteristics,type -a2a92cfd-3492-465f-9587-e789f4620af8,Speicher_1,033d0230-4aee-47cf-91f9-81f5f40e60b0,,,,"cosPhiFixed:{(0.0,0.98)}",95d4c980-d9e1-4813-9f2a-b0942488a570 diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index 38626c593d..33e400b58e 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -158,6 +158,15 @@ simona.runtime.participant.hp = { individualConfigs = [] } +simona.runtime.participant.storage = { + defaultConfig = { + calculateMissingReactivePowerWithModel = false + uuids = ["default"] + scaling = 1.0 + } + individualConfigs = [] +} + # # # # # # ATTENTION: calculateMissingReactivePowerWithModel and scaling is ignored here. # # # # # diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index cb446ae680..6397292e89 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -54,6 +54,15 @@ EvcsRuntimeConfig { lowestEvSoc: Double | 0.2 # Defines the lowest possible state of charge (SoC) that an EV is allowed to uncharge in vehicle to grid (V2G) mode } +#@define extends BaseRuntimeConfig +StorageRuntimeConfig { + baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, + # but cannot removed bc otherwise StorageRuntimeConfig is handled as String + initialSoc: Double | 0 # Defines initial soc of storages as a share of capacity, 0-1 + #@optional + targetSoc: Double +} + #@define extends BaseRuntimeConfig EmRuntimeConfig { # # # # # @@ -324,6 +333,10 @@ simona.runtime.participant = { defaultConfig = HpRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") individualConfigs = [HpRuntimeConfig] } + storage = { + defaultConfig = StorageRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") + individualConfigs = [StorageRuntimeConfig] + } em = { defaultConfig = EmRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") individualConfigs = [EmRuntimeConfig] diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala index 7ba4abef77..4ff5c60f7d 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -23,6 +23,7 @@ import edu.ie3.simona.agent.participant.hp.HpAgent import edu.ie3.simona.agent.participant.load.LoadAgent import edu.ie3.simona.agent.participant.pv.PvAgent import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData +import edu.ie3.simona.agent.participant.storage.StorageAgent import edu.ie3.simona.agent.participant.wec.WecAgent import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig._ @@ -127,8 +128,7 @@ class GridAgentController( curSysPart, ) => curSysPart match { - case entity @ (_: BmInput | _: ChpInput | _: EvInput | - _: StorageInput) => + case entity @ (_: BmInput | _: ChpInput | _: EvInput) => ( notProcessedElements + entity.getClass.getSimpleName, availableSystemParticipants, @@ -409,6 +409,20 @@ class GridAgentController( s"Unable to find thermal island grid for heat pump '${hpInput.getUuid}' with thermal bus '${hpInput.getThermalBus.getUuid}'." ) } + case input: StorageInput => + buildStorage( + input, + participantConfigUtil.getOrDefault[StorageRuntimeConfig]( + input.getUuid + ), + environmentRefs.primaryServiceProxy, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfigUtil.getOrDefault(NotifierIdentifier.Storage), + maybeControllingEm, + ) case input: SystemParticipantInput => throw new NotImplementedError( s"Building ${input.getClass.getSimpleName} is not implemented, yet." @@ -770,6 +784,63 @@ class GridAgentController( ) .toTyped + /** Creates a storage agent and determines the needed additional information + * for later initialization of the agent. + * + * @param storageInput + * Storage input model to derive information from + * @param modelConfiguration + * User-provided configuration for this specific storage model + * @param primaryServiceProxy + * Reference to the primary data service proxy + * @param simulationStartDate + * First wall clock time in simulation + * @param simulationEndDate + * Last wall clock time in simulation + * @param resolution + * Frequency of power flow calculations + * @param requestVoltageDeviationThreshold + * Maximum deviation in p.u. of request voltages to be considered equal + * @param outputConfig + * Configuration of the output behavior + * @param maybeControllingEm + * The parent EmAgent, if applicable + * @return + * The [[StorageAgent]] 's [[ActorRef]] + */ + private def buildStorage( + storageInput: StorageInput, + modelConfiguration: SimonaConfig.StorageRuntimeConfig, + primaryServiceProxy: ClassicRef, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + resolution: Long, + requestVoltageDeviationThreshold: Double, + outputConfig: NotifierConfig, + maybeControllingEm: Option[ActorRef[FlexResponse]] = None, + ): ActorRef[ParticipantMessage] = + gridAgentContext.toClassic + .simonaActorOf( + StorageAgent.props( + environmentRefs.scheduler.toClassic, + ParticipantInitializeStateData( + storageInput, + modelConfiguration, + primaryServiceProxy, + None, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + maybeControllingEm, + ), + listener.map(_.toClassic), + ), + storageInput.getId, + ) + .toTyped + /** Builds an [[EmAgent]] from given input * * @param emInput diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgent.scala new file mode 100644 index 0000000000..f38328ea3b --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgent.scala @@ -0,0 +1,68 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant.storage + +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.simona.agent.participant.ParticipantAgent +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.statedata.ParticipantStateData +import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData +import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig +import edu.ie3.simona.model.participant.StorageModel +import edu.ie3.simona.model.participant.StorageModel.{ + StorageRelevantData, + StorageState, +} +import org.apache.pekko.actor.{ActorRef, Props} + +object StorageAgent { + def props( + scheduler: ActorRef, + initStateData: ParticipantInitializeStateData[ + StorageInput, + StorageRuntimeConfig, + ApparentPower, + ], + listener: Iterable[ActorRef], + ): Props = + Props( + new StorageAgent( + scheduler, + initStateData, + listener, + ) + ) +} + +/** Creating a battery storage agent + * + * @param scheduler + * Actor reference of the scheduler + * @param listener + * List of listeners interested in results + */ +class StorageAgent( + scheduler: ActorRef, + initStateData: ParticipantInitializeStateData[ + StorageInput, + StorageRuntimeConfig, + ApparentPower, + ], + override val listener: Iterable[ActorRef], +) extends ParticipantAgent[ + ApparentPower, + StorageRelevantData, + StorageState, + ParticipantStateData[ApparentPower], + StorageInput, + StorageRuntimeConfig, + StorageModel, + ]( + scheduler, + initStateData, + ) + with StorageAgentFundamentals {} diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala new file mode 100644 index 0000000000..ffa44746c5 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -0,0 +1,374 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant.storage + +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.datamodel.models.result.system.{ + StorageResult, + SystemParticipantResult, +} +import edu.ie3.simona.agent.ValueStore +import edu.ie3.simona.agent.participant.ParticipantAgent.getAndCheckNodalVoltage +import edu.ie3.simona.agent.participant.ParticipantAgentFundamentals +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ApparentPower, + ZERO_POWER, +} +import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService +import edu.ie3.simona.agent.participant.statedata.BaseStateData.{ + FlexControlledData, + ParticipantModelBaseStateData, +} +import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputModelContainer +import edu.ie3.simona.agent.participant.statedata.{ + BaseStateData, + ParticipantStateData, +} +import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig +import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent +import edu.ie3.simona.event.notifier.NotifierConfig +import edu.ie3.simona.exceptions.agent.{ + AgentInitializationException, + InvalidRequestException, +} +import edu.ie3.simona.model.participant.StorageModel.{ + StorageRelevantData, + StorageState, +} +import edu.ie3.simona.model.participant.{FlexChangeIndicator, StorageModel} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexRequest, + FlexResponse, +} +import edu.ie3.simona.util.SimonaConstants +import edu.ie3.simona.util.TickUtil.TickLong +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.ReactivePower +import org.apache.pekko.actor.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.typed.{ActorRef => TypedActorRef} +import squants.Each +import squants.energy.Kilowatts + +import java.time.ZonedDateTime +import java.util.UUID +import scala.collection.SortedSet +import scala.reflect.{ClassTag, classTag} + +trait StorageAgentFundamentals + extends ParticipantAgentFundamentals[ + ApparentPower, + StorageRelevantData, + StorageState, + ParticipantStateData[ApparentPower], + StorageInput, + StorageRuntimeConfig, + StorageModel, + ] { + this: StorageAgent => + override val alternativeResult: ApparentPower = ZERO_POWER + + override protected val pdClassTag: ClassTag[ApparentPower] = + classTag[ApparentPower] + + /** Abstract definition, individual implementations found in individual agent + * fundamental classes + */ + override def determineModelBaseStateData( + inputModel: ParticipantStateData.InputModelContainer[StorageInput], + modelConfig: StorageRuntimeConfig, + services: Iterable[SecondaryDataService[_ <: Data.SecondaryData]], + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + resolution: Long, + requestVoltageDeviationThreshold: Double, + outputConfig: NotifierConfig, + maybeEmAgent: Option[TypedActorRef[FlexResponse]], + ): BaseStateData.ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ] = { + if (maybeEmAgent.isEmpty) + throw new AgentInitializationException( + "StorageAgent needs to be EM-controlled." + ) + + /* Build the calculation model */ + val model = + buildModel( + inputModel, + modelConfig, + simulationStartDate, + simulationEndDate, + ) + + ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ]( + simulationStartDate, + simulationEndDate, + model, + services, + outputConfig, + SortedSet.empty, + Map.empty, + requestVoltageDeviationThreshold, + ValueStore.forVoltage( + resolution, + Each( + inputModel.electricalInputModel.getNode + .getvTarget() + .to(PowerSystemUnits.PU) + .getValue + .doubleValue + ), + ), + ValueStore(resolution), + ValueStore(resolution), + ValueStore(resolution), + ValueStore(resolution), + maybeEmAgent.map( + FlexControlledData(_, self.toTyped[FlexRequest]) + ), + ) + } + + override def buildModel( + inputModel: InputModelContainer[StorageInput], + modelConfig: StorageRuntimeConfig, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + ): StorageModel = StorageModel( + inputModel.electricalInputModel, + modelConfig.scaling, + simulationStartDate, + simulationEndDate, + modelConfig.initialSoc, + modelConfig.targetSoc, + ) + + override protected def createInitialState( + baseStateData: BaseStateData.ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ] + ): StorageState = StorageState( + baseStateData.model.eStorage * baseStateData.model.initialSoc, + Kilowatts(0d), + SimonaConstants.INIT_SIM_TICK, + ) + + override protected def createCalcRelevantData( + baseStateData: ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ], + tick: Long, + ): StorageRelevantData = + StorageRelevantData(tick) + + override val calculateModelPowerFunc: ( + Long, + BaseStateData.ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ], + StorageState, + squants.Dimensionless, + ) => ApparentPower = + (_, _, _, _) => + throw new InvalidRequestException( + "PV model cannot be run without secondary data." + ) + + override def calculatePowerWithSecondaryDataAndGoToIdle( + baseStateData: ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ], + modelState: StorageState, + currentTick: Long, + scheduler: ActorRef, + ): State = + throw new InvalidRequestException( + "StorageAgent cannot be used without EM control" + ) + + override def averageResults( + tickToResults: Map[Long, ApparentPower], + windowStart: Long, + windowEnd: Long, + activeToReactivePowerFuncOpt: Option[ + squants.Power => ReactivePower + ], + ): ApparentPower = ParticipantAgentFundamentals.averageApparentPower( + tickToResults, + windowStart, + windowEnd, + activeToReactivePowerFuncOpt, + log, + ) + + override protected def buildResult( + uuid: UUID, + dateTime: ZonedDateTime, + result: ApparentPower, + ): SystemParticipantResult = new StorageResult( + dateTime, + uuid, + result.p.toMegawatts.asMegaWatt, + result.q.toMegavars.asMegaVar, + (-1d).asPercent, // dummy value + ) + + /** Additional actions on a new calculated simulation result. Overridden here + * because SOC needs to be calculated. + * + * @param baseStateData + * The base state data + * @param result + * that has been calculated for the current tick + * @param currentTick + * the current tick + * @return + * updated base state data + */ + override protected def handleCalculatedResult( + baseStateData: ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ], + result: ApparentPower, + currentTick: Long, + ): ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ] = { + + // announce last result to listeners + if (baseStateData.outputConfig.simulationResultInfo) { + val uuid = baseStateData.modelUuid + val dateTime = currentTick.toDateTime(baseStateData.startDate) + + val storedEnergy = baseStateData.stateDataStore + .get(currentTick) + .getOrElse( + throw new IllegalStateException( + s"State data for current tick $currentTick should be available." + ) + ) + .storedEnergy + + val soc = Each( + storedEnergy / baseStateData.model.eStorage + ).toPercent.asPercent + + val storageResult = new StorageResult( + dateTime, + uuid, + result.p.toMegawatts.asMegaWatt, + result.q.toMegavars.asMegaVar, + soc, + ) + + notifyListener(ParticipantResultEvent(storageResult)) + } + + baseStateData.copy( + resultValueStore = ValueStore.updateValueStore( + baseStateData.resultValueStore, + currentTick, + result, + ) + ) + } + + /** Handle an active power change by flex control. + * + * @param tick + * Tick, in which control is issued + * @param baseStateData + * Base state data of the agent + * @param data + * Calculation relevant data + * @param lastState + * Last known model state + * @param setPower + * Setpoint active power + * @return + * Updated model state, a result model and a [[FlexChangeIndicator]] + */ + override def handleControlledPowerChange( + tick: Long, + baseStateData: BaseStateData.ParticipantModelBaseStateData[ + ApparentPower, + StorageRelevantData, + StorageState, + StorageModel, + ], + data: StorageRelevantData, + lastState: StorageState, + setPower: squants.Power, + ): (StorageState, ApparentPower, FlexChangeIndicator) = { + val (updatedState, flexChangeIndicator) = + baseStateData.model.handleControlledPowerChange(data, lastState, setPower) + + val voltage = getAndCheckNodalVoltage(baseStateData, tick) + val reactivePower = baseStateData.model.calculateReactivePower( + setPower, + voltage, + ) + + // TODO: Actually change state and calculate the next tick, when something happens + + (updatedState, ApparentPower(setPower, reactivePower), flexChangeIndicator) + } + + /** Update the last known model state with the given external, relevant data + * + * @param tick + * Tick to update state for + * @param modelState + * Last known model state + * @param calcRelevantData + * Data, relevant for calculation + * @param nodalVoltage + * Current nodal voltage of the agent + * @param model + * Model for calculation + * @return + * The updated state at given tick under consideration of calculation + * relevant data + */ + override protected def updateState( + tick: Long, + modelState: StorageState, + calcRelevantData: StorageRelevantData, + nodalVoltage: squants.Dimensionless, + model: StorageModel, + ): StorageState = ??? + +} diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index c64c62ca3f..008886b99d 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -871,6 +871,73 @@ object SimonaConfig { } + final case class StorageRuntimeConfig( + override val calculateMissingReactivePowerWithModel: scala.Boolean, + override val scaling: scala.Double, + override val uuids: scala.List[java.lang.String], + initialSoc: scala.Double, + targetSoc: scala.Option[scala.Double], + ) extends BaseRuntimeConfig( + calculateMissingReactivePowerWithModel, + scaling, + uuids, + ) + object StorageRuntimeConfig { + def apply( + c: com.typesafe.config.Config, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): SimonaConfig.StorageRuntimeConfig = { + SimonaConfig.StorageRuntimeConfig( + initialSoc = + if (c.hasPathOrNull("initialSoc")) c.getDouble("initialSoc") else 0, + targetSoc = + if (c.hasPathOrNull("targetSoc")) Some(c.getDouble("targetSoc")) + else None, + calculateMissingReactivePowerWithModel = $_reqBln( + parentPath, + c, + "calculateMissingReactivePowerWithModel", + $tsCfgValidator, + ), + scaling = $_reqDbl(parentPath, c, "scaling", $tsCfgValidator), + uuids = $_L$_str(c.getList("uuids"), parentPath, $tsCfgValidator), + ) + } + private def $_reqBln( + parentPath: java.lang.String, + c: com.typesafe.config.Config, + path: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.Boolean = { + if (c == null) false + else + try c.getBoolean(path) + catch { + case e: com.typesafe.config.ConfigException => + $tsCfgValidator.addBadPath(parentPath + path, e) + false + } + } + + private def $_reqDbl( + parentPath: java.lang.String, + c: com.typesafe.config.Config, + path: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.Double = { + if (c == null) 0 + else + try c.getDouble(path) + catch { + case e: com.typesafe.config.ConfigException => + $tsCfgValidator.addBadPath(parentPath + path, e) + 0 + } + } + + } + final case class TransformerControlGroup( measurements: scala.List[java.lang.String], transformers: scala.List[java.lang.String], @@ -2316,6 +2383,7 @@ object SimonaConfig { load: SimonaConfig.Simona.Runtime.Participant.Load, pv: SimonaConfig.Simona.Runtime.Participant.Pv, requestVoltageDeviationThreshold: scala.Double, + storage: SimonaConfig.Simona.Runtime.Participant.Storage, wec: SimonaConfig.Simona.Runtime.Participant.Wec, ) object Participant { @@ -2589,6 +2657,51 @@ object SimonaConfig { } } + final case class Storage( + defaultConfig: SimonaConfig.StorageRuntimeConfig, + individualConfigs: scala.List[SimonaConfig.StorageRuntimeConfig], + ) + object Storage { + def apply( + c: com.typesafe.config.Config, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): SimonaConfig.Simona.Runtime.Participant.Storage = { + SimonaConfig.Simona.Runtime.Participant.Storage( + defaultConfig = SimonaConfig.StorageRuntimeConfig( + if (c.hasPathOrNull("defaultConfig")) + c.getConfig("defaultConfig") + else + com.typesafe.config.ConfigFactory + .parseString("defaultConfig{}"), + parentPath + "defaultConfig.", + $tsCfgValidator, + ), + individualConfigs = $_LSimonaConfig_StorageRuntimeConfig( + c.getList("individualConfigs"), + parentPath, + $tsCfgValidator, + ), + ) + } + private def $_LSimonaConfig_StorageRuntimeConfig( + cl: com.typesafe.config.ConfigList, + parentPath: java.lang.String, + $tsCfgValidator: $TsCfgValidator, + ): scala.List[SimonaConfig.StorageRuntimeConfig] = { + import scala.jdk.CollectionConverters._ + cl.asScala + .map(cv => + SimonaConfig.StorageRuntimeConfig( + cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, + parentPath, + $tsCfgValidator, + ) + ) + .toList + } + } + final case class Wec( defaultConfig: SimonaConfig.WecRuntimeConfig, individualConfigs: scala.List[SimonaConfig.WecRuntimeConfig], @@ -2681,6 +2794,12 @@ object SimonaConfig { if (c.hasPathOrNull("requestVoltageDeviationThreshold")) c.getDouble("requestVoltageDeviationThreshold") else 1e-14, + storage = SimonaConfig.Simona.Runtime.Participant.Storage( + if (c.hasPathOrNull("storage")) c.getConfig("storage") + else com.typesafe.config.ConfigFactory.parseString("storage{}"), + parentPath + "storage.", + $tsCfgValidator, + ), wec = SimonaConfig.Simona.Runtime.Participant.Wec( if (c.hasPathOrNull("wec")) c.getConfig("wec") else com.typesafe.config.ConfigFactory.parseString("wec{}"), diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala new file mode 100644 index 0000000000..f1f948f5d3 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -0,0 +1,380 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant + +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.model.SystemComponent +import edu.ie3.simona.model.participant.StorageModel.{ + RefTargetSocParams, + StorageRelevantData, + StorageState, +} +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.OperationInterval +import squants.Each +import squants.energy.{KilowattHours, Kilowatts, Watts} + +import java.time.ZonedDateTime +import java.util.UUID + +final case class StorageModel( + uuid: UUID, + id: String, + operationInterval: OperationInterval, + qControl: QControl, + sRated: squants.Power, + cosPhiRated: Double, + eStorage: squants.Energy, + pMax: squants.Power, + eta: squants.Dimensionless, + dod: squants.Dimensionless, + initialSoc: Double, // TODO this is ugly and should be solved in a different way, as this value is only used outside the model + targetSoc: Option[Double], // TODO only needed for initializing fields +) extends SystemParticipant[StorageRelevantData, ApparentPower, StorageState]( + uuid, + id, + operationInterval, + qControl, + sRated, + cosPhiRated, + ) { + + private val minEnergy = eStorage * dod.toEach + + // Tolerance fitting for capacities up to GWh + // FIXME make dependent on capacity + private implicit val doubleTolerance: squants.Power = Watts(1e-3) + + /** In order to avoid faulty flexibility options, we want to avoid offering + * charging/discharging that could last less than one second. + */ + private val toleranceMargin = pMax * squants.Seconds(1d) + + /** Minimal allowed energy with tolerance margin added + */ + private val minEnergyWithMargin = minEnergy + (toleranceMargin / eta.toEach) + + /** Maximum allowed energy with tolerance margin added + */ + private val maxEnergyWithMargin = eStorage - (toleranceMargin * eta.toEach) + + private val refTargetSoc = targetSoc.map { target => + val targetEnergy = eStorage * target + + val targetWithPosMargin = // FIXME this should be division + targetEnergy + (toleranceMargin * eta.toEach) + + val targetWithNegMargin = + targetEnergy - (toleranceMargin * eta.toEach) + + RefTargetSocParams( + targetEnergy, + targetWithPosMargin, + targetWithNegMargin, + ) + } + + /** Calculate the power behaviour based on the given data. + * + * @param tick + * Regarded instant in simulation + * @param voltage + * Nodal voltage magnitude + * @param modelState + * Current state of the model + * @param data + * Further needed, secondary data + * @return + * A tuple of active and reactive power + */ + override def calculatePower( + tick: Long, + voltage: squants.Dimensionless, + modelState: StorageState, + data: StorageRelevantData, + ): ApparentPower = ??? + + override protected def calculateActivePower( + modelState: StorageState, + data: StorageRelevantData, + ): squants.Power = + throw new NotImplementedError( + "Storage model cannot calculate power without flexibility control." + ) + + override def determineFlexOptions( + data: StorageRelevantData, + lastState: StorageState, + ): ProvideFlexOptions = { + val currentStoredEnergy = + determineCurrentState(lastState, data.currentTick) + + val chargingPossible = !isFull(currentStoredEnergy) + val dischargingPossible = !isEmpty(currentStoredEnergy) + + val refPower = refTargetSoc + .map { targetParams => + if (currentStoredEnergy <= targetParams.targetWithPosMargin) { + if (currentStoredEnergy >= targetParams.targetWithNegMargin) { + // is within target +/- margin, no charging needed + Kilowatts(0d) + } else { + // below target - margin, charge up to target + pMax + } + } else { + // above target + margin, discharge to target + pMax * (-1d) + } + } + .getOrElse { + // no target set + Kilowatts(0d) + } + + ProvideMinMaxFlexOptions( + uuid, + refPower, + if (dischargingPossible) pMax * (-1) else Kilowatts(0d), + if (chargingPossible) pMax else Kilowatts(0d), + ) + } + + override def handleControlledPowerChange( + data: StorageRelevantData, + lastState: StorageState, + setPower: squants.Power, + ): (StorageState, FlexChangeIndicator) = { + val currentStoredEnergy = + determineCurrentState(lastState, data.currentTick) + + // net power after considering efficiency + val netPower = + if (setPower ~= Kilowatts(0d)) { + // if power is close to zero, set it to zero + Kilowatts(0d) + } else if (setPower > Kilowatts(0d)) { + if (isFull(currentStoredEnergy)) + Kilowatts(0d) // do not keep charging if we're already full + else + // multiply eta if we're charging + setPower * eta.toEach + } else { + if (isEmpty(currentStoredEnergy)) + Kilowatts(0d) // do not keep discharging if we're already empty + else + // divide by eta if we're discharging + // (draining the battery more than we get as output) + setPower * eta.toEach // FIXME this should be division. Check as well with SoC Calculation. + } + + val currentState = + StorageState( + currentStoredEnergy, + netPower, + data.currentTick, + ) // FIXME this should be setPower instead of netPower ? Because the EM receives / sees Power after considering eta / output power. Internal "discharging" needs to be done with netpower! Check whether EM get's setpower or netPower. + + // if the storage is at minimum or maximum charged energy AND we are charging + // or discharging, flex options will be different at the next activation + val isEmptyOrFull = + isEmpty(currentStoredEnergy) || isFull(currentStoredEnergy) + // if target soc is enabled, we can also be at that exact point + val isAtTarget = refTargetSoc.exists { targetParams => + currentStoredEnergy <= targetParams.targetWithPosMargin && + currentStoredEnergy >= targetParams.targetWithNegMargin + } + val isChargingOrDischarging = netPower != Kilowatts(0d) + // if we've been triggered just before we hit the minimum or maximum energy, + // and we're still discharging or charging respectively (happens in edge cases), + // we already set netPower to zero (see above) and also want to refresh flex options + // at the next activation. + // Similarly, if the ref target margin area is hit before hitting target SOC, we want + // to refresh flex options. + val hasObsoleteFlexOptions = + (isFull(currentStoredEnergy) && setPower > Kilowatts(0d)) || + (isEmpty(currentStoredEnergy) && setPower < Kilowatts(0d)) || + (isAtTarget && setPower != Kilowatts(0d)) + + val activateAtNextTick = + ((isEmptyOrFull || isAtTarget) && isChargingOrDischarging) || hasObsoleteFlexOptions + + // calculate the time span until we're full or empty, if applicable + val maybeTimeSpan = + if (!isChargingOrDischarging) { + // we're at 0 kW, do nothing + None + } else if (netPower > Kilowatts(0d)) { + // we're charging, calculate time until we're full or at target energy + + val closestEnergyTarget = refTargetSoc + .flatMap { targetParams => + Option.when( + currentStoredEnergy <= targetParams.targetWithNegMargin + )(targetParams.targetSoc) + } + .getOrElse(eStorage) + + val energyToFull = closestEnergyTarget - currentStoredEnergy + Some(energyToFull / netPower) + } else { + // we're discharging, calculate time until we're at lowest energy allowed or at target energy + + val closestEnergyTarget = refTargetSoc + .flatMap { targetParams => + Option.when( + currentStoredEnergy >= targetParams.targetWithPosMargin + )(targetParams.targetSoc) + } + .getOrElse(minEnergy) + + val energyToEmpty = currentStoredEnergy - closestEnergyTarget + Some(energyToEmpty / (netPower * (-1))) + } + + // calculate the tick from time span + val maybeNextTick = maybeTimeSpan.map { timeSpan => + val ticksToEmpty = Math.round(timeSpan.toSeconds) + data.currentTick + ticksToEmpty + } + + (currentState, FlexChangeIndicator(activateAtNextTick, maybeNextTick)) + } + + private def determineCurrentState( + lastState: StorageState, + currentTick: Long, + ): squants.Energy = { + val timespan = currentTick - lastState.tick + val energyChange = lastState.chargingPower * squants.Seconds(timespan) + + val newEnergy = lastState.storedEnergy + energyChange + + // don't allow under- or overcharge e.g. due to tick rounding error + // allow charges below dod though since batteries can start at 0 kWh + // TODO don't allow SOCs below dod + KilowattHours(0d).max(eStorage.min(newEnergy)) + } + + /** @param storedEnergy + * the stored energy amount to check + * @return + * whether the given stored energy is greater than the maximum charged + * energy allowed (minus a tolerance margin) + */ + private def isFull(storedEnergy: squants.Energy): Boolean = + storedEnergy >= maxEnergyWithMargin + + /** @param storedEnergy + * the stored energy amount to check + * @return + * whether the given stored energy is less than the minimal charged energy + * allowed (plus a tolerance margin) + */ + private def isEmpty(storedEnergy: squants.Energy): Boolean = + storedEnergy <= minEnergyWithMargin +} + +object StorageModel { + + final case class StorageRelevantData( + currentTick: Long + ) extends CalcRelevantData + + final case class StorageState( + storedEnergy: squants.Energy, + chargingPower: squants.Power, + tick: Long, + ) extends ModelState + + final case class RefTargetSocParams( + targetSoc: squants.Energy, + targetWithPosMargin: squants.Energy, + targetWithNegMargin: squants.Energy, + ) + + def apply( + inputModel: StorageInput, + scalingFactor: Double, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + initialSoc: Double, + targetSoc: Option[Double], + ): StorageModel = { + + val scaledInput = inputModel.copy().scale(scalingFactor).build() + + { + // TODO have this in some fail fast + val dod = + scaledInput.getType.getDod.to(PowerSystemUnits.PU).getValue.doubleValue + + if (initialSoc > dod) + throw new RuntimeException( + s"Storage ${scaledInput.getUuid}: Initial SOC can't be lower than DOD" + ) + + if (targetSoc.exists(_ < dod)) + throw new RuntimeException( + s"Storage ${scaledInput.getUuid}: Target SOC can't be lower than DOD" + ) + } + + /* Determine the operation interval */ + val operationInterval: OperationInterval = + SystemComponent.determineOperationInterval( + simulationStartDate, + simulationEndDate, + scaledInput.getOperationTime, + ) + + // build the fixed feed in model + val model = StorageModel( + scaledInput.getUuid, + scaledInput.getId, + operationInterval, + QControl.apply(scaledInput.getqCharacteristics), + Kilowatts( + scaledInput.getType.getsRated + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + scaledInput.getType.getCosPhiRated, + KilowattHours( + scaledInput.getType.geteStorage + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ), + Kilowatts( + scaledInput.getType.getpMax + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + Each( + scaledInput.getType.getEta.to(PowerSystemUnits.PU).getValue.doubleValue + ), + Each( + scaledInput.getType.getDod.to(PowerSystemUnits.PU).getValue.doubleValue + ), + initialSoc, + targetSoc, + ) + + // TODO include activePowerGradient, lifeTime, lifeCycle ? + + model.enable() + model + } + +} diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index b041129c90..6de141228e 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -88,6 +88,7 @@ object ConfigUtil { subConfig.pv.individualConfigs, subConfig.evcs.individualConfigs, subConfig.wec.individualConfigs, + subConfig.storage.individualConfigs, subConfig.em.individualConfigs, ).flatten ), @@ -98,6 +99,7 @@ object ConfigUtil { subConfig.evcs.defaultConfig, subConfig.wec.defaultConfig, subConfig.hp.defaultConfig, + subConfig.storage.defaultConfig, subConfig.em.defaultConfig, ).map { conf => conf.getClass -> conf }.toMap, ) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy new file mode 100644 index 0000000000..8dbcb9bddc --- /dev/null +++ b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy @@ -0,0 +1,381 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant + +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.NodeInput +import edu.ie3.datamodel.models.input.OperatorInput +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.system.type.StorageTypeInput +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.util.TimeUtil +import edu.ie3.util.scala.quantities.Sq +import scala.Option +import spock.lang.Shared +import spock.lang.Specification +import squants.energy.* + +import static edu.ie3.util.quantities.PowerSystemUnits.* +import static tech.units.indriya.quantity.Quantities.getQuantity + +class StorageModelTest extends Specification { + + @Shared + StorageInput inputModel + @Shared + static final Double TOLERANCE = 1e-10 + + def setupSpec() { + def nodeInput = new NodeInput( + UUID.fromString("ad39d0b9-5ad6-4588-8d92-74c7d7de9ace"), + "NodeInput", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + getQuantity(1d, PU), + false, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.LV, + -1) + + def typeInput = new StorageTypeInput( + UUID.fromString("fbee4995-24dd-45e4-9c85-7d986fe99ff3"), + "Test_StorageTypeInput", + getQuantity(100d, EURO), + getQuantity(101d, EURO_PER_MEGAWATTHOUR), + getQuantity(100d, KILOWATTHOUR), + getQuantity(13d, KILOVOLTAMPERE), + 0.997, + getQuantity(10d, KILOWATT), + getQuantity(0.03, PU_PER_HOUR), + getQuantity(0.9, PU), + getQuantity(20d, PERCENT), + getQuantity(43800.0, HOUR), + 100000 + ) + + inputModel = new StorageInput( + UUID.randomUUID(), + "Test_StorageInput", + new OperatorInput(UUID.randomUUID(), "NO_OPERATOR"), + OperationTime.notLimited(), + nodeInput, + CosPhiFixed.CONSTANT_CHARACTERISTIC, + null, + typeInput + ) + } + + def buildStorageModel(Option targetSoc = Option.empty()) { + return StorageModel.apply(inputModel, 1, + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z"), + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T01:00:00Z"), + 0d, + targetSoc) + } + + def "Calculate flex options"() { + given: + def storageModel = buildStorageModel() + def startTick = 3600L + def data = new StorageModel.StorageRelevantData(startTick + timeDelta) + def oldState = new StorageModel.StorageState( + Sq.create(lastStored.doubleValue(), KilowattHours$.MODULE$), + Sq.create(lastPower.doubleValue(), Kilowatts$.MODULE$), + startTick + ) + + when: + def result = (ProvideMinMaxFlexOptions) storageModel.determineFlexOptions(data, oldState) + + then: + Math.abs(result.ref().toKilowatts() - pRef) < TOLERANCE + Math.abs(result.min().toKilowatts() - pMin) < TOLERANCE + Math.abs(result.max().toKilowatts() - pMax) < TOLERANCE + + where: + lastStored | lastPower | timeDelta || pRef | pMin | pMax + // UNCHANGED STATE + // completely empty + 0 | 0 | 1 || 0 | 0 | 10 + // at lowest allowed charge + 20 | 0 | 1 || 0 | 0 | 10 + // at a tiny bit above lowest allowed charge + 20.011d | 0 | 1 || 0 | -10 | 10 + // at mid-level charge + 60 | 0 | 1 || 0 | -10 | 10 + // almost fully charged + 99.989d | 0 | 1 || 0 | -10 | 10 + // fully charged + 100 | 0 | 1 || 0 | -10 | 0 + // CHANGED STATE + // discharged to lowest allowed charge + 30 | -10 | 3600 || 0 | 0 | 10 + // almost discharged to lowest allowed charge + 30 | -10 | 3590 || 0 | -10 | 10 + // charged to mid-level charge + 50 | 10 | 3600 || 0 | -10 | 10 + // discharged to mid-level charge + 70 | -10 | 3600 || 0 | -10 | 10 + // almost fully charged + 95 | 4.98 | 3600 || 0 | -10 | 10 + // fully charged + 95 | 5 | 3600 || 0 | -10 | 0 + } + + def "Calculate flex options with target SOC"() { + given: + def storageModel = buildStorageModel(Option.apply(0.5d)) + def startTick = 3600L + def data = new StorageModel.StorageRelevantData(startTick + 1) + def oldState = new StorageModel.StorageState( + Sq.create(lastStored.doubleValue(), KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = (ProvideMinMaxFlexOptions) storageModel.determineFlexOptions(data, oldState) + + then: + Math.abs(result.ref().toKilowatts() - pRef) < TOLERANCE + Math.abs(result.min().toKilowatts() - pMin) < TOLERANCE + Math.abs(result.max().toKilowatts() - pMax) < TOLERANCE + + where: + lastStored || pRef | pMin | pMax + // completely empty + 0 || 10 | 0 | 10 + // at lowest allowed charge + 20 || 10 | 0 | 10 + // below margin of ref power target + 49.9974 || 10 | -10 | 10 + // within margin below ref power target + 49.9976 || 0 | -10 | 10 + // exactly at ref power target + 50 || 0 | -10 | 10 + // within margin above ref power target + 50.0024 || 0 | -10 | 10 + // above margin of ref power target + 50.0026 || -10 | -10 | 10 + // at mid-level charge + 60 || -10 | -10 | 10 + // fully charged + 100 || -10 | -10 | 0 + } + + def "Handle controlled power change"() { + given: + def storageModel = buildStorageModel() + def startTick = 3600L + def data = new StorageModel.StorageRelevantData(startTick + 1) + def oldState = new StorageModel.StorageState( + Sq.create(lastStored.doubleValue(), KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = storageModel.handleControlledPowerChange( + data, + oldState, + Sq.create(setPower.doubleValue(), Kilowatts$.MODULE$) + ) + + then: + Math.abs(result._1.chargingPower().toKilowatts() - expPower.doubleValue()) < TOLERANCE + result._1.tick() == startTick + 1 + Math.abs(result._1.storedEnergy().toKilowattHours() - lastStored.doubleValue()) < TOLERANCE + def flexChangeIndication = result._2 + flexChangeIndication.changesAtTick().defined == expScheduled + flexChangeIndication.changesAtTick().map(x -> x == startTick + 1 + expDelta).getOrElse(_ -> true) + flexChangeIndication.changesAtNextActivation() == expActiveNext + + where: + lastStored | setPower || expPower | expActiveNext | expScheduled | expDelta + // no power + 0 | 0 || 0 | false | false | 0 + 50 | 0 || 0 | false | false | 0 + 100 | 0 || 0 | false | false | 0 + // charging on empty + 0 | 1 || 0.9 | true | true | 100 * 3600 / 0.9 + 0 | 2.5 || 2.25 | true | true | 40 * 3600 / 0.9 + 0 | 5 || 4.5 | true | true | 20 * 3600 / 0.9 + 0 | 10 || 9 | true | true | 10 * 3600 / 0.9 + // charging on half full + 50 | 5 || 4.5 | false | true | 10 * 3600 / 0.9 + 50 | 10 || 9 | false | true | 5 * 3600 / 0.9 + // discharging on half full + 50 | -5 || -4.5 | false | true | 6 * 3600 / 0.9 + 50 | -10 || -9 | false | true | 3 * 3600 / 0.9 + // discharging on full + 100 | -5 || -4.5 | true | true | 16 * 3600 / 0.9 + 100 | -10 || -9 | true | true | 8 * 3600 / 0.9 + } + + def "Handle controlled power change with ref target SOC"() { + given: + def storageModel = buildStorageModel(Option.apply(0.5d)) + def startTick = 3600L + def data = new StorageModel.StorageRelevantData(startTick + 1) + def oldState = new StorageModel.StorageState( + Sq.create(lastStored.doubleValue(), KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = storageModel.handleControlledPowerChange( + data, + oldState, + Sq.create(setPower.doubleValue(), Kilowatts$.MODULE$) + ) + + then: + Math.abs(result._1.chargingPower().toKilowatts() - expPower.doubleValue()) < TOLERANCE + result._1.tick() == startTick + 1 + Math.abs(result._1.storedEnergy().toKilowattHours() - lastStored.doubleValue()) < TOLERANCE + def flexChangeIndication = result._2 + flexChangeIndication.changesAtTick().defined == expScheduled + flexChangeIndication.changesAtTick().map(x -> x == startTick + 1 + expDelta).getOrElse(_ -> true) + flexChangeIndication.changesAtNextActivation() == expActiveNext + + where: + lastStored | setPower || expPower | expActiveNext | expScheduled | expDelta + // no power + 0 | 0 || 0 | false | false | 0 + 50 | 0 || 0 | false | false | 0 + 100 | 0 || 0 | false | false | 0 + // charging on empty + 0 | 1 || 0.9 | true | true | 50 * 3600 / 0.9 + 0 | 2.5 || 2.25 | true | true | 20 * 3600 / 0.9 + 0 | 5 || 4.5 | true | true | 10 * 3600 / 0.9 + 0 | 10 || 9 | true | true | 5 * 3600 / 0.9 + // charging on target ref + 50 | 5 || 4.5 | true | true | 10 * 3600 / 0.9 + 50 | 10 || 9 | true | true | 5 * 3600 / 0.9 + // discharging on target ref + 50 | -5 || -4.5 | true | true | 6 * 3600 / 0.9 + 50 | -10 || -9 | true | true | 3 * 3600 / 0.9 + // discharging on full + 100 | -5 || -4.5 | true | true | 10 * 3600 / 0.9 + 100 | -10 || -9 | true | true | 5 * 3600 / 0.9 + } + + def "Handle the edge case of discharging in tolerance margins"() { + given: + def storageModel = buildStorageModel() + def startTick = 1800L + def data = new StorageModel.StorageRelevantData(startTick + 1) + // margin is at ~ 20.0030864 kWh + def oldState = new StorageModel.StorageState( + Sq.create(20.002d, KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = storageModel.handleControlledPowerChange( + data, + oldState, + Sq.create(-5d, Kilowatts$.MODULE$) + ) + + then: + Math.abs(result._1.chargingPower().toKilowatts()) < TOLERANCE + result._1.tick() == startTick + 1 + Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE + def flexChangeIndication = result._2 + !flexChangeIndication.changesAtTick().defined + flexChangeIndication.changesAtNextActivation() + } + + def "Handle the edge case of charging in tolerance margins"() { + given: + def storageModel = buildStorageModel() + def startTick = 1800L + def data = new StorageModel.StorageRelevantData(startTick + 1) + // margin is at ~ 99.9975 kWh + def oldState = new StorageModel.StorageState( + Sq.create(99.999d, KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = storageModel.handleControlledPowerChange( + data, + oldState, + Sq.create(9d, Kilowatts$.MODULE$) + ) + + then: + Math.abs(result._1.chargingPower().toKilowatts()) < TOLERANCE + result._1.tick() == startTick + 1 + Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE + def flexChangeIndication = result._2 + !flexChangeIndication.changesAtTick().defined + flexChangeIndication.changesAtNextActivation() + } + + def "Handle the edge case of discharging in positive target margin"() { + given: + def storageModel = buildStorageModel(Option.apply(0.3d)) + def startTick = 1800L + def data = new StorageModel.StorageRelevantData(startTick + 1) + // margin is at ~ 30.0025 kWh + def oldState = new StorageModel.StorageState( + Sq.create(30.0024d, KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = storageModel.handleControlledPowerChange( + data, + oldState, + Sq.create(-10d, Kilowatts$.MODULE$) + ) + + then: + Math.abs(result._1.chargingPower().toKilowatts() - (-9d)) < TOLERANCE + result._1.tick() == startTick + 1 + Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE + def flexChangeIndication = result._2 + flexChangeIndication.changesAtTick() == Option.apply(startTick + 1L + 4001L) + flexChangeIndication.changesAtNextActivation() + } + + def "Handle the edge case of charging in negative target margin"() { + given: + def storageModel = buildStorageModel(Option.apply(0.4d)) + def startTick = 1800L + def data = new StorageModel.StorageRelevantData(startTick + 1) + // margin is at ~ 39.9975 kWh + def oldState = new StorageModel.StorageState( + Sq.create(39.998d, KilowattHours$.MODULE$), + Sq.create(0d, Kilowatts$.MODULE$), + startTick + ) + + when: + def result = storageModel.handleControlledPowerChange( + data, + oldState, + Sq.create(5d, Kilowatts$.MODULE$) + ) + + then: + Math.abs(result._1.chargingPower().toKilowatts() - (4.5d)) < TOLERANCE + result._1.tick() == startTick + 1 + Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE + def flexChangeIndication = result._2 + flexChangeIndication.changesAtTick() == Option.apply(startTick + 1L + 48002L) + flexChangeIndication.changesAtNextActivation() + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala index cc0ce1f43b..09f6acb019 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala @@ -12,6 +12,7 @@ import edu.ie3.simona.agent.participant.hp.HpAgent import edu.ie3.simona.agent.participant.load.LoadAgent.FixedLoadAgent import edu.ie3.simona.agent.participant.pv.PvAgent import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData +import edu.ie3.simona.agent.participant.storage.StorageAgent import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent @@ -96,6 +97,291 @@ class EmAgentIT private implicit val classicSystem: ActorSystem = system.toClassic "An em agent" when { + "having load, pv and storage agents connected" should { + "be initialized correctly and run through some activations" in { + val resultListener = TestProbe[ResultEvent]("ResultListener") + val primaryServiceProxy = + TestProbe[ServiceMessage]("PrimaryServiceProxy") + val weatherService = TestProbe[ServiceMessage]("WeatherService") + val scheduler = TestProbe[SchedulerMessage]("Scheduler") + + val emAgent = spawn( + EmAgent( + emInput, + modelConfig, + outputConfigOn, + "PRIORITIZED", + simulationStartDate, + parent = Left(scheduler.ref), + listener = Iterable(resultListener.ref), + ), + "EmAgent", + ) + + val loadAgent = TestActorRef( + new FixedLoadAgent( + scheduler = scheduler.ref.toClassic, + initStateData = ParticipantInitializeStateData( + loadInput, + LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = true, + scaling = 1d, + modelBehaviour = "fix", + reference = "power", + uuids = List.empty, + ), + primaryServiceProxy.ref.toClassic, + None, + simulationStartDate, + simulationEndDate, + resolution, + simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, + outputConfigOff, + Some(emAgent), + ), + listener = Iterable(resultListener.ref.toClassic), + ), + "LoadAgent", + ) + val pvAgent = TestActorRef( + new PvAgent( + scheduler = scheduler.ref.toClassic, + initStateData = ParticipantInitializeStateData( + pvInput, + PvRuntimeConfig( + calculateMissingReactivePowerWithModel = true, + scaling = 2d, + uuids = List.empty, + ), + primaryServiceProxy.ref.toClassic, + Iterable(ActorWeatherService(weatherService.ref.toClassic)), + simulationStartDate, + simulationEndDate, + resolution, + simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, + outputConfigOff, + Some(emAgent), + ), + listener = Iterable(resultListener.ref.toClassic), + ), + "PvAgent", + ) + val storageAgent = TestActorRef( + new StorageAgent( + scheduler = scheduler.ref.toClassic, + initStateData = ParticipantInitializeStateData( + householdStorageInput, + StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = true, + scaling = 1d, + uuids = List.empty, + initialSoc = 0d, + targetSoc = None, + ), + primaryServiceProxy.ref.toClassic, + None, + simulationStartDate, + simulationEndDate, + resolution, + simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, + outputConfigOff, + Some(emAgent), + ), + listener = Iterable(resultListener.ref.toClassic), + ), + "StorageAgent", + ) + + scheduler.expectNoMessage() + + /* INIT */ + + // load + loadAgent ! Activation(INIT_SIM_TICK) + + primaryServiceProxy.expectMessage( + PrimaryServiceRegistrationMessage(loadInput.getUuid) + ) + loadAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) + + // the order of the two messages is not given + val emAgentActivation = scheduler + .receiveMessages(2) + .flatMap { + case Completion(ref, maybeNewTick) => + ref shouldBe loadAgent.toTyped + maybeNewTick shouldBe None + None + case ScheduleActivation(ref, tick, unlockKey) => + // em agent schedules itself + tick shouldBe 0 + unlockKey shouldBe None + Some(ref) + case unexpected => + fail(s"Received unexpected message $unexpected") + } + .headOption + .value + + // pv + pvAgent ! Activation(INIT_SIM_TICK) + + primaryServiceProxy.expectMessage( + PrimaryServiceRegistrationMessage(pvInput.getUuid) + ) + pvAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) + + // deal with weather service registration + weatherService.expectMessage( + RegisterForWeatherMessage( + pvInput.getNode.getGeoPosition.getY, + pvInput.getNode.getGeoPosition.getX, + ) + ) + + pvAgent ! RegistrationSuccessfulMessage( + weatherService.ref.toClassic, + Some(0L), + ) + + scheduler.expectMessage(Completion(pvAgent)) + + // storage + storageAgent ! Activation(INIT_SIM_TICK) + + primaryServiceProxy.expectMessage( + PrimaryServiceRegistrationMessage(householdStorageInput.getUuid) + ) + storageAgent ! RegistrationFailedMessage( + primaryServiceProxy.ref.toClassic + ) + + scheduler.expectMessage(Completion(storageAgent)) + + /* TICK 0 + LOAD: 0.000269 MW + PV: -0.005685 MW + STORAGE: SOC 0 % + -> charge with 5 kW + -> remaining -0.0004161 MW + */ + + emAgentActivation ! Activation(0) + + pvAgent ! ProvideWeatherMessage( + 0, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(400d), + WattsPerSquareMeter(200d), + Celsius(0d), + MetersPerSecond(0d), + ), + Some(7200), + ) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(emResult: EmResult) => + emResult.getInputModel shouldBe emInput.getUuid + emResult.getTime shouldBe 0L.toDateTime + emResult.getP should equalWithTolerance( + (-0.000416087825).asMegaWatt + ) + emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) + } + + scheduler.expectMessage(Completion(emAgentActivation, Some(7200))) + + /* TICK 7200 + LOAD: 0.000269 MW (unchanged) + PV: -0.003797 MW + STORAGE: SOC 63.3 % + -> charge with 3.5282 kW + -> remaining 0 MW + */ + + emAgentActivation ! Activation(7200) + + pvAgent ! ProvideWeatherMessage( + 7200, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(300d), + WattsPerSquareMeter(500d), + Celsius(0d), + MetersPerSecond(0d), + ), + Some(14400), + ) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(emResult: EmResult) => + emResult.getInputModel shouldBe emInput.getUuid + emResult.getTime shouldBe 7200.toDateTime + emResult.getP should equalWithTolerance(0.asMegaWatt) + emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) + } + + scheduler.expectMessage(Completion(emAgentActivation, Some(13107))) + + /* TICK 13107 + LOAD: 0.000269 MW (unchanged) + PV: -0.003797 MW (unchanged) + STORAGE: SOC 100 % + -> charge with 0 kW + -> remaining -0.003528 MW + */ + + emAgentActivation ! Activation(13107) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(emResult: EmResult) => + emResult.getInputModel shouldBe emInput.getUuid + emResult.getTime shouldBe 13107L.toDateTime + emResult.getP should equalWithTolerance( + (-0.0035281545552).asMegaWatt + ) + emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) + } + + scheduler.expectMessage(Completion(emAgentActivation, Some(14400))) + + /* TICK 14400 + LOAD: 0.000269 MW (unchanged) + PV: -0.000066 MW + STORAGE: SOC 100 % + -> charge with -0.202956 kW + -> remaining 0 MW + */ + + // send weather data before activation, which can happen + // it got cloudy now... + pvAgent ! ProvideWeatherMessage( + 14400, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(5d), + WattsPerSquareMeter(5d), + Celsius(0d), + MetersPerSecond(0d), + ), + Some(21600), + ) + + emAgentActivation ! Activation(14400) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(emResult: EmResult) => + emResult.getInputModel shouldBe emInput.getUuid + emResult.getTime shouldBe 14400L.toDateTime + emResult.getP should equalWithTolerance(0.asMegaWatt) + emResult.getQ should equalWithTolerance(0.000088285537.asMegaVar) + } + + scheduler.expectMessage(Completion(emAgentActivation, Some(21600))) + + } + } "having load, pv and heat pump agents connected" should { "be initialized correctly and run through some activations" in { diff --git a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala new file mode 100644 index 0000000000..27ac18a340 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala @@ -0,0 +1,646 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant + +import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.datamodel.models.input.system.characteristic.QV +import edu.ie3.datamodel.models.result.system.StorageResult +import edu.ie3.simona.agent.ValueStore +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData +import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ + ParticipantInitializeStateData, + ParticipantInitializingStateData, + SimpleInputContainer, +} +import edu.ie3.simona.agent.participant.storage.StorageAgent +import edu.ie3.simona.agent.state.AgentState.Idle +import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig +import edu.ie3.simona.event.ResultEvent.{ + FlexOptionsResultEvent, + ParticipantResultEvent, +} +import edu.ie3.simona.event.notifier.NotifierConfig +import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} +import edu.ie3.simona.ontology.messages.Activation +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.ontology.messages.PowerMessage.{ + AssetPowerChangedMessage, + RequestAssetPowerMessage, +} +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage +import edu.ie3.simona.test.ParticipantAgentSpec +import edu.ie3.simona.test.common.input.StorageInputTestData +import edu.ie3.simona.util.ConfigUtil +import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import edu.ie3.simona.util.TickUtil.TickLong +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.{Megavars, ReactivePower, Vars} +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.{ActorRef, ActorSystem} +import org.apache.pekko.testkit.{TestFSMRef, TestProbe} +import squants.{Each, Power} +import squants.energy.{Kilowatts, Megawatts, Watts} + +import java.time.ZonedDateTime +import scala.collection.SortedMap + +class StorageAgentModelCalculationSpec + extends ParticipantAgentSpec( + ActorSystem( + "StorageAgentModelCalculationSpec", + ConfigFactory + .parseString(""" + |akka.loggers =["akka.event.slf4j.Slf4jLogger"] + |akka.loglevel="DEBUG" + """.stripMargin), + ) + ) + with StorageInputTestData { + + protected implicit val simulationStartDate: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") + protected val simulationEndDate: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T01:00:00Z") + + /* Alter the input model to have a voltage sensitive reactive power calculation */ + private val storageInputQv = storageInput + .copy() + .qCharacteristics(new QV("qV:{(0.95,-0.625),(1.05,0.625)}")) + .build() + + /* Assign this test to receive the result events from agent */ + override val systemListener: Iterable[ActorRef] = Vector(self) + + private val simonaConfig: SimonaConfig = + createSimonaConfig( + LoadModelBehaviour.FIX, + LoadReference.ActivePower(Kilowatts(0d)), + ) + private val outputConfig = NotifierConfig( + simulationResultInfo = true, + powerRequestReply = false, + flexResult = true, + ) + private val configUtil = ConfigUtil.ParticipantConfigUtil( + simonaConfig.simona.runtime.participant + ) + private val modelConfig = configUtil.getOrDefault[StorageRuntimeConfig]( + storageInputQv.getUuid + ) + private val services = Iterable.empty + private val resolution = simonaConfig.simona.powerflow.resolution.getSeconds + + private implicit val powerTolerance: Power = Watts(0.1) + private implicit val reactivePowerTolerance: ReactivePower = Vars(0.1) + + "A storage agent with model calculation depending on no secondary data service" should { + val emAgent = TestProbe("EmAgent") + + val initStateData = ParticipantInitializeStateData[ + StorageInput, + StorageRuntimeConfig, + ApparentPower, + ]( + inputModel = storageInputQv, + modelConfig = modelConfig, + secondaryDataServices = services, + simulationStartDate = simulationStartDate, + simulationEndDate = simulationEndDate, + resolution = resolution, + requestVoltageDeviationThreshold = + simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, + outputConfig = outputConfig, + primaryServiceProxy = primaryServiceProxy.ref, + maybeEmAgent = Some(emAgent.ref.toTyped), + ) + + "end in correct state with correct state data after initialisation" in { + val storageAgent = TestFSMRef( + new StorageAgent( + scheduler = scheduler.ref, + initStateData = initStateData, + listener = Iterable.empty, + ) + ) + + scheduler.send(storageAgent, Activation(INIT_SIM_TICK)) + + /* Actor should ask for registration with primary service */ + primaryServiceProxy.expectMsg( + PrimaryServiceRegistrationMessage(storageInputQv.getUuid) + ) + /* State should be information handling and having correct state data */ + storageAgent.stateName shouldBe HandleInformation + storageAgent.stateData match { + case ParticipantInitializingStateData( + inputModel, + modelConfig, + secondaryDataServices, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + maybeEmAgent, + ) => + inputModel shouldBe SimpleInputContainer(storageInputQv) + modelConfig shouldBe modelConfig + secondaryDataServices shouldBe services + simulationStartDate shouldBe simulationStartDate + simulationEndDate shouldBe simulationEndDate + resolution shouldBe resolution + requestVoltageDeviationThreshold shouldBe simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold + outputConfig shouldBe outputConfig + maybeEmAgent shouldBe Some(emAgent.ref.toTyped) + case unsuitableStateData => + fail(s"Agent has unsuitable state data '$unsuitableStateData'.") + } + + /* Refuse registration */ + primaryServiceProxy.send( + storageAgent, + RegistrationFailedMessage(primaryServiceProxy.ref), + ) + + emAgent.expectMsg( + RegisterParticipant( + storageInputQv.getUuid, + storageAgent.toTyped, + storageInputQv, + ) + ) + emAgent.expectMsg( + ScheduleFlexRequest(storageInputQv.getUuid, 0) + ) + + scheduler.expectMsg(Completion(storageAgent.toTyped)) + + /* ... as well as corresponding state and state data */ + storageAgent.stateName shouldBe Idle + storageAgent.stateData match { + case ParticipantModelBaseStateData( + startDate, + endDate, + _, + secondaryDataServices, + outputConfig, + additionalActivationTicks, + foreseenDataTicks, + _, + voltageValueStore, + resultValueStore, + requestValueStore, + _, + _, + _, + ) => + /* Base state data */ + startDate shouldBe simulationStartDate + endDate shouldBe simulationEndDate + secondaryDataServices shouldBe services + outputConfig shouldBe outputConfig + additionalActivationTicks shouldBe empty + foreseenDataTicks shouldBe Map.empty + voltageValueStore shouldBe ValueStore( + resolution, + SortedMap(0L -> Each(1.0)), + ) + resultValueStore shouldBe ValueStore( + resolution + ) + requestValueStore shouldBe ValueStore[ApparentPower]( + resolution + ) + case unrecognized => + fail( + s"Did not find expected state data $ParticipantModelBaseStateData, but $unrecognized" + ) + } + } + + "answer with zero power, if asked directly after initialisation" in { + val storageAgent = TestFSMRef( + new StorageAgent( + scheduler = scheduler.ref, + initStateData = initStateData, + listener = Iterable.empty, + ) + ) + + scheduler.send(storageAgent, Activation(INIT_SIM_TICK)) + + /* Refuse registration with primary service */ + primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] + primaryServiceProxy.send( + storageAgent, + RegistrationFailedMessage(primaryServiceProxy.ref), + ) + + emAgent.expectMsgType[RegisterParticipant] + emAgent.expectMsg(ScheduleFlexRequest(storageInputQv.getUuid, 0)) + + /* I'm not interested in the content of the Completion */ + scheduler.expectMsgType[Completion] + + storageAgent.stateName shouldBe Idle + /* State data has already been tested */ + + storageAgent ! RequestAssetPowerMessage( + 0, + Each(1d), + Each(0d), + ) + expectMsg( + AssetPowerChangedMessage( + Megawatts(0d), + Megavars(0d), + ) + ) + + inside(storageAgent.stateData) { + case modelBaseStateData: ParticipantModelBaseStateData[_, _, _, _] => + modelBaseStateData.requestValueStore shouldBe ValueStore[ + ApparentPower + ]( + resolution, + SortedMap( + 0L -> ApparentPower( + Megawatts(0d), + Megavars(0d), + ) + ), + ) + case _ => + fail( + s"Did not find expected state data $ParticipantModelBaseStateData, but ${storageAgent.stateData}" + ) + } + } + + "provide correct flex options when in Idle" in { + val resultListener = TestProbe("ResultListener") + + val storageAgent = TestFSMRef( + new StorageAgent( + scheduler = scheduler.ref, + initStateData = initStateData, + listener = Iterable(resultListener.ref), + ) + ) + + scheduler.send(storageAgent, Activation(INIT_SIM_TICK)) + + /* Refuse registration with primary service */ + primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] + primaryServiceProxy.send( + storageAgent, + RegistrationFailedMessage(primaryServiceProxy.ref), + ) + + emAgent.expectMsgType[RegisterParticipant] + emAgent.expectMsg(ScheduleFlexRequest(storageInputQv.getUuid, 0)) + + /* I am not interested in the Completion */ + scheduler.expectMsgType[Completion] + awaitAssert(storageAgent.stateName shouldBe Idle) + /* State data is tested in another test */ + + val pMax = Kilowatts( + storageInputQv.getType.getpMax + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ) + + /* TICK 0 (expected activation) + - charging with pMax (12.961 kW) + - expecting changing flex options indicator (charging from empty) + */ + + emAgent.send(storageAgent, RequestFlexOptions(0)) + + emAgent.expectMsgType[ProvideFlexOptions] match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe storageInputQv.getUuid + refPower should approximate(Kilowatts(0.0)) + minPower should approximate(Kilowatts(0.0)) + maxPower should approximate(pMax) + } + + resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => + flexResult.getInputModel shouldBe storageInputQv.getUuid + flexResult.getTime shouldBe 0.toDateTime(simulationStartDate) + flexResult.getpRef should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMin should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMax should beEquivalentTo(storageInputQv.getType.getpMax) + } + + emAgent.send( + storageAgent, + IssuePowerControl( + 0, + Kilowatts(storageInputQv.getType.getpMax().getValue.doubleValue()), + ), + ) + + // next potential activation at fully charged battery: + // net power = 12.961kW * 0.92 = 11.92412kW + // time to charge fully ~= 16.7727262054h = 60382 ticks (rounded) + emAgent.expectMsgPF() { + case FlexCtrlCompletion( + modelUuid, + result, + requestAtNextActivation, + requestAtTick, + ) => + modelUuid shouldBe storageInputQv.getUuid + result.p should approximate(pMax) + result.q should approximate(Megavars(0)) + requestAtNextActivation shouldBe true + requestAtTick shouldBe Some(60382) + } + + resultListener.expectMsgPF() { + case ParticipantResultEvent(result: StorageResult) => + result.getInputModel shouldBe storageInputQv.getUuid + result.getTime shouldBe 0.toDateTime(simulationStartDate) + result.getP should beEquivalentTo(storageInputQv.getType.getpMax) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0d.asPercent) + } + + /* TICK 28800 (unplanned activation) + - charging with 9 kW + - expecting trigger revoke + */ + + // Re-request flex options, since we've been asked to + emAgent.send(storageAgent, RequestFlexOptions(28800)) + + emAgent.expectMsgType[ProvideFlexOptions] match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe storageInputQv.getUuid + refPower should approximate(Kilowatts(0.0)) + minPower should approximate(pMax * -1) + maxPower should approximate(pMax) + } + + resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => + flexResult.getInputModel shouldBe storageInputQv.getUuid + flexResult.getTime shouldBe 28800.toDateTime(simulationStartDate) + flexResult.getpRef should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMin should beEquivalentTo( + storageInputQv.getType.getpMax().multiply(-1) + ) + flexResult.getpMax should beEquivalentTo(storageInputQv.getType.getpMax) + } + + emAgent.send(storageAgent, IssuePowerControl(28800, Kilowatts(9))) + + // after 8 hours, we're at about half full storage: 95.39296 kWh + // net power = 9kW * 0.92 = 8.28kW + // time to charge fully ~= 12.6337004831h = 45481 ticks (rounded) from now + // current tick is 28800, thus: 28800 + 45481 = 74281 + emAgent.expectMsgPF() { + case FlexCtrlCompletion( + modelUuid, + result, + requestAtNextActivation, + requestAtTick, + ) => + modelUuid shouldBe storageInputQv.getUuid + result.p should approximate(Kilowatts(9)) + result.q should approximate(Megavars(0)) + requestAtNextActivation shouldBe false + requestAtTick shouldBe Some(74281) + } + + resultListener.expectMsgPF() { + case ParticipantResultEvent(result: StorageResult) => + result.getInputModel shouldBe storageInputQv.getUuid + result.getTime shouldBe 28800.toDateTime(simulationStartDate) + result.getP should beEquivalentTo(9d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(47.69648d.asPercent) + } + + /* TICK 36000 (unplanned activation) + - discharging with pMax (-12.961 kW) + - expecting trigger revoke + */ + + emAgent.send( + storageAgent, + IssuePowerControl( + 36000, + Kilowatts( + storageInputQv.getType.getpMax().multiply(-1).getValue.doubleValue + ), + ), + ) + + // after 2 hours, we're at: 111.95296 kWh + // net power = -12.961kW * 0.92 = -11.92412kW + // time to discharge until lowest energy (40 kWh) ~= 6.03423648873h = 21723 ticks (rounded) from now + // current tick is 36000, thus: 36000 + 21723 = 57723 + emAgent.expectMsgPF() { + case FlexCtrlCompletion( + modelUuid, + result, + requestAtNextActivation, + requestAtTick, + ) => + modelUuid shouldBe storageInputQv.getUuid + result.p should approximate(pMax * -1) + result.q should approximate(Megavars(0)) + requestAtNextActivation shouldBe false + requestAtTick shouldBe Some(57723) + } + + resultListener.expectMsgPF() { + case ParticipantResultEvent(result: StorageResult) => + result.getInputModel shouldBe storageInputQv.getUuid + result.getTime shouldBe 36000.toDateTime(simulationStartDate) + result.getP should beEquivalentTo( + storageInputQv.getType.getpMax().multiply(-1) + ) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(55.97648d.asPercent) + } + + /* TICK 43200 (unplanned activation) + - charging with 12 kW + - expecting trigger revoke + */ + + emAgent.send(storageAgent, IssuePowerControl(43200, Kilowatts(12))) + + // after 2 hours, we're at: 88.10472 kWh + // net power = 12 * 0.92 = 11.04 kW + // time to charge until full ~= 10.135442029h = 36488 ticks (rounded) from now + // current tick is 43200, thus: 43200 + 36488 = 79688 + emAgent.expectMsgPF() { + case FlexCtrlCompletion( + modelUuid, + result, + requestAtNextActivation, + requestAtTick, + ) => + modelUuid shouldBe storageInputQv.getUuid + result.p should approximate(Kilowatts(12)) + result.q should approximate(Megavars(0)) + requestAtNextActivation shouldBe false + requestAtTick shouldBe Some(79688) + } + + resultListener.expectMsgPF() { + case ParticipantResultEvent(result: StorageResult) => + result.getInputModel shouldBe storageInputQv.getUuid + result.getTime shouldBe 43200.toDateTime(simulationStartDate) + result.getP should beEquivalentTo(12d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(44.05236d.asPercent) + } + + /* TICK 79688 (expected activation) + - discharging with 12 kW + - expecting changing flex options indicator (discharging from full) + */ + + // Request flex options + emAgent.send(storageAgent, RequestFlexOptions(79688)) + + emAgent.expectMsgType[ProvideFlexOptions] match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe storageInputQv.getUuid + refPower should approximate(Kilowatts(0.0)) + minPower should approximate(pMax * -1) + maxPower should approximate(Kilowatts(0.0)) + } + + resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => + flexResult.getInputModel shouldBe storageInputQv.getUuid + flexResult.getTime shouldBe 79688.toDateTime(simulationStartDate) + flexResult.getpRef should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMin should beEquivalentTo( + storageInputQv.getType.getpMax().multiply(-1) + ) + flexResult.getpMax should beEquivalentTo(0d.asKiloWatt) + } + + emAgent.send(storageAgent, IssuePowerControl(79688, Kilowatts(-12))) + + // we're full now at 200 kWh + // net power = -12 * 0.92 = -11.04 kW + // time to discharge until lowest energy ~= 14.4927536232h = 52174 ticks (rounded) from now + // current tick is 79688, thus: 79688 + 52174 = 131862 + emAgent.expectMsgPF() { + case FlexCtrlCompletion( + modelUuid, + result, + requestAtNextActivation, + requestAtTick, + ) => + modelUuid shouldBe storageInputQv.getUuid + result.p should approximate(Kilowatts(-12)) + result.q should approximate(Megavars(0)) + requestAtNextActivation shouldBe true + requestAtTick shouldBe Some(131862) + } + + resultListener.expectMsgPF() { + case ParticipantResultEvent(result: StorageResult) => + result.getInputModel shouldBe storageInputQv.getUuid + result.getTime shouldBe 79688.toDateTime(simulationStartDate) + result.getP should beEquivalentTo((-12d).asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(100d.asPercent) + } + + /* TICK 131862 (expected activation) + - no charging + - expecting no changing flex options indicator + */ + + // Request flex options + emAgent.send(storageAgent, RequestFlexOptions(131862)) + + emAgent.expectMsgType[ProvideFlexOptions] match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe storageInputQv.getUuid + refPower should approximate(Kilowatts(0.0)) + minPower should approximate(Kilowatts(0.0)) + maxPower should approximate(pMax) + } + + resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => + flexResult.getInputModel shouldBe storageInputQv.getUuid + flexResult.getTime shouldBe 131862.toDateTime(simulationStartDate) + flexResult.getpRef should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMin should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMax should beEquivalentTo(storageInputQv.getType.getpMax) + } + + emAgent.send(storageAgent, IssuePowerControl(131862, Kilowatts(0d))) + + // we're not charging or discharging, no new expected tick + emAgent.expectMsgPF() { + case FlexCtrlCompletion( + modelUuid, + result, + requestAtNextActivation, + requestAtTick, + ) => + modelUuid shouldBe storageInputQv.getUuid + result.p should approximate(Kilowatts(0)) + result.q should approximate(Megavars(0)) + requestAtNextActivation shouldBe false + requestAtTick shouldBe None + } + + resultListener.expectMsgPF() { + case ParticipantResultEvent(result: StorageResult) => + result.getInputModel shouldBe storageInputQv.getUuid + result.getTime shouldBe 131862.toDateTime(simulationStartDate) + result.getP should beEquivalentTo(0.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(19.999866666667d.asPercent) + } + + } + + } +} diff --git a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala index 26cb038257..e9a1010194 100644 --- a/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/ConfigTestData.scala @@ -121,6 +121,15 @@ trait ConfigTestData { | individualConfigs = [] |} | + |simona.runtime.participant.storage = { + | defaultConfig = { + | calculateMissingReactivePowerWithModel = false + | uuids = ["default"] + | scaling = 1.0 + | } + | individualConfigs = [] + |} + | |simona.runtime.participant.em = { | defaultConfig = { | calculateMissingReactivePowerWithModel = false diff --git a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala index c1d93d7476..6f16001303 100644 --- a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala @@ -203,6 +203,21 @@ trait DefaultTestData { | ] |} | + |simona.runtime.participant.storage = { + | defaultConfig = { + | calculateMissingReactivePowerWithModel = false + | uuids = ["default"] + | scaling = 1.0 + | } + | individualConfigs = [ + | { + | calculateMissingReactivePowerWithModel = false + | uuids = ["9abe950d-362e-4ffe-b686-500f84d8f368"] + | scaling = 1.0 + | } + | ] + |} + | |simona.runtime.participant.em = { | defaultConfig = { | calculateMissingReactivePowerWithModel = false diff --git a/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala new file mode 100644 index 0000000000..6f39fcf208 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala @@ -0,0 +1,50 @@ +/* + * © 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.models.OperationTime +import edu.ie3.datamodel.models.input.OperatorInput +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.datamodel.models.input.system.`type`.StorageTypeInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.simona.test.common.DefaultTestData +import edu.ie3.util.quantities.PowerSystemUnits._ +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units._ + +import java.util.UUID + +trait StorageInputTestData extends DefaultTestData with NodeInputTestData { + + protected val storageTypeInput = new StorageTypeInput( + UUID.fromString("fbee4995-24dd-45e4-9c85-7d986fe99ff3"), + "Dummy_StorageTypeInput", + Quantities.getQuantity(100d, EURO), + Quantities.getQuantity(101d, EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(200d, KILOWATTHOUR), + Quantities.getQuantity(13d, KILOVOLTAMPERE), + 0.997, + Quantities.getQuantity(12.961, KILOWATT), + Quantities.getQuantity(0.03, PU_PER_HOUR), + Quantities.getQuantity(0.92, PU), + Quantities.getQuantity(20d, PERCENT), + Quantities.getQuantity(43800.0, HOUR), + 100000, + ) + + protected val storageInput = new StorageInput( + UUID.randomUUID(), + "Dummy_StorageInput", + new OperatorInput(UUID.randomUUID(), "NO_OPERATOR"), + OperationTime.notLimited(), + nodeInputNoSlackNs04KvA, + CosPhiFixed.CONSTANT_CHARACTERISTIC, + null, + storageTypeInput, + ) + +} diff --git a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala index 1356fef9cb..92d295097a 100644 --- a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala +++ b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala @@ -58,7 +58,7 @@ class ConfigUtilSpec inside(actual) { case ParticipantConfigUtil(configs, defaultConfigs) => configs shouldBe Map.empty[UUID, SimonaConfig.LoadRuntimeConfig] - defaultConfigs.size shouldBe 7 + defaultConfigs.size shouldBe 8 inside(defaultConfigs.get(classOf[LoadRuntimeConfig])) { case Some( @@ -118,7 +118,7 @@ class ConfigUtilSpec UUID.fromString("49f250fa-41ff-4434-a083-79c98d260a76") ) - defaultConfigs.size shouldBe 7 + defaultConfigs.size shouldBe 8 inside(defaultConfigs.get(classOf[LoadRuntimeConfig])) { case Some( LoadRuntimeConfig( @@ -331,7 +331,7 @@ class ConfigUtilSpec UUID.fromString("49f250fa-41ff-4434-a083-79c98d260a76") ) - defaultConfigs.size shouldBe 7 + defaultConfigs.size shouldBe 8 inside(defaultConfigs.get(classOf[FixedFeedInRuntimeConfig])) { case Some( FixedFeedInRuntimeConfig( From 96acba9bfbafb6aad1fdae4194b4e93ff32d9b51 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 20 Mar 2024 09:57:16 +0100 Subject: [PATCH 11/67] Adding to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33813e38c3..3dd0d8f214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implemented scaling of all relevant input parameters [#764](https://github.com/ie3-institute/simona/issues/764) - Consider scaling factor with flex options [#734](https://github.com/ie3-institute/simona/issues/734) - Implementation of Energy Management Agents [#204](https://github.com/ie3-institute/simona/issues/204) +- Implementation of StorageAgent [#309](https://github.com/ie3-institute/simona/issues/309) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) From 054ddb65080d7484ce1a276c9fd7a5d29f84504e Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 20 Mar 2024 17:40:25 +0100 Subject: [PATCH 12/67] A little bit of tidying --- .../storage/StorageAgentFundamentals.scala | 14 ++-- .../model/participant/StorageModel.scala | 71 ++++++++++--------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala index ffa44746c5..92081bcc52 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -49,12 +49,12 @@ import edu.ie3.simona.util.SimonaConstants import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.ActorRef import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps import org.apache.pekko.actor.typed.{ActorRef => TypedActorRef} -import squants.Each -import squants.energy.Kilowatts +import squants.{Dimensionless, Each, Power} import java.time.ZonedDateTime import java.util.UUID @@ -167,7 +167,7 @@ trait StorageAgentFundamentals ] ): StorageState = StorageState( baseStateData.model.eStorage * baseStateData.model.initialSoc, - Kilowatts(0d), + zeroKW, SimonaConstants.INIT_SIM_TICK, ) @@ -191,7 +191,7 @@ trait StorageAgentFundamentals StorageModel, ], StorageState, - squants.Dimensionless, + Dimensionless, ) => ApparentPower = (_, _, _, _) => throw new InvalidRequestException( @@ -218,7 +218,7 @@ trait StorageAgentFundamentals windowStart: Long, windowEnd: Long, activeToReactivePowerFuncOpt: Option[ - squants.Power => ReactivePower + Power => ReactivePower ], ): ApparentPower = ParticipantAgentFundamentals.averageApparentPower( tickToResults, @@ -331,7 +331,7 @@ trait StorageAgentFundamentals ], data: StorageRelevantData, lastState: StorageState, - setPower: squants.Power, + setPower: Power, ): (StorageState, ApparentPower, FlexChangeIndicator) = { val (updatedState, flexChangeIndicator) = baseStateData.model.handleControlledPowerChange(data, lastState, setPower) @@ -367,7 +367,7 @@ trait StorageAgentFundamentals tick: Long, modelState: StorageState, calcRelevantData: StorageRelevantData, - nodalVoltage: squants.Dimensionless, + nodalVoltage: Dimensionless, model: StorageModel, ): StorageState = ??? diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index f1f948f5d3..7edc87518c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -19,8 +19,9 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptio import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.OperationInterval -import squants.Each +import edu.ie3.util.scala.quantities.DefaultQuantities._ import squants.energy.{KilowattHours, Kilowatts, Watts} +import squants.{Dimensionless, Each, Energy, Power, Seconds} import java.time.ZonedDateTime import java.util.UUID @@ -30,12 +31,12 @@ final case class StorageModel( id: String, operationInterval: OperationInterval, qControl: QControl, - sRated: squants.Power, + sRated: Power, cosPhiRated: Double, - eStorage: squants.Energy, - pMax: squants.Power, - eta: squants.Dimensionless, - dod: squants.Dimensionless, + eStorage: Energy, + pMax: Power, + eta: Dimensionless, + dod: Dimensionless, initialSoc: Double, // TODO this is ugly and should be solved in a different way, as this value is only used outside the model targetSoc: Option[Double], // TODO only needed for initializing fields ) extends SystemParticipant[StorageRelevantData, ApparentPower, StorageState]( @@ -51,12 +52,12 @@ final case class StorageModel( // Tolerance fitting for capacities up to GWh // FIXME make dependent on capacity - private implicit val doubleTolerance: squants.Power = Watts(1e-3) + private implicit val doubleTolerance: Power = Watts(1e-3) /** In order to avoid faulty flexibility options, we want to avoid offering * charging/discharging that could last less than one second. */ - private val toleranceMargin = pMax * squants.Seconds(1d) + private val toleranceMargin = pMax * Seconds(1d) /** Minimal allowed energy with tolerance margin added */ @@ -97,7 +98,7 @@ final case class StorageModel( */ override def calculatePower( tick: Long, - voltage: squants.Dimensionless, + voltage: Dimensionless, modelState: StorageState, data: StorageRelevantData, ): ApparentPower = ??? @@ -105,7 +106,7 @@ final case class StorageModel( override protected def calculateActivePower( modelState: StorageState, data: StorageRelevantData, - ): squants.Power = + ): Power = throw new NotImplementedError( "Storage model cannot calculate power without flexibility control." ) @@ -125,7 +126,7 @@ final case class StorageModel( if (currentStoredEnergy <= targetParams.targetWithPosMargin) { if (currentStoredEnergy >= targetParams.targetWithNegMargin) { // is within target +/- margin, no charging needed - Kilowatts(0d) + zeroKW } else { // below target - margin, charge up to target pMax @@ -137,39 +138,39 @@ final case class StorageModel( } .getOrElse { // no target set - Kilowatts(0d) + zeroKW } ProvideMinMaxFlexOptions( uuid, refPower, - if (dischargingPossible) pMax * (-1) else Kilowatts(0d), - if (chargingPossible) pMax else Kilowatts(0d), + if (dischargingPossible) pMax * (-1) else zeroKW, + if (chargingPossible) pMax else zeroKW, ) } override def handleControlledPowerChange( data: StorageRelevantData, lastState: StorageState, - setPower: squants.Power, + setPower: Power, ): (StorageState, FlexChangeIndicator) = { val currentStoredEnergy = determineCurrentState(lastState, data.currentTick) // net power after considering efficiency val netPower = - if (setPower ~= Kilowatts(0d)) { + if (setPower ~= zeroKW) { // if power is close to zero, set it to zero - Kilowatts(0d) - } else if (setPower > Kilowatts(0d)) { + zeroKW + } else if (setPower > zeroKW) { if (isFull(currentStoredEnergy)) - Kilowatts(0d) // do not keep charging if we're already full + zeroKW // do not keep charging if we're already full else // multiply eta if we're charging setPower * eta.toEach } else { if (isEmpty(currentStoredEnergy)) - Kilowatts(0d) // do not keep discharging if we're already empty + zeroKW // do not keep discharging if we're already empty else // divide by eta if we're discharging // (draining the battery more than we get as output) @@ -192,7 +193,7 @@ final case class StorageModel( currentStoredEnergy <= targetParams.targetWithPosMargin && currentStoredEnergy >= targetParams.targetWithNegMargin } - val isChargingOrDischarging = netPower != Kilowatts(0d) + val isChargingOrDischarging = netPower != zeroKW // if we've been triggered just before we hit the minimum or maximum energy, // and we're still discharging or charging respectively (happens in edge cases), // we already set netPower to zero (see above) and also want to refresh flex options @@ -200,9 +201,9 @@ final case class StorageModel( // Similarly, if the ref target margin area is hit before hitting target SOC, we want // to refresh flex options. val hasObsoleteFlexOptions = - (isFull(currentStoredEnergy) && setPower > Kilowatts(0d)) || - (isEmpty(currentStoredEnergy) && setPower < Kilowatts(0d)) || - (isAtTarget && setPower != Kilowatts(0d)) + (isFull(currentStoredEnergy) && setPower > zeroKW) || + (isEmpty(currentStoredEnergy) && setPower < zeroKW) || + (isAtTarget && setPower != zeroKW) val activateAtNextTick = ((isEmptyOrFull || isAtTarget) && isChargingOrDischarging) || hasObsoleteFlexOptions @@ -212,7 +213,7 @@ final case class StorageModel( if (!isChargingOrDischarging) { // we're at 0 kW, do nothing None - } else if (netPower > Kilowatts(0d)) { + } else if (netPower > zeroKW) { // we're charging, calculate time until we're full or at target energy val closestEnergyTarget = refTargetSoc @@ -252,16 +253,16 @@ final case class StorageModel( private def determineCurrentState( lastState: StorageState, currentTick: Long, - ): squants.Energy = { + ): Energy = { val timespan = currentTick - lastState.tick - val energyChange = lastState.chargingPower * squants.Seconds(timespan) + val energyChange = lastState.chargingPower * Seconds(timespan) val newEnergy = lastState.storedEnergy + energyChange // don't allow under- or overcharge e.g. due to tick rounding error // allow charges below dod though since batteries can start at 0 kWh // TODO don't allow SOCs below dod - KilowattHours(0d).max(eStorage.min(newEnergy)) + zeroKWH.max(eStorage.min(newEnergy)) } /** @param storedEnergy @@ -270,7 +271,7 @@ final case class StorageModel( * whether the given stored energy is greater than the maximum charged * energy allowed (minus a tolerance margin) */ - private def isFull(storedEnergy: squants.Energy): Boolean = + private def isFull(storedEnergy: Energy): Boolean = storedEnergy >= maxEnergyWithMargin /** @param storedEnergy @@ -279,7 +280,7 @@ final case class StorageModel( * whether the given stored energy is less than the minimal charged energy * allowed (plus a tolerance margin) */ - private def isEmpty(storedEnergy: squants.Energy): Boolean = + private def isEmpty(storedEnergy: Energy): Boolean = storedEnergy <= minEnergyWithMargin } @@ -290,15 +291,15 @@ object StorageModel { ) extends CalcRelevantData final case class StorageState( - storedEnergy: squants.Energy, - chargingPower: squants.Power, + storedEnergy: Energy, + chargingPower: Power, tick: Long, ) extends ModelState final case class RefTargetSocParams( - targetSoc: squants.Energy, - targetWithPosMargin: squants.Energy, - targetWithNegMargin: squants.Energy, + targetSoc: Energy, + targetWithPosMargin: Energy, + targetWithNegMargin: Energy, ) def apply( From 8404a12f6affa49fe83121a2607e91beaaf4347f Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Tue, 9 Apr 2024 18:28:54 +0200 Subject: [PATCH 13/67] Implementing requested change. --- .../scala/edu/ie3/simona/main/RunSimona.scala | 18 ++++++++---------- .../edu/ie3/simona/sim/setup/SimonaSetup.scala | 4 ++++ .../sim/setup/SimonaStandaloneSetup.scala | 2 ++ .../edu/ie3/simona/sim/SimonaSimSpec.scala | 2 ++ .../ie3/simona/sim/setup/SimonaSetupSpec.scala | 2 ++ 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/main/RunSimona.scala b/src/main/scala/edu/ie3/simona/main/RunSimona.scala index 3ba84bbef0..8265ed0cdb 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimona.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimona.scala @@ -11,7 +11,6 @@ import edu.ie3.simona.sim.setup.SimonaSetup import edu.ie3.util.scala.quantities.QuantityUtil import org.apache.pekko.util.Timeout -import java.io.File import java.nio.file.Path import java.util.Locale import scala.concurrent.duration.FiniteDuration @@ -41,7 +40,7 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { val successful = run(simonaSetup) - printGoodbye(successful) + printGoodbye(successful, simonaSetup.logOutputDir) // prevents cutting of the log when having a fast simulation Thread.sleep(1000) @@ -56,7 +55,10 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { ) } - private def printGoodbye(successful: Boolean): Unit = { + private def printGoodbye( + successful: Boolean, + outputPath: String = "", + ): Unit = { val myWords = Array( "\"Vielleicht ist heute ein besonders guter Tag zum Sterben.\" - Worf (in Star Trek: Der erste Kontakt)", "\"Assimiliert das!\" - Worf (in Star Trek: Der erste Kontakt)", @@ -75,14 +77,10 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { // to ensure that the link to the log is printed last Thread.sleep(1000) - val root: Path = Path.of( - new File(".").getAbsoluteFile.getParent, - "logs", - "simona", - "simona.log", - ) + val path = Path.of(outputPath).resolve("simona.log").toUri + logger.error( - s"Simulation stopped due to the occurrence of an error! The full log can be found here: $root" + s"Simulation stopped due to the occurrence of an error! The full log can be found here: $path" ) } } diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala index f96a144a36..af38bc0101 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala @@ -38,6 +38,10 @@ trait SimonaSetup { */ val args: Array[String] + /** Directory of the log output. + */ + val logOutputDir: String + /** Creates the runtime event listener * * @param context diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala index 6788a652f2..2c96bd5cd2 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -66,6 +66,7 @@ class SimonaStandaloneSetup( resultFileHierarchy: ResultFileHierarchy, runtimeEventQueue: Option[LinkedBlockingQueue[RuntimeEvent]] = None, override val args: Array[String], + override val logOutputDir: String, ) extends SimonaSetup { override def gridAgents( @@ -391,5 +392,6 @@ object SimonaStandaloneSetup extends LazyLogging with SetupHelper { resultFileHierarchy, runtimeEventQueue, mainArgs, + resultFileHierarchy.logOutputDir, ) } diff --git a/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala b/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala index c6ffda8fab..a3c1e6be02 100644 --- a/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala @@ -398,6 +398,8 @@ object SimonaSimSpec { override val args: Array[String] = Array.empty[String] + override val logOutputDir: String = "" + override def runtimeEventListener( context: ActorContext[_] ): ActorRef[RuntimeEventListener.Request] = context.spawn( diff --git a/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala b/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala index d8cc467dd9..407ca5cb08 100644 --- a/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala @@ -32,6 +32,8 @@ class SimonaSetupSpec extends UnitSpec with SimonaSetup with SubGridGateMokka { override val args: Array[String] = Array.empty[String] + override val logOutputDir: String = "" + override def runtimeEventListener( context: ActorContext[_] ): ActorRef[RuntimeEventListener.Request] = From 6afc63a67092dfd2680f00c9ac3129263a708836 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Mon, 6 May 2024 09:06:28 +0200 Subject: [PATCH 14/67] fix power double tolerance --- .../edu/ie3/simona/model/participant/StorageModel.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 7edc87518c..497eb0d9fe 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -50,9 +50,8 @@ final case class StorageModel( private val minEnergy = eStorage * dod.toEach - // Tolerance fitting for capacities up to GWh - // FIXME make dependent on capacity - private implicit val doubleTolerance: Power = Watts(1e-3) + // max Tolerance 1W till GWh storage + private implicit val doubleTolerance: Power = eStorage/Seconds(1) * 3.6e-12 /** In order to avoid faulty flexibility options, we want to avoid offering * charging/discharging that could last less than one second. From 837d83827866622824eff9db077ad671eaab98f6 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Mon, 6 May 2024 09:08:37 +0200 Subject: [PATCH 15/67] remove adapt to removed dod, lifecycle etc. --- .../ie3/simona/model/participant/StorageModel.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 497eb0d9fe..35d1608a25 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -20,7 +20,7 @@ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMin import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.OperationInterval import edu.ie3.util.scala.quantities.DefaultQuantities._ -import squants.energy.{KilowattHours, Kilowatts, Watts} +import squants.energy.{KilowattHours, Kilowatts} import squants.{Dimensionless, Each, Energy, Power, Seconds} import java.time.ZonedDateTime @@ -48,10 +48,10 @@ final case class StorageModel( cosPhiRated, ) { - private val minEnergy = eStorage * dod.toEach + private val minEnergy = zeroKWH // max Tolerance 1W till GWh storage - private implicit val doubleTolerance: Power = eStorage/Seconds(1) * 3.6e-12 + private implicit val doubleTolerance: Power = eStorage / Seconds(1) * 3.6e-12 /** In order to avoid faulty flexibility options, we want to avoid offering * charging/discharging that could last less than one second. @@ -259,9 +259,7 @@ final case class StorageModel( val newEnergy = lastState.storedEnergy + energyChange // don't allow under- or overcharge e.g. due to tick rounding error - // allow charges below dod though since batteries can start at 0 kWh - // TODO don't allow SOCs below dod - zeroKWH.max(eStorage.min(newEnergy)) + minEnergy.max(eStorage.min(newEnergy)) } /** @param storedEnergy @@ -371,7 +369,7 @@ object StorageModel { targetSoc, ) - // TODO include activePowerGradient, lifeTime, lifeCycle ? + // TODO include activePowerGradient,? model.enable() model From 10b979588636b6741ed5747fbaa2f9d27f606a41 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Mon, 6 May 2024 10:57:38 +0200 Subject: [PATCH 16/67] move storage init params to ConfigFailFast --- docs/readthedocs/config.md | 9 ++ .../ie3/simona/config/ConfigFailFast.scala | 40 ++++++ .../model/participant/StorageModel.scala | 16 --- .../StorageAgentModelCalculationSpec.scala | 6 +- .../simona/config/ConfigFailFastSpec.scala | 122 +++++++++++++++++- 5 files changed, 172 insertions(+), 21 deletions(-) diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index f964378c67..fdaa24fa42 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -196,6 +196,15 @@ The load reference can scale the load model behaviour to reach the given annual If an individual configuration is to be assigned, the default configuration parameters must be adjusted accordingly. Runtime configurations of other system participants are done similarly, except that model behavior and reference are not defined. +### Storage runtime configuration + +The storage model requires default parameter for the inital state of charge (Soc) and the target Soc for electrical energy storages. Soc's need to be between 0.0 and <= 1.0. + + initialSoc = "0.0" + targetSoc = "1.0" + +Individual configuration can be assigned accordingly. + ## Event configuration Tba: diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index 48ea507133..2cf588a0c8 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -148,6 +148,9 @@ case object ConfigFailFast extends LazyLogging { /* Check control scheme definitions */ simonaConfig.simona.control.foreach(checkControlSchemes) + + /* Check correct parameterization of storages */ + checkStoragesConfig(simonaConfig.simona.runtime.participant.storage) } /** Checks for valid sink configuration @@ -643,6 +646,43 @@ case object ConfigFailFast extends LazyLogging { } } + /** Check the suitability of storage config parameters. + * + * @param StorageRuntimeConfig + * RuntimeConfig of Storages + */ + private def checkStoragesConfig( + storageConfig: SimonaConfig.Simona.Runtime.Participant.Storage + ): Unit = { + if ( + storageConfig.defaultConfig.initialSoc < 0.0 || storageConfig.defaultConfig.initialSoc > 1.0 + ) + throw new RuntimeException( + s"StorageRuntimeConfig: Default initial SOC needs to be between 0.0 and 1.0." + ) + + if ( + storageConfig.defaultConfig.targetSoc.exists( + _ < 0.0 + ) || storageConfig.defaultConfig.targetSoc.exists(_ > 1.0) + ) + throw new RuntimeException( + s"StorageRuntimeConfig: Default target SOC needs to be between 0.0 and 1.0." + ) + + storageConfig.individualConfigs.foreach { config => + if (config.initialSoc < 0.0 || config.initialSoc > 1.0) + throw new RuntimeException( + s"StorageRuntimeConfig: ${config.uuids} initial SOC needs to be between 0.0 and 1.0." + ) + + if (config.targetSoc.exists(_ < 0.0) || config.targetSoc.exists(_ > 1.0)) + throw new RuntimeException( + s"StorageRuntimeConfig: ${config.uuids} target SOC needs to be between 0.0 and 1.0." + ) + } + } + /** Check the default config * * @param config diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 35d1608a25..ddac3b6ab5 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -310,22 +310,6 @@ object StorageModel { val scaledInput = inputModel.copy().scale(scalingFactor).build() - { - // TODO have this in some fail fast - val dod = - scaledInput.getType.getDod.to(PowerSystemUnits.PU).getValue.doubleValue - - if (initialSoc > dod) - throw new RuntimeException( - s"Storage ${scaledInput.getUuid}: Initial SOC can't be lower than DOD" - ) - - if (targetSoc.exists(_ < dod)) - throw new RuntimeException( - s"Storage ${scaledInput.getUuid}: Target SOC can't be lower than DOD" - ) - } - /* Determine the operation interval */ val operationInterval: OperationInterval = SystemComponent.determineOperationInterval( diff --git a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala index 27ac18a340..5103c7dc5c 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala @@ -11,6 +11,8 @@ import edu.ie3.datamodel.models.input.system.StorageInput import edu.ie3.datamodel.models.input.system.characteristic.QV import edu.ie3.datamodel.models.result.system.StorageResult import edu.ie3.simona.agent.ValueStore +import edu.ie3.simona.agent.grid.GridAgentMessages.AssetPowerChangedMessage +import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ @@ -32,10 +34,6 @@ import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions -import edu.ie3.simona.ontology.messages.PowerMessage.{ - AssetPowerChangedMessage, - RequestAssetPowerMessage, -} import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index ddf44cc83d..422b3f87a9 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -22,8 +22,8 @@ import edu.ie3.simona.test.common.{ConfigTestData, UnitSpec} import edu.ie3.simona.util.ConfigUtil.{CsvConfigUtil, NotifierIdentifier} import edu.ie3.util.TimeUtil -import java.time.{Duration, ZonedDateTime} import java.time.temporal.ChronoUnit +import java.time.{Duration, ZonedDateTime} class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "Validating the configs" when { @@ -1028,6 +1028,126 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } } + "checking the parameterization of storages" should { + val checkStorageConfigs = + PrivateMethod[Unit](Symbol("checkStoragesConfig")) + + "throw exception if default initial SOC is negative" in { + + val defaultConfig: SimonaConfig.StorageRuntimeConfig = + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(java.util.UUID.randomUUID().toString), + -0.5, + Some(0.8), + ) + val storageConfig = SimonaConfig.Simona.Runtime.Participant + .Storage(defaultConfig, List.empty) + + intercept[RuntimeException] { + ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) + }.getMessage shouldBe "StorageRuntimeConfig: Default initial SOC needs to be between 0.0 and 1.0." + } + + "throw exception if default target SOC is negative" in { + val defaultConfig: SimonaConfig.StorageRuntimeConfig = + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(java.util.UUID.randomUUID().toString), + 0.5, + Some(-0.8), + ) + val storageConfig = SimonaConfig.Simona.Runtime.Participant + .Storage(defaultConfig, List.empty) + + intercept[RuntimeException] { + ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) + }.getMessage shouldBe "StorageRuntimeConfig: Default target SOC needs to be between 0.0 and 1.0." + } + + "throw exception if individual initial SOC is negative" in { + val uuid = java.util.UUID.randomUUID().toString + val defaultConfig: SimonaConfig.StorageRuntimeConfig = + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(java.util.UUID.randomUUID().toString), + 0.5, + Some(0.8), + ) + val individualConfig: List[SimonaConfig.StorageRuntimeConfig] = List( + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(uuid), + -0.5, + Some(0.8), + ) + ) + val storageConfig = SimonaConfig.Simona.Runtime.Participant + .Storage(defaultConfig, individualConfig) + + intercept[RuntimeException] { + ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) + }.getMessage shouldBe s"StorageRuntimeConfig: List($uuid) initial SOC needs to be between 0.0 and 1.0." + } + + "throw exception if individual target SOC is negative" in { + val uuid = java.util.UUID.randomUUID().toString + val defaultConfig: SimonaConfig.StorageRuntimeConfig = + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(java.util.UUID.randomUUID().toString), + 0.5, + Some(0.8), + ) + val individualConfig: List[SimonaConfig.StorageRuntimeConfig] = List( + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(uuid), + 0.5, + Some(-0.8), + ) + ) + val storageConfig = SimonaConfig.Simona.Runtime.Participant + .Storage(defaultConfig, individualConfig) + + intercept[RuntimeException] { + ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) + }.getMessage shouldBe s"StorageRuntimeConfig: List($uuid) target SOC needs to be between 0.0 and 1.0." + } + + "not throw exception if all parameters are in parameter range" in { + val defaultConfig: SimonaConfig.StorageRuntimeConfig = + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(java.util.UUID.randomUUID().toString), + 0.5, + Some(0.8), + ) + val individualConfig: List[SimonaConfig.StorageRuntimeConfig] = List( + SimonaConfig.StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + 1.0, + List(java.util.UUID.randomUUID().toString), + 0.5, + Some(0.8), + ) + ) + val storageConfig = SimonaConfig.Simona.Runtime.Participant + .Storage(defaultConfig, individualConfig) + + noException should be thrownBy { + ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) + } + } + } + "validating the typesafe config" when { "checking the availability of pekko logger parameterization" should { val checkPekkoLoggers = PrivateMethod[Unit](Symbol("checkPekkoLoggers")) From 963d8efc2bee5553c6eb44b6284a4f18f3e2bfb3 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Tue, 7 May 2024 10:18:20 +0200 Subject: [PATCH 17/67] Adapting to newly introduced method in `IdCoordinateSource`. --- .../service/weather/SampleWeatherSource.scala | 9 +++ .../service/weather/WeatherSource.scala | 65 ++++++------------- .../service/weather/WeatherSourceSpec.scala | 41 +++++------- 3 files changed, 43 insertions(+), 72 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala index 5788aca327..2ecf881c3f 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala @@ -168,6 +168,15 @@ object SampleWeatherSource { else Vector.empty[CoordinateDistance].asJava } + + override def findCornerPoints( + coordinate: Point, + distance: ComparableQuantity[Length], + ): util.List[CoordinateDistance] = + findCornerPoints( + coordinate, + getClosestCoordinates(coordinate, 9, distance), + ) } // these lists contain the hourly weather values for each first of the month of 2011 + january of 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 cb8fec8db8..1872fca6c8 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -100,54 +100,27 @@ trait WeatherSource { ): Try[Iterable[CoordinateDistance]] = { val queryPoint = coordinate.toPoint - /* Go and get the nearest coordinates, that are known to the weather source */ - val nearestCoords = idCoordinateSource - .getClosestCoordinates( - queryPoint, - amountOfInterpolationCoords, - maxCoordinateDistance, - ) - .asScala + /* Go and get the corner coordinates, that are within a given distance */ + val possibleCornerPoints = idCoordinateSource.findCornerPoints( + queryPoint, + maxCoordinateDistance, + ) - nearestCoords.find(coordinateDistance => - coordinateDistance.getCoordinateB.equalsExact(queryPoint, 1e-6) - ) match { - case Some(exactHit) => - /* The queried coordinate hit one of the weather coordinates. Don't average and take it directly */ - Success(Vector(exactHit)) - case None => - /* There aren't enough coordinates inside the given distance */ - if (nearestCoords.size < amountOfInterpolationCoords) { - Failure( - ServiceException( - s"There are not enough coordinates for averaging. Found ${nearestCoords.size} within the given distance of " + - s"$maxCoordinateDistance but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." - ) - ) - } else { - /* Check, if the queried coordinate is surrounded at each quadrant */ - val (topLeft, topRight, bottomLeft, bottomRight) = nearestCoords - .map(_.getCoordinateB) - .foldLeft((false, false, false, false)) { - case ((tl, tr, bl, br), point) => - ( - tl || (point.getX < queryPoint.getX && point.getY > queryPoint.getY), - tr || (point.getX > queryPoint.getX && point.getY > queryPoint.getY), - bl || (point.getX < queryPoint.getX && point.getY < queryPoint.getY), - br || (point.getX > queryPoint.getX && point.getY < queryPoint.getY), - ) - } + val nr = possibleCornerPoints.size() - /* There has to be a coordinate in each quadrant */ - if (topLeft && topRight && bottomLeft && bottomRight) - Success(nearestCoords) - else - Failure( - ServiceException( - s"The queried point shall be surrounded by $amountOfInterpolationCoords weather coordinates, which are in each quadrant. This is not the case." - ) - ) - } + if (nr == 1) { + // found one exact match + Success(possibleCornerPoints.asScala) + } else if (nr == amountOfInterpolationCoords) { + // found enough points for interpolating + Success(possibleCornerPoints.asScala) + } else { + Failure( + ServiceException( + s"There are not enough coordinates for averaging. Found ${possibleCornerPoints.size()} within the given distance of " + + s"$maxCoordinateDistance but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." + ) + ) } } diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala index a9daaa0f35..3da4ac7f85 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala @@ -6,16 +6,8 @@ package edu.ie3.simona.service.weather -import edu.ie3.datamodel.io.factory.timeseries.{ - CosmoIdCoordinateFactory, - IconIdCoordinateFactory, - IdCoordinateFactory, -} import edu.ie3.datamodel.io.source.IdCoordinateSource -import edu.ie3.simona.exceptions.{ - InvalidConfigParameterException, - ServiceException, -} +import edu.ie3.simona.exceptions.ServiceException import edu.ie3.simona.ontology.messages.services.WeatherMessage import edu.ie3.simona.service.weather.WeatherSource.{ AgentCoordinates, @@ -35,7 +27,7 @@ import java.util.Optional import javax.measure.quantity.Length import scala.jdk.CollectionConverters._ import scala.jdk.OptionConverters._ -import scala.util.{Failure, Success, Try} +import scala.util.{Failure, Success} class WeatherSourceSpec extends UnitSpec { private val coordinate0 = GeoUtils.buildPoint(51.47, 7.41) @@ -47,29 +39,17 @@ class WeatherSourceSpec extends UnitSpec { 9, ) match { case Failure(exception: ServiceException) => - exception.getMessage shouldBe "There are not enough coordinates for averaging. Found 8 within the given distance of 400000 m but need 9. Please make sure that there are enough coordinates within the given distance." + exception.getMessage shouldBe "There are not enough coordinates for averaging. Found 4 within the given distance of 400000 m but need 9. Please make sure that there are enough coordinates within the given distance." case _ => fail("You shall not pass!") } } "issue a ServiceException, if there are not enough coordinates in max distance available" in { DummyWeatherSource.getNearestCoordinatesWithDistances( AgentCoordinates(coordinate0.getY, coordinate0.getX), - 9, - ) match { - case Failure(exception: ServiceException) => - exception.getMessage shouldBe "There are not enough coordinates for averaging. Found 8 within the given distance of 400000 m but need 9. Please make sure that there are enough coordinates within the given distance." - case _ => fail("You shall not pass!") - } - } - - "issue a ServiceException, if the queried coordinate is not surrounded by the found weather coordinates" in { - val agentCoordinates = AgentCoordinates(51.3, 7.3) - DummyWeatherSource.getNearestCoordinatesWithDistances( - agentCoordinates, - 4, + 5, ) match { case Failure(exception: ServiceException) => - exception.getMessage shouldBe "The queried point shall be surrounded by 4 weather coordinates, which are in each quadrant. This is not the case." + exception.getMessage shouldBe "There are not enough coordinates for averaging. Found 4 within the given distance of 400000 m but need 5. Please make sure that there are enough coordinates within the given distance." case _ => fail("You shall not pass!") } } @@ -244,7 +224,7 @@ class WeatherSourceSpec extends UnitSpec { case Failure(exception: ServiceException) => exception.getMessage shouldBe "Determination of coordinate weights failed." exception.getCause shouldBe ServiceException( - "There are not enough coordinates for averaging. Found 8 within the given distance of 400000 m but need 9. Please make sure that there are enough coordinates within the given distance." + "There are not enough coordinates for averaging. Found 4 within the given distance of 400000 m but need 9. Please make sure that there are enough coordinates within the given distance." ) case _ => fail("You shall not pass!") } @@ -415,5 +395,14 @@ case object WeatherSourceSpec { ): util.List[CoordinateDistance] = { calculateCoordinateDistances(coordinate, n, coordinateToId.keySet.asJava) } + + override def findCornerPoints( + coordinate: Point, + distance: ComparableQuantity[Length], + ): util.List[CoordinateDistance] = + findCornerPoints( + coordinate, + getClosestCoordinates(coordinate, 9, distance), + ) } } From 8b4538671a8d42d1127f139d23ff3fbbf06c9ec1 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Fri, 31 May 2024 08:35:36 +0200 Subject: [PATCH 18/67] spotless --- docs/readthedocs/allpages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/readthedocs/allpages.md b/docs/readthedocs/allpages.md index b97e268aa3..d58cac0a20 100644 --- a/docs/readthedocs/allpages.md +++ b/docs/readthedocs/allpages.md @@ -14,4 +14,4 @@ maxdepth: 1 * !allpages -``` \ No newline at end of file +``` From 3b81a673cc8ce8ebddff42ab83f31867eae3fdfa Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Fri, 31 May 2024 11:27:50 +0200 Subject: [PATCH 19/67] Fix simulation stopping at unhandled messages in `DBFSAlgorithm`. --- CHANGELOG.md | 1 + .../edu/ie3/simona/agent/grid/DBFSAlgorithm.scala | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa9ff87da1..4c548d431c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Default RefSystem using the unit `Volt` for low voltage grids [#811](https://github.com/ie3-institute/simona/issues/811) - Fixed grid within GridSpec test [#806](https://github.com/ie3-institute/simona/issues/806) - Fixed log entry after power flow calculation [#814](https://github.com/ie3-institute/simona/issues/814) +- Simulation stopping at unhandled messages in `DBFSAlgorithm` [#821](https://github.com/ie3-institute/simona/issues/821) ## [3.0.0] - 2023-08-07 diff --git a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala index 24236f43d3..6a7b1a2f77 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -490,9 +490,10 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // return to Idle idle(cleanedGridAgentBaseData) - case _ => - // preventing "match may not be exhaustive" - Behaviors.unhandled + case (message, _) => + ctx.log.debug(s"Received the message $message to early. Stash away!") + buffer.stash(message) + Behaviors.same } } @@ -802,9 +803,10 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { buffer.stash(powerResponse) Behaviors.same - case _ => - // preventing "match may not be exhaustive" - Behaviors.unhandled + case (message, _) => + ctx.log.debug(s"Received the message $message to early. Stash away!") + buffer.stash(message) + Behaviors.same } } From 8fe1a71bc19e69d436b2bdb9858993532f431114 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Thu, 6 Jun 2024 12:29:08 +0200 Subject: [PATCH 20/67] Add possibility to flush out `Cylindrical Storage Results`. --- CHANGELOG.md | 1 + input/samples/vn_simona/vn_simona.conf | 4 + .../ie3/simona/config/ConfigFailFast.scala | 86 ++++++++++++++++--- .../edu/ie3/simona/util/ConfigUtil.scala | 11 ++- .../ie3/simona/util/EntityMapperUtil.scala | 6 +- .../simona/config/ConfigFailFastSpec.scala | 84 +++++++++++++++++- .../edu/ie3/simona/util/ConfigUtilSpec.scala | 7 +- 7 files changed, 174 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa9ff87da1..c05424bddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Consider scaling factor with flex options [#734](https://github.com/ie3-institute/simona/issues/734) - Implementation of Energy Management Agents [#204](https://github.com/ie3-institute/simona/issues/204) - Providing documentation for EmAgent protocols and algorithms [#774](https://github.com/ie3-institute/simona/issues/774) +- Option to flush out `CylindricalStorageResults` [#826](https://github.com/ie3-institute/simona/issues/826) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index 401d783120..55c14f26a2 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -92,6 +92,10 @@ simona.output.thermal = { { notifier = "house", simulationResult = true + }, + { + notifier = "cylindricalstorage", + simulationResult = true } ] } diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index df199e55c5..0eeabbf6cc 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -9,13 +9,7 @@ package edu.ie3.simona.config import com.typesafe.config.{Config, ConfigException} import com.typesafe.scalalogging.LazyLogging import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.InfluxDb1x -import edu.ie3.simona.config.SimonaConfig.{ - BaseOutputConfig, - RefSystemConfig, - ResultKafkaParams, - Simona, - TransformerControlGroup, -} +import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.io.result.ResultSinkType import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} @@ -144,6 +138,11 @@ case object ConfigFailFast extends LazyLogging { simonaConfig.simona.output.participant ) + /* Check all output configurations for thermal models */ + checkThermalOutputConfig( + simonaConfig.simona.output.thermal + ) + /* Check power flow resolution configuration */ checkPowerFlowResolutionConfiguration(simonaConfig.simona.powerflow) @@ -429,8 +428,8 @@ case object ConfigFailFast extends LazyLogging { /** Sanity checks for a [[SimonaConfig.RefSystemConfig]] * - * @param refSystem - * the [[SimonaConfig.RefSystemConfig]] that should be checked + * @param refSystems + * a list of [[SimonaConfig.RefSystemConfig]]s that should be checked */ private def checkRefSystem(refSystems: List[RefSystemConfig]): Unit = { @@ -563,6 +562,21 @@ case object ConfigFailFast extends LazyLogging { checkIndividualParticipantsOutputConfigs(subConfig.individualConfigs) } + /** Check the config sub tree for output parameterization + * + * @param subConfig + * Output sub config tree for participants + */ + private def checkThermalOutputConfig( + subConfig: SimonaConfig.Simona.Output.Thermal + ): Unit = { + checkDefaultBaseOutputConfig( + subConfig.defaultConfig, + defaultString = "default", + ) + checkIndividualThermalOutputConfigs(subConfig.individualConfigs) + } + /** Checks resolution of power flow calculation * * @param powerFlow @@ -690,6 +704,34 @@ case object ConfigFailFast extends LazyLogging { s"There are multiple output configurations for participant types '${duplicateKeys.mkString(",")}'." ) + implicit val participantNotifiers: Set[NotifierIdentifier.Value] = + NotifierIdentifier.getParticipantIdentifiers + configs.foreach(checkBaseOutputConfig) + } + + /** Checks the thermal output configurations on duplicates + * + * @param configs + * List of individual config entries + */ + private def checkIndividualThermalOutputConfigs( + configs: List[SimonaConfig.BaseOutputConfig] + ): Unit = { + val duplicateKeys = configs + .map(config => StringUtils.cleanString(config.notifier).toLowerCase()) + .groupMapReduce(identity)(_ => 1)(_ + _) + .filter { case (_, count) => + count > 1 + } + .keys + + if (duplicateKeys.nonEmpty) + throw new InvalidConfigParameterException( + s"There are multiple output configurations for participant types '${duplicateKeys.mkString(",")}'." + ) + + implicit val thermalNotifiers: Set[NotifierIdentifier.Value] = + NotifierIdentifier.getThermalIdentifiers configs.foreach(checkBaseOutputConfig) } @@ -697,23 +739,39 @@ case object ConfigFailFast extends LazyLogging { * * @param config * to be checked + * @param exceptedNotifiers + * a set of all valid identifiers */ - private def checkBaseOutputConfig(config: BaseOutputConfig): Unit = { - checkNotifierIdentifier(config.notifier) + private def checkBaseOutputConfig( + config: BaseOutputConfig + )(implicit exceptedNotifiers: Set[NotifierIdentifier.Value]): Unit = { + checkNotifierIdentifier(config.notifier, exceptedNotifiers) } /** Check the validity of the identifier String * * @param id * identifier String to check + * @param exceptedNotifiers + * a set of all valid identifiers */ - private def checkNotifierIdentifier(id: String): Unit = { + private def checkNotifierIdentifier( + id: String, + exceptedNotifiers: Set[NotifierIdentifier.Value], + ): Unit = { try { - NotifierIdentifier(id) + val notifier = NotifierIdentifier(id) + + if (!exceptedNotifiers.contains(notifier)) { + throw new InvalidConfigParameterException( + s"The identifier '$id' you provided is not valid. Valid input: ${exceptedNotifiers.map(_.toString).mkString(",")}" + ) + } + } catch { case e: NoSuchElementException => throw new InvalidConfigParameterException( - s"The identifier '$id' you provided is not valid. Valid input: ${NotifierIdentifier.values.map(_.toString).mkString(",")}", + s"The identifier '$id' you provided is not valid. Valid input: ${exceptedNotifiers.map(_.toString).mkString(",")}", e, ) } diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index b041129c90..d9fd6ee24f 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -148,8 +148,7 @@ object ConfigUtil { if (defaultConfig.simulationResultInfo) { val notifiers = if (thermal) NotifierIdentifier.getThermalIdentifiers - else - NotifierIdentifier.values -- NotifierIdentifier.getThermalIdentifiers + else NotifierIdentifier.getParticipantIdentifiers /* Generally inform about all simulation results, but not on those, that are explicitly marked */ notifiers -- configs.flatMap { case ( @@ -292,10 +291,16 @@ object ConfigUtil { val Wec: Value = Value("wec") val Hp: Value = Value("hp") val House: Value = Value("house") + val CylindricalStorage: Value = Value("cylindricalstorage") + + /** All participant identifiers */ + def getParticipantIdentifiers: Set[Value] = + (NotifierIdentifier.values -- getThermalIdentifiers).toSet /** All thermal identifiers */ def getThermalIdentifiers: Set[Value] = Set( - NotifierIdentifier.House + NotifierIdentifier.House, + NotifierIdentifier.CylindricalStorage, ) } diff --git a/src/main/scala/edu/ie3/simona/util/EntityMapperUtil.scala b/src/main/scala/edu/ie3/simona/util/EntityMapperUtil.scala index ed9192dcb1..3308813320 100644 --- a/src/main/scala/edu/ie3/simona/util/EntityMapperUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/EntityMapperUtil.scala @@ -8,7 +8,10 @@ package edu.ie3.simona.util import edu.ie3.datamodel.models.result.ResultEntity import edu.ie3.datamodel.models.result.system._ -import edu.ie3.datamodel.models.result.thermal.ThermalHouseResult +import edu.ie3.datamodel.models.result.thermal.{ + CylindricalStorageResult, + ThermalHouseResult, +} import edu.ie3.simona.util.ConfigUtil.NotifierIdentifier import edu.ie3.simona.util.ConfigUtil.NotifierIdentifier._ @@ -27,6 +30,7 @@ object EntityMapperUtil { Em -> classOf[EmResult], Hp -> classOf[HpResult], House -> classOf[ThermalHouseResult], + CylindricalStorage -> classOf[CylindricalStorageResult], ) /** Get the classes of [[ResultEntity]], that are issued by the notifier, that diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index 227d8f05cf..77d59d7dfa 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -680,15 +680,19 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "identify faulty notifier identifiers" in { intercept[InvalidConfigParameterException] { - ConfigFailFast invokePrivate checkNotifierIdentifier("whatever") - }.getMessage shouldBe s"The identifier 'whatever' you provided is not valid. Valid input: ${NotifierIdentifier.values.map(_.toString).mkString(",")}" + ConfigFailFast invokePrivate checkNotifierIdentifier( + "whatever", + NotifierIdentifier.getParticipantIdentifiers, + ) + }.getMessage shouldBe s"The identifier 'whatever' you provided is not valid. Valid input: ${NotifierIdentifier.getParticipantIdentifiers.map(_.toString).mkString(",")}" } "let all valid notifier identifiers pass" in { noException shouldBe thrownBy { - NotifierIdentifier.values.map(id => + NotifierIdentifier.getParticipantIdentifiers.map(id => ConfigFailFast invokePrivate checkNotifierIdentifier( - id.toString + id.toString, + NotifierIdentifier.getParticipantIdentifiers, ) ) } @@ -758,6 +762,78 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } } + "Checking thermal output configs" should { + val checkNotifierIdentifier = + PrivateMethod[Unit](Symbol("checkNotifierIdentifier")) + + "identify faulty notifier identifiers" in { + intercept[InvalidConfigParameterException] { + ConfigFailFast invokePrivate checkNotifierIdentifier( + "whatever", + NotifierIdentifier.getThermalIdentifiers, + ) + }.getMessage shouldBe s"The identifier 'whatever' you provided is not valid. Valid input: ${NotifierIdentifier.getThermalIdentifiers.map(_.toString).mkString(",")}" + } + + "let all valid notifier identifiers pass" in { + noException shouldBe thrownBy { + Set("house", "cylindricalstorage").map(id => + ConfigFailFast invokePrivate checkNotifierIdentifier( + id, + NotifierIdentifier.getThermalIdentifiers, + ) + ) + } + } + + val checkIndividualThermalOutputConfigs = + PrivateMethod[Unit]( + Symbol("checkIndividualThermalOutputConfigs") + ) + + "let distinct configs pass" in { + val validInput = List( + SimonaConfig.SimpleOutputConfig( + notifier = "house", + simulationResult = false, + ), + SimonaConfig.SimpleOutputConfig( + notifier = "cylindricalstorage", + simulationResult = false, + ), + ) + + noException shouldBe thrownBy { + ConfigFailFast invokePrivate checkIndividualThermalOutputConfigs( + validInput + ) + } + } + + "throw an exception, when there is a duplicate entry for the same model type" in { + val invalidInput = List( + SimonaConfig.SimpleOutputConfig( + notifier = "house", + simulationResult = false, + ), + SimonaConfig.SimpleOutputConfig( + notifier = "cylindricalstorage", + simulationResult = false, + ), + SimonaConfig.SimpleOutputConfig( + notifier = "house", + simulationResult = false, + ), + ) + + intercept[InvalidConfigParameterException]( + ConfigFailFast invokePrivate checkIndividualThermalOutputConfigs( + invalidInput + ) + ).getMessage shouldBe "There are multiple output configurations for participant types 'house'." + } + } + "Checking data sinks" should { val checkDataSink = PrivateMethod[Unit](Symbol("checkDataSink")) diff --git a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala index 1356fef9cb..56d478d3cf 100644 --- a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala +++ b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala @@ -721,9 +721,10 @@ class ConfigUtilSpec ), ) val configUtil = OutputConfigUtil(inputConfig) - val expectedResult: Set[Value] = NotifierIdentifier.values -- Vector( - NotifierIdentifier.PvPlant - ) -- NotifierIdentifier.getThermalIdentifiers.toVector + val expectedResult: Set[Value] = + NotifierIdentifier.getParticipantIdentifiers -- Vector( + NotifierIdentifier.PvPlant + ) configUtil.simulationResultIdentifiersToConsider( false From 52529c416659bd4dd586788f1fff9868ee4b91b6 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Thu, 6 Jun 2024 12:34:23 +0200 Subject: [PATCH 21/67] Enhancing the config documentation. --- docs/readthedocs/config.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index f964378c67..73004b387c 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -147,6 +147,33 @@ simona.output.participant.individualConfigs = [ ] ``` +#### Output configuration of thermal elements + +To use the default configuration the default notifier has to be used. By setting "simulationResult" to true, the thermal elements is enabled to return its results. + +``` +simona.output.thermal.defaultConfig = { + notifier = "default", + simulationResult = true +} +``` + +The default configuration applies to all models except the ones with individual configurations assigned. +If individual configurations have to be performed for certain thermal elements, these must be listed with the corresponding notifier as in the following example. + +``` +simona.output.thermal.individualConfigs = [ + { + notifier = "house", + simulationResult = true + }, + { + notifier = "cylindricalstorage", + simulationResult = true + } +] +``` + Further model classes which can be used to load the outcome of a system simulation are described in [PSDM](https://powersystemdatamodel.readthedocs.io/en/latest/models/models.html#result). Data sources and data sinks are explained in the [I/O-capabilities](https://powersystemdatamodel.readthedocs.io/en/latest/io/basiciousage.html) section of the PSDM. From 8c06effdc93bf930b7654f04c561431c608faa19 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Thu, 6 Jun 2024 17:10:31 +0200 Subject: [PATCH 22/67] Abstracting some methods in `ConfigFailFast`. --- .../ie3/simona/config/ConfigFailFast.scala | 67 +++++++------------ .../simona/config/ConfigFailFastSpec.scala | 30 +++++---- 2 files changed, 41 insertions(+), 56 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index 0eeabbf6cc..f324f35678 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -555,11 +555,10 @@ case object ConfigFailFast extends LazyLogging { ) ) - checkDefaultBaseOutputConfig( - subConfig.defaultConfig, - defaultString = "default", - ) - checkIndividualParticipantsOutputConfigs(subConfig.individualConfigs) + implicit val elementType: String = "participant" + + checkDefaultBaseOutputConfig(subConfig.defaultConfig) + checkIndividualOutputConfigs(subConfig.individualConfigs) } /** Check the config sub tree for output parameterization @@ -570,11 +569,9 @@ case object ConfigFailFast extends LazyLogging { private def checkThermalOutputConfig( subConfig: SimonaConfig.Simona.Output.Thermal ): Unit = { - checkDefaultBaseOutputConfig( - subConfig.defaultConfig, - defaultString = "default", - ) - checkIndividualThermalOutputConfigs(subConfig.individualConfigs) + implicit val elementType: String = "thermal" + checkDefaultBaseOutputConfig(subConfig.defaultConfig) + checkIndividualOutputConfigs(subConfig.individualConfigs) } /** Checks resolution of power flow calculation @@ -671,26 +668,26 @@ case object ConfigFailFast extends LazyLogging { */ private def checkDefaultBaseOutputConfig( config: SimonaConfig.BaseOutputConfig, - defaultString: String, - ): Unit = { + defaultString: String = "default", + )(implicit elementType: String): Unit = { if ( StringUtils .cleanString(config.notifier) .toLowerCase != StringUtils.cleanString(defaultString).toLowerCase ) logger.warn( - s"You provided '${config.notifier}' as model type for the default participant output config. This will not be considered!" + s"You provided '${config.notifier}' as model type for the default $elementType output config. This will not be considered!" ) } - /** Checks the participant output configurations on duplicates + /** Checks the given output configurations on duplicates * * @param configs * List of individual config entries */ - private def checkIndividualParticipantsOutputConfigs( + private def checkIndividualOutputConfigs( configs: List[SimonaConfig.BaseOutputConfig] - ): Unit = { + )(implicit elementType: String): Unit = { val duplicateKeys = configs .map(config => StringUtils.cleanString(config.notifier).toLowerCase()) .groupMapReduce(identity)(_ => 1)(_ + _) @@ -701,37 +698,21 @@ case object ConfigFailFast extends LazyLogging { if (duplicateKeys.nonEmpty) throw new InvalidConfigParameterException( - s"There are multiple output configurations for participant types '${duplicateKeys.mkString(",")}'." + s"There are multiple output configurations for $elementType types '${duplicateKeys.mkString(",")}'." ) - implicit val participantNotifiers: Set[NotifierIdentifier.Value] = - NotifierIdentifier.getParticipantIdentifiers - configs.foreach(checkBaseOutputConfig) - } - - /** Checks the thermal output configurations on duplicates - * - * @param configs - * List of individual config entries - */ - private def checkIndividualThermalOutputConfigs( - configs: List[SimonaConfig.BaseOutputConfig] - ): Unit = { - val duplicateKeys = configs - .map(config => StringUtils.cleanString(config.notifier).toLowerCase()) - .groupMapReduce(identity)(_ => 1)(_ + _) - .filter { case (_, count) => - count > 1 + implicit val exceptedNotifiers: Set[NotifierIdentifier.Value] = + elementType match { + case "participant" => + NotifierIdentifier.getParticipantIdentifiers + case "thermal" => + NotifierIdentifier.getThermalIdentifiers + case other => + throw new InvalidConfigParameterException( + s"The output config for $other has no notifiers!" + ) } - .keys - - if (duplicateKeys.nonEmpty) - throw new InvalidConfigParameterException( - s"There are multiple output configurations for participant types '${duplicateKeys.mkString(",")}'." - ) - implicit val thermalNotifiers: Set[NotifierIdentifier.Value] = - NotifierIdentifier.getThermalIdentifiers configs.foreach(checkBaseOutputConfig) } diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index 77d59d7dfa..b6ed1e1625 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -698,9 +698,9 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } } - val checkIndividualParticipantsOutputConfigs = + val checkIndividualOutputConfigs = PrivateMethod[Unit]( - Symbol("checkIndividualParticipantsOutputConfigs") + Symbol("checkIndividualOutputConfigs") ) "let distinct configs pass" in { @@ -726,8 +726,9 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ) noException shouldBe thrownBy { - ConfigFailFast invokePrivate checkIndividualParticipantsOutputConfigs( - validInput + ConfigFailFast invokePrivate checkIndividualOutputConfigs( + validInput, + "participant", ) } } @@ -755,8 +756,9 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ) intercept[InvalidConfigParameterException]( - ConfigFailFast invokePrivate checkIndividualParticipantsOutputConfigs( - invalidInput + ConfigFailFast invokePrivate checkIndividualOutputConfigs( + invalidInput, + "participant", ) ).getMessage shouldBe "There are multiple output configurations for participant types 'load'." } @@ -786,9 +788,9 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } } - val checkIndividualThermalOutputConfigs = + val checkIndividualOutputConfigs = PrivateMethod[Unit]( - Symbol("checkIndividualThermalOutputConfigs") + Symbol("checkIndividualOutputConfigs") ) "let distinct configs pass" in { @@ -804,8 +806,9 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ) noException shouldBe thrownBy { - ConfigFailFast invokePrivate checkIndividualThermalOutputConfigs( - validInput + ConfigFailFast invokePrivate checkIndividualOutputConfigs( + validInput, + "thermal", ) } } @@ -827,10 +830,11 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ) intercept[InvalidConfigParameterException]( - ConfigFailFast invokePrivate checkIndividualThermalOutputConfigs( - invalidInput + ConfigFailFast invokePrivate checkIndividualOutputConfigs( + invalidInput, + "thermal", ) - ).getMessage shouldBe "There are multiple output configurations for participant types 'house'." + ).getMessage shouldBe "There are multiple output configurations for thermal types 'house'." } } From 16e9aae3ae3a6da4c06123aadf1882fbb44cebec Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Tue, 11 Jun 2024 17:19:59 +0200 Subject: [PATCH 23/67] Fixing log messages. --- src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala index 6a7b1a2f77..8dc1b65ae8 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -491,7 +491,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { idle(cleanedGridAgentBaseData) case (message, _) => - ctx.log.debug(s"Received the message $message to early. Stash away!") + ctx.log.debug(s"Received the message $message too early. Stash away!") buffer.stash(message) Behaviors.same } @@ -804,7 +804,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { Behaviors.same case (message, _) => - ctx.log.debug(s"Received the message $message to early. Stash away!") + ctx.log.debug(s"Received the message $message too early. Stash away!") buffer.stash(message) Behaviors.same } From 76f20ec5cfa3e9cf97c0626e6d2f365ca720a8ea Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 15:37:18 +0200 Subject: [PATCH 24/67] Adapting test to removal of dod Signed-off-by: Sebastian Peter --- .../model/participant/StorageModelTest.groovy | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy index 8dbcb9bddc..2dcd5459b8 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy @@ -103,10 +103,8 @@ class StorageModelTest extends Specification { // UNCHANGED STATE // completely empty 0 | 0 | 1 || 0 | 0 | 10 - // at lowest allowed charge - 20 | 0 | 1 || 0 | 0 | 10 - // at a tiny bit above lowest allowed charge - 20.011d | 0 | 1 || 0 | -10 | 10 + // at a tiny bit above empty + 0.011d | 0 | 1 || 0 | -10 | 10 // at mid-level charge 60 | 0 | 1 || 0 | -10 | 10 // almost fully charged @@ -114,14 +112,14 @@ class StorageModelTest extends Specification { // fully charged 100 | 0 | 1 || 0 | -10 | 0 // CHANGED STATE - // discharged to lowest allowed charge - 30 | -10 | 3600 || 0 | 0 | 10 + // discharged to empty + 10 | -10 | 3600 || 0 | 0 | 10 // almost discharged to lowest allowed charge - 30 | -10 | 3590 || 0 | -10 | 10 + 10 | -10 | 3590 || 0 | -10 | 10 // charged to mid-level charge - 50 | 10 | 3600 || 0 | -10 | 10 + 40 | 10 | 3600 || 0 | -10 | 10 // discharged to mid-level charge - 70 | -10 | 3600 || 0 | -10 | 10 + 60 | -10 | 3600 || 0 | -10 | 10 // almost fully charged 95 | 4.98 | 3600 || 0 | -10 | 10 // fully charged @@ -151,8 +149,6 @@ class StorageModelTest extends Specification { lastStored || pRef | pMin | pMax // completely empty 0 || 10 | 0 | 10 - // at lowest allowed charge - 20 || 10 | 0 | 10 // below margin of ref power target 49.9974 || 10 | -10 | 10 // within margin below ref power target @@ -211,11 +207,11 @@ class StorageModelTest extends Specification { 50 | 5 || 4.5 | false | true | 10 * 3600 / 0.9 50 | 10 || 9 | false | true | 5 * 3600 / 0.9 // discharging on half full - 50 | -5 || -4.5 | false | true | 6 * 3600 / 0.9 - 50 | -10 || -9 | false | true | 3 * 3600 / 0.9 + 50 | -5 || -4.5 | false | true | 10 * 3600 / 0.9 + 50 | -10 || -9 | false | true | 5 * 3600 / 0.9 // discharging on full - 100 | -5 || -4.5 | true | true | 16 * 3600 / 0.9 - 100 | -10 || -9 | true | true | 8 * 3600 / 0.9 + 100 | -5 || -4.5 | true | true | 20 * 3600 / 0.9 + 100 | -10 || -9 | true | true | 10 * 3600 / 0.9 } def "Handle controlled power change with ref target SOC"() { @@ -260,8 +256,8 @@ class StorageModelTest extends Specification { 50 | 5 || 4.5 | true | true | 10 * 3600 / 0.9 50 | 10 || 9 | true | true | 5 * 3600 / 0.9 // discharging on target ref - 50 | -5 || -4.5 | true | true | 6 * 3600 / 0.9 - 50 | -10 || -9 | true | true | 3 * 3600 / 0.9 + 50 | -5 || -4.5 | true | true | 10 * 3600 / 0.9 + 50 | -10 || -9 | true | true | 5 * 3600 / 0.9 // discharging on full 100 | -5 || -4.5 | true | true | 10 * 3600 / 0.9 100 | -10 || -9 | true | true | 5 * 3600 / 0.9 @@ -272,9 +268,9 @@ class StorageModelTest extends Specification { def storageModel = buildStorageModel() def startTick = 1800L def data = new StorageModel.StorageRelevantData(startTick + 1) - // margin is at ~ 20.0030864 kWh + // margin is at ~ 0.0030864 kWh def oldState = new StorageModel.StorageState( - Sq.create(20.002d, KilowattHours$.MODULE$), + Sq.create(0.002d, KilowattHours$.MODULE$), Sq.create(0d, Kilowatts$.MODULE$), startTick ) @@ -347,7 +343,7 @@ class StorageModelTest extends Specification { result._1.tick() == startTick + 1 Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE def flexChangeIndication = result._2 - flexChangeIndication.changesAtTick() == Option.apply(startTick + 1L + 4001L) + flexChangeIndication.changesAtTick() == Option.apply(startTick + 1L + 12001L) flexChangeIndication.changesAtNextActivation() } From 08cb5f1c02846f333a4bac58c6a15e39179dee7e Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 15:37:51 +0200 Subject: [PATCH 25/67] Removing stub Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant/StorageModel.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index ddac3b6ab5..b475dd0c38 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -100,7 +100,10 @@ final case class StorageModel( voltage: Dimensionless, modelState: StorageState, data: StorageRelevantData, - ): ApparentPower = ??? + ): ApparentPower = + throw new NotImplementedError( + "Storage model cannot calculate power without flexibility control." + ) override protected def calculateActivePower( modelState: StorageState, From cfbf71426cc9f4639f78cccf1b938deed476f2e7 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 15:40:29 +0200 Subject: [PATCH 26/67] Improving comment Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index b475dd0c38..b5105d39d2 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -54,7 +54,8 @@ final case class StorageModel( private implicit val doubleTolerance: Power = eStorage / Seconds(1) * 3.6e-12 /** In order to avoid faulty flexibility options, we want to avoid offering - * charging/discharging that could last less than one second. + * charging/discharging that could last less than our smallest possible time + * delta, which is one second. */ private val toleranceMargin = pMax * Seconds(1d) From 535bffc0cc6a388622a51f83f0ac2a65de11a495 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 15:57:11 +0200 Subject: [PATCH 27/67] Correct usage of ETA, adapting StorageModelTest Signed-off-by: Sebastian Peter --- .../model/participant/StorageModel.scala | 6 ++--- .../model/participant/StorageModelTest.groovy | 26 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index b5105d39d2..36ddaccbae 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -70,8 +70,8 @@ final case class StorageModel( private val refTargetSoc = targetSoc.map { target => val targetEnergy = eStorage * target - val targetWithPosMargin = // FIXME this should be division - targetEnergy + (toleranceMargin * eta.toEach) + val targetWithPosMargin = + targetEnergy + (toleranceMargin / eta.toEach) val targetWithNegMargin = targetEnergy - (toleranceMargin * eta.toEach) @@ -177,7 +177,7 @@ final case class StorageModel( else // divide by eta if we're discharging // (draining the battery more than we get as output) - setPower * eta.toEach // FIXME this should be division. Check as well with SoC Calculation. + setPower / eta.toEach } val currentState = diff --git a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy index 2dcd5459b8..112a3ef373 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy @@ -156,9 +156,9 @@ class StorageModelTest extends Specification { // exactly at ref power target 50 || 0 | -10 | 10 // within margin above ref power target - 50.0024 || 0 | -10 | 10 + 50.0030 || 0 | -10 | 10 // above margin of ref power target - 50.0026 || -10 | -10 | 10 + 50.0031 || -10 | -10 | 10 // at mid-level charge 60 || -10 | -10 | 10 // fully charged @@ -207,11 +207,11 @@ class StorageModelTest extends Specification { 50 | 5 || 4.5 | false | true | 10 * 3600 / 0.9 50 | 10 || 9 | false | true | 5 * 3600 / 0.9 // discharging on half full - 50 | -5 || -4.5 | false | true | 10 * 3600 / 0.9 - 50 | -10 || -9 | false | true | 5 * 3600 / 0.9 + 50 | -4.5 || -5 | false | true | 10 * 3600 + 50 | -9 || -10 | false | true | 5 * 3600 // discharging on full - 100 | -5 || -4.5 | true | true | 20 * 3600 / 0.9 - 100 | -10 || -9 | true | true | 10 * 3600 / 0.9 + 100 | -4.5 || -5 | true | true | 20 * 3600 + 100 | -9 || -10 | true | true | 10 * 3600 } def "Handle controlled power change with ref target SOC"() { @@ -256,11 +256,11 @@ class StorageModelTest extends Specification { 50 | 5 || 4.5 | true | true | 10 * 3600 / 0.9 50 | 10 || 9 | true | true | 5 * 3600 / 0.9 // discharging on target ref - 50 | -5 || -4.5 | true | true | 10 * 3600 / 0.9 - 50 | -10 || -9 | true | true | 5 * 3600 / 0.9 + 50 | -4.5 || -5 | true | true | 10 * 3600 + 50 | -9 || -10 | true | true | 5 * 3600 // discharging on full - 100 | -5 || -4.5 | true | true | 10 * 3600 / 0.9 - 100 | -10 || -9 | true | true | 5 * 3600 / 0.9 + 100 | -4.5 || -5 | true | true | 10 * 3600 + 100 | -9 || -10 | true | true | 5 * 3600 } def "Handle the edge case of discharging in tolerance margins"() { @@ -335,15 +335,15 @@ class StorageModelTest extends Specification { def result = storageModel.handleControlledPowerChange( data, oldState, - Sq.create(-10d, Kilowatts$.MODULE$) + Sq.create(-9d, Kilowatts$.MODULE$) ) then: - Math.abs(result._1.chargingPower().toKilowatts() - (-9d)) < TOLERANCE + Math.abs(result._1.chargingPower().toKilowatts() - (-10d)) < TOLERANCE result._1.tick() == startTick + 1 Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE def flexChangeIndication = result._2 - flexChangeIndication.changesAtTick() == Option.apply(startTick + 1L + 12001L) + flexChangeIndication.changesAtTick() == Option.apply(startTick + 1L + 10801L) flexChangeIndication.changesAtNextActivation() } From 2240a8ca320c6858d925dcc6dffee125efdd4460 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 19:49:26 +0200 Subject: [PATCH 28/67] Saving setPower in model state and using adapted setPower within reply to EM Signed-off-by: Sebastian Peter --- .../storage/StorageAgentFundamentals.scala | 11 +++- .../model/participant/StorageModel.scala | 56 ++++++++++++------- .../model/participant/StorageModelTest.groovy | 56 +++++++++---------- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala index 92081bcc52..4116a93325 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -335,16 +335,23 @@ trait StorageAgentFundamentals ): (StorageState, ApparentPower, FlexChangeIndicator) = { val (updatedState, flexChangeIndicator) = baseStateData.model.handleControlledPowerChange(data, lastState, setPower) + // In edge cases, the model does not accept the given set power + // and returns an adapted value + val updatedSetPower = updatedState.chargingPower val voltage = getAndCheckNodalVoltage(baseStateData, tick) val reactivePower = baseStateData.model.calculateReactivePower( - setPower, + updatedSetPower, voltage, ) // TODO: Actually change state and calculate the next tick, when something happens - (updatedState, ApparentPower(setPower, reactivePower), flexChangeIndicator) + ( + updatedState, + ApparentPower(updatedSetPower, reactivePower), + flexChangeIndicator, + ) } /** Update the last known model state with the given external, relevant data diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 36ddaccbae..d0892c43d0 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -152,6 +152,16 @@ final case class StorageModel( ) } + private def calcNetPower(setPower: Power): Power = + if (setPower > zeroKW) { + // multiply eta if we're charging + setPower * eta.toEach + } else { + // divide by eta if we're discharging + // (draining the battery more than we get as output) + setPower / eta.toEach + } + override def handleControlledPowerChange( data: StorageRelevantData, lastState: StorageState, @@ -160,32 +170,28 @@ final case class StorageModel( val currentStoredEnergy = determineCurrentState(lastState, data.currentTick) - // net power after considering efficiency - val netPower = - if (setPower ~= zeroKW) { + val adaptedSetPower = + if ( // if power is close to zero, set it to zero + (setPower ~= zeroKW) + // do not keep charging if we're already full + || (setPower > zeroKW && isFull(currentStoredEnergy)) + // do not keep discharging if we're already empty + || (setPower < zeroKW && isEmpty(currentStoredEnergy)) + ) zeroKW - } else if (setPower > zeroKW) { - if (isFull(currentStoredEnergy)) - zeroKW // do not keep charging if we're already full - else - // multiply eta if we're charging - setPower * eta.toEach - } else { - if (isEmpty(currentStoredEnergy)) - zeroKW // do not keep discharging if we're already empty - else - // divide by eta if we're discharging - // (draining the battery more than we get as output) - setPower / eta.toEach - } + else + setPower + + // net power after considering efficiency + val netPower = calcNetPower(adaptedSetPower) val currentState = StorageState( currentStoredEnergy, - netPower, + adaptedSetPower, data.currentTick, - ) // FIXME this should be setPower instead of netPower ? Because the EM receives / sees Power after considering eta / output power. Internal "discharging" needs to be done with netpower! Check whether EM get's setpower or netPower. + ) // if the storage is at minimum or maximum charged energy AND we are charging // or discharging, flex options will be different at the next activation @@ -258,7 +264,8 @@ final case class StorageModel( currentTick: Long, ): Energy = { val timespan = currentTick - lastState.tick - val energyChange = lastState.chargingPower * Seconds(timespan) + val netPower = calcNetPower(lastState.chargingPower) + val energyChange = netPower * Seconds(timespan) val newEnergy = lastState.storedEnergy + energyChange @@ -291,6 +298,15 @@ object StorageModel { currentTick: Long ) extends CalcRelevantData + /** @param storedEnergy + * The amount of currently stored energy + * @param chargingPower + * The power with which the storage is (dis-)charging, valid until the next + * state. Gross value that is valid outside the model, i.e. before + * considering efficiency etc. + * @param tick + * The tick at which this state is valid + */ final case class StorageState( storedEnergy: Energy, chargingPower: Power, diff --git a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy index 112a3ef373..f6ad21ef0c 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy @@ -113,17 +113,17 @@ class StorageModelTest extends Specification { 100 | 0 | 1 || 0 | -10 | 0 // CHANGED STATE // discharged to empty - 10 | -10 | 3600 || 0 | 0 | 10 + 10 | -9 | 3600 || 0 | 0 | 10 // almost discharged to lowest allowed charge - 10 | -10 | 3590 || 0 | -10 | 10 + 10 | -9 | 3590 || 0 | -10 | 10 // charged to mid-level charge - 40 | 10 | 3600 || 0 | -10 | 10 + 41 | 10 | 3600 || 0 | -10 | 10 // discharged to mid-level charge - 60 | -10 | 3600 || 0 | -10 | 10 + 60 | -9 | 3600 || 0 | -10 | 10 // almost fully charged - 95 | 4.98 | 3600 || 0 | -10 | 10 + 95.5 | 4.98 | 3600 || 0 | -10 | 10 // fully charged - 95 | 5 | 3600 || 0 | -10 | 0 + 95.5 | 5 | 3600 || 0 | -10 | 0 } def "Calculate flex options with target SOC"() { @@ -199,19 +199,19 @@ class StorageModelTest extends Specification { 50 | 0 || 0 | false | false | 0 100 | 0 || 0 | false | false | 0 // charging on empty - 0 | 1 || 0.9 | true | true | 100 * 3600 / 0.9 - 0 | 2.5 || 2.25 | true | true | 40 * 3600 / 0.9 - 0 | 5 || 4.5 | true | true | 20 * 3600 / 0.9 - 0 | 10 || 9 | true | true | 10 * 3600 / 0.9 + 0 | 1 || 1 | true | true | 100 * 3600 / 0.9 + 0 | 2.5 || 2.5 | true | true | 40 * 3600 / 0.9 + 0 | 5 || 5 | true | true | 20 * 3600 / 0.9 + 0 | 10 || 10 | true | true | 10 * 3600 / 0.9 // charging on half full - 50 | 5 || 4.5 | false | true | 10 * 3600 / 0.9 - 50 | 10 || 9 | false | true | 5 * 3600 / 0.9 + 50 | 5 || 5 | false | true | 10 * 3600 / 0.9 + 50 | 10 || 10 | false | true | 5 * 3600 / 0.9 // discharging on half full - 50 | -4.5 || -5 | false | true | 10 * 3600 - 50 | -9 || -10 | false | true | 5 * 3600 + 50 | -4.5 || -4.5 | false | true | 10 * 3600 + 50 | -9 || -9 | false | true | 5 * 3600 // discharging on full - 100 | -4.5 || -5 | true | true | 20 * 3600 - 100 | -9 || -10 | true | true | 10 * 3600 + 100 | -4.5 || -4.5 | true | true | 20 * 3600 + 100 | -9 || -9 | true | true | 10 * 3600 } def "Handle controlled power change with ref target SOC"() { @@ -248,19 +248,19 @@ class StorageModelTest extends Specification { 50 | 0 || 0 | false | false | 0 100 | 0 || 0 | false | false | 0 // charging on empty - 0 | 1 || 0.9 | true | true | 50 * 3600 / 0.9 - 0 | 2.5 || 2.25 | true | true | 20 * 3600 / 0.9 - 0 | 5 || 4.5 | true | true | 10 * 3600 / 0.9 - 0 | 10 || 9 | true | true | 5 * 3600 / 0.9 + 0 | 1 || 1 | true | true | 50 * 3600 / 0.9 + 0 | 2.5 || 2.5 | true | true | 20 * 3600 / 0.9 + 0 | 5 || 5 | true | true | 10 * 3600 / 0.9 + 0 | 10 || 10 | true | true | 5 * 3600 / 0.9 // charging on target ref - 50 | 5 || 4.5 | true | true | 10 * 3600 / 0.9 - 50 | 10 || 9 | true | true | 5 * 3600 / 0.9 + 50 | 5 || 5 | true | true | 10 * 3600 / 0.9 + 50 | 10 || 10 | true | true | 5 * 3600 / 0.9 // discharging on target ref - 50 | -4.5 || -5 | true | true | 10 * 3600 - 50 | -9 || -10 | true | true | 5 * 3600 + 50 | -4.5 || -4.5 | true | true | 10 * 3600 + 50 | -9 || -9 | true | true | 5 * 3600 // discharging on full - 100 | -4.5 || -5 | true | true | 10 * 3600 - 100 | -9 || -10 | true | true | 5 * 3600 + 100 | -4.5 || -4.5 | true | true | 10 * 3600 + 100 | -9 || -9 | true | true | 5 * 3600 } def "Handle the edge case of discharging in tolerance margins"() { @@ -339,7 +339,7 @@ class StorageModelTest extends Specification { ) then: - Math.abs(result._1.chargingPower().toKilowatts() - (-10d)) < TOLERANCE + Math.abs(result._1.chargingPower().toKilowatts() - (-9d)) < TOLERANCE result._1.tick() == startTick + 1 Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE def flexChangeIndication = result._2 @@ -367,7 +367,7 @@ class StorageModelTest extends Specification { ) then: - Math.abs(result._1.chargingPower().toKilowatts() - (4.5d)) < TOLERANCE + Math.abs(result._1.chargingPower().toKilowatts() - (5d)) < TOLERANCE result._1.tick() == startTick + 1 Math.abs(result._1.storedEnergy().toKilowattHours() - oldState.storedEnergy().toKilowattHours()) < TOLERANCE def flexChangeIndication = result._2 From ad44f3823a4a07f25478c2869804802324dff632 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 19:53:39 +0200 Subject: [PATCH 29/67] Removing TODO relating to activePowerGradient, not implemented for now Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index d0892c43d0..2d3a234751 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -373,8 +373,6 @@ object StorageModel { targetSoc, ) - // TODO include activePowerGradient,? - model.enable() model } From faca4980a20f2b69c0ee39d5827c400b18e2a921 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 19:57:43 +0200 Subject: [PATCH 30/67] Added TODO Signed-off-by: Sebastian Peter --- .../agent/participant/storage/StorageAgentFundamentals.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala index 4116a93325..483fb893fb 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -376,6 +376,6 @@ trait StorageAgentFundamentals calcRelevantData: StorageRelevantData, nodalVoltage: Dimensionless, model: StorageModel, - ): StorageState = ??? + ): StorageState = ??? // TODO implement? } From b83ec0a50b4d89e22ac4f8673a4ba174511dc2a6 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 21 Jun 2024 21:44:56 +0200 Subject: [PATCH 31/67] Adapted test to fixed discharging net power Signed-off-by: Sebastian Peter --- .../StorageAgentModelCalculationSpec.scala | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala index 5103c7dc5c..d7c8378ce5 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala @@ -461,9 +461,9 @@ class StorageAgentModelCalculationSpec ) // after 2 hours, we're at: 111.95296 kWh - // net power = -12.961kW * 0.92 = -11.92412kW - // time to discharge until lowest energy (40 kWh) ~= 6.03423648873h = 21723 ticks (rounded) from now - // current tick is 36000, thus: 36000 + 21723 = 57723 + // net power = -12.961kW / 0.92 = -14.08804348kW + // time to discharge until lowest energy (0 kWh) ~= 7.946664856h = 28608 ticks (rounded) from now + // current tick is 36000, thus: 36000 + 28608 = 64608 emAgent.expectMsgPF() { case FlexCtrlCompletion( modelUuid, @@ -475,7 +475,7 @@ class StorageAgentModelCalculationSpec result.p should approximate(pMax * -1) result.q should approximate(Megavars(0)) requestAtNextActivation shouldBe false - requestAtTick shouldBe Some(57723) + requestAtTick shouldBe Some(64608) } resultListener.expectMsgPF() { @@ -496,10 +496,10 @@ class StorageAgentModelCalculationSpec emAgent.send(storageAgent, IssuePowerControl(43200, Kilowatts(12))) - // after 2 hours, we're at: 88.10472 kWh + // after 2 hours, we're at: 83.77687304 kWh // net power = 12 * 0.92 = 11.04 kW - // time to charge until full ~= 10.135442029h = 36488 ticks (rounded) from now - // current tick is 43200, thus: 43200 + 36488 = 79688 + // time to charge until full ~= 10.52745715h = 37899 ticks (rounded) from now + // current tick is 43200, thus: 43200 + 37899 = 81099 emAgent.expectMsgPF() { case FlexCtrlCompletion( modelUuid, @@ -511,7 +511,7 @@ class StorageAgentModelCalculationSpec result.p should approximate(Kilowatts(12)) result.q should approximate(Megavars(0)) requestAtNextActivation shouldBe false - requestAtTick shouldBe Some(79688) + requestAtTick shouldBe Some(81099) } resultListener.expectMsgPF() { @@ -520,16 +520,16 @@ class StorageAgentModelCalculationSpec result.getTime shouldBe 43200.toDateTime(simulationStartDate) result.getP should beEquivalentTo(12d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(44.05236d.asPercent) + result.getSoc should beEquivalentTo(41.88843652173913d.asPercent) } - /* TICK 79688 (expected activation) + /* TICK 81099 (expected activation) - discharging with 12 kW - expecting changing flex options indicator (discharging from full) */ // Request flex options - emAgent.send(storageAgent, RequestFlexOptions(79688)) + emAgent.send(storageAgent, RequestFlexOptions(81099)) emAgent.expectMsgType[ProvideFlexOptions] match { case ProvideMinMaxFlexOptions( @@ -546,7 +546,7 @@ class StorageAgentModelCalculationSpec resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => flexResult.getInputModel shouldBe storageInputQv.getUuid - flexResult.getTime shouldBe 79688.toDateTime(simulationStartDate) + flexResult.getTime shouldBe 81099.toDateTime(simulationStartDate) flexResult.getpRef should beEquivalentTo(0d.asKiloWatt) flexResult.getpMin should beEquivalentTo( storageInputQv.getType.getpMax().multiply(-1) @@ -554,12 +554,12 @@ class StorageAgentModelCalculationSpec flexResult.getpMax should beEquivalentTo(0d.asKiloWatt) } - emAgent.send(storageAgent, IssuePowerControl(79688, Kilowatts(-12))) + emAgent.send(storageAgent, IssuePowerControl(81099, Kilowatts(-12))) // we're full now at 200 kWh - // net power = -12 * 0.92 = -11.04 kW - // time to discharge until lowest energy ~= 14.4927536232h = 52174 ticks (rounded) from now - // current tick is 79688, thus: 79688 + 52174 = 131862 + // net power = -12 / 0.92 = -13.04347826 kW + // time to discharge until empty ~= 15.33333333h = 55200 ticks from now + // current tick is 79688, thus: 81099 + 55200 = 136299 emAgent.expectMsgPF() { case FlexCtrlCompletion( modelUuid, @@ -571,25 +571,25 @@ class StorageAgentModelCalculationSpec result.p should approximate(Kilowatts(-12)) result.q should approximate(Megavars(0)) requestAtNextActivation shouldBe true - requestAtTick shouldBe Some(131862) + requestAtTick shouldBe Some(136299) } resultListener.expectMsgPF() { case ParticipantResultEvent(result: StorageResult) => result.getInputModel shouldBe storageInputQv.getUuid - result.getTime shouldBe 79688.toDateTime(simulationStartDate) + result.getTime shouldBe 81099.toDateTime(simulationStartDate) result.getP should beEquivalentTo((-12d).asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(100d.asPercent) } - /* TICK 131862 (expected activation) + /* TICK 136299 (expected activation) - no charging - expecting no changing flex options indicator */ // Request flex options - emAgent.send(storageAgent, RequestFlexOptions(131862)) + emAgent.send(storageAgent, RequestFlexOptions(136299)) emAgent.expectMsgType[ProvideFlexOptions] match { case ProvideMinMaxFlexOptions( @@ -606,13 +606,13 @@ class StorageAgentModelCalculationSpec resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => flexResult.getInputModel shouldBe storageInputQv.getUuid - flexResult.getTime shouldBe 131862.toDateTime(simulationStartDate) + flexResult.getTime shouldBe 136299.toDateTime(simulationStartDate) flexResult.getpRef should beEquivalentTo(0d.asKiloWatt) flexResult.getpMin should beEquivalentTo(0d.asKiloWatt) flexResult.getpMax should beEquivalentTo(storageInputQv.getType.getpMax) } - emAgent.send(storageAgent, IssuePowerControl(131862, Kilowatts(0d))) + emAgent.send(storageAgent, IssuePowerControl(136299, Kilowatts(0d))) // we're not charging or discharging, no new expected tick emAgent.expectMsgPF() { @@ -632,10 +632,10 @@ class StorageAgentModelCalculationSpec resultListener.expectMsgPF() { case ParticipantResultEvent(result: StorageResult) => result.getInputModel shouldBe storageInputQv.getUuid - result.getTime shouldBe 131862.toDateTime(simulationStartDate) + result.getTime shouldBe 136299.toDateTime(simulationStartDate) result.getP should beEquivalentTo(0.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(19.999866666667d.asPercent) + result.getSoc should beEquivalentTo(0d.asPercent) } } From 4693145abb08a7c87773a6b93c4f0ff45b72cc49 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 24 Jun 2024 08:44:51 +0200 Subject: [PATCH 32/67] Implementing requested changes. --- .../edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala index 37c42e8ed0..0a3eb13e08 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -66,7 +66,8 @@ class SimonaStandaloneSetup( resultFileHierarchy: ResultFileHierarchy, runtimeEventQueue: Option[LinkedBlockingQueue[RuntimeEvent]] = None, override val args: Array[String], - override val logOutputDir: String, +)( + override val logOutputDir: String = resultFileHierarchy.logOutputDir ) extends SimonaSetup { override def gridAgents( @@ -392,6 +393,5 @@ object SimonaStandaloneSetup extends LazyLogging with SetupHelper { resultFileHierarchy, runtimeEventQueue, mainArgs, - resultFileHierarchy.logOutputDir, - ) + )() } From fd5b4a4fefe844aebb6e474e67f6b766b524f095 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 08:48:12 +0200 Subject: [PATCH 33/67] Removed obsolete TODO Signed-off-by: Sebastian Peter --- .../agent/participant/storage/StorageAgentFundamentals.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala index 483fb893fb..563f2d7c76 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -345,8 +345,6 @@ trait StorageAgentFundamentals voltage, ) - // TODO: Actually change state and calculate the next tick, when something happens - ( updatedState, ApparentPower(updatedSetPower, reactivePower), From 32767e928aa4c1c6cd26e20a5aea3d853aa53f81 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 08:52:03 +0200 Subject: [PATCH 34/67] Solved TODO Signed-off-by: Sebastian Peter --- .../agent/participant/storage/StorageAgentFundamentals.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala index 563f2d7c76..8d9a84e9a5 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -374,6 +374,9 @@ trait StorageAgentFundamentals calcRelevantData: StorageRelevantData, nodalVoltage: Dimensionless, model: StorageModel, - ): StorageState = ??? // TODO implement? + ): StorageState = + throw new InvalidRequestException( + "StorageAgent cannot be used without EM control" + ) } From 61ecd29af0b5d7a417f131b47b86c36c18360532 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 09:00:30 +0200 Subject: [PATCH 35/67] Remove TODOs that won't be solved with the current implementation Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 2d3a234751..bca9764876 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -37,8 +37,8 @@ final case class StorageModel( pMax: Power, eta: Dimensionless, dod: Dimensionless, - initialSoc: Double, // TODO this is ugly and should be solved in a different way, as this value is only used outside the model - targetSoc: Option[Double], // TODO only needed for initializing fields + initialSoc: Double, + targetSoc: Option[Double], ) extends SystemParticipant[StorageRelevantData, ApparentPower, StorageState]( uuid, id, From fb41c8682e6544dfb7759c08d71db7c3f1c2ff65 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 09:23:52 +0200 Subject: [PATCH 36/67] Refactored and fixed tolerances Signed-off-by: Sebastian Peter --- .../ie3/simona/model/participant/StorageModel.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index bca9764876..3cf76134ae 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -50,22 +50,25 @@ final case class StorageModel( private val minEnergy = zeroKWH - // max Tolerance 1W till GWh storage - private implicit val doubleTolerance: Power = eStorage / Seconds(1) * 3.6e-12 + /** Tolerance for power comparisons. Amounts to 1 W for 1 GWh storage + */ + private implicit val powerTolerance: Power = eStorage / Seconds(1) / 3.6e12 /** In order to avoid faulty flexibility options, we want to avoid offering * charging/discharging that could last less than our smallest possible time * delta, which is one second. */ - private val toleranceMargin = pMax * Seconds(1d) + private val toleranceMargin: Energy = pMax * Seconds(1d) /** Minimal allowed energy with tolerance margin added */ - private val minEnergyWithMargin = minEnergy + (toleranceMargin / eta.toEach) + private val minEnergyWithMargin: Energy = + minEnergy + (toleranceMargin / eta.toEach) /** Maximum allowed energy with tolerance margin added */ - private val maxEnergyWithMargin = eStorage - (toleranceMargin * eta.toEach) + private val maxEnergyWithMargin: Energy = + eStorage - (toleranceMargin * eta.toEach) private val refTargetSoc = targetSoc.map { target => val targetEnergy = eStorage * target From 4311dad38db542dbfadcae414908aab183638bce Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 09:31:22 +0200 Subject: [PATCH 37/67] More ScalaDoc Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant/StorageModel.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 3cf76134ae..e6448b079f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -316,6 +316,14 @@ object StorageModel { tick: Long, ) extends ModelState + /** @param targetSoc + * The SOC that the StorageModel aims at, i.e. that it prefers to + * charge/discharge towards + * @param targetWithPosMargin + * The targetSoc plus a tolerance margin + * @param targetWithNegMargin + * The targetSoc minus a tolerance margin + */ final case class RefTargetSocParams( targetSoc: Energy, targetWithPosMargin: Energy, From e59874241508465bfca6dd24e5045133da3863cf Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 09:32:01 +0200 Subject: [PATCH 38/67] Removing duplicate ScalaDoc Signed-off-by: Sebastian Peter --- .../ie3/simona/model/participant/StorageModel.scala | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index e6448b079f..9672a10708 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -86,19 +86,6 @@ final case class StorageModel( ) } - /** Calculate the power behaviour based on the given data. - * - * @param tick - * Regarded instant in simulation - * @param voltage - * Nodal voltage magnitude - * @param modelState - * Current state of the model - * @param data - * Further needed, secondary data - * @return - * A tuple of active and reactive power - */ override def calculatePower( tick: Long, voltage: Dimensionless, From 81b5212a6cd825f3d564a4037a54a1ff24abdd71 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 11:45:19 +0200 Subject: [PATCH 39/67] Enhancing commentary on tolerance values Signed-off-by: Sebastian Peter --- .../model/participant/StorageModel.scala | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 9672a10708..25a741172e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -50,13 +50,26 @@ final case class StorageModel( private val minEnergy = zeroKWH - /** Tolerance for power comparisons. Amounts to 1 W for 1 GWh storage + /** Tolerance for power comparisons. With very small (dis-)charging powers, + * problems can occur when calculating the future tick at which storage is + * full or empty. For sufficiently large time frames, the maximum Long value + * ([[Long.MaxValue]]) can be exceeded, thus the Long value overflows and we + * get undefined behavior. + * + * Thus, small (dis-)charging powers compared to storage capacity have to be + * set to zero. The given tolerance value below amounts to 1 W for 1 GWh + * storage capacity and is sufficient in preventing Long overflows. */ private implicit val powerTolerance: Power = eStorage / Seconds(1) / 3.6e12 - /** In order to avoid faulty flexibility options, we want to avoid offering - * charging/discharging that could last less than our smallest possible time - * delta, which is one second. + /** In order to avoid faulty behavior of storages, we want to avoid offering + * charging/discharging when storage is very close to full, empty or to a + * target. + * + * In particular, we want to avoid offering the option to (dis-)charge if + * that operation could last less than our smallest possible time step, which + * is one second. Thus, we establish a safety margin of the energy + * (dis-)charged with maximum power in one second. */ private val toleranceMargin: Energy = pMax * Seconds(1d) From 85cd460691bd96b5578b576fdacf81eb9cdb86ea Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 11:49:01 +0200 Subject: [PATCH 40/67] Enhancing commentary Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 25a741172e..97de5ecbfa 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -177,9 +177,9 @@ final case class StorageModel( if ( // if power is close to zero, set it to zero (setPower ~= zeroKW) - // do not keep charging if we're already full + // do not keep charging if we're already full (including safety margin) || (setPower > zeroKW && isFull(currentStoredEnergy)) - // do not keep discharging if we're already empty + // do not keep discharging if we're already empty (including safety margin) || (setPower < zeroKW && isEmpty(currentStoredEnergy)) ) zeroKW From d11d80643eacf65d753aeefc0899380eb3e8dcb5 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 24 Jun 2024 11:52:05 +0200 Subject: [PATCH 41/67] Tiny improvement Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 97de5ecbfa..7a8877a16d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -266,9 +266,9 @@ final case class StorageModel( lastState: StorageState, currentTick: Long, ): Energy = { - val timespan = currentTick - lastState.tick + val timespan = Seconds(currentTick - lastState.tick) val netPower = calcNetPower(lastState.chargingPower) - val energyChange = netPower * Seconds(timespan) + val energyChange = netPower * timespan val newEnergy = lastState.storedEnergy + energyChange From e4095289a4b29755684246e3379fb8ecc69a7151 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 24 Jun 2024 13:17:15 +0200 Subject: [PATCH 42/67] Adapting some parts. --- build.gradle | 2 +- src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala | 8 -------- .../ie3/simona/service/weather/SampleWeatherSource.scala | 4 ++++ .../ie3/simona/service/weather/WeatherSourceSpec.scala | 4 ++++ .../simona/service/weather/WeatherSourceWrapperSpec.scala | 6 ++---- .../ie3/simona/test/common/input/EmInputTestData.scala | 4 ---- 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index c18367c433..192cee24d6 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { /* Exclude our own nested dependencies */ exclude group: 'com.github.ie3-institute' } - implementation('com.github.ie3-institute:PowerSystemDataModel:5.0.1') { + implementation('com.github.ie3-institute:PowerSystemDataModel:6.0-SNAPSHOT') { exclude group: 'org.apache.logging.log4j' exclude group: 'org.slf4j' /* Exclude our own nested dependencies */ diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index 782a4a9bb5..6373f495cd 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -15,14 +15,6 @@ import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ SqlParams, } import edu.ie3.simona.config.SimonaConfig.Simona.Output.Sink.InfluxDb1x -import edu.ie3.simona.config.SimonaConfig.{ - BaseCsvParams, - BaseOutputConfig, - RefSystemConfig, - ResultKafkaParams, - Simona, - TransformerControlGroup, -} import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.exceptions.InvalidConfigParameterException import edu.ie3.simona.io.result.ResultSinkType diff --git a/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala index 2ecf881c3f..c8045ced59 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala @@ -177,6 +177,10 @@ object SampleWeatherSource { coordinate, getClosestCoordinates(coordinate, 9, distance), ) + + override def validate(): Unit = { + /* nothing to do here */ + } } // these lists contain the hourly weather values for each first of the month of 2011 + january of diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala index 3da4ac7f85..3aacd91ff0 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala @@ -404,5 +404,9 @@ case object WeatherSourceSpec { coordinate, getClosestCoordinates(coordinate, 9, distance), ) + + override def validate(): Unit = { + /* nothing to do here */ + } } } 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 ab00e32bdf..3c937e0976 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -318,11 +318,9 @@ object WeatherSourceWrapperSpec { ), ) - override def getSourceFields[C <: WeatherValue]( - entityClass: Class[C] - ): Optional[util.Set[String]] = + override def getSourceFields: Optional[util.Set[String]] = // only required for validation - Optional.empty + Optional.empty() override def getWeather( timeInterval: ClosedInterval[ZonedDateTime] diff --git a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala index 4c349c603e..893fcfd8dc 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala @@ -32,7 +32,6 @@ import edu.ie3.simona.util.ConfigUtil import edu.ie3.util.quantities.PowerSystemUnits._ import squants.energy.Kilowatts import tech.units.indriya.quantity.Quantities -import tech.units.indriya.unit.Units._ import java.util.UUID import scala.jdk.CollectionConverters.SeqHasAsJava @@ -78,9 +77,6 @@ trait EmInputTestData Quantities.getQuantity(5d, KILOWATT), Quantities.getQuantity(0.03, PU_PER_HOUR), Quantities.getQuantity(0.95, PU), - Quantities.getQuantity(20d, PERCENT), - Quantities.getQuantity(50000d, HOUR), - 100000, ) protected val householdStorageInput = new StorageInput( From dbe27c05e2e48eda6db3a747305b7be661efad6d Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 24 Jun 2024 13:25:51 +0200 Subject: [PATCH 43/67] Fixing `Codacy` issue. --- .../service/weather/WeatherSource.scala | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 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 1872fca6c8..12a26c3f6c 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -106,21 +106,20 @@ trait WeatherSource { maxCoordinateDistance, ) - val nr = possibleCornerPoints.size() - - if (nr == 1) { - // found one exact match - Success(possibleCornerPoints.asScala) - } else if (nr == amountOfInterpolationCoords) { - // found enough points for interpolating - Success(possibleCornerPoints.asScala) - } else { - Failure( - ServiceException( - s"There are not enough coordinates for averaging. Found ${possibleCornerPoints.size()} within the given distance of " + - s"$maxCoordinateDistance but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." + possibleCornerPoints.size() match { + case 1 => + // found one exact match + Success(possibleCornerPoints.asScala) + case nr if nr == amountOfInterpolationCoords => + // found enough points for interpolating + Success(possibleCornerPoints.asScala) + case invalidNo => + Failure( + ServiceException( + s"There are not enough coordinates for averaging. Found $invalidNo within the given distance of " + + s"$maxCoordinateDistance but need $amountOfInterpolationCoords. Please make sure that there are enough coordinates within the given distance." + ) ) - ) } } From b54294be67e0738acb0422cc200f4e7ecbda65b5 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 24 Jun 2024 13:51:56 +0200 Subject: [PATCH 44/67] Updating PSDM. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 192cee24d6..68ca2f3287 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { /* Exclude our own nested dependencies */ exclude group: 'com.github.ie3-institute' } - implementation('com.github.ie3-institute:PowerSystemDataModel:6.0-SNAPSHOT') { + implementation('com.github.ie3-institute:PowerSystemDataModel:5.1.0') { exclude group: 'org.apache.logging.log4j' exclude group: 'org.slf4j' /* Exclude our own nested dependencies */ From 4eadaf81d172567673aa80c55f11561d215897c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:23:58 +0000 Subject: [PATCH 45/67] Bump org.scalatest:scalatest_2.13 from 3.2.18 to 3.2.19 (#833) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c18367c433..e7fdf21625 100644 --- a/build.gradle +++ b/build.gradle @@ -103,7 +103,7 @@ dependencies { testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' testImplementation 'org.mockito:mockito-core:5.12.0' // mocking framework - testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.18" + testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.19" testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.8' //scalatest html output testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' testImplementation "org.apache.pekko:pekko-testkit_${scalaVersion}:${pekkoVersion}" // pekko testkit From 00b96a490a3a00bf399077caf1cc6fdf2f9e867e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:42:15 +0000 Subject: [PATCH 46/67] Bump testContainerVersion from 0.41.3 to 0.41.4 (#830) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e7fdf21625..25a1d22448 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ ext { tscfgVersion = '1.0.0' scapegoatVersion = '2.1.6' - testContainerVersion = '0.41.3' + testContainerVersion = '0.41.4' scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins } From 4c98a0837821cc26fe907e28810c5dc117daa369 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Tue, 25 Jun 2024 11:29:09 +0200 Subject: [PATCH 47/67] Updating to PSDM version 5.1.0 --- CHANGELOG.md | 1 + build.gradle | 2 +- .../simona/service/weather/SampleWeatherSource.scala | 12 ++++++++++++ .../simona/service/weather/WeatherSourceSpec.scala | 12 ++++++++++++ .../service/weather/WeatherSourceWrapperSpec.scala | 4 +--- .../simona/test/common/input/EmInputTestData.scala | 3 --- 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c05424bddc..503802b799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactoring of `GridAgent` messages [#736](https://github.com/ie3-institute/simona/issues/736) - Rewrote PVModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Making configuration of `RefSystem` via config optional [#769](https://github.com/ie3-institute/simona/issues/769) +- Updated PSDM to version 5.1.0 [#835](https://github.com/ie3-institute/simona/issues/835) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) diff --git a/build.gradle b/build.gradle index c18367c433..68ca2f3287 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { /* Exclude our own nested dependencies */ exclude group: 'com.github.ie3-institute' } - implementation('com.github.ie3-institute:PowerSystemDataModel:5.0.1') { + implementation('com.github.ie3-institute:PowerSystemDataModel:5.1.0') { exclude group: 'org.apache.logging.log4j' exclude group: 'org.slf4j' /* Exclude our own nested dependencies */ diff --git a/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala index 5788aca327..37a5cd8fa9 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala @@ -168,6 +168,18 @@ object SampleWeatherSource { else Vector.empty[CoordinateDistance].asJava } + + override def findCornerPoints( + point: Point, + distance: ComparableQuantity[Length], + ): util.List[CoordinateDistance] = { + // just a dummy implementation, because this is just a sample weather source + getClosestCoordinates(point, 4, distance) + } + + override def validate(): Unit = { + /* nothing to do here */ + } } // these lists contain the hourly weather values for each first of the month of 2011 + january of diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala index 2da8e6c63e..aa6fd74bc6 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala @@ -450,5 +450,17 @@ case object WeatherSourceSpec { ): util.List[CoordinateDistance] = { calculateCoordinateDistances(coordinate, n, coordinateToId.keySet.asJava) } + + override def findCornerPoints( + point: Point, + distance: ComparableQuantity[Length], + ): util.List[CoordinateDistance] = { + // just a dummy implementation + getClosestCoordinates(point, 4, distance) + } + + override def validate(): Unit = { + /* nothing to do here */ + } } } 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 ab00e32bdf..fcad677f91 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -318,9 +318,7 @@ object WeatherSourceWrapperSpec { ), ) - override def getSourceFields[C <: WeatherValue]( - entityClass: Class[C] - ): Optional[util.Set[String]] = + override def getSourceFields: Optional[util.Set[String]] = // only required for validation Optional.empty diff --git a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala index 4c349c603e..7eeb67574c 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala @@ -78,9 +78,6 @@ trait EmInputTestData Quantities.getQuantity(5d, KILOWATT), Quantities.getQuantity(0.03, PU_PER_HOUR), Quantities.getQuantity(0.95, PU), - Quantities.getQuantity(20d, PERCENT), - Quantities.getQuantity(50000d, HOUR), - 100000, ) protected val householdStorageInput = new StorageInput( From deb4e8905b8c35db3ba9279e08c979a6cf850d4c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 26 Jun 2024 08:48:32 +0200 Subject: [PATCH 48/67] Fixing error message Signed-off-by: Sebastian Peter --- .../agent/participant/storage/StorageAgentFundamentals.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala index 8d9a84e9a5..c233288d67 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgentFundamentals.scala @@ -195,7 +195,7 @@ trait StorageAgentFundamentals ) => ApparentPower = (_, _, _, _) => throw new InvalidRequestException( - "PV model cannot be run without secondary data." + "Storage model cannot be run without secondary data." ) override def calculatePowerWithSecondaryDataAndGoToIdle( From 97f5daeba72a0ef501ae0c0df3bbbeb8d4b8691d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 06:51:32 +0000 Subject: [PATCH 49/67] Bump org.scoverage from 8.0.3 to 8.1 (#837) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 25a1d22448..eb68f80d8b 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { id "kr.motd.sphinx" version "2.10.1" // documentation generation id "com.github.johnrengelman.shadow" version "8.1.1" // fat jar id "org.sonarqube" version "5.0.0.4638" // sonarqube - id "org.scoverage" version "8.0.3" // scala code coverage scoverage + id "org.scoverage" version "8.1" // scala code coverage scoverage id "com.github.maiflai.scalatest" version "0.32" // run scalatest without specific spec task id 'org.hidetake.ssh' version '2.11.2' id 'net.thauvin.erik.gradle.semver' version '1.0.4' // semantic versioning From 49d2049cb81147e1d47f0ccce7f29086aa0134df Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 26 Jun 2024 12:26:38 +0200 Subject: [PATCH 50/67] Adapting StorageModelTest to changes in PSDM Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant/StorageModelTest.groovy | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy index f6ad21ef0c..3c115e5e50 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy @@ -54,9 +54,6 @@ class StorageModelTest extends Specification { getQuantity(10d, KILOWATT), getQuantity(0.03, PU_PER_HOUR), getQuantity(0.9, PU), - getQuantity(20d, PERCENT), - getQuantity(43800.0, HOUR), - 100000 ) inputModel = new StorageInput( From de45c968d7e0eebfa454b881d9259128dbbc4e61 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 26 Jun 2024 16:49:32 +0200 Subject: [PATCH 51/67] Defined logOutputDir as method, not value Signed-off-by: Sebastian Peter --- src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala | 2 +- .../edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala | 6 +++--- src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala | 2 +- .../scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala index 8809c4ea6b..bfb4e16cee 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaSetup.scala @@ -40,7 +40,7 @@ trait SimonaSetup { /** Directory of the log output. */ - val logOutputDir: String + def logOutputDir: String /** Creates the runtime event listener * diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala index 0a3eb13e08..32711a4a3d 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -66,10 +66,10 @@ class SimonaStandaloneSetup( resultFileHierarchy: ResultFileHierarchy, runtimeEventQueue: Option[LinkedBlockingQueue[RuntimeEvent]] = None, override val args: Array[String], -)( - override val logOutputDir: String = resultFileHierarchy.logOutputDir ) extends SimonaSetup { + override def logOutputDir: String = resultFileHierarchy.logOutputDir + override def gridAgents( context: ActorContext[_], environmentRefs: EnvironmentRefs, @@ -393,5 +393,5 @@ object SimonaStandaloneSetup extends LazyLogging with SetupHelper { resultFileHierarchy, runtimeEventQueue, mainArgs, - )() + ) } diff --git a/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala b/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala index be2ecac3d3..ad028070d2 100644 --- a/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/SimonaSimSpec.scala @@ -398,7 +398,7 @@ object SimonaSimSpec { override val args: Array[String] = Array.empty[String] - override val logOutputDir: String = "" + override def logOutputDir: String = throw new NotImplementedError() override def runtimeEventListener( context: ActorContext[_] diff --git a/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala b/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala index ba62417007..55e7310266 100644 --- a/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala +++ b/src/test/scala/edu/ie3/simona/sim/setup/SimonaSetupSpec.scala @@ -32,7 +32,7 @@ class SimonaSetupSpec extends UnitSpec with SimonaSetup with SubGridGateMokka { override val args: Array[String] = Array.empty[String] - override val logOutputDir: String = "" + override def logOutputDir: String = throw new NotImplementedError() override def runtimeEventListener( context: ActorContext[_] From 16e874e4dac0d095c75c61549f1cb80c5f7a2dc8 Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 26 Jun 2024 17:19:42 +0200 Subject: [PATCH 52/67] Delete "Indices and tables" on the index page --- CHANGELOG.md | 1 + docs/readthedocs/allpages.md | 17 ----------------- docs/readthedocs/conf.py | 2 +- docs/readthedocs/index.rst | 9 +-------- 4 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 docs/readthedocs/allpages.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a008103c1d..52efa60e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Default RefSystem using the unit `Volt` for low voltage grids [#811](https://github.com/ie3-institute/simona/issues/811) - Fixed grid within GridSpec test [#806](https://github.com/ie3-institute/simona/issues/806) - Fixed log entry after power flow calculation [#814](https://github.com/ie3-institute/simona/issues/814) +- Delete "Indices and tables" on the index page [#375](https://github.com/ie3-institute/simona/issues/375) ## [3.0.0] - 2023-08-07 diff --git a/docs/readthedocs/allpages.md b/docs/readthedocs/allpages.md deleted file mode 100644 index d58cac0a20..0000000000 --- a/docs/readthedocs/allpages.md +++ /dev/null @@ -1,17 +0,0 @@ -(allpages)= - -# All Pages - -Here are all available pages listed: - -```{toctree} ---- -maxdepth: 1 ---- -.. toctree:: - :maxdepth: 2 - :glob: - - * - !allpages -``` diff --git a/docs/readthedocs/conf.py b/docs/readthedocs/conf.py index e78a262a87..16f145a060 100644 --- a/docs/readthedocs/conf.py +++ b/docs/readthedocs/conf.py @@ -37,7 +37,7 @@ html_theme = 'sphinx_rtd_theme' html_short_title = "simona" htmlhelp_basename = 'simona-doc' -html_use_index = True +html_use_index = False html_show_sourcelink = False html_static_path = ['_static'] diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index 1360094243..03fb56bbb2 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -18,11 +18,4 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu models developersguide references - allpages - -Indices and tables -================== - -For more information, please visit the :doc:`All Pages ` page. - - + index \ No newline at end of file From 52667804871d2ecc9279b656ae4c02513aff5cb1 Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 26 Jun 2024 17:23:58 +0200 Subject: [PATCH 53/67] fmt --- docs/readthedocs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index 03fb56bbb2..e9029962ab 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -17,5 +17,4 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu config models developersguide - references - index \ No newline at end of file + references \ No newline at end of file From 95a9c5fd8676f7c9e8120eba9663cfb11949c911 Mon Sep 17 00:00:00 2001 From: pierrepetersmeier Date: Wed, 26 Jun 2024 17:37:26 +0200 Subject: [PATCH 54/67] fmt --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c05424bddc..f684989eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Default RefSystem using the unit `Volt` for low voltage grids [#811](https://github.com/ie3-institute/simona/issues/811) - Fixed grid within GridSpec test [#806](https://github.com/ie3-institute/simona/issues/806) - Fixed log entry after power flow calculation [#814](https://github.com/ie3-institute/simona/issues/814) +- Delete "Indices and tables" on the index page [#375](https://github.com/ie3-institute/simona/issues/375) ## [3.0.0] - 2023-08-07 From a3000b3f50348277d704e1caf871c27b47256b84 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 27 Jun 2024 09:32:59 +0200 Subject: [PATCH 55/67] Reverting merge slips --- CHANGELOG.md | 1 + docs/readthedocs/conf.py | 1 + docs/readthedocs/index.rst | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f684989eea..6de5f04b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactoring of `GridAgent` messages [#736](https://github.com/ie3-institute/simona/issues/736) - Rewrote PVModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Making configuration of `RefSystem` via config optional [#769](https://github.com/ie3-institute/simona/issues/769) +- Updated PSDM to version 5.1.0 [#835](https://github.com/ie3-institute/simona/issues/835) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) diff --git a/docs/readthedocs/conf.py b/docs/readthedocs/conf.py index 16f145a060..6ec36e73ef 100644 --- a/docs/readthedocs/conf.py +++ b/docs/readthedocs/conf.py @@ -44,6 +44,7 @@ # PlantUML options plantuml = 'plantuml' + # Intersphinx for references to external ReadTheDocs intersphinx_mapping = { 'psdm': ('https://powersystemdatamodel.readthedocs.io/en/latest/', None), diff --git a/docs/readthedocs/index.rst b/docs/readthedocs/index.rst index e9029962ab..5ad7bc0ffd 100644 --- a/docs/readthedocs/index.rst +++ b/docs/readthedocs/index.rst @@ -17,4 +17,4 @@ Institute of Energy Systems, Energy Efficiency and Energy Economics at TU Dortmu config models developersguide - references \ No newline at end of file + references From b5eb5545ed207eecb82c8117a446dee237dffcd6 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 1 Jul 2024 09:02:43 +0200 Subject: [PATCH 56/67] Specifying the stashed messages. --- .../ie3/simona/agent/grid/DBFSAlgorithm.scala | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala index 8dc1b65ae8..7999c226f0 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -490,9 +490,12 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // return to Idle idle(cleanedGridAgentBaseData) - case (message, _) => - ctx.log.debug(s"Received the message $message too early. Stash away!") - buffer.stash(message) + // handles power request that arrive to early + case (requestGridPower: RequestGridPower, _) => + ctx.log.debug( + s"Received the message $requestGridPower too early. Stash away!" + ) + buffer.stash(requestGridPower) Behaviors.same } } @@ -784,7 +787,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // happens only when we received slack data and power values before we received a request to provide grid data // (only possible when first simulation triggered and this agent is faster in this state as the request // by a superior grid arrives) - case (powerResponse: PowerResponse, _: GridAgentBaseData) => + case (powerResponse: PowerResponse, _) => ctx.log.debug( "Received Request for Grid Power too early. Stashing away" ) @@ -792,20 +795,11 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { buffer.stash(powerResponse) Behaviors.same - // happens only when we received slack data and power values before we received a request to provide grid - // (only possible when first simulation triggered and this agent is faster - // with its power flow calculation in this state as the request by a superior grid arrives) - case (powerResponse: PowerResponse, _: PowerFlowDoneData) => + case (requestGridPower: RequestGridPower, _) => ctx.log.debug( - "Received Request for Grid Power too early. Stashing away" + s"Received the message $requestGridPower too early. Stashing away!" ) - - buffer.stash(powerResponse) - Behaviors.same - - case (message, _) => - ctx.log.debug(s"Received the message $message too early. Stash away!") - buffer.stash(message) + buffer.stash(requestGridPower) Behaviors.same } } From 9b8289aea95f388d169c0e2695f68a0db2e57a9c Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Tue, 2 Jul 2024 10:11:46 +0200 Subject: [PATCH 57/67] fix uuid of controlling ems when building participantActor --- .../scala/edu/ie3/simona/agent/grid/GridAgentController.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala index 7ba4abef77..ed6e605130 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -208,7 +208,7 @@ class GridAgentController( participant, thermalIslandGridsByBusId, environmentRefs, - allEms.get(participant.getUuid), + allEms.get(participant.getControllingEm.get().getUuid), ) introduceAgentToEnvironment(actorRef) // return uuid to actorRef From a9ca84662990efac30dd56695ad6b573394cdd37 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Tue, 2 Jul 2024 11:05:28 +0200 Subject: [PATCH 58/67] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d10d20b89..58aff37bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed grid within GridSpec test [#806](https://github.com/ie3-institute/simona/issues/806) - Fixed log entry after power flow calculation [#814](https://github.com/ie3-institute/simona/issues/814) - Delete "Indices and tables" on the index page [#375](https://github.com/ie3-institute/simona/issues/375) +- Fixed provision of controllingEms within buildParticipantToActorRef [#841](https://github.com/ie3-institute/simona/issues/841) ## [3.0.0] - 2023-08-07 From 557afe809f4585bf187f7466129907bada8def52 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Tue, 2 Jul 2024 14:45:49 +0200 Subject: [PATCH 59/67] refactor emActorRef to match tests --- .../edu/ie3/simona/agent/grid/GridAgentController.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala index ed6e605130..d3c02e20db 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -201,6 +201,9 @@ class GridAgentController( .map { participant => val node = participant.getNode + val controllingEm: Option[UUID] = + participant.getControllingEm.toScala.map(_.getUuid) + val emActorRef = controllingEm.flatMap(allEms.get) val actorRef = buildParticipantActor( participantsConfig.requestVoltageDeviationThreshold, participantConfigUtil, @@ -208,7 +211,7 @@ class GridAgentController( participant, thermalIslandGridsByBusId, environmentRefs, - allEms.get(participant.getControllingEm.get().getUuid), + emActorRef, ) introduceAgentToEnvironment(actorRef) // return uuid to actorRef From 8614f569bf65b0a3bc01acd02ba98e96fac38efa Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 2 Jul 2024 16:52:39 +0200 Subject: [PATCH 60/67] Enhance storage runtime configuration documentation Signed-off-by: Sebastian Peter --- docs/readthedocs/config.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index 039d250336..eaef457673 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -225,7 +225,8 @@ Runtime configurations of other system participants are done similarly, except t ### Storage runtime configuration -The storage model requires default parameter for the inital state of charge (Soc) and the target Soc for electrical energy storages. Soc's need to be between 0.0 and <= 1.0. +The storage model takes parameters for the inital state of charge (SOC) and the target SOC for electrical energy storages, with 0.0 <= SOC <= 1.0. +The initial SOC defaults to 0%, while the target SOC is optional. When no target SOC is set, the reference behavior (see flexibility messages) of storages is 0 kW. initialSoc = "0.0" targetSoc = "1.0" From 72a3ae0c8a6efd3b2be03350416167ee2d294d20 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 2 Jul 2024 16:53:35 +0200 Subject: [PATCH 61/67] Fixing typo Signed-off-by: Sebastian Peter --- docs/readthedocs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index eaef457673..ec8244a7e9 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -225,7 +225,7 @@ Runtime configurations of other system participants are done similarly, except t ### Storage runtime configuration -The storage model takes parameters for the inital state of charge (SOC) and the target SOC for electrical energy storages, with 0.0 <= SOC <= 1.0. +The storage model takes parameters for the initial state of charge (SOC) and the target SOC for electrical energy storages, with 0.0 <= SOC <= 1.0. The initial SOC defaults to 0%, while the target SOC is optional. When no target SOC is set, the reference behavior (see flexibility messages) of storages is 0 kW. initialSoc = "0.0" From 9a4b0eb75c005ac2bc64d10f78799d51e9585d88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 07:08:46 +0000 Subject: [PATCH 62/67] Bump org.apache.poi:poi-ooxml from 5.2.5 to 5.3.0 (#843) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 17d5a8694e..84357015f7 100644 --- a/build.gradle +++ b/build.gradle @@ -145,7 +145,7 @@ dependencies { implementation "com.sksamuel.avro4s:avro4s-core_${scalaVersion}:4.1.2" implementation 'org.apache.commons:commons-math3:3.6.1' // apache commons math3 - implementation 'org.apache.poi:poi-ooxml:5.2.5' // used for FilenameUtils + implementation 'org.apache.poi:poi-ooxml:5.3.0' // used for FilenameUtils implementation 'javax.measure:unit-api:2.2' implementation 'tech.units:indriya:2.2' // quantities implementation "org.typelevel:squants_${scalaVersion}:1.8.3" From b37d99cb841eb5f2b67641e9d151d195d14db585 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Wed, 3 Jul 2024 10:49:10 +0200 Subject: [PATCH 63/67] refactoring due to codacy issue --- .../edu/ie3/simona/agent/grid/GridAgentController.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala index d3c02e20db..aa97f858d8 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -201,9 +201,10 @@ class GridAgentController( .map { participant => val node = participant.getNode - val controllingEm: Option[UUID] = - participant.getControllingEm.toScala.map(_.getUuid) - val emActorRef = controllingEm.flatMap(allEms.get) + val controllingEm = + participant.getControllingEm.toScala.flatMap(em => Option(em.getUuid)) + val emActorRef = controllingEm.flatMap(uuid => allEms.get(uuid)) + val actorRef = buildParticipantActor( participantsConfig.requestVoltageDeviationThreshold, participantConfigUtil, From 1d747899bce44784b4a7c4db94480d5642b3f597 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 4 Jul 2024 12:16:34 +0200 Subject: [PATCH 64/67] Fixing variable naming Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index d7814edc08..a376bd7002 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -254,8 +254,8 @@ final case class StorageModel( // calculate the tick from time span val maybeNextTick = maybeTimeSpan.map { timeSpan => - val ticksToEmpty = Math.round(timeSpan.toSeconds) - data.currentTick + ticksToEmpty + val timeSpanTicks = Math.round(timeSpan.toSeconds) + data.currentTick + timeSpanTicks } (currentState, FlexChangeIndicator(activateAtNextTick, maybeNextTick)) From 687225ccf2b9fd5841e2fb9d20904537f4961d35 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 4 Jul 2024 12:32:17 +0200 Subject: [PATCH 65/67] Improved grammar Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/model/participant/StorageModel.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index a376bd7002..02f82d2670 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -62,7 +62,7 @@ final case class StorageModel( private implicit val powerTolerance: Power = eStorage / Seconds(1) / 3.6e12 /** In order to avoid faulty behavior of storages, we want to avoid offering - * charging/discharging when storage is very close to full, empty or to a + * charging/discharging when storage is very close to full, to empty or to a * target. * * In particular, we want to avoid offering the option to (dis-)charge if From bc214b695abcce73a9f6437a8ea8e92e84f2b976 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Thu, 4 Jul 2024 14:44:37 +0200 Subject: [PATCH 66/67] refactor providing controllingEm to reviewers comments --- .../simona/agent/grid/GridAgentController.scala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala index aa97f858d8..43c111a47f 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -202,8 +202,16 @@ class GridAgentController( val node = participant.getNode val controllingEm = - participant.getControllingEm.toScala.flatMap(em => Option(em.getUuid)) - val emActorRef = controllingEm.flatMap(uuid => allEms.get(uuid)) + participant.getControllingEm.toScala + .map(_.getUuid) + .map(uuid => + allEms.getOrElse( + uuid, + throw new CriticalFailureException( + s"EM actor with UUID $uuid not found." + ), + ) + ) val actorRef = buildParticipantActor( participantsConfig.requestVoltageDeviationThreshold, @@ -212,7 +220,7 @@ class GridAgentController( participant, thermalIslandGridsByBusId, environmentRefs, - emActorRef, + controllingEm, ) introduceAgentToEnvironment(actorRef) // return uuid to actorRef From e015c319e933fcf75ac8da5a65a211fd4019f1b8 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 4 Jul 2024 16:38:40 +0200 Subject: [PATCH 67/67] More realistic capex and opex parameters Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant/StorageModelTest.groovy | 4 ++-- .../edu/ie3/simona/test/common/input/EmInputTestData.scala | 4 ++-- .../ie3/simona/test/common/input/StorageInputTestData.scala | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy index 3c115e5e50..a28a405901 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/StorageModelTest.groovy @@ -46,8 +46,8 @@ class StorageModelTest extends Specification { def typeInput = new StorageTypeInput( UUID.fromString("fbee4995-24dd-45e4-9c85-7d986fe99ff3"), "Test_StorageTypeInput", - getQuantity(100d, EURO), - getQuantity(101d, EURO_PER_MEGAWATTHOUR), + getQuantity(10000d, EURO), + getQuantity(0.05d, EURO_PER_MEGAWATTHOUR), getQuantity(100d, KILOWATTHOUR), getQuantity(13d, KILOVOLTAMPERE), 0.997, diff --git a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala index 7eeb67574c..50fbdd6095 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala @@ -70,8 +70,8 @@ trait EmInputTestData protected val householdStorageTypeInput = new StorageTypeInput( UUID.randomUUID(), "Dummy_Household_StorageTypeInput", - Quantities.getQuantity(100d, EURO), - Quantities.getQuantity(101d, EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(4000d, EURO), + Quantities.getQuantity(0.05d, EURO_PER_MEGAWATTHOUR), Quantities.getQuantity(15d, KILOWATTHOUR), Quantities.getQuantity(5d, KILOVOLTAMPERE), 0.997, diff --git a/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala index 6d59b8009f..99135b24fa 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/StorageInputTestData.scala @@ -22,8 +22,8 @@ trait StorageInputTestData extends DefaultTestData with NodeInputTestData { protected val storageTypeInput = new StorageTypeInput( UUID.fromString("fbee4995-24dd-45e4-9c85-7d986fe99ff3"), "Dummy_StorageTypeInput", - Quantities.getQuantity(100d, EURO), - Quantities.getQuantity(101d, EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(15000d, EURO), + Quantities.getQuantity(0.05d, EURO_PER_MEGAWATTHOUR), Quantities.getQuantity(200d, KILOWATTHOUR), Quantities.getQuantity(13d, KILOVOLTAMPERE), 0.997,