diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f354d442a..1b59efdfe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Throw exception if the slack node is not directly conected to a transformer. [#525](https://github.com/ie3-institute/simona/issues/525) - Added support for topologies without transformers and slack grids with multiple nodes [#1099](https://github.com/ie3-institute/simona/issues/1099) - Checking the number of slack nodes [#1122](https://github.com/ie3-institute/simona/issues/1122) +- Enhance exception message in case of InvalidGridException [#1124](https://github.com/ie3-institute/simona/issues/1124) +- Integration test for thermal grids [#1145](https://github.com/ie3-institute/simona/issues/1145) +- Added `VoltageLimits` [#1133](https://github.com/ie3-institute/simona/issues/1133) +- Introducing new ParticipantAgent and ParticipantModel [#1134](https://github.com/ie3-institute/simona/issues/1134) +- Using new `ParticipantAgent.Request` messages everywhere [#1195](https://github.com/ie3-institute/simona/issues/1195) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) @@ -121,6 +126,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactor ThermalEnergyDemand definitions [#917](https://github.com/ie3-institute/simona/issues/917) - Rewrote PvModelIT from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Fix negative required energy demand for thermalHouse [#1127](https://github.com/ie3-institute/simona/issues/1127) +- Refactored EM messages [#1138](https://github.com/ie3-institute/simona/issues/1138) +- `OperationInterval` should extend `RightOpenInterval` [#1142](https://github.com/ie3-institute/simona/issues/1142) +- Enhance EmAggregate of SelfOpt to cope with other targetLimits [#1131](https://github.com/ie3-institute/simona/issues/1131) +- Switched to `pureconfig` [#608](https://github.com/ie3-institute/simona/issues/608) +- Removing generated methods and cleaning up in config [#1170](https://github.com/ie3-institute/simona/issues/1170) +- Changed `pvInput` values in `PvInputTestData` to more realistic values [#1144](https://github.com/ie3-institute/simona/issues/1144) +- Refactor `RuntimeConfig` [#1172](https://github.com/ie3-institute/simona/issues/1172) - Updated to changes of PSDM release v6.0.0 [#1107](https://github.com/ie3-institute/simona/issues/1107) ### Fixed @@ -166,6 +178,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed SonarQube quality gate using the right link for PRs or Branches [#1061](https://github.com/ie3-institute/simona/issues/1061) - Fixed ignored EM strategy [#1091](https://github.com/ie3-institute/simona/issues/1091) - EM should output flex option results even if it has no parent [#1112](https://github.com/ie3-institute/simona/issues/1112) +- Rename `PrimaryDataWithApparentPower` to `PrimaryDataWithComplexPower` [#1140](https://github.com/ie3-institute/simona/issues/1140) +- Refactoring of `ThermalGrid.handleInfeed` to fix thermal storage recharge correctly when empty [#930](https://github.com/ie3-institute/simona/issues/930) +- Move `ScheduleServiceActivation` out of `RegistrationResponseMessage` [#1143](https://github.com/ie3-institute/simona/issues/1143) +- Check for runningHp when handling infeed to thermalGrid [#1167](https://github.com/ie3-institute/simona/issues/1167) ## [3.0.0] - 2023-08-07 diff --git a/build.gradle b/build.gradle index 03c5a06671..cb48595a42 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,6 @@ ext { pekkoVersion = '1.1.3' jtsVersion = '1.20.0' confluentKafkaVersion = '7.4.0' - tscfgVersion = '1.2.4' scapegoatVersion = '3.1.4' testContainerVersion = '0.41.8' @@ -44,7 +43,6 @@ java { apply from: scriptsLocation + 'spotless.gradle' apply from: scriptsLocation + 'checkJavaVersion.gradle' -apply from: scriptsLocation + 'tscfg.gradle' // config tasks apply from: scriptsLocation + 'documentation.gradle' // documentation tasks + configuration apply from: scriptsLocation + 'tests.gradle' // tasks for tests apply from: scriptsLocation + 'sonarqube.gradle' // sonarqube config @@ -128,8 +126,8 @@ dependencies { /* config */ implementation 'com.typesafe:config:1.4.3' - implementation "com.github.carueda:tscfg_2.13:$tscfgVersion" implementation "com.github.scopt:scopt_${scalaVersion}:4.1.0" // cmd args parser + implementation "com.github.pureconfig:pureconfig_${scalaVersion}:0.17.8" // JTS implementation ("org.locationtech.jts:jts-core:${jtsVersion}"){ @@ -200,6 +198,12 @@ tasks.withType(ScalaCompile) { "-P:scapegoat:disabledInspections:VariableShadowing", "-P:scapegoat:ignoredFiles:.*/SimonaConfig.scala" // see scapegoat-sbt page for this param ] + // increasing stack size required for our pureconfig configuration + // see: https://github.com/pureconfig/pureconfig/issues/481#issuecomment-647760554 + scalaCompileOptions.forkOptions.jvmArgs = [ + '-Xss2m', + '-XX:-UseGCOverheadLimit' + ] } // separate scapegoat report for test classes diff --git a/docs/readthedocs/_static/bibliography/bibtexAll.bib b/docs/readthedocs/_static/bibliography/bibtexAll.bib index 8761d828b4..521736c76c 100644 --- a/docs/readthedocs/_static/bibliography/bibtexAll.bib +++ b/docs/readthedocs/_static/bibliography/bibtexAll.bib @@ -190,4 +190,19 @@ @MISC{Radiation_ECMWF author = {Robin Hogan}, title = {Radiation Quantities in the ECMWF model and MARS}, howpublished = {\url{https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf}} -} \ No newline at end of file +} + +@misc{EN_50160, + title = {EN 50160:2020-11, Voltage characteristics of electricity supplied by public electricity networks}, + publisher = {CENELEC}, + type = {NORM}, + year = {2022}, + date = {2022-12-01}, +} + +@online{EU_2017/1485, + author = {{The European Commission}}, + title = {COMMISSION REGULATION (EU) 2017/1485 of 2 August 2017 establishing a guideline on electricity transmission system operation}, + date = {2017-08-02}, + url = {https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX%3A32017R1485}, +} diff --git a/docs/readthedocs/config.md b/docs/readthedocs/config.md index f6e968659a..925c040239 100644 --- a/docs/readthedocs/config.md +++ b/docs/readthedocs/config.md @@ -245,6 +245,7 @@ Tba: ## Grid configuration +### Reference System The reference system contains a list of voltage levels. Each element includes the nominal apparent power, the nominal voltage and the separate configuration of each voltage level. The voltage level configuration is composed of the identifier and the nominal voltage. @@ -268,6 +269,32 @@ simona.gridConfig.refSystems = [ Further typical voltage levels which can be used in the simulation and the configuration of individual reference systems are described in the documentation of [reference system](models/reference_system). +### Voltage limits + +The voltage limits contains a list of voltage levels. Each element includes the minimal and maximal allowed voltage and +the separate configuration of each voltage level. The voltage level configuration is composed of the identifier and the +nominal voltage. + +The configuration of a voltage limits is optional. If no configuration is provided by the user, the default +[voltage limits](models/voltage_limits) that includes all common german voltage levels is used. For those users +who need other voltage levels than the common german voltage levels or different voltage limits, they can configure +their limits as shown below. + +The voltage limits can be configured as follows: + +``` +simona.gridConfig.voltageLimits = [ + {vMin = 0.9, vMax = 1.1, voltLvls = [{id = "Lv", vNom = "0.4 kV"}]}, + {vMin = 0.9, vMax = 1.1, voltLvls = [{id = "Mv", vNom = "20 kV"}]}, + {vMin = 0.9, vMax = 1.1, voltLvls = [{id = "Hv", vNom = "110 kV"}]}, + {vMin = 0.9, vMax = 1.05, voltLvls = [{id = "EHV", vNom = "380 kV"}]}, +] +``` + +Further typical voltage levels which can be used in the simulation and the configuration of individual voltage limits +are described in the documentation of [voltage limits](models/voltage_limits). + + ## Power flow configuration Maximum allowed deviation in power between two sweeps, before overall convergence is assumed: diff --git a/docs/readthedocs/models.md b/docs/readthedocs/models.md index c9a1f870f2..1218e2a66c 100644 --- a/docs/readthedocs/models.md +++ b/docs/readthedocs/models.md @@ -17,6 +17,7 @@ models/three_winding_transformer_model models/reference_system models/thermal_grid_model models/thermal_house_model +models/voltage_limits ``` ## System Participant Related Models diff --git a/docs/readthedocs/models/voltage_limits.md b/docs/readthedocs/models/voltage_limits.md new file mode 100644 index 0000000000..0b54bd5261 --- /dev/null +++ b/docs/readthedocs/models/voltage_limits.md @@ -0,0 +1,65 @@ +(voltage_limits)= + +# Voltage Limits + +The voltage limits are built up by specifying the included voltage levels. The following table describes typical network +levels and their parameterization. They are primarily used for the optional congestion management. + +## Default voltage limits + +```{list-table} +:widths: auto +:header-rows: 1 + +* - Voltage level (id) + - Minimal voltage (vMin) + - Maximal voltage (vMax) + +* - LV + - 0.9 p.u. + - 1.1 p.u. + +* - MV (10 kV) + - 0.9 p.u. + - 1.1 p.u. + +* - MV (20 kV) + - 0.9 p.u. + - 1.1 p.u. + +* - MV (30 kV) + - 0.9 p.u. + - 1.1 p.u. + +* - HV + - 0.9 p.u. + - 1.1 p.u. + +* - EHV (220 kV) + - 0.9 p.u. + - 1.118 p.u. + +* - EHV (380 kV) + - 0.9 p.u. + - 1.05 p.u. +``` + +**References:** + +* {cite:cts}`EN_50160` +* {cite:cts}`EU_2017/1485` + +## Configuration of the voltage limits + +To configure the voltage limits, the voltage levels listed in the table above must be selected and integrated into the +config. The individual voltage level configuration (voltLvls) according to the example below. Each voltage level consists +of an identifier and the nominal voltage. + +``` +simona.gridConfig.voltageLimits = [ + {vMin = 0.9, vMax = 1.1, voltLvls = [{id = "Lv", vNom = "0.4 kV"}]}, + {vMin = 0.9, vMax = 1.1, voltLvls = [{id = "Mv", vNom = "20 kV"}]}, + {vMin = 0.9, vMax = 1.1, voltLvls = [{id = "Hv", vNom = "110 kV"}]}, + {vMin = 0.9, vMax = 1.05, voltLvls = [{id = "EHV", vNom = "380 kV"}]}, +] +``` diff --git a/docs/readthedocs/requirements.txt b/docs/readthedocs/requirements.txt index 4b84f2dcf4..b5bb61809f 100644 --- a/docs/readthedocs/requirements.txt +++ b/docs/readthedocs/requirements.txt @@ -1,7 +1,7 @@ Sphinx==8.1.3 sphinx-rtd-theme==3.0.2 sphinxcontrib-plantuml==0.30 -myst-parser==4.0.0 +myst-parser==4.0.1 markdown-it-py==3.0.0 sphinx-hoverxref==1.4.2 sphinxcontrib-bibtex==2.6.3 diff --git a/gradle/scripts/documentation.gradle b/gradle/scripts/documentation.gradle index d9c2988b86..848bca2851 100644 --- a/gradle/scripts/documentation.gradle +++ b/gradle/scripts/documentation.gradle @@ -25,3 +25,19 @@ javadoc() { destinationDir = file("${project.projectDir}/docs/javadoc") classpath = project.sourceSets.main.compileClasspath } + +/** + * Task to generate the ScalaDoc + */ +scaladoc() { + description 'Generates scala API doc at the correct place.' + group 'Documentation' + + source = sourceSets.main.allSource + destinationDir = file("${project.projectDir}/docs/scaladoc") + classpath = project.sourceSets.main.compileClasspath + + // Exclude SimonaConfig because implicits used by pureconfig's + // ConfigSource.load and ConfigWriter.apply lead to failure of scaladoc + exclude("edu/ie3/simona/config/SimonaConfig.scala") +} diff --git a/gradle/scripts/tscfg.gradle b/gradle/scripts/tscfg.gradle deleted file mode 100644 index e81ad99320..0000000000 --- a/gradle/scripts/tscfg.gradle +++ /dev/null @@ -1,56 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// Generate config classes reflecting the simona-config-tempalte.conf template file -//////////////////////////////////////////////////////////////////////////// -task genConfigClass { - doLast { - def tscfgJarFile = project.file('build/tscfg-' + tscfgVersion + '.jar') - if (!tscfgJarFile.exists() || !tscfgJarFile.isFile()) { - download.run { - src 'https://github.com/carueda/tscfg/releases/download/v' + tscfgVersion + '/tscfg-' + tscfgVersion + '.jar' - dest buildDir - } - } - javaexec { - main = "-jar" - args = [ - "build/tscfg-${tscfgVersion}.jar", - "--spec", - "src/main/resources/config/config-template.conf", - "--scala", - "--durations", - "--pn", - "edu.ie3.simona.config", - "--cn", - "SimonaConfig", - "--dd", - "src/main/scala/edu/ie3/simona/config/" - ] - } - } -} - -//////////////////////////////////////////////////////////////////////////// -// Generate sample config file reflecting the application.conf template file -// This can be used to setup a new simulation configuration -//////////////////////////////////////////////////////////////////////////// -task genConfigSample { - doLast { - def tscfgJarFile = project.file('build/tscfg-' + tscfgVersion + '.jar') - if (!tscfgJarFile.exists() || !tscfgJarFile.isFile()) { - download.run { - src 'https://github.com/carueda/tscfg/releases/download/v' + tscfgVersion + '/tscfg-' + tscfgVersion + '.jar' - dest buildDir - } - } - javaexec { - main = "-jar" - args = [ - "build/tscfg-${tscfgVersion}.jar", - "--spec", - "src/main/resources/config/config-template.conf", - "--tpl", - "input/samples/configSample.conf" - ] - } - } -} diff --git a/input/samples/vn_simona/vn_simona.conf b/input/samples/vn_simona/vn_simona.conf index b911acf2c1..3b0de64485 100644 --- a/input/samples/vn_simona/vn_simona.conf +++ b/input/samples/vn_simona/vn_simona.conf @@ -106,8 +106,8 @@ simona.output.log.level = "INFO" ################################################################## # Runtime Configuration // todo refactor as this naming is misleading and partly unneeded ################################################################## -simona.runtime.selected_subgrids = [] -simona.runtime.selected_volt_lvls = [] +simona.runtime.selectedSubgrids = [] +simona.runtime.selectedVoltLvls = [] simona.runtime.participant.load = { defaultConfig = { @@ -170,6 +170,7 @@ simona.runtime.participant.storage = { calculateMissingReactivePowerWithModel = false uuids = ["default"] scaling = 1.0 + initialSoc = 0.0 } individualConfigs = [] } diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf deleted file mode 100644 index 6c9d886711..0000000000 --- a/src/main/resources/config/config-template.conf +++ /dev/null @@ -1,384 +0,0 @@ -#@define -VoltLvlConfig { - id: string # identifier of the voltage level - vNom: string # nominal voltage, expl.: 10 kV -} - -#@define -RefSystemConfig { - sNom: string # Nominal power, expl.: 600 kVA - vNom: string # Nominal voltage, expl.: 0.4 kV - #@optional - voltLvls: [VoltLvlConfig] # Voltage levels to apply to - #@optional - gridIds: [string] # Sub grid numbers to apply to, expl.: 1,2,4..10 -} - -#@define abstract extends !java.io.Serializable -BaseRuntimeConfig { - uuids: [string] # Unique id to identify the system participant models this config applies for - scaling: double # General scaling factor of the system participant model - calculateMissingReactivePowerWithModel: boolean # When receiving primary data: Fill up with 0 or model func -} - -#@define extends BaseRuntimeConfig -LoadRuntimeConfig { - baseRuntimeConfig: BaseRuntimeConfig - modelBehaviour: string # How the model behaves. Possible values: fix, profile, random - reference: string # Scaling reference for the load model. Possible values: power, energy -} - -#@define extends BaseRuntimeConfig -FixedFeedInRuntimeConfig { - baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, - # but cannot removed bc otherwise FixedFeedInRuntimeConfig is handled as String -} - -#@define extends BaseRuntimeConfig -PvRuntimeConfig { - baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, - # but cannot removed bc otherwise PvRuntimeConfig is handled as String -} - -#@define extends BaseRuntimeConfig -WecRuntimeConfig { - baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, - # but cannot removed bc otherwise WecRuntimeConfig is handled as String -} - -#@define extends BaseRuntimeConfig -EvcsRuntimeConfig { - baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, - # but cannot removed bc otherwise EvcsRuntimeConfig is handled as String - chargingStrategy: String | "maxPower" # can be one of maxPower, constantPower, gridOrientedScheduling or marketOrientedScheduling - 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 { - # # # # # - # ATTENTION: calculateMissingReactivePowerWithModel and scaling is ignored here. - # Cleaner solution is possible with different config framework. - # # # # # - baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, - # but cannot removed bc otherwise EmRuntimeConfig is handled as String - curtailRegenerative: Boolean | false - aggregateFlex: String | "SELF_OPT_EXCL_REG" -} - -#@define extends BaseRuntimeConfig -HpRuntimeConfig { - baseRuntimeConfig: BaseRuntimeConfig # this entry is ignored by the config generator, - # but cannot removed bc otherwise HpRuntimeConfig is handled as String -} - -#@define abstract -CsvParams { - directoryPath: "string" - isHierarchic: "boolean" - csvSep: "string" -} - -#@define extends CsvParams -BaseCsvParams { - base: CsvParams -} - -#@define extends CsvParams -PrimaryDataCsvParams { - base: CsvParams - timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X" # default pattern from PSDM:TimeBasedSimpleValueFactory -} - -#@define abstract -KafkaParams { - runId: string - bootstrapServers: string - schemaRegistryUrl: string - linger: int // in ms -} - -#@define extends KafkaParams -ResultKafkaParams { - base: KafkaParams - topicNodeRes = string -} - -#@define extends KafkaParams -RuntimeKafkaParams { - base: KafkaParams - topic = string -} - -#@define abstract -BaseOutputConfig { - notifier: string # Result event notifier - simulationResult: boolean # Inform listeners about new simulation result -} - -#@define extends BaseOutputConfig -SimpleOutputConfig { - base: BaseOutputConfig -} - -#@define extends BaseOutputConfig -ParticipantBaseOutputConfig { - base: BaseOutputConfig - powerRequestReply: boolean # Inform listeners about power request replies - flexResult: boolean | false -} - -#@define -GridOutputConfig { - notifier: string # Result event notifier - nodes: boolean | false - lines: boolean | false - switches: boolean | false - transformers2w: boolean | false - transformers3w: boolean | false -} - -#@define -TransformerControlGroup { - measurements: [string] - transformers: [string] - vMax: Double - vMin: Double -} - -################################################################## -# Agentsim -################################################################## -simona.simulationName = "string" -simona.time.startDateTime = "2011-05-01T00:00:00Z" -simona.time.endDateTime = "2011-05-01T01:00:00Z" -#@optional -simona.time.schedulerReadyCheckWindow = int - -################################################################## -# Input Parameters -################################################################## -simona.input.primary = { - #@optional - csvParams = PrimaryDataCsvParams - #@optional - influxDb1xParams = { - url: string - port: Int - database: string - timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X" # default pattern from PSDM:TimeBasedSimpleValueFactory - } - #@optional - sqlParams = { - jdbcUrl: string - userName: string - password: string - schemaName: string | "public" - timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X" # default pattern from PSDM:TimeBasedSimpleValueFactory - } - #@optional - couchbaseParams = { - url: string - bucketName: string - userName: string - password: string - coordinateColumnName: string - keyPrefix: string - timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X" # default pattern from PSDM:TimeBasedSimpleValueFactory - } -} -simona.input.grid.datasource.id = "string" -#@optional -simona.input.grid.datasource.csvParams = BaseCsvParams -simona.input.weather.datasource = { - - scheme = "string" | "icon" # Describes the scheme, in which the data comes. Permissible values : icon, cosmo - #@optional - sampleParams = { - use: "boolean" | true # dummy parameter to allow sample data - } - #@optional - timestampPattern = string - #@optional - resolution = long - maxCoordinateDistance = double | 50000 - #@optional - csvParams = BaseCsvParams - #@optional - influxDb1xParams = { - url: string - port: Int - database: string - } - #@optional - sqlParams = { - jdbcUrl: string - userName: string - password: string - tableName: string - schemaName: string | "public" - } - #@optional - couchbaseParams = { - url: string - bucketName: string - userName: string - password: string - coordinateColumnName: string - keyPrefix: string - } - - coordinateSource = { - gridModel = "string" | "icon" - #@optional - csvParams = BaseCsvParams - #@optional - sampleParams = { - use: "boolean" | true # dummy parameter to allow sample data - } - #@optional - sqlParams = { - jdbcUrl: string - userName: string - password: string - tableName: string - schemaName: string | "public" - } - } -} -################################################################## -# Output Parameters -################################################################## -simona.output.base { - dir = string - addTimestampToOutputDir = true -} - -#@optional -simona.output.sink.csv { - fileFormat = ".csv" - isHierarchic = Boolean | false - filePrefix = "" - fileSuffix = "" - compressOutputs = "Boolean" | false -} -#@optional -simona.output.sink.influxDb1x { - url = string - port = Int - database = string -} - -#@optional -simona.output.sink.kafka = ResultKafkaParams - -simona.output.grid = GridOutputConfig -simona.output.participant = { - defaultConfig = ParticipantBaseOutputConfig - individualConfigs = [ParticipantBaseOutputConfig] -} -simona.output.thermal = { - defaultConfig = SimpleOutputConfig - individualConfigs = [SimpleOutputConfig] -} -simona.output.flex = Boolean | false - -simona.output.log.level = String | "INFO" - -################################################################## -# Runtime Configuration // todo refactor as this naming is misleading -################################################################## -#@optional -simona.runtime.selected_subgrids = [int] // todo convert this into a list of objects with startId, endId to allow for ranges and multiple ranges -#@optional -simona.runtime.selected_volt_lvls = [VoltLvlConfig] - -simona.runtime.listener = { - #@optional - eventsToProcess = [string] - - #@optional - kafka = RuntimeKafkaParams -} - -simona.runtime.participant = { - requestVoltageDeviationThreshold = "double | 1E-14" # Nodal voltages deviating more than this between requests in the same tick, are considered being different - load = { - defaultConfig = LoadRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") - individualConfigs = [LoadRuntimeConfig] - } - pv = { - defaultConfig = PvRuntimeConfig - individualConfigs = [PvRuntimeConfig] - } - fixedFeedIn = { - defaultConfig = FixedFeedInRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") - individualConfigs = [FixedFeedInRuntimeConfig] - } - wec = { - defaultConfig = WecRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") - individualConfigs = [WecRuntimeConfig] - } - evcs = { - defaultConfig = EvcsRuntimeConfig # Mandatory default config (uuids are ignored, best provide "default") - individualConfigs = [EvcsRuntimeConfig] - } - hp = { - 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] - } -} - -################################################################## -# Power Flow Configuration -################################################################## -simona.powerflow.maxSweepPowerDeviation = Double // the maximum allowed deviation in power between two sweeps, before overall convergence is assumed -simona.powerflow.sweepTimeout = "duration:seconds | 30 seconds" // maximum timeout for a sweep -simona.powerflow.newtonraphson.epsilon = [Double] -simona.powerflow.newtonraphson.iterations = Int -simona.powerflow.resolution = "duration:seconds | 1 hour" -simona.powerflow.stopOnFailure = boolean | false - -################################################################## -# Grid Configuration -################################################################## - -#@optional -simona.gridConfig.refSystems = [RefSystemConfig] - -################################################################## -# Event Configuration -################################################################## -#@optional -simona.event.listener = [ - { - fullClassPath = string - #@optional - eventsToProcess = [string] - } -] - -################################################################## -# Configuration of Control Schemes -################################################################## -#@optional -simona.control = { - transformer = [TransformerControlGroup] -} diff --git a/src/main/scala/edu/ie3/simona/agent/em/EmAgent.scala b/src/main/scala/edu/ie3/simona/agent/em/EmAgent.scala index dae0db52b6..a2df2748f1 100644 --- a/src/main/scala/edu/ie3/simona/agent/em/EmAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/em/EmAgent.scala @@ -10,7 +10,7 @@ import edu.ie3.datamodel.models.input.EmInput import edu.ie3.datamodel.models.result.system.{EmResult, FlexOptionsResult} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.statedata.BaseStateData.FlexControlledData -import edu.ie3.simona.config.SimonaConfig.EmRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.EmRuntimeConfig import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.{ FlexOptionsResultEvent, @@ -108,8 +108,7 @@ object EmAgent { .map { parentEm => val flexAdapter = ctx.messageAdapter[FlexRequest](Flex) - parentEm ! RegisterParticipant( - inputModel.getUuid, + parentEm ! RegisterControlledAsset( flexAdapter, inputModel, ) @@ -151,12 +150,12 @@ object EmAgent { core: EmDataCore.Inactive, ): Behavior[Request] = Behaviors.receivePartial { - case (_, RegisterParticipant(model, actor, spi)) => - val updatedModelShell = modelShell.addParticipant(model, spi) - val updatedCore = core.addParticipant(actor, model) + case (_, RegisterControlledAsset(actor, spi)) => + val updatedModelShell = modelShell.addParticipant(spi.getUuid, spi) + val updatedCore = core.addParticipant(actor, spi.getUuid) inactive(emData, updatedModelShell, updatedCore) - case (_, ScheduleFlexRequest(participant, newTick, scheduleKey)) => + case (_, ScheduleFlexActivation(participant, newTick, scheduleKey)) => val (maybeSchedule, newCore) = core .handleSchedule(participant, newTick) @@ -172,7 +171,7 @@ object EmAgent { scheduleTick, scheduleKey, ), - _.emAgent ! ScheduleFlexRequest( + _.emAgent ! ScheduleFlexActivation( modelShell.uuid, scheduleTick, scheduleKey, 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 ae5140899d..6e3bf2d2a6 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/DBFSAlgorithm.scala @@ -23,9 +23,9 @@ import edu.ie3.simona.agent.grid.GridAgentData.{ } import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.ExchangeVoltage import edu.ie3.simona.agent.grid.GridAgentMessages._ -import edu.ie3.simona.agent.participant.ParticipantAgent.{ - FinishParticipantSimulation, - ParticipantMessage, +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + GridSimulationFinished, RequestAssetPowerMessage, } import edu.ie3.simona.event.RuntimeEvent.PowerFlowFailed @@ -37,14 +37,12 @@ import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.SquantsUtils.RichElectricPotential import org.apache.pekko.actor.typed.scaladsl.AskPattern._ -import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps import org.apache.pekko.actor.typed.scaladsl.{ ActorContext, Behaviors, StashBuffer, } -import org.apache.pekko.actor.typed.{ActorRef, Behavior, Scheduler} -import org.apache.pekko.pattern.ask +import org.apache.pekko.actor.typed.{ActorRef, ActorSystem, Behavior, Scheduler} import org.apache.pekko.util.{Timeout => PekkoTimeout} import org.slf4j.Logger import squants.Each @@ -354,8 +352,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // To model the exchanged power from the superior grid's point of view, -1 has to be multiplied. // (Inferior grid is a feed in facility to superior grid, which is negative then). Analogously for load case. ( - refSystem.pInSi(pInPu) * (-1), - refSystem.qInSi(qInPu) * (-1), + refSystem.pInSi(pInPu) * -1, + refSystem.qInSi(qInPu) * -1, ) case _ => /* TODO: As long as there are no multiple slack nodes, provide "real" power only for the slack node */ @@ -444,6 +442,8 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { FinishGridSimulationTrigger(currentTick), gridAgentBaseData: GridAgentBaseData, ) => + val nextTick = currentTick + constantData.resolution + // inform my child grids about the end of this grid simulation gridAgentBaseData.inferiorGridGates .map { @@ -458,7 +458,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { gridAgentBaseData.gridEnv.nodeToAssetAgents.foreach { case (_, actors) => actors.foreach { actor => - actor ! FinishParticipantSimulation(currentTick) + actor ! GridSimulationFinished(currentTick, nextTick) } } @@ -484,7 +484,7 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { // / inform scheduler that we are done with the whole simulation and request new trigger for next time step constantData.environmentRefs.scheduler ! Completion( constantData.activationAdapter, - Some(currentTick + constantData.resolution), + Some(nextTick), ) // return to Idle @@ -1120,15 +1120,17 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { private def askForAssetPowers( currentTick: Long, sweepValueStore: Option[SweepValueStore], - nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]], refSystem: RefSystem, askTimeout: Duration, )(implicit ctx: ActorContext[GridAgent.Request] ): Boolean = { - implicit val timeout: PekkoTimeout = PekkoTimeout.create(askTimeout) implicit val ec: ExecutionContext = ctx.executionContext + implicit val timeout: PekkoTimeout = PekkoTimeout.create(askTimeout) + implicit val system: ActorSystem[_] = ctx.system + ctx.log.debug(s"asking assets for power values: {}", nodeToAssetAgents) if (nodeToAssetAgents.values.flatten.nonEmpty) { @@ -1162,16 +1164,21 @@ trait DBFSAlgorithm extends PowerFlowSupport with GridResultsSupport { ) } - (assetAgent.toClassic ? RequestAssetPowerMessage( - currentTick, - eInPu, - fInPU, - )).map { - case providedPowerValuesMessage: AssetPowerChangedMessage => - (assetAgent, providedPowerValuesMessage) - case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => - (assetAgent, assetPowerUnchangedMessage) - } + assetAgent + .ask(replyTo => + RequestAssetPowerMessage( + currentTick, + eInPu, + fInPU, + replyTo, + ) + ) + .map { + case providedPowerValuesMessage: AssetPowerChangedMessage => + (assetAgent, providedPowerValuesMessage) + case assetPowerUnchangedMessage: AssetPowerUnchangedMessage => + (assetAgent, assetPowerUnchangedMessage) + } }) }.toVector ) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala index f7e82f854c..ae3fa010ec 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgent.scala @@ -14,7 +14,7 @@ import edu.ie3.simona.agent.grid.GridAgentData.{ GridAgentInitData, } import edu.ie3.simona.agent.grid.GridAgentMessages._ -import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.exceptions.agent.GridAgentInitializationException @@ -132,6 +132,7 @@ object GridAgent extends DBFSAlgorithm { val gridModel = GridModel( subGridContainer, refSystem, + gridAgentInitData.voltageLimits, TimeUtil.withDefaults.toZonedDateTime( constantData.simonaConfig.simona.time.startDateTime ), @@ -156,7 +157,8 @@ object GridAgent extends DBFSAlgorithm { ) /* Reassure, that there are also calculation models for the given uuids */ - val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef[ParticipantMessage]]] = + val nodeToAssetAgentsMap + : Map[UUID, Set[ActorRef[ParticipantAgent.Request]]] = gridAgentController .buildSystemParticipants(subGridContainer, thermalGridsByBusId) .map { case (uuid: UUID, actorSet) => 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 c6b689fd55..ad3149c136 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentController.scala @@ -12,7 +12,6 @@ import edu.ie3.datamodel.models.input.system._ import edu.ie3.simona.actor.SimonaActorNaming._ import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.em.EmAgent -import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.{ ActorExtEvDataService, ActorWeatherService, @@ -25,8 +24,9 @@ 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.agent.participant2.ParticipantAgent +import edu.ie3.simona.config.RuntimeConfig._ import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.CriticalFailureException @@ -67,15 +67,14 @@ import scala.jdk.OptionConverters.RichOptional * System participant listeners * @param log * The logging adapter to use here - * * @since 2019-07-18 */ class GridAgentController( - gridAgentContext: ActorContext[_], + gridAgentContext: ActorContext[GridAgent.Request], environmentRefs: EnvironmentRefs, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, - participantsConfig: SimonaConfig.Simona.Runtime.Participant, + participantsConfig: Participant, outputConfig: SimonaConfig.Simona.Output.Participant, resolution: Long, listener: Iterable[ActorRef[ResultEvent]], @@ -84,7 +83,7 @@ class GridAgentController( def buildSystemParticipants( subGridContainer: SubGridContainer, thermalIslandGridsByBusId: Map[UUID, ThermalGrid], - ): Map[UUID, Set[ActorRef[ParticipantMessage]]] = { + ): Map[UUID, Set[ActorRef[ParticipantAgent.Request]]] = { val systemParticipants = filterSysParts(subGridContainer, environmentRefs) @@ -174,12 +173,12 @@ class GridAgentController( * A map from coupling point to set of actor references */ private def buildParticipantToActorRef( - participantsConfig: SimonaConfig.Simona.Runtime.Participant, + participantsConfig: Participant, outputConfig: SimonaConfig.Simona.Output.Participant, participants: Vector[SystemParticipantInput], thermalIslandGridsByBusId: Map[UUID, ThermalGrid], environmentRefs: EnvironmentRefs, - ): Map[UUID, Set[ActorRef[ParticipantMessage]]] = { + ): Map[UUID, Set[ActorRef[ParticipantAgent.Request]]] = { /* Prepare the config util for the participant models, which (possibly) utilizes as map to speed up the initialization * phase */ val participantConfigUtil = @@ -226,7 +225,7 @@ class GridAgentController( // return uuid to actorRef node.getUuid -> actorRef } - .toSet[(UUID, ActorRef[ParticipantMessage])] + .toSet[(UUID, ActorRef[ParticipantAgent.Request])] .groupMap(entry => entry._1)(entry => entry._2) } @@ -323,7 +322,7 @@ class GridAgentController( thermalIslandGridsByBusId: Map[UUID, ThermalGrid], environmentRefs: EnvironmentRefs, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = participantInputModel match { + ): ActorRef[ParticipantAgent.Request] = participantInputModel match { case input: FixedFeedInInput => buildFixedFeedIn( input, @@ -479,7 +478,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( FixedFeedInAgent.props( @@ -536,7 +535,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( LoadAgent.props( @@ -596,7 +595,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( PvAgent.props( @@ -656,7 +655,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( EvcsAgent.props( @@ -713,7 +712,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( HpAgent.props( @@ -774,7 +773,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]], - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( WecAgent.props( @@ -823,7 +822,7 @@ class GridAgentController( */ private def buildStorage( storageInput: StorageInput, - modelConfiguration: SimonaConfig.StorageRuntimeConfig, + modelConfiguration: StorageRuntimeConfig, primaryServiceProxy: ClassicRef, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, @@ -831,7 +830,7 @@ class GridAgentController( requestVoltageDeviationThreshold: Double, outputConfig: NotifierConfig, maybeControllingEm: Option[ActorRef[FlexResponse]] = None, - ): ActorRef[ParticipantMessage] = + ): ActorRef[ParticipantAgent.Request] = gridAgentContext.toClassic .simonaActorOf( StorageAgent.props( @@ -894,7 +893,7 @@ class GridAgentController( * Reference to the actor to add to the environment */ private def introduceAgentToEnvironment( - actorRef: ActorRef[ParticipantMessage] + actorRef: ActorRef[ParticipantAgent.Request] ): Unit = { gridAgentContext.watch(actorRef) environmentRefs.scheduler ! ScheduleActivation( diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala index 6b77430487..0414b8e030 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentData.scala @@ -13,10 +13,10 @@ import edu.ie3.powerflow.model.PowerFlowResult.SuccessFullPowerFlowResult.ValidN import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.agent.grid.ReceivedValuesStore.NodeToReceivedPower -import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.ResultEvent -import edu.ie3.simona.model.grid.{GridModel, RefSystem} +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.Activation import org.apache.pekko.actor.typed.ActorRef @@ -69,12 +69,17 @@ object GridAgentData { * @param subGridGateToActorRef * information on inferior and superior grid connections [[SubGridGate]] s * and [[ActorRef]] s of the corresponding [[GridAgent]]s + * @param refSystem + * of the grid + * @param voltageLimits + * of the grid, used to evaluate voltage congestion */ final case class GridAgentInitData( subGridContainer: SubGridContainer, thermalIslandGrids: Seq[ThermalGrid], subGridGateToActorRef: Map[SubGridGate, ActorRef[GridAgent.Request]], refSystem: RefSystem, + voltageLimits: VoltageLimits, ) extends GridAgentData with GridAgentDataHelper { override protected val subgridGates: Vector[SubGridGate] = @@ -121,7 +126,7 @@ object GridAgentData { def apply( gridModel: GridModel, subgridGateToActorRef: Map[SubGridGate, ActorRef[GridAgent.Request]], - nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]], superiorGridNodeUuids: Vector[UUID], inferiorGridGates: Vector[SubGridGate], powerFlowParams: PowerFlowParams, diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala index e024cfe3f6..14fed6de88 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala @@ -172,7 +172,7 @@ object GridAgentMessages { final case object FailedPowerFlow extends PowerResponse /** Provide power values as a reply to a - * [[edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage]] + * [[edu.ie3.simona.agent.participant2.ParticipantAgent.RequestAssetPowerMessage]] * * @param p * Unchanged active power @@ -185,7 +185,7 @@ object GridAgentMessages { ) extends ProvidedPowerResponse /** Provide values as a reply to a - * [[edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage]]. + * [[edu.ie3.simona.agent.participant2.ParticipantAgent.RequestAssetPowerMessage]]. * In contrast to [[AssetPowerChangedMessage]], this message indicates that * the same values for [[p]] and [[q]] has been sent again as in the previous * request diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala index e6ce82ca19..4ead274a14 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridEnvironment.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.agent.grid import edu.ie3.datamodel.graph.SubGridGate -import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.model.grid.GridModel import org.apache.pekko.actor.typed.ActorRef @@ -27,5 +27,5 @@ import java.util.UUID final case class GridEnvironment( gridModel: GridModel, subgridGateToActorRef: Map[SubGridGate, ActorRef[GridAgent.Request]], - nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]], ) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala index 9c5d456760..9a69b41393 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/ReceivedValuesStore.scala @@ -16,7 +16,7 @@ import edu.ie3.simona.agent.grid.ReceivedValuesStore.{ NodeToReceivedPower, NodeToReceivedSlackVoltage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.agent.participant2.ParticipantAgent import org.apache.pekko.actor.typed.ActorRef import java.util.UUID @@ -69,7 +69,7 @@ object ReceivedValuesStore { * `empty` [[ReceivedValuesStore]] with pre-initialized options as `None` */ def empty( - nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]], inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef[ GridAgent.Request ]], @@ -97,14 +97,14 @@ object ReceivedValuesStore { * `empty` [[NodeToReceivedPower]] with pre-initialized options as `None` */ private def buildEmptyNodeToReceivedPowerMap( - nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]], inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef[ GridAgent.Request ]], ): NodeToReceivedPower = { /* Collect everything, that I expect from my asset agents */ val assetsToReceivedPower: NodeToReceivedPower = nodeToAssetAgents.collect { - case (uuid: UUID, actorRefs: Set[ActorRef[ParticipantMessage]]) => + case (uuid: UUID, actorRefs: Set[ActorRef[ParticipantAgent.Request]]) => (uuid, actorRefs.map(actorRef => actorRef -> None).toMap) } @@ -161,7 +161,7 @@ object ReceivedValuesStore { * `empty` [[NodeToReceivedSlackVoltage]] and [[NodeToReceivedPower]] */ private def buildEmptyReceiveMaps( - nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantMessage]]], + nodeToAssetAgents: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]], inferiorSubGridGateToActorRef: Map[SubGridGate, ActorRef[ GridAgent.Request ]], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala index 19eff90211..237f952a6e 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala @@ -7,14 +7,13 @@ package edu.ie3.simona.agent.participant import edu.ie3.datamodel.models.input.system.SystemParticipantInput +import edu.ie3.simona.agent.grid.GridAgentMessages.ProvidedPowerResponse import edu.ie3.simona.agent.participant.ParticipantAgent.{ - FinishParticipantSimulation, - RequestAssetPowerMessage, StartCalculationTrigger, getAndCheckNodalVoltage, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.statedata.BaseStateData.{ @@ -27,6 +26,14 @@ import edu.ie3.simona.agent.participant.statedata.{ DataCollectionStateData, ParticipantStateData, } +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + GridSimulationFinished, + PrimaryRegistrationSuccessfulMessage, + RegistrationFailedMessage, + RegistrationResponseMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.{ @@ -34,7 +41,7 @@ import edu.ie3.simona.agent.state.ParticipantAgentState.{ HandleInformation, } import edu.ie3.simona.agent.{SimonaAgent, ValueStore} -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.InconsistentStateException import edu.ie3.simona.io.result.AccompaniedSimulationResult @@ -47,16 +54,11 @@ import edu.ie3.simona.model.participant.{ } import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexActivation, FlexResponse, IssueFlexControl, - FlexActivation, -} -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ - PrimaryServiceRegistrationMessage, - ProvisionMessage, - RegistrationResponseMessage, } +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.{ActorRef => TypedActorRef} @@ -89,12 +91,12 @@ import scala.reflect.ClassTag * @since 2019-07-04 */ abstract class ParticipantAgent[ - PD <: PrimaryDataWithApparentPower[PD], + PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, D <: ParticipantStateData[PD], I <: SystemParticipantInput, - MC <: SimonaConfig.BaseRuntimeConfig, + MC <: BaseRuntimeConfig, M <: SystemParticipant[CD, PD, MS], ]( scheduler: ActorRef, @@ -129,7 +131,8 @@ abstract class ParticipantAgent[ * that will confirm, otherwise, a failed registration is announced. */ holdTick(INIT_SIM_TICK) initStateData.primaryServiceProxy ! PrimaryServiceRegistrationMessage( - initStateData.inputModel.electricalInputModel.getUuid + context.self, + initStateData.inputModel.electricalInputModel.getUuid, ) goto(HandleInformation) using ParticipantInitializingStateData( initStateData.inputModel, @@ -193,7 +196,7 @@ abstract class ParticipantAgent[ ) case Event( - msg: ProvisionMessage[Data], + msg: DataProvision[Data], baseStateData: BaseStateData[PD], ) => /* Somebody has sent new primary or secondary data. Collect, what is expected for this tick. Go over to data @@ -201,7 +204,7 @@ abstract class ParticipantAgent[ handleDataProvisionAndGoToHandleInformation(msg, baseStateData, scheduler) case Event( - RequestAssetPowerMessage(requestTick, eInPu, fInPu), + RequestAssetPowerMessage(requestTick, eInPu, fInPu, replyTo), baseStateData: BaseStateData[PD], ) => /* Determine the reply and stay in this state (or stash the message if the request cannot yet be answered) */ @@ -211,10 +214,11 @@ abstract class ParticipantAgent[ eInPu, fInPu, alternativeResult, + replyTo, ) case Event( - FinishParticipantSimulation(tick), + GridSimulationFinished(tick, _), baseStateData: BaseStateData[PD], ) => // clean up agent result value store @@ -259,7 +263,7 @@ abstract class ParticipantAgent[ when(HandleInformation) { /* Receive registration confirm from primary data service -> Set up actor for replay of data */ case Event( - RegistrationSuccessfulMessage(serviceRef, maybeNextDataTick), + PrimaryRegistrationSuccessfulMessage(serviceRef, nextTick, _), ParticipantInitializingStateData( inputModel: InputModelContainer[I], modelConfig: MC, @@ -281,13 +285,13 @@ abstract class ParticipantAgent[ resolution, requestVoltageDeviationThreshold, outputConfig, - serviceRef -> maybeNextDataTick, + serviceRef -> Some(nextTick), scheduler, ) /* Receive registration refuse from primary data service -> Set up actor for model calculation */ case Event( - RegistrationResponseMessage.RegistrationFailedMessage(_), + RegistrationFailedMessage(_), ParticipantInitializingStateData( inputModel: InputModelContainer[I], modelConfig: MC, @@ -347,7 +351,7 @@ abstract class ParticipantAgent[ )(stateData.baseStateData.outputConfig) case Event( - msg: ProvisionMessage[_], + msg: DataProvision[_], stateData @ DataCollectionStateData( baseStateData: BaseStateData[PD], data, @@ -387,7 +391,7 @@ abstract class ParticipantAgent[ ) case Event( - RequestAssetPowerMessage(currentTick, _, _), + RequestAssetPowerMessage(currentTick, _, _, _), DataCollectionStateData(_, data, yetTriggered), ) => if (log.isDebugEnabled) { @@ -461,7 +465,7 @@ abstract class ParticipantAgent[ scheduler, ) - case Event(RequestAssetPowerMessage(currentTick, _, _), _) => + case Event(RequestAssetPowerMessage(currentTick, _, _, _), _) => log.debug( s"Got asset power request for tick {} from '{}'. Will answer it later.", currentTick, @@ -470,7 +474,7 @@ abstract class ParticipantAgent[ stash() stay() - case Event(_: ProvisionMessage[_], _) | Event(Activation(_), _) => + case Event(_: DataProvision[_], _) | Event(Activation(_), _) => /* I got faced to new data, also I'm not ready to handle it, yet OR I got asked to do something else, while I'm * still busy, I will put it aside and answer it later */ stash() @@ -645,7 +649,7 @@ abstract class ParticipantAgent[ * state change to [[HandleInformation]] with updated base state data */ def handleDataProvisionAndGoToHandleInformation( - msg: ProvisionMessage[Data], + msg: DataProvision[Data], baseStateData: BaseStateData[PD], scheduler: ActorRef, ): FSM.State[AgentState, ParticipantStateData[PD]] @@ -808,6 +812,8 @@ abstract class ParticipantAgent[ * Imaginary part of the complex, dimensionless nodal voltage * @param alternativeResult * Alternative result to use, if no reasonable result can be obtained + * @param replyTo + * Actor reference to send the reply to * @return * The very same state with updated request value store */ @@ -817,6 +823,7 @@ abstract class ParticipantAgent[ eInPu: Dimensionless, fInPu: Dimensionless, alternativeResult: PD, + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[PD]] /** Abstract definition to notify result listeners from every participant @@ -853,27 +860,6 @@ abstract class ParticipantAgent[ object ParticipantAgent { - trait ParticipantMessage - - /** Request the power values for the requested tick from an AssetAgent and - * provide the latest nodal voltage - * - * @param currentTick - * The tick that power values are requested for - * @param eInPu - * Real part of the complex, dimensionless nodal voltage - * @param fInPu - * Imaginary part of the complex, dimensionless nodal voltage - */ - final case class RequestAssetPowerMessage( - currentTick: Long, - eInPu: Dimensionless, - fInPu: Dimensionless, - ) extends ParticipantMessage - - final case class FinishParticipantSimulation(tick: Long) - extends ParticipantMessage - final case class StartCalculationTrigger(tick: Long) /** Verifies that a nodal voltage value has been provided in the model diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala index a505696943..1b9b11226f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala @@ -18,6 +18,7 @@ import edu.ie3.simona.agent.ValueStore import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, + ProvidedPowerResponse, } import edu.ie3.simona.agent.participant.ParticipantAgent.StartCalculationTrigger import edu.ie3.simona.agent.participant.ParticipantAgentFundamentals.RelevantResultValues @@ -26,7 +27,7 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ComplexPower, ComplexPowerAndHeat, EnrichableData, - PrimaryDataWithApparentPower, + PrimaryDataWithComplexPower, } import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService @@ -43,13 +44,19 @@ import edu.ie3.simona.agent.participant.statedata.{ DataCollectionStateData, ParticipantStateData, } +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationResponseMessage, + RegistrationSuccessfulMessage, +} import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.{ Calculate, HandleInformation, } -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.{ FlexOptionsResultEvent, @@ -75,10 +82,6 @@ import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions -import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ - ProvisionMessage, - RegistrationResponseMessage, -} import edu.ie3.simona.util.TickUtil._ import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble @@ -103,12 +106,12 @@ import scala.util.{Failure, Success, Try} /** Useful functions to use in [[ParticipantAgent]] s */ protected trait ParticipantAgentFundamentals[ - PD <: PrimaryDataWithApparentPower[PD], + PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, D <: ParticipantStateData[PD], I <: SystemParticipantInput, - MC <: SimonaConfig.BaseRuntimeConfig, + MC <: BaseRuntimeConfig, M <: SystemParticipant[CD, PD, MS], ] extends ServiceRegistration[PD, CD, MS, D, I, MC, M] { this: ParticipantAgent[PD, CD, MS, D, I, MC, M] => @@ -273,8 +276,7 @@ protected trait ParticipantAgentFundamentals[ // register with EM if applicable maybeEmAgent.foreach { emAgent => - emAgent ! RegisterParticipant( - inputModel.electricalInputModel.getUuid, + emAgent ! RegisterControlledAsset( self.toTyped[FlexRequest], inputModel.electricalInputModel, ) @@ -308,7 +310,7 @@ protected trait ParticipantAgentFundamentals[ maybeEmAgent.foreach { emAgent => // flex is scheduled for tick 0, if no first tick available - emAgent ! ScheduleFlexRequest( + emAgent ! ScheduleFlexActivation( inputModel.electricalInputModel.getUuid, newTick.getOrElse(0), ) @@ -430,16 +432,17 @@ protected trait ParticipantAgentFundamentals[ stateData: CollectRegistrationConfirmMessages[PD], ): FSM.State[AgentState, ParticipantStateData[PD]] = registrationResponse match { - case RegistrationResponseMessage.RegistrationSuccessfulMessage( + case RegistrationSuccessfulMessage( serviceRef, - maybeNextTick, + firstDataTick, ) => val remainingResponses = stateData.pendingResponses.filter(_ != serviceRef) /* If the sender announces a new next tick, add it to the list of expected ticks, else remove the current entry */ val foreseenDataTicks = - stateData.baseStateData.foreseenDataTicks + (serviceRef -> maybeNextTick) + stateData.baseStateData.foreseenDataTicks + + (serviceRef -> Some(firstDataTick)) if (remainingResponses.isEmpty) { /* All agent have responded. Determine the next to be used state data and reply completion to scheduler. */ @@ -467,7 +470,7 @@ protected trait ParticipantAgentFundamentals[ foreseenNextDataTicks = foreseenDataTicksFlattened, ) } - case RegistrationResponseMessage.RegistrationFailedMessage(serviceRef) => + case RegistrationFailedMessage(serviceRef) => self ! PoisonPill throw new ActorNotRegisteredException( s"Registration of actor $actorName for $serviceRef failed." @@ -488,7 +491,7 @@ protected trait ParticipantAgentFundamentals[ * state change to [[HandleInformation]] with updated base state data */ override def handleDataProvisionAndGoToHandleInformation( - msg: ProvisionMessage[Data], + msg: DataProvision[Data], baseStateData: BaseStateData[PD], scheduler: ActorRef, ): FSM.State[AgentState, ParticipantStateData[PD]] = { @@ -1059,7 +1062,7 @@ protected trait ParticipantAgentFundamentals[ val maybeEmAgent = modelStateData.flexStateData.map(_.emAgent) maybeEmAgent.foreach { - _ ! ScheduleFlexRequest( + _ ! ScheduleFlexActivation( modelStateData.model.getUuid, maybeNextTick.getOrElse(0), ) @@ -1165,7 +1168,7 @@ protected trait ParticipantAgentFundamentals[ } /** Determining the reply to an - * [[edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage]], + * [[edu.ie3.simona.agent.participant2.ParticipantAgent.RequestAssetPowerMessage]], * send this answer and stay in the current state. If no reply can be * determined (because an activation or incoming data is expected), the * message is stashed. @@ -1188,6 +1191,8 @@ protected trait ParticipantAgentFundamentals[ * Imaginary part of the complex, dimensionless nodal voltage * @param alternativeResult * Alternative result to use, if no reasonable result can be obtained + * @param replyTo + * Actor reference to send the reply to * @return * The very same state with updated request value store */ @@ -1197,6 +1202,7 @@ protected trait ParticipantAgentFundamentals[ eInPu: Dimensionless, fInPu: Dimensionless, alternativeResult: PD, + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[PD]] = { /* Check, if there is any calculation foreseen for this tick. If so, wait with the response. */ val activationExpected = @@ -1243,6 +1249,7 @@ protected trait ParticipantAgentFundamentals[ updatedVoltageStore, nodalVoltage, lastNodalVoltage, + replyTo, ).getOrElse { /* If a fast reply is not possible, determine it the old-fashioned way */ determineReply( @@ -1252,6 +1259,7 @@ protected trait ParticipantAgentFundamentals[ nodalVoltage, updatedVoltageStore, alternativeResult, + replyTo, ) } } @@ -1275,6 +1283,8 @@ protected trait ParticipantAgentFundamentals[ * Magnitude of the complex, dimensionless nodal voltage * @param lastNodalVoltage * Lastly known magnitude of the complex, dimensionless nodal voltage + * @param replyTo + * Actor reference to send the reply to * @return * Option on a possible fast state change */ @@ -1285,9 +1295,8 @@ protected trait ParticipantAgentFundamentals[ voltageValueStore: ValueStore[Dimensionless], nodalVoltage: Dimensionless, lastNodalVoltage: Option[(Long, Dimensionless)], + replyTo: TypedActorRef[ProvidedPowerResponse], ): Option[FSM.State[AgentState, ParticipantStateData[PD]]] = { - implicit val outputConfig: NotifierConfig = - baseStateData.outputConfig mostRecentRequest match { case Some((mostRecentRequestTick, latestProvidedValues)) if mostRecentRequestTick == requestTick => @@ -1298,12 +1307,14 @@ protected trait ParticipantAgentFundamentals[ case externalBaseStateData: FromOutsideBaseStateData[M, PD] => /* When data is provided from outside it is NOT altered depending on the node voltage. Send an * AssetPowerUnchangedMessage */ + replyTo ! AssetPowerUnchangedMessage( + latestProvidedValues.p, + latestProvidedValues.q, + ) + Some( stay() using externalBaseStateData.copy( voltageValueStore = voltageValueStore - ) replying AssetPowerUnchangedMessage( - latestProvidedValues.p, - latestProvidedValues.q, ) ) case modelBaseStateData: ParticipantModelBaseStateData[ @@ -1323,11 +1334,13 @@ protected trait ParticipantAgentFundamentals[ if (lastVoltage ~= nodalVoltage) { /* This is the very same request (same tick and same nodal voltage). Reply with * AssetPowerUnchangedMessage */ + replyTo ! AssetPowerUnchangedMessage( + latestProvidedValues.p, + latestProvidedValues.q, + ) + Some( - stay() using modelBaseStateData replying AssetPowerUnchangedMessage( - latestProvidedValues.p, - latestProvidedValues.q, - ) + stay() using modelBaseStateData ) } else { /* If the voltage is not exactly equal, continue to determine the correct reply. */ @@ -1346,7 +1359,7 @@ protected trait ParticipantAgentFundamentals[ } /** Determine a reply on a - * [[edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage]] + * [[edu.ie3.simona.agent.participant2.ParticipantAgent.RequestAssetPowerMessage]] * by looking up the detailed simulation results, averaging them and * returning the equivalent state transition. * @@ -1362,6 +1375,8 @@ protected trait ParticipantAgentFundamentals[ * Value store with updated nodal voltages * @param alternativeResult * Alternative result to use, if no reasonable result can be obtained + * @param replyTo + * Actor reference to send the reply to * @return * Matching state transition */ @@ -1372,6 +1387,7 @@ protected trait ParticipantAgentFundamentals[ nodalVoltage: Dimensionless, updatedVoltageValueStore: ValueStore[Dimensionless], alternativeResult: PD, + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[PD]] = { /* No fast reply possible --> Some calculations have to be made */ mostRecentRequest match { @@ -1406,10 +1422,12 @@ protected trait ParticipantAgentFundamentals[ voltageValueStore = updatedVoltageValueStore, ) - stay() using nextStateData replying AssetPowerChangedMessage( + replyTo ! AssetPowerChangedMessage( lastResult.p, nextReactivePower, ) + + stay() using nextStateData case unexpectedStateData => throw new IllegalStateException( s"The request reply should not be re-calculated for state data '$unexpectedStateData'" @@ -1435,6 +1453,7 @@ protected trait ParticipantAgentFundamentals[ nodalVoltage, updatedVoltageValueStore, alternativeResult, + replyTo, ) case None => /* There is no simulation result at all. Reply with zero power */ @@ -1443,6 +1462,7 @@ protected trait ParticipantAgentFundamentals[ alternativeResult, requestTick, updatedVoltageValueStore, + replyTo, ) } } @@ -1555,6 +1575,8 @@ protected trait ParticipantAgentFundamentals[ * Voltage value store to be used in the updated base state data * @param alternativeResult * If no relevant data are apparent, then use this result instead + * @param replyTo + * Actor reference to send the reply to * @return * The very same state as the agent currently is in, but with updated base * state data @@ -1566,6 +1588,7 @@ protected trait ParticipantAgentFundamentals[ nodalVoltage: Dimensionless, voltageValueStore: ValueStore[Dimensionless], alternativeResult: PD, + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[PD]] = { if (relevantResults.relevantData.nonEmpty) { averagePowerAndStay( @@ -1576,6 +1599,7 @@ protected trait ParticipantAgentFundamentals[ relevantResults.windowEnd, nodalVoltage, voltageValueStore, + replyTo, ) } else { log.debug( @@ -1587,6 +1611,7 @@ protected trait ParticipantAgentFundamentals[ alternativeResult, requestTick, voltageValueStore, + replyTo, ) } } @@ -1610,6 +1635,8 @@ protected trait ParticipantAgentFundamentals[ * Nodal voltage magnitude in the moment of request * @param voltageValueStore * Voltage value store to be used in the updated base state data + * @param replyTo + * Actor reference to send the reply to * @return * The very same state as the agent currently is in, but with updated base * state data @@ -1622,6 +1649,7 @@ protected trait ParticipantAgentFundamentals[ windowEndTick: Long, nodalVoltage: Dimensionless, voltageValueStore: ValueStore[Dimensionless], + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[PD]] = { val averageResult = determineAverageResult( baseData, @@ -1635,6 +1663,7 @@ protected trait ParticipantAgentFundamentals[ averageResult, requestTick, voltageValueStore, + replyTo, ) } @@ -1714,6 +1743,8 @@ protected trait ParticipantAgentFundamentals[ * Tick of the most recent request * @param voltageValueStore * Voltage value store to be used in the updated base state data + * @param replyTo + * Actor reference to send the reply to * @return * The very same state as the agent currently is in, but with updated base * state data @@ -1723,6 +1754,7 @@ protected trait ParticipantAgentFundamentals[ averageResult: PD, requestTick: Long, voltageValueStore: ValueStore[Dimensionless], + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[PD]] = { val updatedRequestValueStore = ValueStore.updateValueStore( @@ -1741,7 +1773,8 @@ protected trait ParticipantAgentFundamentals[ averageResult.toComplexPower match { case ComplexPower(p, q) => - stay() using nextStateData replying AssetPowerChangedMessage(p, q) + replyTo ! AssetPowerChangedMessage(p, q) + stay() using nextStateData } } diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala b/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala index 5308b4e768..05138806d4 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.participant import org.apache.pekko.actor.ActorRef import edu.ie3.datamodel.models.input.system.{EvcsInput, SystemParticipantInput} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.{ @@ -17,7 +17,7 @@ import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.{ ActorWeatherService, } import edu.ie3.simona.agent.participant.statedata.ParticipantStateData -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.exceptions.agent.ServiceRegistrationException import edu.ie3.simona.model.participant.{ CalcRelevantData, @@ -28,12 +28,12 @@ import edu.ie3.simona.ontology.messages.services.EvMessage.RegisterForEvDataMess import edu.ie3.simona.ontology.messages.services.WeatherMessage.RegisterForWeatherMessage trait ServiceRegistration[ - PD <: PrimaryDataWithApparentPower[PD], + PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, D <: ParticipantStateData[PD], I <: SystemParticipantInput, - MC <: SimonaConfig.BaseRuntimeConfig, + MC <: BaseRuntimeConfig, M <: SystemParticipant[CD, PD, MS], ] { this: ParticipantAgent[PD, CD, MS, D, I, MC, M] => diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index 7da7b04c5c..9cef839628 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -15,7 +15,6 @@ import edu.ie3.util.scala.quantities.{Kilovars, ReactivePower} import squants.energy.{Kilowatts, Power} import tech.units.indriya.ComparableQuantity -import java.time.ZonedDateTime import scala.jdk.OptionConverters.RichOptional import scala.util.{Failure, Success, Try} @@ -39,16 +38,40 @@ object Data { def toComplexPower: ComplexPower } + /** Class that provides some static functionality for primary data, such as + * producing zero values and scaling the data. + * + * @tparam T + * The type of primary data + */ + sealed trait PrimaryDataExtra[T <: PrimaryData] { + + /** Returns a zero value of the desired type + */ + def zero: T + + /** Scales given primary data by the given factor + * + * @param data + * The primary data to scale + * @param factor + * The factor to scale by + * @return + * The scaled primary data + */ + def scale(data: T, factor: Double): T + } + object PrimaryData { - sealed trait EnrichableData[E <: PrimaryDataWithApparentPower[E]] { + sealed trait EnrichableData[E <: PrimaryDataWithComplexPower[E]] { def add(q: ReactivePower): E } - /** Denoting all primary data, that carry apparent power + /** Denoting all primary data, that carry complex power */ - sealed trait PrimaryDataWithApparentPower[ - +T <: PrimaryDataWithApparentPower[T] + sealed trait PrimaryDataWithComplexPower[ + +T <: PrimaryDataWithComplexPower[T] ] extends PrimaryData { val q: ReactivePower @@ -81,6 +104,13 @@ object Data { ComplexPower(p, q) } + object ActivePowerExtra extends PrimaryDataExtra[ActivePower] { + override def zero: ActivePower = ActivePower(zeroKW) + + override def scale(data: ActivePower, factor: Double): ActivePower = + ActivePower(data.p * factor) + } + /** Active and Reactive power as participant simulation result * * @param p @@ -91,13 +121,20 @@ object Data { final case class ComplexPower( override val p: Power, override val q: ReactivePower, - ) extends PrimaryDataWithApparentPower[ComplexPower] { + ) extends PrimaryDataWithComplexPower[ComplexPower] { override def toComplexPower: ComplexPower = this override def withReactivePower(q: ReactivePower): ComplexPower = copy(q = q) } + object ComplexPowerExtra extends PrimaryDataExtra[ComplexPower] { + override def zero: ComplexPower = ComplexPower(zeroKW, zeroKVAr) + + override def scale(data: ComplexPower, factor: Double): ComplexPower = + ComplexPower(data.p * factor, data.q * factor) + } + /** Active power and heat demand as participant simulation result * * @param p @@ -121,6 +158,17 @@ object Data { ComplexPowerAndHeat(p, q, qDot) } + object ActivePowerAndHeatExtra + extends PrimaryDataExtra[ActivePowerAndHeat] { + override def zero: ActivePowerAndHeat = ActivePowerAndHeat(zeroKW, zeroKW) + + override def scale( + data: ActivePowerAndHeat, + factor: Double, + ): ActivePowerAndHeat = + ActivePowerAndHeat(data.p * factor, data.qDot * factor) + } + /** Apparent power and heat demand as participant simulation result * * @param p @@ -134,7 +182,7 @@ object Data { override val p: Power, override val q: ReactivePower, override val qDot: Power, - ) extends PrimaryDataWithApparentPower[ComplexPowerAndHeat] + ) extends PrimaryDataWithComplexPower[ComplexPowerAndHeat] with Heat { override def toComplexPower: ComplexPower = ComplexPower(p, q) @@ -143,6 +191,42 @@ object Data { copy(q = q) } + object ComplexPowerAndHeatExtra + extends PrimaryDataExtra[ComplexPowerAndHeat] { + override def zero: ComplexPowerAndHeat = + ComplexPowerAndHeat(zeroKW, zeroKVAr, zeroKW) + + override def scale( + data: ComplexPowerAndHeat, + factor: Double, + ): ComplexPowerAndHeat = + ComplexPowerAndHeat( + data.p * factor, + data.q * factor, + data.qDot * factor, + ) + } + + def getPrimaryDataExtra( + value: Class[_ <: Value] + ): PrimaryDataExtra[_ <: PrimaryData] = { + val heatAndS = classOf[HeatAndSValue] + val s = classOf[SValue] + val heatAndP = classOf[HeatAndPValue] + val p = classOf[PValue] + + value match { + case `heatAndS` => ComplexPowerAndHeatExtra + case `s` => ComplexPowerExtra + case `heatAndP` => ActivePowerAndHeatExtra + case `p` => ActivePowerExtra + case other => + throw new IllegalArgumentException( + s"Value class '$other' is not supported." + ) + } + } + implicit class RichValue(private val value: Value) { def toPrimaryData: Try[PrimaryData] = value match { @@ -241,7 +325,6 @@ object Data { */ trait SecondaryData extends Data object SecondaryData { - final case class DateTime(dateTime: ZonedDateTime) extends SecondaryData final case class WholesalePrice(price: ComparableQuantity[EnergyPrice]) extends SecondaryData } diff --git a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala index 5b37ae70e3..e4d998fb9f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala @@ -26,7 +26,7 @@ import edu.ie3.simona.agent.participant.{ } import edu.ie3.simona.agent.state.AgentState.Idle import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation -import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.EvcsRuntimeConfig import edu.ie3.simona.model.participant.evcs.EvcsModel import edu.ie3.simona.model.participant.evcs.EvcsModel.{ EvcsRelevantData, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala index 9cf2f747d8..4b839e2356 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala @@ -13,7 +13,10 @@ import edu.ie3.datamodel.models.result.system.{ SystemParticipantResult, } import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.grid.GridAgentMessages.AssetPowerChangedMessage +import edu.ie3.simona.agent.grid.GridAgentMessages.{ + AssetPowerChangedMessage, + ProvidedPowerResponse, +} import edu.ie3.simona.agent.participant.ParticipantAgent.getAndCheckNodalVoltage import edu.ie3.simona.agent.participant.ParticipantAgentFundamentals import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower @@ -32,7 +35,7 @@ import edu.ie3.simona.agent.participant.statedata.{ } import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.EvcsRuntimeConfig import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ @@ -562,6 +565,8 @@ protected trait EvcsAgentFundamentals * Value store with updated nodal voltages * @param alternativeResult * Alternative result to use, if no reasonable result can be obtained + * @param replyTo + * Actor reference to send the reply to * @return * Matching state transition */ @@ -572,6 +577,7 @@ protected trait EvcsAgentFundamentals nodalVoltage: squants.Dimensionless, updatedVoltageValueStore: ValueStore[squants.Dimensionless], alternativeResult: ComplexPower, + replyTo: TypedActorRef[ProvidedPowerResponse], ): FSM.State[AgentState, ParticipantStateData[ComplexPower]] = { /* No fast reply possible --> Some calculations have to be made */ mostRecentRequest match { @@ -607,10 +613,12 @@ protected trait EvcsAgentFundamentals voltageValueStore = updatedVoltageValueStore, ) - stay() using nextStateData replying AssetPowerChangedMessage( + replyTo ! AssetPowerChangedMessage( lastResult.p, nextReactivePower, ) + + stay() using nextStateData case unexpectedStateData => throw new IllegalStateException( s"The request reply should not be re-calculated for state data '$unexpectedStateData'" @@ -668,6 +676,7 @@ protected trait EvcsAgentFundamentals nodalVoltage, updatedVoltageValueStore, alternativeResult, + replyTo, ) case None => /* There is no simulation result at all. Reply with zero power */ @@ -676,6 +685,7 @@ protected trait EvcsAgentFundamentals alternativeResult, requestTick, updatedVoltageValueStore, + replyTo, ) } } diff --git a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala index 38826657e2..8ee881025e 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgent.scala @@ -11,7 +11,7 @@ import edu.ie3.simona.agent.participant.ParticipantAgent import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.SimonaConfig.FixedFeedInRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData import edu.ie3.simona.model.participant.FixedFeedInModel import edu.ie3.simona.model.participant.ModelState.ConstantState diff --git a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala index 2ed0495610..d7303135b7 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/fixedfeedin/FixedFeedInAgentFundamentals.scala @@ -29,7 +29,7 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputModelContainer import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.SimonaConfig.FixedFeedInRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ InconsistentStateException, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgent.scala index 7861575ef5..dc7b9e7c82 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgent.scala @@ -13,7 +13,7 @@ import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.SimonaConfig.HpRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.HpRuntimeConfig import edu.ie3.simona.model.participant.HpModel import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} import org.apache.pekko.actor.{ActorRef, Props} diff --git a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala index 534126b986..d99b8f4e8e 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala @@ -32,7 +32,7 @@ import edu.ie3.simona.agent.participant.statedata.{ } import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.SimonaConfig.HpRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.HpRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ AgentInitializationException, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala index 67607acbf1..5354eaf2a8 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgent.scala @@ -16,7 +16,7 @@ import edu.ie3.simona.agent.participant.load.LoadAgentFundamentals.{ } import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig import edu.ie3.simona.model.participant.CalcRelevantData.LoadRelevantData import edu.ie3.simona.model.participant.ModelState.ConstantState import edu.ie3.simona.model.participant.load.profile.ProfileLoadModel diff --git a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala index e01c463115..1df2fe1a82 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala @@ -29,7 +29,7 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputModelContainer import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.InconsistentStateException import edu.ie3.simona.io.result.AccompaniedSimulationResult diff --git a/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgent.scala index a46f7478a8..4027891d54 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgent.scala @@ -13,7 +13,7 @@ import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.SimonaConfig.PvRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.PvRuntimeConfig import edu.ie3.simona.model.participant.ModelState.ConstantState import edu.ie3.simona.model.participant.PvModel import edu.ie3.simona.model.participant.PvModel.PvRelevantData diff --git a/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala index b7107d6233..4d908d43d1 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/pv/PvAgentFundamentals.scala @@ -30,7 +30,7 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputModelContainer import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.SimonaConfig.PvRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.PvRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ AgentInitializationException, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala index 04650f4763..05b3916c35 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.agent.participant.statedata import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.event.notifier.NotifierConfig @@ -33,10 +33,10 @@ import scala.collection.SortedSet * agents * * @tparam PD - * Type of [[PrimaryDataWithApparentPower]], that the represented Participant + * Type of [[PrimaryDataWithComplexPower]], that the represented Participant * produces */ -trait BaseStateData[+PD <: PrimaryDataWithApparentPower[PD]] +trait BaseStateData[+PD <: PrimaryDataWithComplexPower[PD]] extends ParticipantStateData[PD] { /** The date, that fits the tick 0 @@ -90,7 +90,7 @@ object BaseStateData { /** The agent is supposed to carry out model calculations * * @tparam PD - * Type of [[PrimaryDataWithApparentPower]], that the represented + * Type of [[PrimaryDataWithComplexPower]], that the represented * Participant produces * @tparam CD * Type of [[CalcRelevantData]], that is required by the included model @@ -98,7 +98,7 @@ object BaseStateData { * Restricting the model to a certain class */ trait ModelBaseStateData[ - +PD <: PrimaryDataWithApparentPower[PD], + +PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, +M <: SystemParticipant[_ <: CalcRelevantData, PD, MS], @@ -156,7 +156,7 @@ object BaseStateData { _ <: CalcRelevantData, P, _, - ], +P <: PrimaryDataWithApparentPower[P]]( + ], +P <: PrimaryDataWithComplexPower[P]]( model: M, override val startDate: ZonedDateTime, override val endDate: ZonedDateTime, @@ -207,7 +207,7 @@ object BaseStateData { * Type of model, the base state data is attached to */ final case class ParticipantModelBaseStateData[ - +PD <: PrimaryDataWithApparentPower[PD], + +PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, M <: SystemParticipant[_ <: CalcRelevantData, PD, MS], @@ -272,7 +272,7 @@ object BaseStateData { * @return * A copy of the base data with updated value stores */ - def updateBaseStateData[PD <: PrimaryDataWithApparentPower[PD]]( + def updateBaseStateData[PD <: PrimaryDataWithComplexPower[PD]]( baseStateData: BaseStateData[PD], updatedResultValueStore: ValueStore[PD], updatedRequestValueStore: ValueStore[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala index 1ce8cf9352..fafb1e5775 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.participant.statedata import org.apache.pekko.actor.ActorRef import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import scala.reflect.{ClassTag, classTag} @@ -24,10 +24,10 @@ import scala.reflect.{ClassTag, classTag} * True, if an [[edu.ie3.simona.ontology.messages.Activation]] has already * arrived * @tparam PD - * Type of the [[PrimaryDataWithApparentPower]], that the model will produce + * Type of the [[PrimaryDataWithComplexPower]], that the model will produce * or receive as primary data */ -final case class DataCollectionStateData[+PD <: PrimaryDataWithApparentPower[ +final case class DataCollectionStateData[+PD <: PrimaryDataWithComplexPower[ PD ]]( baseStateData: BaseStateData[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala index 786caf63b0..c5e4750823 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala @@ -8,10 +8,10 @@ package edu.ie3.simona.agent.participant.statedata import edu.ie3.datamodel.models.input.container.ThermalGrid import edu.ie3.datamodel.models.input.system.SystemParticipantInput -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.FlexResponse import org.apache.pekko.actor.typed.ActorRef @@ -66,7 +66,7 @@ object ParticipantStateData { */ final case class ParticipantInitializingStateData[ I <: SystemParticipantInput, - C <: SimonaConfig.BaseRuntimeConfig, + C <: BaseRuntimeConfig, PD <: PrimaryData, ]( inputModel: InputModelContainer[I], @@ -110,7 +110,7 @@ object ParticipantStateData { */ final case class ParticipantInitializeStateData[ I <: SystemParticipantInput, - C <: SimonaConfig.BaseRuntimeConfig, + C <: BaseRuntimeConfig, PD <: PrimaryData, ]( inputModel: InputModelContainer[I], @@ -129,7 +129,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, - C <: SimonaConfig.BaseRuntimeConfig, + C <: BaseRuntimeConfig, PD <: PrimaryData, ]( inputModel: I, @@ -158,7 +158,7 @@ object ParticipantStateData { ) def apply[ I <: SystemParticipantInput, - C <: SimonaConfig.BaseRuntimeConfig, + C <: BaseRuntimeConfig, PD <: PrimaryData, ]( inputModel: I, @@ -189,7 +189,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, - C <: SimonaConfig.BaseRuntimeConfig, + C <: BaseRuntimeConfig, PD <: PrimaryData, ]( inputModel: I, @@ -220,7 +220,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, - C <: SimonaConfig.BaseRuntimeConfig, + C <: BaseRuntimeConfig, PD <: PrimaryData, ]( inputModel: I, @@ -261,11 +261,11 @@ object ParticipantStateData { * Mapping from service provider to foreseen next tick, it will send new * data * @tparam PD - * Type of [[PrimaryDataWithApparentPower]], that is covered by given + * Type of [[PrimaryDataWithComplexPower]], that is covered by given * [[BaseStateData]] */ final case class CollectRegistrationConfirmMessages[ - +PD <: PrimaryDataWithApparentPower[PD] + +PD <: PrimaryDataWithComplexPower[PD] ]( baseStateData: BaseStateData[PD], pendingResponses: Iterable[ClassicActorRef], 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 index 84eda8c92b..c773024760 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/storage/StorageAgent.scala @@ -11,7 +11,7 @@ import edu.ie3.simona.agent.participant.ParticipantAgent import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower 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.config.RuntimeConfig.StorageRuntimeConfig import edu.ie3.simona.model.participant.StorageModel import edu.ie3.simona.model.participant.StorageModel.{ StorageRelevantData, 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 bfa312e68a..7c0c36fa16 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 @@ -30,7 +30,7 @@ import edu.ie3.simona.agent.participant.statedata.{ BaseStateData, ParticipantStateData, } -import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.StorageRuntimeConfig import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ diff --git a/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgent.scala index f8858ded76..e208701ca6 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgent.scala @@ -13,7 +13,7 @@ import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData -import edu.ie3.simona.config.SimonaConfig.WecRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.WecRuntimeConfig import edu.ie3.simona.model.participant.ModelState.ConstantState import edu.ie3.simona.model.participant.WecModel import edu.ie3.simona.model.participant.WecModel._ diff --git a/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala index 1cbe2e7cf3..c266c0ea43 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala @@ -27,7 +27,7 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.InputMode import edu.ie3.simona.agent.participant.wec.WecAgent.neededServices import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle -import edu.ie3.simona.config.SimonaConfig.WecRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.WecRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ AgentInitializationException, diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala new file mode 100644 index 0000000000..0f33542e1b --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -0,0 +1,563 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import breeze.numerics.{pow, sqrt} +import edu.ie3.simona.agent.grid.GridAgentMessages.{ + AssetPowerChangedMessage, + AssetPowerUnchangedMessage, + ProvidedPowerResponse, +} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.{ + PrimaryData, + PrimaryDataExtra, +} +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.ParticipantModelShell +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.util.scala.Scope +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import org.apache.pekko.actor.{ActorRef => ClassicRef} +import squants.{Dimensionless, Each} + +import scala.reflect.ClassTag + +/** Agent that represents and acts on behalf of any system participant model, + * which is defined as a subclass of + * [[edu.ie3.simona.model.participant2.ParticipantModel]]. + */ +object ParticipantAgent { + + sealed trait Request + + /** This is extended by all requests that activate an [[ParticipantAgent]], + * i.e. activations, flex requests and control messages. + */ + private[participant2] sealed trait ActivationRequest extends Request { + val tick: Long + } + + /** Wrapper for an [[Activation]] to be received by an adapter. Activations + * can only be received if this agent is not EM-controlled. + * + * @param tick + * The tick to activate. + */ + private[participant2] final case class ParticipantActivation( + override val tick: Long + ) extends ActivationRequest + + /** Wrapper for [[FlexRequest]] messages to be received by an adapter (if this + * [[ParticipantAgent]] is EM-controlled). + * + * @param msg + * The wrapped flex request. + */ + private[participant2] final case class Flex(msg: FlexRequest) + extends ActivationRequest { + override val tick: Long = msg.tick + } + + /** Messages that are sent by services as responses to registration requests. + */ + sealed trait RegistrationResponseMessage extends Request { + val serviceRef: ClassicRef + } + + /** Message confirming a successful registration with a secondary service. + */ + final case class RegistrationSuccessfulMessage( + override val serviceRef: ClassicRef, + firstDataTick: Long, + ) extends RegistrationResponseMessage + + /** Message confirming a successful registration with the primary service. + * + * @param firstDataTick + * The first tick at which data will be sent. + * @param primaryDataExtra + * Extra functionality specific to the primary data class. + * @tparam P + * The type of primary data to be received. + */ + final case class PrimaryRegistrationSuccessfulMessage[ + P <: PrimaryData: ClassTag + ]( + override val serviceRef: ClassicRef, + firstDataTick: Long, + primaryDataExtra: PrimaryDataExtra[P], + ) extends RegistrationResponseMessage + + /** Message announcing a failed registration. + */ + final case class RegistrationFailedMessage( + override val serviceRef: ClassicRef + ) extends RegistrationResponseMessage + + /** Data provision messages sent by data services. + */ + sealed trait DataInputMessage extends Request { + + /** The current tick. + */ + val tick: Long + + /** The sending service actor ref. + */ + val serviceRef: ClassicRef + + /** Next tick at which data could arrive. If None, no data is expected for + * the rest of the simulation. + */ + val nextDataTick: Option[Long] + } + + /** Providing primary or secondary data to the [[ParticipantAgent]]. + * + * @param data + * The data. + * @tparam D + * The type of the provided data. + */ + final case class DataProvision[D <: Data]( + override val tick: Long, + override val serviceRef: ClassicRef, + data: D, + override val nextDataTick: Option[Long], + ) extends DataInputMessage + + /** Providing the information that no data will be provided by the sending + * service for the current tick. The participant could thus potentially skip + * calculations for the current tick and reschedule calculation for the next + * data tick. + */ + final case class NoDataProvision( + override val tick: Long, + override val serviceRef: ClassicRef, + override val nextDataTick: Option[Long], + ) extends DataInputMessage + + /** This message, sent by the [[edu.ie3.simona.agent.grid.GridAgent]], + * requests the power values for the requested tick from this + * [[ParticipantAgent]] and provides the latest nodal voltage. + * + * @param tick + * The current tick. + * @param eInPu + * Real part of the complex, dimensionless nodal voltage. + * @param fInPu + * Imaginary part of the complex, dimensionless nodal voltage. + * @param replyTo + * Actor reference to send the reply to + */ + final case class RequestAssetPowerMessage( + tick: Long, + eInPu: Dimensionless, + fInPu: Dimensionless, + replyTo: ActorRef[ProvidedPowerResponse], + ) extends Request + + /** Message announcing that calculations by the + * [[edu.ie3.simona.agent.grid.GridAgent]] have come to an end and regular + * participant activities can continue. + * + * @param tick + * The current tick. + * @param nextRequestTick + * The next tick at which asset power is requested via + * [[RequestAssetPowerMessage]]. + */ + final case class GridSimulationFinished( + tick: Long, + nextRequestTick: Long, + ) extends Request + + /** Data object that holds the actor reference to the + * [[edu.ie3.simona.scheduler.Scheduler]] activating this agent, indicating + * that this [[ParticipantAgent]] is not EM-controlled. + * + * @param scheduler + * The scheduler that is activating this agent. + * @param activationAdapter + * The activation adapter handling [[Activation]] messages. + */ + final case class SchedulerData( + scheduler: ActorRef[SchedulerMessage], + activationAdapter: ActorRef[Activation], + ) + + /** Data object that holds the actor reference to the corresponding + * [[edu.ie3.simona.agent.em.EmAgent]], indicating that this + * [[ParticipantAgent]] is not EM-controlled. + * + * @param emAgent + * The parent EmAgent that is controlling this agent. + * @param flexAdapter + * The flex adapter handling [[FlexRequest]] messages. + * @param lastFlexOptions + * Last flex options that have been calculated for this agent. + */ + final case class FlexControlledData( + emAgent: ActorRef[FlexResponse], + flexAdapter: ActorRef[FlexRequest], + lastFlexOptions: Option[ProvideFlexOptions] = None, + ) + + /** A request to the [[edu.ie3.simona.model.participant2.ParticipantModel]] + * outside of regular requests related to participant operation. + */ + trait ParticipantRequest extends Request { + + /** The tick for which the request is valid, which is the current tick. + */ + val tick: Long + } + + def apply( + modelShell: ParticipantModelShell[_, _], + inputHandler: ParticipantInputHandler, + gridAdapter: ParticipantGridAdapter, + resultListener: Iterable[ActorRef[ResultEvent]], + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = + Behaviors.receivePartial { + case (ctx, request: ParticipantRequest) => + val updatedShell = modelShell + .updateModelInput( + inputHandler.getData, + gridAdapter.nodalVoltage, + request.tick, + ) + .handleRequest(ctx, request) + + ParticipantAgent( + updatedShell, + inputHandler, + gridAdapter, + resultListener, + parentData, + ) + + case (_, activation: ActivationRequest) => + val coreWithActivation = inputHandler.handleActivation(activation) + + val (updatedShell, updatedInputHandler, updatedGridAdapter) = + maybeCalculate( + modelShell, + coreWithActivation, + gridAdapter, + resultListener, + parentData, + ) + + ParticipantAgent( + updatedShell, + updatedInputHandler, + updatedGridAdapter, + resultListener, + parentData, + ) + + case (_, msg: DataInputMessage) => + val inputHandlerWithData = inputHandler.handleDataInputMessage(msg) + + val (updatedShell, updatedInputHandler, updatedGridAdapter) = + maybeCalculate( + modelShell, + inputHandlerWithData, + gridAdapter, + resultListener, + parentData, + ) + + ParticipantAgent( + updatedShell, + updatedInputHandler, + updatedGridAdapter, + resultListener, + parentData, + ) + + case ( + ctx, + RequestAssetPowerMessage(currentTick, eInPu, fInPu, replyTo), + ) => + // we do not have to wait for the resulting power of the current tick, + // since the current power is irrelevant for the average power up until now + + val reactivePowerFunc = modelShell.reactivePowerFunc + + val nodalVoltage = Each( + sqrt( + pow(eInPu.toEach, 2) + + pow(fInPu.toEach, 2) + ) + ) + + val updatedGridAdapter = gridAdapter + .handlePowerRequest( + nodalVoltage, + currentTick, + Some(reactivePowerFunc), + ctx.log, + ) + + val result = updatedGridAdapter.avgPowerResult.getOrElse( + throw new CriticalFailureException( + "Power result has not been calculated" + ) + ) + replyTo ! + (if (result.newResult) { + AssetPowerChangedMessage( + result.avgPower.p, + result.avgPower.q, + ) + } else { + AssetPowerUnchangedMessage( + result.avgPower.p, + result.avgPower.q, + ) + }) + + ParticipantAgent( + modelShell, + inputHandler, + updatedGridAdapter, + resultListener, + parentData, + ) + + case (_, GridSimulationFinished(_, nextRequestTick)) => + val gridAdapterFinished = + gridAdapter.updateNextRequestTick(nextRequestTick) + + // Possibly start simulation if we've been activated + val (updatedShell, updatedInputHandler, updatedGridAdapter) = + maybeCalculate( + modelShell, + inputHandler, + gridAdapterFinished, + resultListener, + parentData, + ) + + ParticipantAgent( + updatedShell, + updatedInputHandler, + updatedGridAdapter, + resultListener, + parentData, + ) + } + + /** Starts a model calculation if all requirements have been met. A model + * calculation could be the determination of flex options and operating point + * when EM-controlled, and only operating point when not EM-controlled. + * Requirements include all necessary data having been received and power + * flow calculation having finished, if applicable. + * + * @param modelShell + * The [[ParticipantModelShell]]. + * @param inputHandler + * The [[ParticipantInputHandler]]. + * @param gridAdapter + * The [[ParticipantGridAdapter]]. + * @param listener + * The result listeners. + * @param parentData + * The parent of this [[ParticipantAgent]]. + * @return + * An updated [[ParticipantModelShell]], [[ParticipantInputHandler]] and + * [[ParticipantGridAdapter]]. + */ + private def maybeCalculate( + modelShell: ParticipantModelShell[_, _], + inputHandler: ParticipantInputHandler, + gridAdapter: ParticipantGridAdapter, + listener: Iterable[ActorRef[ResultEvent]], + parentData: Either[SchedulerData, FlexControlledData], + ): ( + ParticipantModelShell[_, _], + ParticipantInputHandler, + ParticipantGridAdapter, + ) = { + if (expectedMessagesReceived(inputHandler, gridAdapter)) { + + val activation = inputHandler.activation.getOrElse( + throw new CriticalFailureException( + "Activation should be present when data collection is complete" + ) + ) + + val (updatedShell, updatedGridAdapter) = Scope(modelShell) + .map( + _.updateModelInput( + inputHandler.getData, + gridAdapter.nodalVoltage, + activation.tick, + ) + ) + .map { shell => + activation match { + case ParticipantActivation(tick) => + val (shellWithOP, gridAdapterWithResult) = + if (isCalculationRequired(shell, inputHandler)) { + val newShell = shell.updateOperatingPoint(tick) + + val results = + newShell.determineResults(tick, gridAdapter.nodalVoltage) + + results.modelResults.foreach { res => + listener.foreach(_ ! ParticipantResultEvent(res)) + } + + val newGridAdapter = + gridAdapter.storePowerValue(results.totalPower, tick) + + (newShell, newGridAdapter) + } else + (shell, gridAdapter) + + val changeIndicator = shellWithOP.getChangeIndicator( + tick, + inputHandler.getNextDataTick, + ) + + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + changeIndicator.changesAtTick, + ), + _ => + throw new CriticalFailureException( + "Received activation while controlled by EM" + ), + ) + (shellWithOP, gridAdapterWithResult) + + case Flex(FlexActivation(tick)) => + val shellWithFlex = + if (isCalculationRequired(shell, inputHandler)) { + shell.updateFlexOptions(tick) + } else + shell + + parentData.fold( + _ => + throw new CriticalFailureException( + "Received flex activation while not controlled by EM" + ), + _.emAgent ! shellWithFlex.flexOptions, + ) + + (shellWithFlex, gridAdapter) + + case Flex(flexControl: IssueFlexControl) => + val shellWithOP = shell.updateOperatingPoint(flexControl) + + // todo we determine results even if no new data arrived, and EM is also activated... + val results = + shellWithOP.determineResults( + flexControl.tick, + gridAdapter.nodalVoltage, + ) + + results.modelResults.foreach { res => + listener.foreach(_ ! ParticipantResultEvent(res)) + } + + val gridAdapterWithResult = + gridAdapter.storePowerValue( + results.totalPower, + flexControl.tick, + ) + + val changeIndicator = shellWithOP.getChangeIndicator( + flexControl.tick, + inputHandler.getNextDataTick, + ) + + parentData.fold( + _ => + throw new CriticalFailureException( + "Received issue flex control while not controlled by EM" + ), + _.emAgent ! FlexCompletion( + shellWithOP.uuid, + changeIndicator.changesAtNextActivation, + changeIndicator.changesAtTick, + ), + ) + + (shellWithOP, gridAdapterWithResult) + } + } + .get + + (updatedShell, inputHandler.completeActivation(), updatedGridAdapter) + } else + (modelShell, inputHandler, gridAdapter) + } + + /** Checks if all required messages needed for calculation have been received. + * These are: + * - agent is activated (activation has been received and not completed + * yet). + * - all required data has been received. + * - the grid adapter is not waiting for power requests (the new voltage + * needs to be received before starting calculations for the current + * tick). + * + * @param inputHandler + * The participant input handler. + * @param gridAdapter + * The participant grid adapter. + * @return + * Whether power can be calculated or not. + */ + private def expectedMessagesReceived( + inputHandler: ParticipantInputHandler, + gridAdapter: ParticipantGridAdapter, + ): Boolean = { + inputHandler.allMessagesReceived && + inputHandler.activation.exists(activation => + !gridAdapter.isPowerRequestAwaited(activation.tick) + ) + } + + /** Checks if conditions for recalculation (i.e. determination of operating + * point, flex options etc.) are present. This is not the case if all + * registered services have delivered [[NoDataProvision]] messages only, but + * can still be the case if the model itself requested recalculation. + * + * @param modelShell + * The model shell. + * @param inputHandler + * The participant input handler. + * @return + */ + private def isCalculationRequired( + modelShell: ParticipantModelShell[_, _], + inputHandler: ParticipantInputHandler, + ): Boolean = + inputHandler.hasNewData || + inputHandler.activation.exists(activation => + modelShell + .getChangeIndicator(activation.tick - 1, None) + .changesAtTick + .contains(activation.tick) + ) + +} diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala new file mode 100644 index 0000000000..88d13c85c5 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -0,0 +1,387 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.datamodel.models.input.system.SystemParticipantInput +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent._ +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.ParticipantModelShell +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + Completion, + ScheduleActivation, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ +import edu.ie3.simona.ontology.messages.services.EvMessage.RegisterForEvDataMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.WeatherMessage.RegisterForWeatherMessage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.service.ServiceType +import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import org.apache.pekko.actor.{ActorRef => ClassicRef} +import squants.Dimensionless + +import java.time.ZonedDateTime + +/** This class helps collect all information required for the initialization of + * a [[ParticipantAgent]]. When initialization succeeds, a [[ParticipantAgent]] + * behavior is returned, having the first simulation activation already + * scheduled with the corresponding [[edu.ie3.simona.scheduler.Scheduler]] or + * [[edu.ie3.simona.agent.em.EmAgent]]. + */ +object ParticipantAgentInit { + + /** Container class that gathers references to relevant actors. + * + * @param gridAgent + * Reference to the grid agent. + * @param primaryServiceProxy + * Reference to the primary service proxy. + * @param services + * References to services by service type. + * @param resultListener + * Reference to the result listeners. + */ + final case class ParticipantRefs( + gridAgent: ActorRef[GridAgent.Request], + primaryServiceProxy: ClassicRef, + services: Map[ServiceType, ClassicRef], + resultListener: Iterable[ActorRef[ResultEvent]], + ) + + /** Container class that holds parameters related to the simulation. + * + * @param expectedPowerRequestTick + * The tick at which the first power request message is expected from + * [[GridAgent]]. + * @param requestVoltageDeviationTolerance + * The voltage request deviation tolerance, outside which reactive power + * has to be recalculated. + * @param simulationStart + * The simulation start date and time. + * @param simulationEnd + * The simulation end date and time. + */ + final case class SimulationParameters( + expectedPowerRequestTick: Long, + requestVoltageDeviationTolerance: Dimensionless, + simulationStart: ZonedDateTime, + simulationEnd: ZonedDateTime, + ) + + /** Starts the initialization process of a [[ParticipantAgent]]. + * + * @param participantInput + * The system participant model input that represents the physical model at + * the core of the agent. + * @param config + * Runtime configuration that has to match the participant type. + * @param participantRefs + * A collection of actor references to actors required for initialization + * and operation. + * @param simulationParams + * Some parameters required for simulation. + * @param parent + * The parent actor scheduling or controlling this participant, i.e. either + * a [[edu.ie3.simona.scheduler.Scheduler]] or an + * [[edu.ie3.simona.agent.em.EmAgent]]. + */ + def apply( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + participantRefs: ParticipantRefs, + simulationParams: SimulationParameters, + parent: Either[ActorRef[SchedulerMessage], ActorRef[FlexResponse]], + ): Behavior[Request] = Behaviors.setup { ctx => + val parentData = parent + .map { em => + val flexAdapter = ctx.messageAdapter[FlexRequest](Flex) + + em ! RegisterControlledAsset( + flexAdapter, + participantInput, + ) + + em ! ScheduleFlexActivation( + participantInput.getUuid, + INIT_SIM_TICK, + ) + + FlexControlledData(em, flexAdapter) + } + .left + .map { scheduler => + { + val activationAdapter = ctx.messageAdapter[Activation] { msg => + ParticipantActivation(msg.tick) + } + + scheduler ! ScheduleActivation( + activationAdapter, + INIT_SIM_TICK, + ) + + SchedulerData(scheduler, activationAdapter) + } + } + + uninitialized( + participantInput, + config, + participantRefs, + simulationParams, + parentData, + ) + } + + /** Waiting for an [[Activation]] message to start the initialization. + */ + private def uninitialized( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + participantRefs: ParticipantRefs, + simulationParams: SimulationParameters, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = Behaviors.receivePartial { + + case (ctx, activation: ActivationRequest) + if activation.tick == INIT_SIM_TICK => + // first, check whether we're just supposed to replay primary data time series + participantRefs.primaryServiceProxy ! PrimaryServiceRegistrationMessage( + ctx.self.toClassic, + participantInput.getUuid, + ) + + waitingForPrimaryProxy( + participantInput, + config, + participantRefs, + simulationParams, + parentData, + ) + + } + + /** Waits for the primary proxy to respond, which decides whether this + * participant uses model calculations or just replays primary data. + */ + private def waitingForPrimaryProxy( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + participantRefs: ParticipantRefs, + simulationParams: SimulationParameters, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = Behaviors.receivePartial { + + case ( + _, + PrimaryRegistrationSuccessfulMessage( + serviceRef, + firstDataTick, + primaryDataExtra, + ), + ) => + // we're supposed to replay primary data, initialize accordingly + val expectedFirstData = Map(serviceRef -> firstDataTick) + + completeInitialization( + ParticipantModelShell.createForPrimaryData( + participantInput, + config, + primaryDataExtra, + simulationParams.simulationStart, + simulationParams.simulationEnd, + ), + expectedFirstData, + participantRefs, + simulationParams, + parentData, + ) + + case (_, RegistrationFailedMessage(_)) => + // we're _not_ supposed to replay primary data, thus initialize the physical model + val modelShell = ParticipantModelShell.createForPhysicalModel( + participantInput, + config, + simulationParams.simulationStart, + simulationParams.simulationEnd, + ) + + val requiredServiceTypes = modelShell.requiredServices.toSet + + if (requiredServiceTypes.isEmpty) { + // not requiring any secondary services, thus we're ready to go + completeInitialization( + modelShell, + Map.empty, + participantRefs, + simulationParams, + parentData, + ) + } else { + // requiring at least one secondary service, thus send out registrations and wait for replies + val requiredServices = requiredServiceTypes + .map(serviceType => + serviceType -> participantRefs.services.getOrElse( + serviceType, + throw new CriticalFailureException( + s"${modelShell.identifier}: Service of type $serviceType is not available." + ), + ) + ) + .toMap + + requiredServices.foreach { case (serviceType, serviceRef) => + registerForService( + participantInput, + modelShell, + serviceType, + serviceRef, + ) + } + + waitingForServices( + modelShell, + participantRefs, + simulationParams, + requiredServices.values.toSet, + parentData = parentData, + ) + } + } + + private def registerForService( + participantInput: SystemParticipantInput, + modelShell: ParticipantModelShell[_, _], + serviceType: ServiceType, + serviceRef: ClassicRef, + ): Unit = + serviceType match { + case ServiceType.WeatherService => + val geoPosition = participantInput.getNode.getGeoPosition + + Option(geoPosition.getY).zip(Option(geoPosition.getX)) match { + case Some((lat, lon)) => + serviceRef ! RegisterForWeatherMessage(lat, lon) + case _ => + throw new CriticalFailureException( + s"${modelShell.identifier} cannot register for weather information at " + + s"node ${participantInput.getNode.getId} (${participantInput.getNode.getUuid}), " + + s"because the geo position (${geoPosition.getY}, ${geoPosition.getX}) is invalid." + ) + } + + case ServiceType.PriceService => + throw new CriticalFailureException( + s"${modelShell.identifier} is trying to register for a ${ServiceType.PriceService}, " + + s"which is currently not supported." + ) + + case ServiceType.EvMovementService => + serviceRef ! RegisterForEvDataMessage(modelShell.uuid) + } + + /** Waiting for replies from secondary services. If all replies have been + * received, we complete the initialization. + */ + private def waitingForServices( + modelShell: ParticipantModelShell[_, _], + participantRefs: ParticipantRefs, + simulationParams: SimulationParameters, + expectedRegistrations: Set[ClassicRef], + expectedFirstData: Map[ClassicRef, Long] = Map.empty, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = + Behaviors.receivePartial { + case (_, RegistrationSuccessfulMessage(serviceRef, nextDataTick)) => + // received registration success message from secondary service + if (!expectedRegistrations.contains(serviceRef)) + throw new CriticalFailureException( + s"${modelShell.identifier}: Registration response from $serviceRef was not expected!" + ) + + val newExpectedRegistrations = expectedRegistrations.excl(serviceRef) + val newExpectedFirstData = + expectedFirstData.updated(serviceRef, nextDataTick) + + if (newExpectedRegistrations.isEmpty) + // all secondary services set up, ready to go + completeInitialization( + modelShell, + newExpectedFirstData, + participantRefs, + simulationParams, + parentData, + ) + else + // there's at least one more service to go, let's wait for confirmation + waitingForServices( + modelShell, + participantRefs, + simulationParams, + newExpectedRegistrations, + newExpectedFirstData, + parentData, + ) + } + + /** Completes initialization, sends a completion message and creates actual + * [[ParticipantAgent]] + */ + private def completeInitialization( + modelShell: ParticipantModelShell[_, _], + expectedData: Map[ClassicRef, Long], + participantRefs: ParticipantRefs, + simulationParams: SimulationParameters, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = { + + val inputHandler = ParticipantInputHandler(expectedData) + + // get first overall activation tick + val firstTick = inputHandler.getDataCompletedTick.orElse( + modelShell + .getChangeIndicator(currentTick = -1, None) + .changesAtTick + ) + + if (firstTick.isEmpty) + throw new CriticalFailureException( + s"${modelShell.identifier}: No new first activation tick determined with expected data $expectedData" + ) + + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + firstTick, + ), + _.emAgent ! FlexCompletion( + modelShell.uuid, + requestAtNextActivation = false, + firstTick, + ), + ) + + ParticipantAgent( + modelShell, + inputHandler, + ParticipantGridAdapter( + participantRefs.gridAgent, + simulationParams.expectedPowerRequestTick, + simulationParams.requestVoltageDeviationTolerance, + ), + participantRefs.resultListener, + parentData, + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala new file mode 100644 index 0000000000..2359201d69 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -0,0 +1,291 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower +import edu.ie3.simona.agent.participant2.ParticipantGridAdapter._ +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroMVAr, zeroMW} +import edu.ie3.util.scala.quantities.{Megavars, QuantityUtil, ReactivePower} +import org.apache.pekko.actor.typed.ActorRef +import org.slf4j.Logger +import squants.energy.Megawatts +import squants.{Dimensionless, Each, Energy, Power} + +import scala.collection.immutable.SortedMap +import scala.util.{Failure, Success} + +/** Stores resulting power values from the participant model and provides + * averaged values to the [[GridAgent]]. Also receives the resulting nodal + * voltage, which might be used for model calculations. + * + * When the power value is requested by the [[GridAgent]], an average (weighted + * by time) of the recent registered power values (from the time of the last + * request on) is determined. This is done in consideration of the voltage + * value calculated by the [[GridAgent]], which is valid for the past time + * frame. For the new time frame starting with the current tick, the voltage is + * preliminarily used, until the next communication with the grid establishes a + * proper new voltage valid for the new time frame. + * + * @param gridAgent + * The actor reference to the [[GridAgent]]. + * @param expectedRequestTick + * The tick at which next power request is expected. + * @param nodalVoltage + * The most recent nodal voltage in p.u. + * @param tickToPower + * Map storing the power values from which averages can be derived. + * @param avgPowerResult + * The calculated average power for the current request window. + * @param requestVoltageDeviationTolerance + * The tolerance for differences in voltage when deciding whether to + * recalculate reactive power. + */ +final case class ParticipantGridAdapter( + gridAgent: ActorRef[GridAgent.Request], + nodalVoltage: Dimensionless, + private val expectedRequestTick: Long, + private val tickToPower: SortedMap[Long, ComplexPower], + avgPowerResult: Option[AvgPowerResult], + private implicit val requestVoltageDeviationTolerance: Dimensionless, +) { + + /** Whether a power request is expected and has not yet arrived, thus is + * awaited, for the given tick. + * + * @param currentTick + * The current tick. + * @return + * Whether a power request is awaited for the given tick. + */ + def isPowerRequestAwaited(currentTick: Long): Boolean = { + expectedRequestTick == currentTick + } + + /** Store a power value that has been determined by the model for the given + * tick. + * + * @param power + * The power value determined by the model. + * @param tick + * The current tick. + * @return + * An adapted [[ParticipantGridAdapter]] that stores given value. + */ + def storePowerValue( + power: ComplexPower, + tick: Long, + ): ParticipantGridAdapter = + copy(tickToPower = tickToPower.updated(tick, power)) + + /** Handles a power request by making sure an average power value has been + * calculated, taking into account the new voltage value. + * + * @param newVoltage + * The updated voltage valid for the recent time window ending at the + * current tick. + * @param currentTick + * The current tick. + * @param reactivePowerFuncOpt + * A model function that determines reactive power given nodal voltage and + * active power. + * @param log + * A logger. + * @return + * An adapted [[ParticipantGridAdapter]] that holds the determined average + * power. + */ + def handlePowerRequest( + newVoltage: Dimensionless, + currentTick: Long, + reactivePowerFuncOpt: Option[ + Dimensionless => Power => ReactivePower + ], + log: Logger, + ): ParticipantGridAdapter = { + if (currentTick != expectedRequestTick) + throw new CriticalFailureException( + s"Power request expected for $expectedRequestTick, but not for current tick $currentTick" + ) + + val result = (avgPowerResult match { + case Some(cache @ AvgPowerResult(windowStart, windowEnd, voltage, _, _)) + if windowEnd == currentTick => + // Results have been calculated for the same tick... + if (voltage =~ newVoltage) { + // ... and same voltage, return cached result + Left(cache) + } else { + // ... and different voltage, results have to be re-calculated with same params + Right(windowStart, windowEnd) + } + case Some(AvgPowerResult(_, windowEnd, _, _, _)) => + // Results have been calculated for a former tick, take former windowEnd as the new windowStart + Right(windowEnd, currentTick) + case None => + // No results have been calculated whatsoever, calculate from simulation start (0) + Right(0L, currentTick) + }).fold( + cachedResult => cachedResult.copy(newResult = false), + { case (windowStart: Long, windowEnd: Long) => + val avgPower = averageApparentPower( + tickToPower, + windowStart, + windowEnd, + reactivePowerFuncOpt.map(_.apply(newVoltage)), + log, + ) + AvgPowerResult( + windowStart, + windowEnd, + newVoltage, + avgPower, + newResult = true, + ) + }, + ) + + val reducedMap = reduceTickToPowerMap(tickToPower, result.windowStart) + + copy( + nodalVoltage = newVoltage, + tickToPower = reducedMap, + avgPowerResult = Some(result), + ) + } + + /** Updates this grid adapter with the given next request tick. + * + * @param nextRequestTick + * The next tick at which power is requested. + * @return + * The updated grid adapter. + */ + def updateNextRequestTick(nextRequestTick: Long): ParticipantGridAdapter = + copy(expectedRequestTick = nextRequestTick) + +} + +object ParticipantGridAdapter { + + /** Result of an average power calculation. + * + * @param windowStart + * The tick at which the time frame starts. + * @param windowEnd + * The tick at which the time frame ends. + * @param voltage + * The voltage used for calculating the average power. + * @param avgPower + * The calculated average power. + * @param newResult + * Whether the calculated power has been calculated anew or reused from + * past calculation. + */ + final case class AvgPowerResult( + windowStart: Long, + windowEnd: Long, + voltage: Dimensionless, + avgPower: ComplexPower, + newResult: Boolean, + ) + + def apply( + gridAgentRef: ActorRef[GridAgent.Request], + expectedRequestTick: Long, + requestVoltageDeviationTolerance: Dimensionless, + ): ParticipantGridAdapter = + new ParticipantGridAdapter( + gridAgent = gridAgentRef, + nodalVoltage = Each(1d), + expectedRequestTick = expectedRequestTick, + tickToPower = SortedMap.empty, + avgPowerResult = None, + requestVoltageDeviationTolerance = requestVoltageDeviationTolerance, + ) + + private def reduceTickToPowerMap( + tickToPower: SortedMap[Long, ComplexPower], + windowStart: Long, + ): SortedMap[Long, ComplexPower] = { + // keep the last entry at or before windowStart + val lastTickBeforeWindowStart = + tickToPower.rangeUntil(windowStart + 1).lastOption + + // remove all entries before or at windowStart + val reducedMap = tickToPower.rangeFrom(windowStart + 1) + + // combine both + lastTickBeforeWindowStart.map(reducedMap + _).getOrElse(reducedMap) + } + + /** Determine the average apparent power within the given time frame. + * + * @param tickToPower + * Mapping from tick to power value. + * @param windowStart + * First tick (inclusive) of the time frame. + * @param windowEnd + * Last tick (exclusive) of the time frame. + * @param activeToReactivePowerFuncOpt + * The optional function determining reactive power given an active power. + * @return + * The averaged complex power. + */ + private def averageApparentPower( + tickToPower: Map[Long, ComplexPower], + windowStart: Long, + windowEnd: Long, + activeToReactivePowerFuncOpt: Option[ + Power => ReactivePower + ] = None, + log: Logger, + ): ComplexPower = { + val p = QuantityUtil.average[Power, Energy]( + tickToPower.map { case (tick, pd) => + tick -> pd.p + }, + windowStart, + windowEnd, + ) match { + case Success(pSuccess) => + pSuccess + case Failure(exception) => + log.warn( + "Unable to determine average active power. Apply 0 instead.", + exception, + ) + zeroMW + } + + val q = QuantityUtil.average[Power, Energy]( + tickToPower.map { case (tick, pd) => + activeToReactivePowerFuncOpt match { + case Some(qFunc) => + // NOTE: The type conversion to Megawatts is done to satisfy the methods type constraints + // and is undone after unpacking the results + tick -> Megawatts(qFunc(pd.p).toMegavars) + case None => tick -> Megawatts(pd.q.toMegavars) + } + }, + windowStart, + windowEnd, + ) match { + case Success(pSuccess) => + Megavars(pSuccess.toMegawatts) + case Failure(exception) => + log.warn( + "Unable to determine average reactive power. Apply 0 instead.", + exception, + ) + zeroMVAr + } + + ComplexPower(p, q) + } +} diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala new file mode 100644 index 0000000000..a024929ff3 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala @@ -0,0 +1,169 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import org.apache.pekko.actor.{ActorRef => ClassicRef} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + ActivationRequest, + DataInputMessage, + DataProvision, + NoDataProvision, +} +import edu.ie3.simona.agent.participant2.ParticipantInputHandler.ReceivedData + +/** This class holds received data, knows what data is expected and can thus + * decide whether all input requirements have been fulfilled. + * + * @param expectedData + * Map of service actor reference to the tick at which data is expected next. + * When data is received, the next tick is updated here. + * @param receivedData + * Map of service actor reference to received data. Here, the most recent + * received data is saved, which might not always be data received at the + * current tick. + * @param activation + * The activation message with which the participant agent was activated, if + * applicable. This is emptied after each tick is completed. + */ +final case class ParticipantInputHandler( + expectedData: Map[ClassicRef, Long], + receivedData: Map[ClassicRef, Option[ReceivedData]], + activation: Option[ActivationRequest], +) { + + /** Handles a received [[ActivationRequest]] by storing the message. + * + * @param activation + * The activation. + * @return + * An updated input handler. + */ + def handleActivation( + activation: ActivationRequest + ): ParticipantInputHandler = + copy(activation = Some(activation)) + + /** Completes an activation by clearing out the stored activation message. + * + * @return + * An updated input handler. + */ + def completeActivation(): ParticipantInputHandler = + copy(activation = None) + + /** Handles a received [[DataInputMessage]] by storing the message and + * updating the expected data that remains to be received. + * + * @param msg + * The received data message. + * @return + * An updated input handler. + */ + def handleDataInputMessage( + msg: DataInputMessage + ): ParticipantInputHandler = { + + val updatedReceivedData = + msg match { + case DataProvision(tick, serviceRef, data, _) => + receivedData + + (serviceRef -> Some(ReceivedData(data, tick))) + case _: NoDataProvision => + receivedData + } + + val updatedExpectedData = msg.nextDataTick + .map { nextTick => + expectedData + (msg.serviceRef -> nextTick) + } + .getOrElse { + expectedData - msg.serviceRef + } + + copy( + expectedData = updatedExpectedData, + receivedData = updatedReceivedData, + ) + } + + /** Determines whether all expected messages for the current tick (activation + * and data input messages) have been received. + * + * @return + * Whether all expected messages were received for the current tick. + */ + def allMessagesReceived: Boolean = activation.exists { activationMsg => + expectedData.forall { case (_, nextTick) => + nextTick > activationMsg.tick + } + } + + /** Determines whether there has been new data received for the current + * activation, which would mean that re-determination of model parameters + * should happen. + * + * @return + * Whether there's new data for the current tick or not. + */ + def hasNewData: Boolean = + activation.exists { activationMsg => + receivedData.values.exists( + _.exists(_.tick == activationMsg.tick) + ) + } + + /** Returns the next tick at which input data is expected. + * + * @return + * The next data tick. + */ + def getNextDataTick: Option[Long] = + expectedData.values.minOption + + /** Returns the tick at which all input data has been updated. Useful for the + * first calculation after initialization, when all data needs to be present + * before first calculation. + * + * @return + * The tick at which all data has been updated once. + */ + def getDataCompletedTick: Option[Long] = + expectedData.values.maxOption + + /** Returns all received input data. + * + * @return + * The received data. + */ + def getData: Seq[Data] = + receivedData.values.flatten.map(_.data).toSeq + +} + +object ParticipantInputHandler { + + /** Holds received data in combination with the tick at which it was received. + */ + final case class ReceivedData(data: Data, tick: Long) + + /** Creates a new [[ParticipantInputHandler]] with the given expected data and + * empty received data and activation fields. + * + * @param expectedData + * Map of service actor reference to the tick at which data is expected + * next. + * @return + * A new [[ParticipantInputHandler]]. + */ + def apply(expectedData: Map[ClassicRef, Long]): ParticipantInputHandler = + new ParticipantInputHandler( + expectedData = expectedData, + receivedData = Map.empty, + activation = None, + ) +} diff --git a/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala b/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala index b71adf4a30..1dfb6a79c5 100644 --- a/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala +++ b/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala @@ -6,8 +6,6 @@ package edu.ie3.simona.api -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{Actor, ActorRef, PoisonPill, Props} import edu.ie3.simona.api.ExtSimAdapter.{Create, ExtSimAdapterStateData, Stop} import edu.ie3.simona.api.data.ontology.ScheduleDataServiceMessage import edu.ie3.simona.api.simulation.ExtSimAdapterData @@ -18,15 +16,17 @@ import edu.ie3.simona.api.simulation.ontology.{ CompletionMessage => ExtCompletionMessage, } import edu.ie3.simona.logging.SimonaActorLogging +import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.ScheduleServiceActivation -import edu.ie3.simona.ontology.messages.Activation +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ScheduleServiceActivation import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.{Actor, ActorRef, PoisonPill, Props} import scala.jdk.OptionConverters._ diff --git a/src/main/scala/edu/ie3/simona/config/ArgsParser.scala b/src/main/scala/edu/ie3/simona/config/ArgsParser.scala index e79c6b94ce..c6c52fb80e 100644 --- a/src/main/scala/edu/ie3/simona/config/ArgsParser.scala +++ b/src/main/scala/edu/ie3/simona/config/ArgsParser.scala @@ -23,7 +23,7 @@ object ArgsParser extends LazyLogging { mainArgs: Array[String], configLocation: Option[String] = None, config: Option[TypesafeConfig] = None, - selectedSubnets: Option[String] = None, + selectedSubgrids: Option[String] = None, selectedVoltLvls: Option[String] = None, clusterType: Option[ClusterType] = None, nodeHost: Option[String] = None, @@ -57,7 +57,7 @@ object ArgsParser extends LazyLogging { "Comma separated list (no whitespaces!) of substitution arguments for simona config." ) opt[String](name = "subnets") - .action((value, args) => args.copy(selectedSubnets = Some(value))) + .action((value, args) => args.copy(selectedSubgrids = Some(value))) .text("Comma separated list (no whitespaces!) of selected subnets.") opt[String](name = "voltlevels") .action((value, args) => args.copy(selectedVoltLvls = Some(value))) @@ -225,8 +225,8 @@ object ArgsParser extends LazyLogging { ConfigFactory.parseString( s"""config = "${parsedArgs.configLocation.get.replace("\\", "\\\\")}" |simona.runtime_configuration { - | selected_subnets = [${parsedArgs.selectedSubnets.getOrElse("")}] - | selected_volt_lvls = [${parsedArgs.selectedVoltLvls + | selectedSubgrids = [${parsedArgs.selectedSubgrids.getOrElse("")}] + | selectedVoltLvls = [${parsedArgs.selectedVoltLvls .getOrElse("")}] |} |""".stripMargin diff --git a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala index c3b16d2ca7..ea471a1ee1 100644 --- a/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/simona/config/ConfigFailFast.scala @@ -8,6 +8,12 @@ package edu.ie3.simona.config import com.typesafe.config.{Config, ConfigException} import com.typesafe.scalalogging.LazyLogging +import edu.ie3.simona.config.RuntimeConfig.{ + BaseRuntimeConfig, + LoadRuntimeConfig, + ParticipantRuntimeConfigs, + StorageRuntimeConfig, +} import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ CouchbaseParams, InfluxDb1xParams, @@ -116,9 +122,10 @@ object ConfigFailFast extends LazyLogging { checkTimeConfig(simonaConfig.simona.time) // check if the provided combinations of refSystems provided are valid - val refSystems = simonaConfig.simona.gridConfig.refSystems - if (refSystems.isDefined) - refSystems.foreach(refsys => checkRefSystem(refsys)) + simonaConfig.simona.gridConfig.refSystems.foreach(checkRefSystem) + + // check if the provided combinations of voltageLimits provided are valid + simonaConfig.simona.gridConfig.voltageLimits.foreach(checkVoltageLimits) /* Check all participant model configurations */ checkParticipantRuntimeConfiguration( @@ -274,7 +281,7 @@ object ConfigFailFast extends LazyLogging { * Sub configuration tree to check */ private def checkParticipantRuntimeConfiguration( - subConfig: SimonaConfig.Simona.Runtime.Participant + subConfig: RuntimeConfig.Participant ): Unit = { if (subConfig.requestVoltageDeviationThreshold < 0) throw new InvalidConfigParameterException( @@ -318,7 +325,7 @@ object ConfigFailFast extends LazyLogging { * the runtime listener config */ private def checkRuntimeListenerConfiguration( - listenerConfig: SimonaConfig.Simona.Runtime.Listener + listenerConfig: RuntimeConfig.Listener ): Unit = { listenerConfig.kafka.foreach(kafka => checkKafkaParams(kafka, Seq(kafka.topic)) @@ -330,8 +337,8 @@ object ConfigFailFast extends LazyLogging { * i.e. uuid and scaling factor */ private def checkBaseRuntimeConfigs( - defaultConfig: SimonaConfig.BaseRuntimeConfig, - individualConfigs: List[SimonaConfig.BaseRuntimeConfig], + defaultConfig: BaseRuntimeConfig, + individualConfigs: List[BaseRuntimeConfig], defaultString: String = "default", ): Unit = { // special default config check @@ -430,7 +437,7 @@ object ConfigFailFast extends LazyLogging { * model behaviour and reference */ private def checkSpecificLoadModelConfig( - loadModelConfig: SimonaConfig.LoadRuntimeConfig + loadModelConfig: LoadRuntimeConfig ): Unit = { if (!LoadModelBehaviour.isEligibleInput(loadModelConfig.modelBehaviour)) throw new InvalidConfigParameterException( @@ -454,72 +461,103 @@ object ConfigFailFast extends LazyLogging { * @param refSystems * a list of [[SimonaConfig.RefSystemConfig]]s that should be checked */ - - private def checkRefSystem(refSystems: List[RefSystemConfig]): Unit = { + private def checkRefSystem( + refSystems: List[RefSystemConfig] + ): Unit = { refSystems.foreach { refSystem => - { - val voltLvls = - refSystem.voltLvls.getOrElse(List.empty[SimonaConfig.VoltLvlConfig]) - val gridIds = refSystem.gridIds.getOrElse(List.empty[String]) + checkGridConfig(refSystem, "refSystem") - if (voltLvls.isEmpty && gridIds.isEmpty) + refSystem.sNom match { + case ConfigConventions.refSystemQuantRegex(_) => + case _ => throw new InvalidConfigParameterException( - "The provided values for voltLvls and gridIds are empty! " + - s"At least one of these optional parameters has to be provided for a valid refSystem! " + - s"Provided refSystem is: $refSystem." + s"Invalid value for sNom from provided refSystem $refSystem. Is a valid unit provided?" ) + } - voltLvls.foreach { voltLvl => - Try(Quantities.getQuantity(voltLvl.vNom)) match { - case Success(quantity) => - if (!quantity.getUnit.isCompatible(Units.VOLT)) - throw new InvalidConfigParameterException( - s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to electrical potential! Please provide the volt level with its unit, e.g. \"20 kV\"" - ) - case Failure(exception) => - throw new InvalidConfigParameterException( - s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to a quantity. Did you provide the volt level with it's unit (e.g. \"20 kV\")?", - exception, - ) - } - } + refSystem.vNom match { + case ConfigConventions.refSystemQuantRegex(_) => + case _ => + throw new InvalidConfigParameterException( + s"Invalid value for vNom from provided refSystem $refSystem. Is a valid unit provided?" + ) + } + } + } - gridIds.foreach { - case gridIdRange @ ConfigConventions.gridIdDotRange(from, to) => - rangeCheck(from.toInt, to.toInt, gridIdRange) - case gridIdRange @ ConfigConventions.gridIdMinusRange(from, to) => - rangeCheck(from.toInt, to.toInt, gridIdRange) - case ConfigConventions.singleGridId(_) => - case gridId => - throw new InvalidConfigParameterException( - s"The provided gridId $gridId is malformed!" - ) - } + /** Sanity checks for a [[SimonaConfig.VoltageLimitsConfig]] + * + * @param voltageLimits + * the [[SimonaConfig.VoltageLimitsConfig]] that should be checked + */ + private def checkVoltageLimits( + voltageLimits: List[VoltageLimitsConfig] + ): Unit = { + voltageLimits.foreach { limit => + checkGridConfig(limit, "voltage limit") - refSystem.sNom match { - case ConfigConventions.refSystemQuantRegex(_) => - case _ => - throw new InvalidConfigParameterException( - s"Invalid value for sNom from provided refSystem $refSystem. Is a valid unit provided?" - ) - } + if (limit.vMin >= limit.vMax) { + throw new InvalidConfigParameterException( + s"Invalid value for vMin and vMax from provided voltage limit $limit. Is vMin smaller than vMax?" + ) + } + } + } + + /** Method to check the common elements of a + * [[SimonaConfig.Simona.GridConfig]]. + * @param gridConfig + * the individual config + * @param configType + * the type of config (e.g. refSystem) + */ + private def checkGridConfig( + gridConfig: GridConfigParams, + configType: String, + ): Unit = { + val voltLvls = gridConfig.voltLvls.getOrElse(List.empty) + val gridIds = gridConfig.gridIds.getOrElse(List.empty) + + if (voltLvls.isEmpty && gridIds.isEmpty) + throw new InvalidConfigParameterException( + "The provided values for voltLvls and gridIds are empty! " + + s"At least one of these optional parameters has to be provided for a valid $configType! " + + s"Provided $configType is: $gridConfig." + ) - refSystem.vNom match { - case ConfigConventions.refSystemQuantRegex(_) => - case _ => + voltLvls.foreach { voltLvl => + Try(Quantities.getQuantity(voltLvl.vNom)) match { + case Success(quantity) => + if (!quantity.getUnit.isCompatible(Units.VOLT)) throw new InvalidConfigParameterException( - s"Invalid value for vNom from provided refSystem $refSystem. Is a valid unit provided?" + s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to electrical potential! Please provide the volt level with its unit, e.g. \"20 kV\"" ) - } - } - - def rangeCheck(from: Int, to: Int, gridIdRange: String): Unit = { - if (from >= to) + case Failure(exception) => throw new InvalidConfigParameterException( - s"Invalid gridId Range $gridIdRange. Start $from cannot be equals or bigger than end $to." + s"The given nominal voltage '${voltLvl.vNom}' cannot be parsed to a quantity. Did you provide the volt level with it's unit (e.g. \"20 kV\")?", + exception, ) } } + + gridIds.foreach { + case gridIdRange @ ConfigConventions.gridIdDotRange(from, to) => + rangeCheck(from.toInt, to.toInt, gridIdRange) + case gridIdRange @ ConfigConventions.gridIdMinusRange(from, to) => + rangeCheck(from.toInt, to.toInt, gridIdRange) + case ConfigConventions.singleGridId(_) => + case gridId => + throw new InvalidConfigParameterException( + s"The provided gridId $gridId is malformed!" + ) + } + + def rangeCheck(from: Int, to: Int, gridIdRange: String): Unit = { + if (from >= to) + throw new InvalidConfigParameterException( + s"Invalid gridId Range $gridIdRange. Start $from cannot be equals or bigger than end $to." + ) + } } private def checkGridDataSource( @@ -815,7 +853,7 @@ object ConfigFailFast extends LazyLogging { * RuntimeConfig of Storages */ private def checkStoragesConfig( - storageRuntimeConfig: SimonaConfig.Simona.Runtime.Participant.Storage + storageRuntimeConfig: ParticipantRuntimeConfigs[StorageRuntimeConfig] ): Unit = { if ( storageRuntimeConfig.defaultConfig.initialSoc < 0.0 || storageRuntimeConfig.defaultConfig.initialSoc > 1.0 diff --git a/src/main/scala/edu/ie3/simona/config/GridConfigParser.scala b/src/main/scala/edu/ie3/simona/config/GridConfigParser.scala new file mode 100644 index 0000000000..bb81bedf96 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/config/GridConfigParser.scala @@ -0,0 +1,221 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.config + +import edu.ie3.datamodel.models.voltagelevels.{ + GermanVoltageLevelUtils, + VoltageLevel, +} +import edu.ie3.simona.config.SimonaConfig.Simona.GridConfig +import edu.ie3.simona.config.SimonaConfig.{ + RefSystemConfig, + VoltLvlConfig, + VoltageLimitsConfig, +} +import edu.ie3.simona.exceptions.InvalidConfigParameterException +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} +import edu.ie3.simona.util.CollectionUtils +import edu.ie3.util.quantities.PowerSystemUnits +import squants.electro.Kilovolts +import squants.energy.{Kilowatts, Megawatts} + +object GridConfigParser { + abstract class ParsedGridConfig[T]( + protected val gridIdMap: Map[Int, T], + protected val voltLvlMap: Map[VoltageLevel, T], + ) { + + /** Returns a [[GridConfig]] based on the provided gridId or the voltLvl as + * fallback if available + * + * @param gridId + * the gridId the refSystem is wanted for + * @param voltLvl + * the voltLvL that is valid for this grid + * @return + * Some(refSystem) if available or None if unavailable + */ + final def find( + gridId: Int, + voltLvl: Option[VoltageLevel] = None, + ): Option[T] = + gridIdMap + .get(gridId) + .orElse(voltLvl.flatMap(voltLvlMap.get)) + } + + final case class ConfigRefSystems( + private val gridIdRefSystems: Map[Int, RefSystem], + private val voltLvLRefSystems: Map[VoltageLevel, RefSystem], + ) extends ParsedGridConfig[RefSystem](gridIdRefSystems, voltLvLRefSystems) + + final case class ConfigVoltageLimits( + private val gridIdVoltageLimits: Map[Int, VoltageLimits], + private val voltLvLVoltageLimits: Map[VoltageLevel, VoltageLimits], + ) extends ParsedGridConfig[VoltageLimits]( + gridIdVoltageLimits, + voltLvLVoltageLimits, + ) + + def parse( + gridConfigs: GridConfig + ): (ConfigRefSystems, ConfigVoltageLimits) = + ( + parseRefSystems(gridConfigs.refSystems), + parseVoltageLimits(gridConfigs.voltageLimits), + ) + + /** Parses the configuration based [[RefSystem]] information based on a list + * of [[RefSystemConfig]] + * + * @param configRefSystems + * the refSystems provided via configuration + * @return + * object that holds two maps with mappings of gridIds and voltLvls to + * RefSystems + */ + def parseRefSystems( + configRefSystems: Option[List[RefSystemConfig]] + ): ConfigRefSystems = { + val defaultRefSystems = ConfigRefSystems( + Map.empty, + Map( + GermanVoltageLevelUtils.LV -> RefSystem(Kilowatts(100), Kilovolts(0.4)), + GermanVoltageLevelUtils.MV_10KV -> RefSystem( + Megawatts(40), + Kilovolts(10), + ), + GermanVoltageLevelUtils.MV_20KV -> RefSystem( + Megawatts(60), + Kilovolts(20), + ), + GermanVoltageLevelUtils.MV_30KV -> RefSystem( + Megawatts(150), + Kilovolts(30), + ), + GermanVoltageLevelUtils.HV -> RefSystem(Megawatts(600), Kilovolts(110)), + GermanVoltageLevelUtils.EHV_220KV -> RefSystem( + Megawatts(800), + Kilovolts(220), + ), + GermanVoltageLevelUtils.EHV_380KV -> RefSystem( + Megawatts(1000), + Kilovolts(380), + ), + ), + ) + + parseWithDefaults[RefSystemConfig, RefSystem, ConfigRefSystems]( + configRefSystems, + refSystem => refSystem.gridIds, + refSystem => refSystem.voltLvls, + refSystem => RefSystem(refSystem.sNom, refSystem.vNom), + (gridIds, voltLvls) => ConfigRefSystems(gridIds, voltLvls), + defaultRefSystems, + )("refSystems") + } + + /** Parses the configuration based [[VoltageLimits]] information based on a + * list of [[VoltageLimitsConfig]] + * + * @param configVoltageLimits + * the refSystems provided via configuration + * @return + * object that holds two maps with mappings of gridIds and voltLvls to + * RefSystems + */ + def parseVoltageLimits( + configVoltageLimits: Option[List[VoltageLimitsConfig]] + ): ConfigVoltageLimits = { + val distributionVoltageLimits = VoltageLimits(0.9, 1.1) + + val defaultVoltageLimits = ConfigVoltageLimits( + Map.empty, + Map( + GermanVoltageLevelUtils.LV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_10KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_20KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_30KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.HV -> distributionVoltageLimits, + GermanVoltageLevelUtils.EHV_220KV -> VoltageLimits(0.9, 1.118), + GermanVoltageLevelUtils.EHV_380KV -> VoltageLimits(0.9, 1.05), + ), + ) + + parseWithDefaults[VoltageLimitsConfig, VoltageLimits, ConfigVoltageLimits]( + configVoltageLimits, + voltageLimit => voltageLimit.gridIds, + voltageLimit => voltageLimit.voltLvls, + voltageLimit => VoltageLimits(voltageLimit.vMin, voltageLimit.vMax), + (gridIds, voltLvls) => ConfigVoltageLimits(gridIds, voltLvls), + defaultVoltageLimits, + )("voltageLimits") + } + + def parseWithDefaults[C, E, T <: ParsedGridConfig[_]]( + configs: Option[List[C]], + gridIds: C => Option[List[String]], + voltLvls: C => Option[List[VoltLvlConfig]], + elementFcn: C => E, + builder: (Map[Int, E], Map[VoltageLevel, E]) => T, + defaults: T, + )(implicit gridConfigType: String): T = configs match { + case Some(configElements) if configElements.nonEmpty => + // units for parsing are not initialized by default + // hence we call them manually + new PowerSystemUnits + + val emptyLists = (List.empty[(Int, E)], List.empty[(VoltageLevel, E)]) + + val (parsedIdList, parsedVoltLvlList) = + configElements.foldLeft(emptyLists) { case ((ids, lvls), config) => + val element = elementFcn(config) + + val parsedGridIds = gridIds(config).getOrElse(List.empty).flatMap { + case ConfigConventions.gridIdDotRange(from, to) => + (from.toInt to to.toInt) + .map(gridId => (gridId, element)) + case ConfigConventions.gridIdMinusRange(from, to) => + (from.toInt to to.toInt) + .map(gridId => (gridId, element)) + case ConfigConventions.singleGridId(singleGridId) => + Seq((singleGridId.toInt, element)) + case unknownGridIdFormat => + throw new InvalidConfigParameterException( + s"Unknown gridId format $unknownGridIdFormat provided for grid config: $config" + ) + } + + val parsedVoltLvls = + voltLvls(config).getOrElse(List.empty).map { voltLvlDef => + (VoltLvlParser.from(voltLvlDef), element) + } + + ( + ids ++ parsedGridIds, + lvls ++ parsedVoltLvls, + ) + } + + if (CollectionUtils.listHasDuplicates(parsedIdList)) { + throw new InvalidConfigParameterException( + s"The provided gridIds in simona.gridConfig.$gridConfigType contain duplicates. " + + "Please check if there are either duplicate entries or overlapping ranges!" + ) + } + + if (CollectionUtils.listHasDuplicates(parsedVoltLvlList)) + throw new InvalidConfigParameterException( + s"The provided voltLvls in simona.gridConfig.$gridConfigType contain duplicates. " + + "Please check your configuration for duplicates in voltLvl entries!" + ) + + builder(parsedIdList.toMap, parsedVoltLvlList.toMap) + case _ => + defaults + } +} diff --git a/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala b/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala deleted file mode 100644 index fbe2f83362..0000000000 --- a/src/main/scala/edu/ie3/simona/config/RefSystemParser.scala +++ /dev/null @@ -1,160 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.config - -import edu.ie3.datamodel.models.voltagelevels.{ - GermanVoltageLevelUtils, - VoltageLevel, -} -import edu.ie3.simona.exceptions.InvalidConfigParameterException -import edu.ie3.simona.model.grid.RefSystem -import edu.ie3.simona.util.CollectionUtils -import edu.ie3.util.quantities.PowerSystemUnits -import squants.electro.{Kilovolts, Volts} -import squants.energy.{Kilowatts, Megawatts} - -/** Parser to parse [[RefSystem]] provided via [[SimonaConfig]] - */ -object RefSystemParser { - final case class ConfigRefSystems( - private val gridIdRefSystems: Map[Int, RefSystem], - private val voltLvLRefSystems: Map[VoltageLevel, RefSystem], - ) { - - /** Returns a [[RefSystem]] based on the provided gridId or the voltLvl as - * fallback if available - * - * @param gridId - * the gridId the refSystem is wanted for - * @param voltLvl - * the voltLvL that is valid for the grid that is wanted - * @return - * Some(refSystem) if available or None if unavailable - */ - def find( - gridId: Int, - voltLvl: Option[VoltageLevel] = None, - ): Option[RefSystem] = - gridIdRefSystems - .get(gridId) - .orElse(voltLvl.flatMap(voltLvLRefSystems.get)) - - } - - /** Parses the configuration based [[RefSystem]] information based on a list - * of [[SimonaConfig.RefSystemConfig]] - * - * @param configRefSystems - * the refSystems provided via configuration - * @return - * object that holds two maps with mappings of gridIds and voltLvls to - * RefSystems - */ - def parse( - configRefSystems: Option[List[SimonaConfig.RefSystemConfig]] - ): ConfigRefSystems = { - val defaultRefSystems = ConfigRefSystems( - Map.empty, - Map( - GermanVoltageLevelUtils.LV -> RefSystem(Kilowatts(100), Kilovolts(0.4)), - GermanVoltageLevelUtils.MV_10KV -> RefSystem( - Megawatts(40), - Kilovolts(10), - ), - GermanVoltageLevelUtils.MV_20KV -> RefSystem( - Megawatts(60), - Kilovolts(20), - ), - GermanVoltageLevelUtils.MV_30KV -> RefSystem( - Megawatts(150), - Kilovolts(30), - ), - GermanVoltageLevelUtils.HV -> RefSystem(Megawatts(600), Kilovolts(110)), - GermanVoltageLevelUtils.EHV_220KV -> RefSystem( - Megawatts(800), - Kilovolts(220), - ), - GermanVoltageLevelUtils.EHV_380KV -> RefSystem( - Megawatts(1000), - Kilovolts(380), - ), - ), - ) - - configRefSystems match { - case Some(refSystems) if refSystems.nonEmpty => - // units for parsing are not initialized by default - // hence we call them manually - new PowerSystemUnits - - val parsedRefSystems = refSystems.flatMap { configRefSystem => - val refSystem = RefSystem(configRefSystem.sNom, configRefSystem.vNom) - - configRefSystem.gridIds.getOrElse(Seq.empty).flatMap { - case ConfigConventions.gridIdDotRange(from, to) => - (from.toInt to to.toInt) - .map(gridId => (gridId, refSystem)) - case ConfigConventions.gridIdMinusRange(from, to) => - (from.toInt to to.toInt) - .map(gridId => (gridId, refSystem)) - case ConfigConventions.singleGridId(singleGridId) => - Seq((singleGridId.toInt, refSystem)) - case unknownGridIdFormat => - throw new InvalidConfigParameterException( - s"Unknown gridId format $unknownGridIdFormat provided for refSystem $configRefSystem" - ) - } ++ configRefSystem.voltLvls.getOrElse(Seq.empty).map { voltLvlDef => - (VoltLvlParser.from(voltLvlDef), refSystem) - } - } - - val gridIdRefSystemsList: List[(Int, RefSystem)] = - parsedRefSystems.flatMap { - case (gridId: Int, refSystems) => - refSystems match { - case refSystem: RefSystem => Some(gridId -> refSystem) - case _ => None - } - case _ => None - } - - val gridIdRefSystems: Map[Int, RefSystem] = - gridIdRefSystemsList.toMap - - if (CollectionUtils.listHasDuplicates(gridIdRefSystemsList)) { - throw new InvalidConfigParameterException( - "The provided gridIds in simona.gridConfig.refSystems contain duplicates. " + - "Please check if there are either duplicate entries or overlapping ranges!" - ) - } - - val voltLvLRefSystemsList: List[(VoltageLevel, RefSystem)] = - parsedRefSystems.flatMap { - case (voltLvl: VoltageLevel, refSystems) => - refSystems match { - case refSystem: RefSystem => Some(voltLvl -> refSystem) - case _ => None - } - case _ => None - } - - if (CollectionUtils.listHasDuplicates(voltLvLRefSystemsList)) - throw new InvalidConfigParameterException( - "The provided voltLvls in simona.gridConfig.refSystems contain duplicates. " + - "Please check your configuration for duplicates in voltLvl entries!" - ) - - val voltLvLRefSys: Map[VoltageLevel, RefSystem] = - parsedRefSystems.collect { case (voltLvl: VoltageLevel, values) => - (voltLvl, values) - }.toMap - - ConfigRefSystems(gridIdRefSystems, voltLvLRefSys) - case _ => defaultRefSystems - } - } -} diff --git a/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala b/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala new file mode 100644 index 0000000000..6e70a48302 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/config/RuntimeConfig.scala @@ -0,0 +1,277 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.config + +import edu.ie3.simona.config.RuntimeConfig._ +import edu.ie3.simona.config.SimonaConfig.{RuntimeKafkaParams, VoltLvlConfig} +import pureconfig.generic.ProductHint +import pureconfig.{CamelCase, ConfigFieldMapping} + +import scala.language.implicitConversions + +/** Runtime configurations for simona. + * @param listener + * runtime listener configuration + * @param participant + * runtime configuration + * @param selectedSubgrids + * option for selected sub grids (default: None) + * @param selectedVoltLvls + * option for selected voltage levels (default: None) + */ +final case class RuntimeConfig( + listener: Listener = Listener(), + participant: Participant = Participant.empty(), + selectedSubgrids: Option[List[Int]] = None, + selectedVoltLvls: Option[List[VoltLvlConfig]] = None, +) + +object RuntimeConfig { + implicit def productHint[T]: ProductHint[T] = + ProductHint[T](ConfigFieldMapping(CamelCase, CamelCase)) + + /** Wraps an [[BaseRuntimeConfig]] with a [[ParticipantRuntimeConfigs]]. + * + * @param config + * to wrap + * @tparam T + * type of config + * @return + * a [[ParticipantRuntimeConfigs]] + */ + implicit def wrap[T <: BaseRuntimeConfig]( + config: T + ): ParticipantRuntimeConfigs[T] = + ParticipantRuntimeConfigs(config) + + final case class Listener( + eventsToProcess: Option[List[String]] = None, + kafka: Option[RuntimeKafkaParams] = None, + ) + + /** Runtime configurations for participants. + * @param em + * runtime configs for energy management systems + * @param evcs + * runtime configs for electrical vehicle charging stations + * @param fixedFeedIn + * runtime configs for fixed feed ins + * @param hp + * runtime configs for heat pumps + * @param load + * runtime configs for loads + * @param pv + * runtime configs for photovoltaic plants + * @param requestVoltageDeviationThreshold + * threshold for the voltage deviation + * @param storage + * runtime configs for electrical storages + * @param wec + * runtime configs for wind energy converters + */ + final case class Participant( + em: ParticipantRuntimeConfigs[EmRuntimeConfig], + evcs: ParticipantRuntimeConfigs[EvcsRuntimeConfig], + fixedFeedIn: ParticipantRuntimeConfigs[FixedFeedInRuntimeConfig], + hp: ParticipantRuntimeConfigs[HpRuntimeConfig], + load: ParticipantRuntimeConfigs[LoadRuntimeConfig], + pv: ParticipantRuntimeConfigs[PvRuntimeConfig], + requestVoltageDeviationThreshold: Double = 1e-14, + storage: ParticipantRuntimeConfigs[StorageRuntimeConfig], + wec: ParticipantRuntimeConfigs[WecRuntimeConfig], + ) + + object Participant { + + /** Returns a [[Participant]] object with default values. + */ + def empty(): Participant = Participant( + em = EmRuntimeConfig(), + evcs = EvcsRuntimeConfig(), + fixedFeedIn = FixedFeedInRuntimeConfig(), + hp = HpRuntimeConfig(), + load = LoadRuntimeConfig(), + pv = PvRuntimeConfig(), + storage = StorageRuntimeConfig(), + wec = WecRuntimeConfig(), + ) + } + + /** Case class contains default and individual configs for simulation runtime. + * @param defaultConfig + * to use + * @param individualConfigs + * specific configs, that are used instead of the [[defaultConfig]] + * @tparam T + * type of runtime config + */ + final case class ParticipantRuntimeConfigs[+T <: BaseRuntimeConfig]( + defaultConfig: T, + individualConfigs: List[T] = List.empty, + ) + + /** Basic trait for all runtime configs. + */ + sealed trait BaseRuntimeConfig { + val calculateMissingReactivePowerWithModel: Boolean + val scaling: Double + val uuids: List[String] + } + + /** Runtime configuration for electric vehicle charging stations. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + * @param chargingStrategy + * the charging strategy to use + * @param lowestEvSoc + * the lowest SOC possible for EV batteries (inverse of max dod) + */ + final case class EvcsRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + chargingStrategy: String = "maxPower", + lowestEvSoc: Double = 0.2, + ) extends BaseRuntimeConfig + + /** Runtime configuration for energy management systems. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + * @param aggregateFlex + * strategy for aggregating flexibilities (default: SELF_OPT_EXCL_REG) + * @param curtailRegenerative + * if regenerative generation can be curtailed (default: false) + */ + final case class EmRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + aggregateFlex: String = "SELF_OPT_EXCL_REG", + curtailRegenerative: Boolean = false, + ) extends BaseRuntimeConfig + + /** Runtime configuration for fixed feed ins. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + */ + final case class FixedFeedInRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + ) extends BaseRuntimeConfig + + /** Runtime configuration for heat pumps. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + */ + final case class HpRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + ) extends BaseRuntimeConfig + + /** Runtime configuration for loads. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + * @param modelBehaviour + * the behaviour of the loads (default: fix) + * @param reference + * defined to which reference a load model behaviour might be scaled + * (default: power) + */ + final case class LoadRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + modelBehaviour: String = "fix", + reference: String = "power", + ) extends BaseRuntimeConfig + + /** Runtime configuration for photovoltaic plants. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + */ + final case class PvRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + ) extends BaseRuntimeConfig + + /** Runtime configuration for electrical storages. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + * @param initialSoc + * the initial state of charge in percent of the storage (default: 0.0) + * @param targetSoc + * option for a targeted state of charge (default: None) + */ + final case class StorageRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + initialSoc: Double = 0d, + targetSoc: Option[Double] = None, + ) extends BaseRuntimeConfig + + /** Runtime configuration for wind energy converters. + * @param calculateMissingReactivePowerWithModel + * if missing reactive power may be filled up with model function (default: + * false) + * @param scaling + * the scaling factor of the power output (default: 1.0) + * @param uuids + * of the models that should use this config, for the default config this + * value is ignored + */ + final case class WecRuntimeConfig( + override val calculateMissingReactivePowerWithModel: Boolean = false, + override val scaling: Double = 1.0, + override val uuids: List[String] = List.empty, + ) extends BaseRuntimeConfig +} diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 0790a6efd9..2fadd4aeb7 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -6,3105 +6,397 @@ package edu.ie3.simona.config -final case class SimonaConfig( - simona: SimonaConfig.Simona -) -object SimonaConfig { - final case class BaseCsvParams( - override val csvSep: java.lang.String, - override val directoryPath: java.lang.String, - override val isHierarchic: scala.Boolean, - ) extends CsvParams(csvSep, directoryPath, isHierarchic) - object BaseCsvParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.BaseCsvParams = { - SimonaConfig.BaseCsvParams( - csvSep = $_reqStr(parentPath, c, "csvSep", $tsCfgValidator), - directoryPath = - $_reqStr(parentPath, c, "directoryPath", $tsCfgValidator), - isHierarchic = $_reqBln(parentPath, c, "isHierarchic", $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 $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - sealed abstract class BaseOutputConfig( - val notifier: java.lang.String, - val simulationResult: scala.Boolean, - ) +import com.typesafe.config.{Config, ConfigRenderOptions} +import edu.ie3.simona.exceptions.CriticalFailureException +import pureconfig._ +import pureconfig.error._ +import pureconfig.generic.ProductHint +import pureconfig.generic.auto._ - sealed abstract class BaseRuntimeConfig( - val calculateMissingReactivePowerWithModel: scala.Boolean, - val scaling: scala.Double, - val uuids: scala.List[java.lang.String], - ) extends java.io.Serializable +import java.time.Duration +import scala.language.implicitConversions +import scala.util.Try - sealed abstract class CsvParams( - val csvSep: java.lang.String, - val directoryPath: java.lang.String, - val isHierarchic: scala.Boolean, - ) +final case class SimonaConfig( + simona: SimonaConfig.Simona +) { + def render(options: ConfigRenderOptions): String = + SimonaConfig.render(this, options) +} - final case class EmRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - aggregateFlex: java.lang.String, - curtailRegenerative: scala.Boolean, - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object EmRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.EmRuntimeConfig = { - SimonaConfig.EmRuntimeConfig( - aggregateFlex = - if (c.hasPathOrNull("aggregateFlex")) c.getString("aggregateFlex") - else "SELF_OPT_EXCL_REG", - curtailRegenerative = - c.hasPathOrNull("curtailRegenerative") && c.getBoolean( - "curtailRegenerative" - ), - 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 - } - } +object SimonaConfig { + // pure config start + implicit def productHint[T]: ProductHint[T] = + ProductHint[T](ConfigFieldMapping(CamelCase, CamelCase)) + + // TODO: replace with finite duration + implicit def durationConvert: ConfigConvert[Duration] = + ConfigConvert.viaStringTry( + str => Try(Duration.parse(("PT" + str).toUpperCase)), + x => x.toString, + ) - 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 - } + /** Method to extract a config from a [[pureconfig.ConfigReader.Result]] + * @param either + * that may contain a config + * @tparam T + * type of config + * @return + * the config, or throws an exception + */ + protected implicit def extract[T]( + either: Either[ConfigReaderFailures, T] + ): T = + either match { + case Left(readerFailures) => + val detailedErrors = readerFailures.toList + .map { + case CannotParse(msg, origin) => + f"CannotParse => $msg, Origin: $origin \n" + case _: CannotRead => + f"CannotRead => Can not read config source} \n" + case ConvertFailure(reason, _, path) => + f"ConvertFailure => Path: $path, Description: ${reason.description} \n" + case ThrowableFailure(throwable, origin) => + f"ThrowableFailure => ${throwable.getMessage}, Origin: $origin \n" + case failure => + f"Unknown failure type => ${failure.toString} \n" + } + .mkString("\n") + throw new CriticalFailureException( + s"Unable to load config due to following failures:\n$detailedErrors" + ) + case Right(conf) => conf } - } + def apply(typeSafeConfig: Config): SimonaConfig = + apply(ConfigSource.fromConfig(typeSafeConfig)) - final case class EvcsRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - chargingStrategy: java.lang.String, - lowestEvSoc: scala.Double, - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object EvcsRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.EvcsRuntimeConfig = { - SimonaConfig.EvcsRuntimeConfig( - chargingStrategy = - if (c.hasPathOrNull("chargingStrategy")) - c.getString("chargingStrategy") - else "maxPower", - lowestEvSoc = - if (c.hasPathOrNull("lowestEvSoc")) c.getDouble("lowestEvSoc") - else 0.2, - 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 - } - } + def apply(confSrc: ConfigObjectSource): SimonaConfig = + confSrc.load[SimonaConfig] - 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 - } - } + def render( + simonaConfig: SimonaConfig, + options: ConfigRenderOptions, + ): String = ConfigWriter[SimonaConfig].to(simonaConfig).render(options) - } - - final case class FixedFeedInRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object FixedFeedInRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.FixedFeedInRuntimeConfig = { - SimonaConfig.FixedFeedInRuntimeConfig( - 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 - } - } + // pure config end - 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 BaseCsvParams( + override val csvSep: String, + override val directoryPath: String, + override val isHierarchic: Boolean, + ) extends CsvParams(csvSep, directoryPath, isHierarchic) - } + sealed abstract class BaseOutputConfig( + val notifier: String, + val simulationResult: Boolean, + ) + sealed abstract class CsvParams( + val csvSep: String, + val directoryPath: String, + val isHierarchic: Boolean, + ) final case class GridOutputConfig( - lines: scala.Boolean, - nodes: scala.Boolean, - notifier: java.lang.String, - switches: scala.Boolean, - transformers2w: scala.Boolean, - transformers3w: scala.Boolean, + lines: Boolean = false, + nodes: Boolean = false, + notifier: String, + switches: Boolean = false, + transformers2w: Boolean = false, + transformers3w: Boolean = false, ) - object GridOutputConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.GridOutputConfig = { - SimonaConfig.GridOutputConfig( - lines = c.hasPathOrNull("lines") && c.getBoolean("lines"), - nodes = c.hasPathOrNull("nodes") && c.getBoolean("nodes"), - notifier = $_reqStr(parentPath, c, "notifier", $tsCfgValidator), - switches = c.hasPathOrNull("switches") && c.getBoolean("switches"), - transformers2w = - c.hasPathOrNull("transformers2w") && c.getBoolean("transformers2w"), - transformers3w = - c.hasPathOrNull("transformers3w") && c.getBoolean("transformers3w"), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - final case class HpRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object HpRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.HpRuntimeConfig = { - SimonaConfig.HpRuntimeConfig( - 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 - } - } - - } sealed abstract class KafkaParams( - val bootstrapServers: java.lang.String, - val linger: scala.Int, - val runId: java.lang.String, - val schemaRegistryUrl: java.lang.String, + val bootstrapServers: String, + val linger: Int, + val runId: String, + val schemaRegistryUrl: String, ) - final case class LoadRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - modelBehaviour: java.lang.String, - reference: java.lang.String, - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object LoadRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.LoadRuntimeConfig = { - SimonaConfig.LoadRuntimeConfig( - modelBehaviour = - $_reqStr(parentPath, c, "modelBehaviour", $tsCfgValidator), - reference = $_reqStr(parentPath, c, "reference", $tsCfgValidator), - 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 - } - } - - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - final case class ParticipantBaseOutputConfig( - override val notifier: java.lang.String, - override val simulationResult: scala.Boolean, - flexResult: scala.Boolean, - powerRequestReply: scala.Boolean, + override val notifier: String, + override val simulationResult: Boolean, + flexResult: Boolean = false, + powerRequestReply: Boolean, ) extends BaseOutputConfig(notifier, simulationResult) - object ParticipantBaseOutputConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.ParticipantBaseOutputConfig = { - SimonaConfig.ParticipantBaseOutputConfig( - flexResult = - c.hasPathOrNull("flexResult") && c.getBoolean("flexResult"), - powerRequestReply = - $_reqBln(parentPath, c, "powerRequestReply", $tsCfgValidator), - notifier = $_reqStr(parentPath, c, "notifier", $tsCfgValidator), - simulationResult = - $_reqBln(parentPath, c, "simulationResult", $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 $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class PrimaryDataCsvParams( - override val csvSep: java.lang.String, - override val directoryPath: java.lang.String, - override val isHierarchic: scala.Boolean, - timePattern: java.lang.String, + override val csvSep: String, + override val directoryPath: String, + override val isHierarchic: Boolean, + timePattern: String = "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", ) extends CsvParams(csvSep, directoryPath, isHierarchic) - object PrimaryDataCsvParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.PrimaryDataCsvParams = { - SimonaConfig.PrimaryDataCsvParams( - timePattern = - if (c.hasPathOrNull("timePattern")) c.getString("timePattern") - else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", - csvSep = $_reqStr(parentPath, c, "csvSep", $tsCfgValidator), - directoryPath = - $_reqStr(parentPath, c, "directoryPath", $tsCfgValidator), - isHierarchic = $_reqBln(parentPath, c, "isHierarchic", $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 $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - final case class PvRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object PvRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.PvRuntimeConfig = { - SimonaConfig.PvRuntimeConfig( - 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 - } - } + sealed trait GridConfigParams { + val gridIds: Option[List[String]] + val voltLvls: Option[List[VoltLvlConfig]] } final case class RefSystemConfig( - gridIds: scala.Option[scala.List[java.lang.String]], - sNom: java.lang.String, - vNom: java.lang.String, - voltLvls: scala.Option[scala.List[SimonaConfig.VoltLvlConfig]], - ) - object RefSystemConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.RefSystemConfig = { - SimonaConfig.RefSystemConfig( - gridIds = - if (c.hasPathOrNull("gridIds")) - scala.Some( - $_L$_str(c.getList("gridIds"), parentPath, $tsCfgValidator) - ) - else None, - sNom = $_reqStr(parentPath, c, "sNom", $tsCfgValidator), - vNom = $_reqStr(parentPath, c, "vNom", $tsCfgValidator), - voltLvls = - if (c.hasPathOrNull("voltLvls")) - scala.Some( - $_LSimonaConfig_VoltLvlConfig( - c.getList("voltLvls"), - parentPath, - $tsCfgValidator, - ) - ) - else None, - ) - } - private def $_LSimonaConfig_VoltLvlConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.VoltLvlConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.VoltLvlConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } + override val gridIds: Option[List[String]] = None, + sNom: String, + vNom: String, + override val voltLvls: Option[List[VoltLvlConfig]] = None, + ) extends GridConfigParams final case class ResultKafkaParams( - override val bootstrapServers: java.lang.String, - override val linger: scala.Int, - override val runId: java.lang.String, - override val schemaRegistryUrl: java.lang.String, - topicNodeRes: java.lang.String, + override val bootstrapServers: String, + override val linger: Int, + override val runId: String, + override val schemaRegistryUrl: String, + topicNodeRes: String, ) extends KafkaParams(bootstrapServers, linger, runId, schemaRegistryUrl) - object ResultKafkaParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.ResultKafkaParams = { - SimonaConfig.ResultKafkaParams( - topicNodeRes = $_reqStr(parentPath, c, "topicNodeRes", $tsCfgValidator), - bootstrapServers = - $_reqStr(parentPath, c, "bootstrapServers", $tsCfgValidator), - linger = $_reqInt(parentPath, c, "linger", $tsCfgValidator), - runId = $_reqStr(parentPath, c, "runId", $tsCfgValidator), - schemaRegistryUrl = - $_reqStr(parentPath, c, "schemaRegistryUrl", $tsCfgValidator), - ) - } - private def $_reqInt( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.Int = { - if (c == null) 0 - else - try c.getInt(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - 0 - } - } - - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class RuntimeKafkaParams( - override val bootstrapServers: java.lang.String, - override val linger: scala.Int, - override val runId: java.lang.String, - override val schemaRegistryUrl: java.lang.String, - topic: java.lang.String, + override val bootstrapServers: String, + override val linger: Int, + override val runId: String, + override val schemaRegistryUrl: String, + topic: String, ) extends KafkaParams(bootstrapServers, linger, runId, schemaRegistryUrl) - object RuntimeKafkaParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.RuntimeKafkaParams = { - SimonaConfig.RuntimeKafkaParams( - topic = $_reqStr(parentPath, c, "topic", $tsCfgValidator), - bootstrapServers = - $_reqStr(parentPath, c, "bootstrapServers", $tsCfgValidator), - linger = $_reqInt(parentPath, c, "linger", $tsCfgValidator), - runId = $_reqStr(parentPath, c, "runId", $tsCfgValidator), - schemaRegistryUrl = - $_reqStr(parentPath, c, "schemaRegistryUrl", $tsCfgValidator), - ) - } - private def $_reqInt( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.Int = { - if (c == null) 0 - else - try c.getInt(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - 0 - } - } - - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class SimpleOutputConfig( - override val notifier: java.lang.String, - override val simulationResult: scala.Boolean, + override val notifier: String, + override val simulationResult: Boolean, ) extends BaseOutputConfig(notifier, simulationResult) - object SimpleOutputConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.SimpleOutputConfig = { - SimonaConfig.SimpleOutputConfig( - notifier = $_reqStr(parentPath, c, "notifier", $tsCfgValidator), - simulationResult = - $_reqBln(parentPath, c, "simulationResult", $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 $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - 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], - vMax: scala.Double, - vMin: scala.Double, + measurements: List[String] = List.empty, + transformers: List[String] = List.empty, + vMax: Double, + vMin: Double, ) - object TransformerControlGroup { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.TransformerControlGroup = { - SimonaConfig.TransformerControlGroup( - measurements = - $_L$_str(c.getList("measurements"), parentPath, $tsCfgValidator), - transformers = - $_L$_str(c.getList("transformers"), parentPath, $tsCfgValidator), - vMax = $_reqDbl(parentPath, c, "vMax", $tsCfgValidator), - vMin = $_reqDbl(parentPath, c, "vMin", $tsCfgValidator), - ) - } - 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 VoltLvlConfig( - id: java.lang.String, - vNom: java.lang.String, + id: String, + vNom: String, ) - object VoltLvlConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.VoltLvlConfig = { - SimonaConfig.VoltLvlConfig( - id = $_reqStr(parentPath, c, "id", $tsCfgValidator), - vNom = $_reqStr(parentPath, c, "vNom", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - final case class WecRuntimeConfig( - override val calculateMissingReactivePowerWithModel: scala.Boolean, - override val scaling: scala.Double, - override val uuids: scala.List[java.lang.String], - ) extends BaseRuntimeConfig( - calculateMissingReactivePowerWithModel, - scaling, - uuids, - ) - object WecRuntimeConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.WecRuntimeConfig = { - SimonaConfig.WecRuntimeConfig( - 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 VoltageLimitsConfig( + override val gridIds: Option[List[String]] = None, + vMax: Double, + vMin: Double, + override val voltLvls: Option[List[VoltLvlConfig]] = None, + ) extends GridConfigParams final case class Simona( - control: scala.Option[SimonaConfig.Simona.Control], - event: SimonaConfig.Simona.Event, - gridConfig: SimonaConfig.Simona.GridConfig, - input: SimonaConfig.Simona.Input, - output: SimonaConfig.Simona.Output, - powerflow: SimonaConfig.Simona.Powerflow, - runtime: SimonaConfig.Simona.Runtime, - simulationName: java.lang.String, - time: SimonaConfig.Simona.Time, + control: Option[Simona.Control] = None, + event: Simona.Event = Simona.Event(), + gridConfig: Simona.GridConfig = Simona.GridConfig(), + input: Simona.Input, + output: Simona.Output, + powerflow: Simona.Powerflow, + runtime: RuntimeConfig, + simulationName: String, + time: Simona.Time = Simona.Time(), ) object Simona { final case class Control( - transformer: scala.List[SimonaConfig.TransformerControlGroup] + transformer: List[TransformerControlGroup] = List.empty ) - object Control { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Control = { - SimonaConfig.Simona.Control( - transformer = $_LSimonaConfig_TransformerControlGroup( - c.getList("transformer"), - parentPath, - $tsCfgValidator, - ) - ) - } - private def $_LSimonaConfig_TransformerControlGroup( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.TransformerControlGroup] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.TransformerControlGroup( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } final case class Event( - listener: scala.Option[ - scala.List[SimonaConfig.Simona.Event.Listener$Elm] - ] + listener: Option[List[Event.Listener$Elm]] = None ) object Event { final case class Listener$Elm( - eventsToProcess: scala.Option[scala.List[java.lang.String]], - fullClassPath: java.lang.String, + eventsToProcess: Option[List[String]] = None, + fullClassPath: String, ) - object Listener$Elm { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Event.Listener$Elm = { - SimonaConfig.Simona.Event.Listener$Elm( - eventsToProcess = - if (c.hasPathOrNull("eventsToProcess")) - scala.Some( - $_L$_str( - c.getList("eventsToProcess"), - parentPath, - $tsCfgValidator, - ) - ) - else None, - fullClassPath = - $_reqStr(parentPath, c, "fullClassPath", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Event = { - SimonaConfig.Simona.Event( - listener = - if (c.hasPathOrNull("listener")) - scala.Some( - $_LSimonaConfig_Simona_Event_Listener$Elm( - c.getList("listener"), - parentPath, - $tsCfgValidator, - ) - ) - else None - ) - } - private def $_LSimonaConfig_Simona_Event_Listener$Elm( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.Simona.Event.Listener$Elm] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.Simona.Event.Listener$Elm( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } } final case class GridConfig( - refSystems: scala.Option[scala.List[SimonaConfig.RefSystemConfig]] + refSystems: Option[List[RefSystemConfig]] = None, + voltageLimits: Option[List[VoltageLimitsConfig]] = None, ) - object GridConfig { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.GridConfig = { - SimonaConfig.Simona.GridConfig( - refSystems = - if (c.hasPathOrNull("refSystems")) - scala.Some( - $_LSimonaConfig_RefSystemConfig( - c.getList("refSystems"), - parentPath, - $tsCfgValidator, - ) - ) - else None - ) - } - private def $_LSimonaConfig_RefSystemConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.RefSystemConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.RefSystemConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } final case class Input( - grid: SimonaConfig.Simona.Input.Grid, - primary: SimonaConfig.Simona.Input.Primary, - weather: SimonaConfig.Simona.Input.Weather, + grid: Input.Grid, + primary: Input.Primary = Input.Primary(), + weather: Input.Weather = Input.Weather(), ) object Input { final case class Grid( - datasource: SimonaConfig.Simona.Input.Grid.Datasource + datasource: Grid.Datasource ) object Grid { final case class Datasource( - csvParams: scala.Option[SimonaConfig.BaseCsvParams], - id: java.lang.String, + csvParams: Option[BaseCsvParams] = None, + id: String, ) - object Datasource { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Grid.Datasource = { - SimonaConfig.Simona.Input.Grid.Datasource( - csvParams = - if (c.hasPathOrNull("csvParams")) - scala.Some( - SimonaConfig.BaseCsvParams( - c.getConfig("csvParams"), - parentPath + "csvParams.", - $tsCfgValidator, - ) - ) - else None, - id = $_reqStr(parentPath, c, "id", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Grid = { - SimonaConfig.Simona.Input.Grid( - datasource = SimonaConfig.Simona.Input.Grid.Datasource( - if (c.hasPathOrNull("datasource")) c.getConfig("datasource") - else - com.typesafe.config.ConfigFactory.parseString("datasource{}"), - parentPath + "datasource.", - $tsCfgValidator, - ) - ) - } } final case class Primary( - couchbaseParams: scala.Option[ - SimonaConfig.Simona.Input.Primary.CouchbaseParams - ], - csvParams: scala.Option[SimonaConfig.PrimaryDataCsvParams], - influxDb1xParams: scala.Option[ - SimonaConfig.Simona.Input.Primary.InfluxDb1xParams - ], - sqlParams: scala.Option[SimonaConfig.Simona.Input.Primary.SqlParams], + couchbaseParams: scala.Option[Primary.CouchbaseParams] = None, + csvParams: Option[PrimaryDataCsvParams] = None, + influxDb1xParams: Option[Primary.InfluxDb1xParams] = None, + sqlParams: Option[Primary.SqlParams] = None, ) object Primary { final case class CouchbaseParams( - bucketName: java.lang.String, - coordinateColumnName: java.lang.String, - keyPrefix: java.lang.String, - password: java.lang.String, - timePattern: java.lang.String, - url: java.lang.String, - userName: java.lang.String, + bucketName: String, + coordinateColumnName: String, + keyPrefix: String, + password: String, + timePattern: String = "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", + url: String, + userName: String, ) - object CouchbaseParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Primary.CouchbaseParams = { - SimonaConfig.Simona.Input.Primary.CouchbaseParams( - bucketName = - $_reqStr(parentPath, c, "bucketName", $tsCfgValidator), - coordinateColumnName = $_reqStr( - parentPath, - c, - "coordinateColumnName", - $tsCfgValidator, - ), - keyPrefix = $_reqStr(parentPath, c, "keyPrefix", $tsCfgValidator), - password = $_reqStr(parentPath, c, "password", $tsCfgValidator), - timePattern = - if (c.hasPathOrNull("timePattern")) c.getString("timePattern") - else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", - url = $_reqStr(parentPath, c, "url", $tsCfgValidator), - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class InfluxDb1xParams( - database: java.lang.String, - port: scala.Int, - timePattern: java.lang.String, - url: java.lang.String, + database: String, + port: Int, + timePattern: String = "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", + url: String, ) - object InfluxDb1xParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Primary.InfluxDb1xParams = { - SimonaConfig.Simona.Input.Primary.InfluxDb1xParams( - database = $_reqStr(parentPath, c, "database", $tsCfgValidator), - port = $_reqInt(parentPath, c, "port", $tsCfgValidator), - timePattern = - if (c.hasPathOrNull("timePattern")) c.getString("timePattern") - else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", - url = $_reqStr(parentPath, c, "url", $tsCfgValidator), - ) - } - private def $_reqInt( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.Int = { - if (c == null) 0 - else - try c.getInt(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - 0 - } - } - - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class SqlParams( - jdbcUrl: java.lang.String, - password: java.lang.String, - schemaName: java.lang.String, - timePattern: java.lang.String, - userName: java.lang.String, + jdbcUrl: String, + password: String, + schemaName: String = "public", + timePattern: String = "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", + userName: String, ) - object SqlParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Primary.SqlParams = { - SimonaConfig.Simona.Input.Primary.SqlParams( - jdbcUrl = $_reqStr(parentPath, c, "jdbcUrl", $tsCfgValidator), - password = $_reqStr(parentPath, c, "password", $tsCfgValidator), - schemaName = - if (c.hasPathOrNull("schemaName")) c.getString("schemaName") - else "public", - timePattern = - if (c.hasPathOrNull("timePattern")) c.getString("timePattern") - else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]X", - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Primary = { - SimonaConfig.Simona.Input.Primary( - couchbaseParams = - if (c.hasPathOrNull("couchbaseParams")) - scala.Some( - SimonaConfig.Simona.Input.Primary.CouchbaseParams( - c.getConfig("couchbaseParams"), - parentPath + "couchbaseParams.", - $tsCfgValidator, - ) - ) - else None, - csvParams = - if (c.hasPathOrNull("csvParams")) - scala.Some( - SimonaConfig.PrimaryDataCsvParams( - c.getConfig("csvParams"), - parentPath + "csvParams.", - $tsCfgValidator, - ) - ) - else None, - influxDb1xParams = - if (c.hasPathOrNull("influxDb1xParams")) - scala.Some( - SimonaConfig.Simona.Input.Primary.InfluxDb1xParams( - c.getConfig("influxDb1xParams"), - parentPath + "influxDb1xParams.", - $tsCfgValidator, - ) - ) - else None, - sqlParams = - if (c.hasPathOrNull("sqlParams")) - scala.Some( - SimonaConfig.Simona.Input.Primary.SqlParams( - c.getConfig("sqlParams"), - parentPath + "sqlParams.", - $tsCfgValidator, - ) - ) - else None, - ) - } } final case class Weather( - datasource: SimonaConfig.Simona.Input.Weather.Datasource + datasource: Weather.Datasource = Weather.Datasource() ) object Weather { final case class Datasource( - coordinateSource: SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource, - couchbaseParams: scala.Option[ - SimonaConfig.Simona.Input.Weather.Datasource.CouchbaseParams - ], - csvParams: scala.Option[SimonaConfig.BaseCsvParams], - influxDb1xParams: scala.Option[ - SimonaConfig.Simona.Input.Weather.Datasource.InfluxDb1xParams - ], - maxCoordinateDistance: scala.Double, - resolution: scala.Option[scala.Long], - sampleParams: scala.Option[ - SimonaConfig.Simona.Input.Weather.Datasource.SampleParams - ], - scheme: java.lang.String, - sqlParams: scala.Option[ - SimonaConfig.Simona.Input.Weather.Datasource.SqlParams - ], - timestampPattern: scala.Option[java.lang.String], + coordinateSource: Datasource.CoordinateSource = + Datasource.CoordinateSource(), + couchbaseParams: Option[Datasource.CouchbaseParams] = None, + csvParams: Option[BaseCsvParams] = None, + influxDb1xParams: Option[Datasource.InfluxDb1xParams] = None, + maxCoordinateDistance: Double = 50000, + resolution: Option[Long] = None, + sampleParams: Option[Datasource.SampleParams] = None, + scheme: String = "icon", + sqlParams: Option[Datasource.SqlParams] = None, + timestampPattern: Option[String] = None, ) object Datasource { final case class CoordinateSource( - csvParams: scala.Option[SimonaConfig.BaseCsvParams], - gridModel: java.lang.String, - sampleParams: scala.Option[ - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams - ], - sqlParams: scala.Option[ - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SqlParams - ], + csvParams: Option[BaseCsvParams] = None, + gridModel: String = "icon", + sampleParams: Option[CoordinateSource.SampleParams] = None, + sqlParams: Option[CoordinateSource.SqlParams] = None, ) object CoordinateSource { final case class SampleParams( - use: scala.Boolean + use: Boolean = true ) - object SampleParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SampleParams = { - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource - .SampleParams( - use = !c.hasPathOrNull("use") || c.getBoolean("use") - ) - } - } final case class SqlParams( - jdbcUrl: java.lang.String, - password: java.lang.String, - schemaName: java.lang.String, - tableName: java.lang.String, - userName: java.lang.String, + jdbcUrl: String, + password: String, + schemaName: String = "public", + tableName: String, + userName: String, ) - object SqlParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource.SqlParams = { - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource - .SqlParams( - jdbcUrl = - $_reqStr(parentPath, c, "jdbcUrl", $tsCfgValidator), - password = - $_reqStr(parentPath, c, "password", $tsCfgValidator), - schemaName = - if (c.hasPathOrNull("schemaName")) - c.getString("schemaName") - else "public", - tableName = - $_reqStr(parentPath, c, "tableName", $tsCfgValidator), - userName = - $_reqStr(parentPath, c, "userName", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource = { - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource( - csvParams = - if (c.hasPathOrNull("csvParams")) - scala.Some( - SimonaConfig.BaseCsvParams( - c.getConfig("csvParams"), - parentPath + "csvParams.", - $tsCfgValidator, - ) - ) - else None, - gridModel = - if (c.hasPathOrNull("gridModel")) c.getString("gridModel") - else "icon", - sampleParams = - if (c.hasPathOrNull("sampleParams")) - scala.Some( - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource - .SampleParams( - c.getConfig("sampleParams"), - parentPath + "sampleParams.", - $tsCfgValidator, - ) - ) - else None, - sqlParams = - if (c.hasPathOrNull("sqlParams")) - scala.Some( - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource - .SqlParams( - c.getConfig("sqlParams"), - parentPath + "sqlParams.", - $tsCfgValidator, - ) - ) - else None, - ) - } } final case class CouchbaseParams( - bucketName: java.lang.String, - coordinateColumnName: java.lang.String, - keyPrefix: java.lang.String, - password: java.lang.String, - url: java.lang.String, - userName: java.lang.String, + bucketName: String, + coordinateColumnName: String, + keyPrefix: String, + password: String, + url: String, + userName: String, ) - object CouchbaseParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.CouchbaseParams = { - SimonaConfig.Simona.Input.Weather.Datasource.CouchbaseParams( - bucketName = - $_reqStr(parentPath, c, "bucketName", $tsCfgValidator), - coordinateColumnName = $_reqStr( - parentPath, - c, - "coordinateColumnName", - $tsCfgValidator, - ), - keyPrefix = - $_reqStr(parentPath, c, "keyPrefix", $tsCfgValidator), - password = $_reqStr(parentPath, c, "password", $tsCfgValidator), - url = $_reqStr(parentPath, c, "url", $tsCfgValidator), - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class InfluxDb1xParams( - database: java.lang.String, - port: scala.Int, - url: java.lang.String, + database: String, + port: Int, + url: String, ) - object InfluxDb1xParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.InfluxDb1xParams = { - SimonaConfig.Simona.Input.Weather.Datasource.InfluxDb1xParams( - database = $_reqStr(parentPath, c, "database", $tsCfgValidator), - port = $_reqInt(parentPath, c, "port", $tsCfgValidator), - url = $_reqStr(parentPath, c, "url", $tsCfgValidator), - ) - } - private def $_reqInt( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.Int = { - if (c == null) 0 - else - try c.getInt(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - 0 - } - } - - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class SampleParams( - use: scala.Boolean + use: Boolean = true ) - object SampleParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.SampleParams = { - SimonaConfig.Simona.Input.Weather.Datasource.SampleParams( - use = !c.hasPathOrNull("use") || c.getBoolean("use") - ) - } - } final case class SqlParams( - jdbcUrl: java.lang.String, - password: java.lang.String, - schemaName: java.lang.String, - tableName: java.lang.String, - userName: java.lang.String, - ) - object SqlParams { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource.SqlParams = { - SimonaConfig.Simona.Input.Weather.Datasource.SqlParams( - jdbcUrl = $_reqStr(parentPath, c, "jdbcUrl", $tsCfgValidator), - password = $_reqStr(parentPath, c, "password", $tsCfgValidator), - schemaName = - if (c.hasPathOrNull("schemaName")) c.getString("schemaName") - else "public", - tableName = - $_reqStr(parentPath, c, "tableName", $tsCfgValidator), - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather.Datasource = { - SimonaConfig.Simona.Input.Weather.Datasource( - coordinateSource = - SimonaConfig.Simona.Input.Weather.Datasource.CoordinateSource( - if (c.hasPathOrNull("coordinateSource")) - c.getConfig("coordinateSource") - else - com.typesafe.config.ConfigFactory - .parseString("coordinateSource{}"), - parentPath + "coordinateSource.", - $tsCfgValidator, - ), - couchbaseParams = - if (c.hasPathOrNull("couchbaseParams")) - scala.Some( - SimonaConfig.Simona.Input.Weather.Datasource - .CouchbaseParams( - c.getConfig("couchbaseParams"), - parentPath + "couchbaseParams.", - $tsCfgValidator, - ) - ) - else None, - csvParams = - if (c.hasPathOrNull("csvParams")) - scala.Some( - SimonaConfig.BaseCsvParams( - c.getConfig("csvParams"), - parentPath + "csvParams.", - $tsCfgValidator, - ) - ) - else None, - influxDb1xParams = - if (c.hasPathOrNull("influxDb1xParams")) - scala.Some( - SimonaConfig.Simona.Input.Weather.Datasource - .InfluxDb1xParams( - c.getConfig("influxDb1xParams"), - parentPath + "influxDb1xParams.", - $tsCfgValidator, - ) - ) - else None, - maxCoordinateDistance = - if (c.hasPathOrNull("maxCoordinateDistance")) - c.getDouble("maxCoordinateDistance") - else 50000, - resolution = - if (c.hasPathOrNull("resolution")) - Some(c.getLong("resolution").longValue()) - else None, - sampleParams = - if (c.hasPathOrNull("sampleParams")) - scala.Some( - SimonaConfig.Simona.Input.Weather.Datasource.SampleParams( - c.getConfig("sampleParams"), - parentPath + "sampleParams.", - $tsCfgValidator, - ) - ) - else None, - scheme = - if (c.hasPathOrNull("scheme")) c.getString("scheme") - else "icon", - sqlParams = - if (c.hasPathOrNull("sqlParams")) - scala.Some( - SimonaConfig.Simona.Input.Weather.Datasource.SqlParams( - c.getConfig("sqlParams"), - parentPath + "sqlParams.", - $tsCfgValidator, - ) - ) - else None, - timestampPattern = - if (c.hasPathOrNull("timestampPattern")) - Some(c.getString("timestampPattern")) - else None, - ) - } - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input.Weather = { - SimonaConfig.Simona.Input.Weather( - datasource = SimonaConfig.Simona.Input.Weather.Datasource( - if (c.hasPathOrNull("datasource")) c.getConfig("datasource") - else - com.typesafe.config.ConfigFactory.parseString("datasource{}"), - parentPath + "datasource.", - $tsCfgValidator, - ) + jdbcUrl: String, + password: String, + schemaName: String = "public", + tableName: String, + userName: String, ) } } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Input = { - SimonaConfig.Simona.Input( - grid = SimonaConfig.Simona.Input.Grid( - if (c.hasPathOrNull("grid")) c.getConfig("grid") - else com.typesafe.config.ConfigFactory.parseString("grid{}"), - parentPath + "grid.", - $tsCfgValidator, - ), - primary = SimonaConfig.Simona.Input.Primary( - if (c.hasPathOrNull("primary")) c.getConfig("primary") - else com.typesafe.config.ConfigFactory.parseString("primary{}"), - parentPath + "primary.", - $tsCfgValidator, - ), - weather = SimonaConfig.Simona.Input.Weather( - if (c.hasPathOrNull("weather")) c.getConfig("weather") - else com.typesafe.config.ConfigFactory.parseString("weather{}"), - parentPath + "weather.", - $tsCfgValidator, - ), - ) - } } final case class Output( - base: SimonaConfig.Simona.Output.Base, - flex: scala.Boolean, - grid: SimonaConfig.GridOutputConfig, - log: SimonaConfig.Simona.Output.Log, - participant: SimonaConfig.Simona.Output.Participant, - sink: SimonaConfig.Simona.Output.Sink, - thermal: SimonaConfig.Simona.Output.Thermal, + base: Output.Base, + flex: Boolean = false, + grid: GridOutputConfig, + log: Output.Log = Output.Log(), + participant: Output.Participant, + sink: Output.Sink = Output.Sink(), + thermal: Output.Thermal, ) object Output { final case class Base( - addTimestampToOutputDir: scala.Boolean, - dir: java.lang.String, + addTimestampToOutputDir: Boolean = true, + dir: String, ) - object Base { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Base = { - SimonaConfig.Simona.Output.Base( - addTimestampToOutputDir = !c.hasPathOrNull( - "addTimestampToOutputDir" - ) || c.getBoolean("addTimestampToOutputDir"), - dir = $_reqStr(parentPath, c, "dir", $tsCfgValidator), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } final case class Log( - level: java.lang.String + level: String = "INFO" ) - object Log { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Log = { - SimonaConfig.Simona.Output.Log( - level = - if (c.hasPathOrNull("level")) c.getString("level") else "INFO" - ) - } - } final case class Participant( - defaultConfig: SimonaConfig.ParticipantBaseOutputConfig, - individualConfigs: scala.List[ - SimonaConfig.ParticipantBaseOutputConfig - ], + defaultConfig: ParticipantBaseOutputConfig, + individualConfigs: List[ParticipantBaseOutputConfig] = List.empty, ) - object Participant { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Participant = { - SimonaConfig.Simona.Output.Participant( - defaultConfig = SimonaConfig.ParticipantBaseOutputConfig( - if (c.hasPathOrNull("defaultConfig")) c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_ParticipantBaseOutputConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_ParticipantBaseOutputConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.ParticipantBaseOutputConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.ParticipantBaseOutputConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } final case class Sink( - csv: scala.Option[SimonaConfig.Simona.Output.Sink.Csv], - influxDb1x: scala.Option[SimonaConfig.Simona.Output.Sink.InfluxDb1x], - kafka: scala.Option[SimonaConfig.ResultKafkaParams], + csv: Option[Sink.Csv] = None, + influxDb1x: Option[Sink.InfluxDb1x] = None, + kafka: Option[ResultKafkaParams] = None, ) object Sink { final case class Csv( - compressOutputs: scala.Boolean, - fileFormat: java.lang.String, - filePrefix: java.lang.String, - fileSuffix: java.lang.String, - isHierarchic: scala.Boolean, + compressOutputs: Boolean = false, + fileFormat: String = ".csv", + filePrefix: String = "", + fileSuffix: String = "", + isHierarchic: Boolean = false, ) - object Csv { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Sink.Csv = { - SimonaConfig.Simona.Output.Sink.Csv( - compressOutputs = - c.hasPathOrNull("compressOutputs") && c.getBoolean( - "compressOutputs" - ), - fileFormat = - if (c.hasPathOrNull("fileFormat")) c.getString("fileFormat") - else ".csv", - filePrefix = - if (c.hasPathOrNull("filePrefix")) c.getString("filePrefix") - else "", - fileSuffix = - if (c.hasPathOrNull("fileSuffix")) c.getString("fileSuffix") - else "", - isHierarchic = - c.hasPathOrNull("isHierarchic") && c.getBoolean("isHierarchic"), - ) - } - } final case class InfluxDb1x( - database: java.lang.String, - port: scala.Int, - url: java.lang.String, + database: String, + port: Int, + url: String, ) - object InfluxDb1x { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Sink.InfluxDb1x = { - SimonaConfig.Simona.Output.Sink.InfluxDb1x( - database = $_reqStr(parentPath, c, "database", $tsCfgValidator), - port = $_reqInt(parentPath, c, "port", $tsCfgValidator), - url = $_reqStr(parentPath, c, "url", $tsCfgValidator), - ) - } - private def $_reqInt( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.Int = { - if (c == null) 0 - else - try c.getInt(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - 0 - } - } - - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Sink = { - SimonaConfig.Simona.Output.Sink( - csv = - if (c.hasPathOrNull("csv")) - scala.Some( - SimonaConfig.Simona.Output.Sink.Csv( - c.getConfig("csv"), - parentPath + "csv.", - $tsCfgValidator, - ) - ) - else None, - influxDb1x = - if (c.hasPathOrNull("influxDb1x")) - scala.Some( - SimonaConfig.Simona.Output.Sink.InfluxDb1x( - c.getConfig("influxDb1x"), - parentPath + "influxDb1x.", - $tsCfgValidator, - ) - ) - else None, - kafka = - if (c.hasPathOrNull("kafka")) - scala.Some( - SimonaConfig.ResultKafkaParams( - c.getConfig("kafka"), - parentPath + "kafka.", - $tsCfgValidator, - ) - ) - else None, - ) - } } final case class Thermal( - defaultConfig: SimonaConfig.SimpleOutputConfig, - individualConfigs: scala.List[SimonaConfig.SimpleOutputConfig], + defaultConfig: SimpleOutputConfig, + individualConfigs: List[SimpleOutputConfig] = List.empty, ) - object Thermal { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output.Thermal = { - SimonaConfig.Simona.Output.Thermal( - defaultConfig = SimonaConfig.SimpleOutputConfig( - if (c.hasPathOrNull("defaultConfig")) c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_SimpleOutputConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_SimpleOutputConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.SimpleOutputConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.SimpleOutputConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Output = { - SimonaConfig.Simona.Output( - base = SimonaConfig.Simona.Output.Base( - if (c.hasPathOrNull("base")) c.getConfig("base") - else com.typesafe.config.ConfigFactory.parseString("base{}"), - parentPath + "base.", - $tsCfgValidator, - ), - flex = c.hasPathOrNull("flex") && c.getBoolean("flex"), - grid = SimonaConfig.GridOutputConfig( - if (c.hasPathOrNull("grid")) c.getConfig("grid") - else com.typesafe.config.ConfigFactory.parseString("grid{}"), - parentPath + "grid.", - $tsCfgValidator, - ), - log = SimonaConfig.Simona.Output.Log( - if (c.hasPathOrNull("log")) c.getConfig("log") - else com.typesafe.config.ConfigFactory.parseString("log{}"), - parentPath + "log.", - $tsCfgValidator, - ), - participant = SimonaConfig.Simona.Output.Participant( - if (c.hasPathOrNull("participant")) c.getConfig("participant") - else com.typesafe.config.ConfigFactory.parseString("participant{}"), - parentPath + "participant.", - $tsCfgValidator, - ), - sink = SimonaConfig.Simona.Output.Sink( - if (c.hasPathOrNull("sink")) c.getConfig("sink") - else com.typesafe.config.ConfigFactory.parseString("sink{}"), - parentPath + "sink.", - $tsCfgValidator, - ), - thermal = SimonaConfig.Simona.Output.Thermal( - if (c.hasPathOrNull("thermal")) c.getConfig("thermal") - else com.typesafe.config.ConfigFactory.parseString("thermal{}"), - parentPath + "thermal.", - $tsCfgValidator, - ), - ) - } } final case class Powerflow( - maxSweepPowerDeviation: scala.Double, - newtonraphson: SimonaConfig.Simona.Powerflow.Newtonraphson, - resolution: java.time.Duration, - stopOnFailure: scala.Boolean, - sweepTimeout: java.time.Duration, + maxSweepPowerDeviation: Double, + newtonraphson: Powerflow.Newtonraphson, + resolution: Duration = Duration.ofHours(1), + stopOnFailure: Boolean = false, + sweepTimeout: Duration = Duration.ofSeconds(30), ) object Powerflow { final case class Newtonraphson( - epsilon: scala.List[scala.Double], - iterations: scala.Int, + epsilon: List[Double] = List.empty, + iterations: Int, ) - object Newtonraphson { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Powerflow.Newtonraphson = { - SimonaConfig.Simona.Powerflow.Newtonraphson( - epsilon = - $_L$_dbl(c.getList("epsilon"), parentPath, $tsCfgValidator), - iterations = $_reqInt(parentPath, c, "iterations", $tsCfgValidator), - ) - } - private def $_reqInt( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.Int = { - if (c == null) 0 - else - try c.getInt(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - 0 - } - } - - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Powerflow = { - SimonaConfig.Simona.Powerflow( - maxSweepPowerDeviation = - $_reqDbl(parentPath, c, "maxSweepPowerDeviation", $tsCfgValidator), - newtonraphson = SimonaConfig.Simona.Powerflow.Newtonraphson( - if (c.hasPathOrNull("newtonraphson")) c.getConfig("newtonraphson") - else - com.typesafe.config.ConfigFactory.parseString("newtonraphson{}"), - parentPath + "newtonraphson.", - $tsCfgValidator, - ), - resolution = - if (c.hasPathOrNull("resolution")) c.getDuration("resolution") - else java.time.Duration.parse("PT1H"), - stopOnFailure = - c.hasPathOrNull("stopOnFailure") && c.getBoolean("stopOnFailure"), - sweepTimeout = - if (c.hasPathOrNull("sweepTimeout")) c.getDuration("sweepTimeout") - else java.time.Duration.parse("PT30S"), - ) - } - 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 Runtime( - listener: SimonaConfig.Simona.Runtime.Listener, - participant: SimonaConfig.Simona.Runtime.Participant, - selected_subgrids: scala.Option[scala.List[scala.Int]], - selected_volt_lvls: scala.Option[scala.List[SimonaConfig.VoltLvlConfig]], - ) - object Runtime { - final case class Listener( - eventsToProcess: scala.Option[scala.List[java.lang.String]], - kafka: scala.Option[SimonaConfig.RuntimeKafkaParams], - ) - object Listener { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Listener = { - SimonaConfig.Simona.Runtime.Listener( - eventsToProcess = - if (c.hasPathOrNull("eventsToProcess")) - scala.Some( - $_L$_str( - c.getList("eventsToProcess"), - parentPath, - $tsCfgValidator, - ) - ) - else None, - kafka = - if (c.hasPathOrNull("kafka")) - scala.Some( - SimonaConfig.RuntimeKafkaParams( - c.getConfig("kafka"), - parentPath + "kafka.", - $tsCfgValidator, - ) - ) - else None, - ) - } - } - - final case class Participant( - em: SimonaConfig.Simona.Runtime.Participant.Em, - evcs: SimonaConfig.Simona.Runtime.Participant.Evcs, - fixedFeedIn: SimonaConfig.Simona.Runtime.Participant.FixedFeedIn, - hp: SimonaConfig.Simona.Runtime.Participant.Hp, - 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 { - final case class Em( - defaultConfig: SimonaConfig.EmRuntimeConfig, - individualConfigs: scala.List[SimonaConfig.EmRuntimeConfig], - ) - object Em { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.Em = { - SimonaConfig.Simona.Runtime.Participant.Em( - defaultConfig = SimonaConfig.EmRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_EmRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_EmRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.EmRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.EmRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - final case class Evcs( - defaultConfig: SimonaConfig.EvcsRuntimeConfig, - individualConfigs: scala.List[SimonaConfig.EvcsRuntimeConfig], - ) - object Evcs { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.Evcs = { - SimonaConfig.Simona.Runtime.Participant.Evcs( - defaultConfig = SimonaConfig.EvcsRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_EvcsRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_EvcsRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.EvcsRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.EvcsRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - final case class FixedFeedIn( - defaultConfig: SimonaConfig.FixedFeedInRuntimeConfig, - individualConfigs: scala.List[SimonaConfig.FixedFeedInRuntimeConfig], - ) - object FixedFeedIn { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.FixedFeedIn = { - SimonaConfig.Simona.Runtime.Participant.FixedFeedIn( - defaultConfig = SimonaConfig.FixedFeedInRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_FixedFeedInRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_FixedFeedInRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.FixedFeedInRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.FixedFeedInRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - final case class Hp( - defaultConfig: SimonaConfig.HpRuntimeConfig, - individualConfigs: scala.List[SimonaConfig.HpRuntimeConfig], - ) - object Hp { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.Hp = { - SimonaConfig.Simona.Runtime.Participant.Hp( - defaultConfig = SimonaConfig.HpRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_HpRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_HpRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.HpRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.HpRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - final case class Load( - defaultConfig: SimonaConfig.LoadRuntimeConfig, - individualConfigs: scala.List[SimonaConfig.LoadRuntimeConfig], - ) - object Load { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.Load = { - SimonaConfig.Simona.Runtime.Participant.Load( - defaultConfig = SimonaConfig.LoadRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_LoadRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_LoadRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.LoadRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.LoadRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - final case class Pv( - defaultConfig: SimonaConfig.PvRuntimeConfig, - individualConfigs: scala.List[SimonaConfig.PvRuntimeConfig], - ) - object Pv { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.Pv = { - SimonaConfig.Simona.Runtime.Participant.Pv( - defaultConfig = SimonaConfig.PvRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_PvRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_PvRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.PvRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.PvRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - 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], - ) - object Wec { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant.Wec = { - SimonaConfig.Simona.Runtime.Participant.Wec( - defaultConfig = SimonaConfig.WecRuntimeConfig( - if (c.hasPathOrNull("defaultConfig")) - c.getConfig("defaultConfig") - else - com.typesafe.config.ConfigFactory - .parseString("defaultConfig{}"), - parentPath + "defaultConfig.", - $tsCfgValidator, - ), - individualConfigs = $_LSimonaConfig_WecRuntimeConfig( - c.getList("individualConfigs"), - parentPath, - $tsCfgValidator, - ), - ) - } - private def $_LSimonaConfig_WecRuntimeConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.WecRuntimeConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.WecRuntimeConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime.Participant = { - SimonaConfig.Simona.Runtime.Participant( - em = SimonaConfig.Simona.Runtime.Participant.Em( - if (c.hasPathOrNull("em")) c.getConfig("em") - else com.typesafe.config.ConfigFactory.parseString("em{}"), - parentPath + "em.", - $tsCfgValidator, - ), - evcs = SimonaConfig.Simona.Runtime.Participant.Evcs( - if (c.hasPathOrNull("evcs")) c.getConfig("evcs") - else com.typesafe.config.ConfigFactory.parseString("evcs{}"), - parentPath + "evcs.", - $tsCfgValidator, - ), - fixedFeedIn = SimonaConfig.Simona.Runtime.Participant.FixedFeedIn( - if (c.hasPathOrNull("fixedFeedIn")) c.getConfig("fixedFeedIn") - else - com.typesafe.config.ConfigFactory.parseString("fixedFeedIn{}"), - parentPath + "fixedFeedIn.", - $tsCfgValidator, - ), - hp = SimonaConfig.Simona.Runtime.Participant.Hp( - if (c.hasPathOrNull("hp")) c.getConfig("hp") - else com.typesafe.config.ConfigFactory.parseString("hp{}"), - parentPath + "hp.", - $tsCfgValidator, - ), - load = SimonaConfig.Simona.Runtime.Participant.Load( - if (c.hasPathOrNull("load")) c.getConfig("load") - else com.typesafe.config.ConfigFactory.parseString("load{}"), - parentPath + "load.", - $tsCfgValidator, - ), - pv = SimonaConfig.Simona.Runtime.Participant.Pv( - if (c.hasPathOrNull("pv")) c.getConfig("pv") - else com.typesafe.config.ConfigFactory.parseString("pv{}"), - parentPath + "pv.", - $tsCfgValidator, - ), - requestVoltageDeviationThreshold = - 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{}"), - parentPath + "wec.", - $tsCfgValidator, - ), - ) - } - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Runtime = { - SimonaConfig.Simona.Runtime( - listener = SimonaConfig.Simona.Runtime.Listener( - if (c.hasPathOrNull("listener")) c.getConfig("listener") - else com.typesafe.config.ConfigFactory.parseString("listener{}"), - parentPath + "listener.", - $tsCfgValidator, - ), - participant = SimonaConfig.Simona.Runtime.Participant( - if (c.hasPathOrNull("participant")) c.getConfig("participant") - else com.typesafe.config.ConfigFactory.parseString("participant{}"), - parentPath + "participant.", - $tsCfgValidator, - ), - selected_subgrids = - if (c.hasPathOrNull("selected_subgrids")) - scala.Some( - $_L$_int( - c.getList("selected_subgrids"), - parentPath, - $tsCfgValidator, - ) - ) - else None, - selected_volt_lvls = - if (c.hasPathOrNull("selected_volt_lvls")) - scala.Some( - $_LSimonaConfig_VoltLvlConfig( - c.getList("selected_volt_lvls"), - parentPath, - $tsCfgValidator, - ) - ) - else None, - ) - } - private def $_LSimonaConfig_VoltLvlConfig( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[SimonaConfig.VoltLvlConfig] = { - import scala.jdk.CollectionConverters._ - cl.asScala - .map(cv => - SimonaConfig.VoltLvlConfig( - cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, - parentPath, - $tsCfgValidator, - ) - ) - .toList - } } final case class Time( - endDateTime: java.lang.String, - schedulerReadyCheckWindow: scala.Option[scala.Int], - startDateTime: java.lang.String, - ) - object Time { - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona.Time = { - SimonaConfig.Simona.Time( - endDateTime = - if (c.hasPathOrNull("endDateTime")) c.getString("endDateTime") - else "2011-05-01T01:00:00Z", - schedulerReadyCheckWindow = - if (c.hasPathOrNull("schedulerReadyCheckWindow")) - Some(c.getInt("schedulerReadyCheckWindow")) - else None, - startDateTime = - if (c.hasPathOrNull("startDateTime")) c.getString("startDateTime") - else "2011-05-01T00:00:00Z", - ) - } - } - - def apply( - c: com.typesafe.config.Config, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): SimonaConfig.Simona = { - SimonaConfig.Simona( - control = - if (c.hasPathOrNull("control")) - scala.Some( - SimonaConfig.Simona.Control( - c.getConfig("control"), - parentPath + "control.", - $tsCfgValidator, - ) - ) - else None, - event = SimonaConfig.Simona.Event( - if (c.hasPathOrNull("event")) c.getConfig("event") - else com.typesafe.config.ConfigFactory.parseString("event{}"), - parentPath + "event.", - $tsCfgValidator, - ), - gridConfig = SimonaConfig.Simona.GridConfig( - if (c.hasPathOrNull("gridConfig")) c.getConfig("gridConfig") - else com.typesafe.config.ConfigFactory.parseString("gridConfig{}"), - parentPath + "gridConfig.", - $tsCfgValidator, - ), - input = SimonaConfig.Simona.Input( - if (c.hasPathOrNull("input")) c.getConfig("input") - else com.typesafe.config.ConfigFactory.parseString("input{}"), - parentPath + "input.", - $tsCfgValidator, - ), - output = SimonaConfig.Simona.Output( - if (c.hasPathOrNull("output")) c.getConfig("output") - else com.typesafe.config.ConfigFactory.parseString("output{}"), - parentPath + "output.", - $tsCfgValidator, - ), - powerflow = SimonaConfig.Simona.Powerflow( - if (c.hasPathOrNull("powerflow")) c.getConfig("powerflow") - else com.typesafe.config.ConfigFactory.parseString("powerflow{}"), - parentPath + "powerflow.", - $tsCfgValidator, - ), - runtime = SimonaConfig.Simona.Runtime( - if (c.hasPathOrNull("runtime")) c.getConfig("runtime") - else com.typesafe.config.ConfigFactory.parseString("runtime{}"), - parentPath + "runtime.", - $tsCfgValidator, - ), - simulationName = - $_reqStr(parentPath, c, "simulationName", $tsCfgValidator), - time = SimonaConfig.Simona.Time( - if (c.hasPathOrNull("time")) c.getConfig("time") - else com.typesafe.config.ConfigFactory.parseString("time{}"), - parentPath + "time.", - $tsCfgValidator, - ), - ) - } - private def $_reqStr( - parentPath: java.lang.String, - c: com.typesafe.config.Config, - path: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): java.lang.String = { - if (c == null) null - else - try c.getString(path) - catch { - case e: com.typesafe.config.ConfigException => - $tsCfgValidator.addBadPath(parentPath + path, e) - null - } - } - - } - - def apply(c: com.typesafe.config.Config): SimonaConfig = { - val $tsCfgValidator: $TsCfgValidator = new $TsCfgValidator() - val parentPath: java.lang.String = "" - val $result = SimonaConfig( - simona = SimonaConfig.Simona( - if (c.hasPathOrNull("simona")) c.getConfig("simona") - else com.typesafe.config.ConfigFactory.parseString("simona{}"), - parentPath + "simona.", - $tsCfgValidator, - ) + endDateTime: String = "2011-05-01T01:00:00Z", + schedulerReadyCheckWindow: Option[Int] = None, + startDateTime: String = "2011-05-01T00:00:00Z", ) - $tsCfgValidator.validate() - $result - } - - private def $_L$_dbl( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[scala.Double] = { - import scala.jdk.CollectionConverters._ - cl.asScala.map(cv => $_dbl(cv)).toList - } - private def $_L$_int( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[scala.Int] = { - import scala.jdk.CollectionConverters._ - cl.asScala.map(cv => $_int(cv)).toList - } - private def $_L$_str( - cl: com.typesafe.config.ConfigList, - parentPath: java.lang.String, - $tsCfgValidator: $TsCfgValidator, - ): scala.List[java.lang.String] = { - import scala.jdk.CollectionConverters._ - cl.asScala.map(cv => $_str(cv)).toList - } - private def $_dbl(cv: com.typesafe.config.ConfigValue): scala.Double = { - val u: Any = cv.unwrapped - if ( - (cv.valueType != com.typesafe.config.ConfigValueType.NUMBER) || - !u.isInstanceOf[java.lang.Number] - ) throw $_expE(cv, "double") - u.asInstanceOf[java.lang.Number].doubleValue() - } - - private def $_expE( - cv: com.typesafe.config.ConfigValue, - exp: java.lang.String, - ) = { - val u: Any = cv.unwrapped - new java.lang.RuntimeException( - s"${cv.origin.lineNumber}: " + - "expecting: " + exp + " got: " + - (if (u.isInstanceOf[java.lang.String]) "\"" + u + "\"" else u) - ) - } - - private def $_int(cv: com.typesafe.config.ConfigValue): scala.Int = { - val u: Any = cv.unwrapped - if ( - (cv.valueType != com.typesafe.config.ConfigValueType.NUMBER) || - !u.isInstanceOf[Integer] - ) throw $_expE(cv, "integer") - u.asInstanceOf[Integer] - } - - private def $_str(cv: com.typesafe.config.ConfigValue): java.lang.String = { - java.lang.String.valueOf(cv.unwrapped()) - } - - final class $TsCfgValidator { - private val badPaths = - scala.collection.mutable.ArrayBuffer[java.lang.String]() - - def addBadPath( - path: java.lang.String, - e: com.typesafe.config.ConfigException, - ): Unit = { - badPaths += s"'$path': ${e.getClass.getName}(${e.getMessage})" - } - - def addInvalidEnumValue( - path: java.lang.String, - value: java.lang.String, - enumName: java.lang.String, - ): Unit = { - badPaths += s"'$path': invalid value $value for enumeration $enumName" - } - - def validate(): Unit = { - if (badPaths.nonEmpty) { - throw new com.typesafe.config.ConfigException( - badPaths.mkString("Invalid configuration:\n ", "\n ", "") - ) {} - } - } } } diff --git a/src/main/scala/edu/ie3/simona/event/listener/RuntimeEventListener.scala b/src/main/scala/edu/ie3/simona/event/listener/RuntimeEventListener.scala index bb97242e13..f36def4902 100644 --- a/src/main/scala/edu/ie3/simona/event/listener/RuntimeEventListener.scala +++ b/src/main/scala/edu/ie3/simona/event/listener/RuntimeEventListener.scala @@ -6,7 +6,7 @@ package edu.ie3.simona.event.listener -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig import edu.ie3.simona.event.RuntimeEvent import edu.ie3.simona.event.RuntimeEvent.PowerFlowFailed import edu.ie3.simona.io.runtime.RuntimeEventSink.RuntimeStats @@ -42,7 +42,7 @@ object RuntimeEventListener { * the [[RuntimeEventListener]] behavior */ def apply( - listenerConf: SimonaConfig.Simona.Runtime.Listener, + listenerConf: RuntimeConfig.Listener, queue: Option[BlockingQueue[RuntimeEvent]], startDateTimeString: String, ): Behavior[Request] = Behaviors.setup { ctx => diff --git a/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala b/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala index a73a854de4..92c8b491ce 100644 --- a/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala +++ b/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala @@ -7,16 +7,17 @@ package edu.ie3.simona.io.result import edu.ie3.datamodel.models.result.ResultEntity -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower /** A class to offer means to transport accompanying results alongside of - * [[PrimaryDataWithApparentPower]], e.g. heat results obtained during a + * [[PrimaryDataWithComplexPower]], e.g. heat results obtained during a * simulation + * * @param primaryData * The original primary data of the electrical asset * @tparam PD * Type of primary data, that is carried */ -final case class AccompaniedSimulationResult[PD <: PrimaryDataWithApparentPower[ +final case class AccompaniedSimulationResult[PD <: PrimaryDataWithComplexPower[ PD ]](primaryData: PD, accompanyingResults: Seq[ResultEntity] = Seq.empty) diff --git a/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala b/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala index fbb1bef45f..7b61f338f0 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimonaStandalone.scala @@ -35,7 +35,8 @@ object RunSimonaStandalone extends RunSimona[SimonaStandaloneSetup] { SimonaStandaloneSetup( parsedConfig, - SimonaStandaloneSetup.buildResultFileHierarchy(parsedConfig), + simonaConfig, + SimonaStandaloneSetup.buildResultFileHierarchy(simonaConfig), mainArgs = arguments.mainArgs, ) } diff --git a/src/main/scala/edu/ie3/simona/model/em/EmAggregateSelfOpt.scala b/src/main/scala/edu/ie3/simona/model/em/EmAggregatePowerOpt.scala similarity index 59% rename from src/main/scala/edu/ie3/simona/model/em/EmAggregateSelfOpt.scala rename to src/main/scala/edu/ie3/simona/model/em/EmAggregatePowerOpt.scala index 4451d4bb00..a1b57292fe 100644 --- a/src/main/scala/edu/ie3/simona/model/em/EmAggregateSelfOpt.scala +++ b/src/main/scala/edu/ie3/simona/model/em/EmAggregatePowerOpt.scala @@ -12,29 +12,38 @@ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMin import edu.ie3.util.scala.quantities.DefaultQuantities._ import squants.Power -/** Aggregates flex reference power with the target of reaching 0kW, while - * optionally excluding positive flex potential of PV/WEC from the calculation +import java.lang.Math.signum + +/** Aggregates flex reference power with the goal of not exceed a defined target + * limit, while optionally excluding positive flex potential of PV/WEC from the + * calculation. If the target limit can't be met, the closes possible operation + * point should be chosen. * + * @param targetPowerAbs + * absolute power target value that should be not be exceeded * @param curtailRegenerative * Whether to include positive flexibility of PV/WEC in reference sum * calculation */ -final case class EmAggregateSelfOpt(curtailRegenerative: Boolean) - extends EmAggregateFlex { +final case class EmAggregatePowerOpt( + targetPowerAbs: Power = zeroKW, + curtailRegenerative: Boolean, +) extends EmAggregateFlex { override def aggregateFlexOptions( flexOptions: Iterable[ (_ <: AssetInput, ProvideMinMaxFlexOptions) ] ): (Power, Power, Power) = { - val (minSum, maxSum) = - flexOptions.foldLeft((zeroKW, zeroKW)) { + val (minSum, refSum, maxSum) = + flexOptions.foldLeft((zeroKW, zeroKW, zeroKW)) { case ( - (sumMin, sumMax), - (_, ProvideMinMaxFlexOptions(_, _, addMin, addMax)), + (sumMin, sumRef, sumMax), + (_, ProvideMinMaxFlexOptions(_, addRef, addMin, addMax)), ) => ( sumMin + addMin, + sumRef + addRef, sumMax + addMax, ) } @@ -55,8 +64,11 @@ final case class EmAggregateSelfOpt(curtailRegenerative: Boolean) } } - // take the closest power possible to zero - val refAgg = minSum.max(maxRefSum.min(zeroKW)) + val targetAbs = if (targetPowerAbs.abs < refSum.abs) { + targetPowerAbs * signum(refSum.toKilowatts) + } else refSum + + val refAgg = minSum.max(maxRefSum.min(targetAbs)) (refAgg, minSum, maxSum) } diff --git a/src/main/scala/edu/ie3/simona/model/em/EmModelShell.scala b/src/main/scala/edu/ie3/simona/model/em/EmModelShell.scala index 6a15d08624..6d453de55c 100644 --- a/src/main/scala/edu/ie3/simona/model/em/EmModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/em/EmModelShell.scala @@ -7,11 +7,13 @@ package edu.ie3.simona.model.em import edu.ie3.datamodel.models.input.AssetInput -import edu.ie3.simona.config.SimonaConfig.EmRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.EmRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW import squants.Power +import squants.energy.Kilowatts import java.util.UUID @@ -126,9 +128,33 @@ object EmModelShell { } val aggregateFlex = modelConfig.aggregateFlex match { - case "SELF_OPT_EXCL_REG" => EmAggregateSelfOpt(false) - case "SELF_OPT" => EmAggregateSelfOpt(true) + case "SELF_OPT_EXCL_REG" => EmAggregatePowerOpt(zeroKW, false) + case "SELF_OPT" => EmAggregatePowerOpt(zeroKW, true) case "SIMPLE_SUM" => EmAggregateSimpleSum + + case powerTargetAbsString + if powerTargetAbsString.startsWith("SELF_POWER_") => + val pattern = """SELF_POWER_([\d.]+)(_EXCL_REG)?""".r + powerTargetAbsString match { + case pattern(value, exclReg) => + try { + val powerTargetAbs = BigDecimal(value) + val curtailRegenerative = exclReg == null + EmAggregatePowerOpt( + Kilowatts(powerTargetAbs), + curtailRegenerative, + ) + } catch { + case _: NumberFormatException => + throw new CriticalFailureException( + s"Invalid numeric value in aggregate flex strategy: $powerTargetAbsString" + ) + } + case _ => + throw new CriticalFailureException( + s"Invalid format for aggregate flex strategy: $powerTargetAbsString" + ) + } case unknown => throw new CriticalFailureException( s"Unknown aggregate flex strategy $unknown" diff --git a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala index 16285b9317..a16031cdc9 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -39,6 +39,7 @@ final case class GridModel( subnetNo: Int, mainRefSystem: RefSystem, gridComponents: GridComponents, + voltageLimits: VoltageLimits, gridControls: GridControls, ) { @@ -66,12 +67,14 @@ object GridModel { def apply( subGridContainer: SubGridContainer, refSystem: RefSystem, + voltageLimits: VoltageLimits, startDate: ZonedDateTime, endDate: ZonedDateTime, simonaConfig: SimonaConfig, ): GridModel = buildAndValidate( subGridContainer, refSystem, + voltageLimits, startDate, endDate, simonaConfig, @@ -407,7 +410,7 @@ object GridModel { noLines && noTransformers2w && noTransformers3w && (noOfNodes > noOfSlackNodes) ) throw new InvalidGridException( - "The grid contains no basic branch elements (lines or transformers)." + f"The grid with subnet number ${gridModel.subnetNo} contains additional nodes beside the slack nodes and no basic branch elements (lines or transformers). This is invalid." ) // slack @@ -500,6 +503,7 @@ object GridModel { private def buildAndValidate( subGridContainer: SubGridContainer, refSystem: RefSystem, + voltageLimits: VoltageLimits, startDate: ZonedDateTime, endDate: ZonedDateTime, simonaConfig: SimonaConfig, @@ -600,6 +604,7 @@ object GridModel { subGridContainer.getSubnet, refSystem, gridComponents, + voltageLimits, GridControls(transformerControlGroups), ) diff --git a/src/main/scala/edu/ie3/simona/model/grid/VoltageLimits.scala b/src/main/scala/edu/ie3/simona/model/grid/VoltageLimits.scala new file mode 100644 index 0000000000..b03fce842d --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/grid/VoltageLimits.scala @@ -0,0 +1,41 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.grid + +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Dimensionless + +/** Case class that holds information about the lower and upper voltage limit of + * the grid. + * @param vMin + * minimal allowed voltage in the grid + * @param vMax + * maximal allowed voltage in the grid + */ +final case class VoltageLimits( + vMin: ComparableQuantity[Dimensionless], + vMax: ComparableQuantity[Dimensionless], +) { + + /** Checks if a given voltage magnitude is within the given limits. + * @param voltage + * to check + * @return + * true if the given voltage magnitude is within the limits + */ + def isInLimits(voltage: ComparableQuantity[Dimensionless]): Boolean = + vMin.isLessThanOrEqualTo(voltage) && voltage.isLessThanOrEqualTo(vMax) +} + +object VoltageLimits { + def apply( + vMin: Double, + vMax: Double, + ): VoltageLimits = VoltageLimits(vMin.asPu, vMax.asPu) +} diff --git a/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala b/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala index 0501fcb025..1deb1c138b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/FixedFeedInModel.scala @@ -9,7 +9,7 @@ package edu.ie3.simona.model.participant import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.models.input.system.FixedFeedInInput import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData import edu.ie3.simona.model.participant.ModelState.ConstantState @@ -93,7 +93,7 @@ final case class FixedFeedInModel( object FixedFeedInModel extends LazyLogging { def apply( inputModel: FixedFeedInInput, - modelConfiguration: SimonaConfig.FixedFeedInRuntimeConfig, + modelConfiguration: FixedFeedInRuntimeConfig, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, ): FixedFeedInModel = { diff --git a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala index 25d8e1d92a..e874e7c028 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -148,9 +148,9 @@ final case class HpModel( ) // Updating the HpState - val updatedState = - calcState(lastHpState, relevantData, turnOn) - (canOperate, canBeOutOfOperation, updatedState) + val updatedHpState = + calcState(lastHpState, relevantData, turnOn, thermalDemandWrapper) + (canOperate, canBeOutOfOperation, updatedHpState) } /** Depending on the input, this function decides whether the heat pump will @@ -212,6 +212,8 @@ final case class HpModel( * data of heat pump including state of the heat pump * @param isRunning * determines whether the heat pump is running or not + * @param demandWrapper + * holds the thermal demands of the thermal units (house, storage) * @return * next [[HpState]] */ @@ -219,17 +221,27 @@ final case class HpModel( lastState: HpState, relevantData: HpRelevantData, isRunning: Boolean, + demandWrapper: ThermalDemandWrapper, ): HpState = { val lastStateStorageQDot = lastState.thermalGridState.storageState .map(_.qDot) .getOrElse(zeroKW) - val (newActivePower, newThermalPower) = + val (newActivePowerHp, newThermalPowerHp, qDotIntoGrid) = { if (isRunning) - (pRated, pThermal) + (pRated, pThermal, pThermal) else if (lastStateStorageQDot < zeroKW) - (zeroKW, lastStateStorageQDot * -1) - else (zeroKW, zeroKW) + (zeroKW, zeroKW, lastStateStorageQDot * (-1)) + else if ( + lastStateStorageQDot == zeroKW && (demandWrapper.houseDemand.hasRequiredDemand || demandWrapper.heatStorageDemand.hasRequiredDemand) + ) + ( + zeroKW, + zeroKW, + thermalGrid.storage.map(_.getChargingPower: squants.Power).get, + ) + else (zeroKW, zeroKW, zeroKW) + } /* Push thermal energy to the thermal grid and get its updated state in return */ val (thermalGridState, maybeThreshold) = @@ -237,15 +249,17 @@ final case class HpModel( relevantData, lastState.thermalGridState, lastState.ambientTemperature.getOrElse(relevantData.ambientTemperature), - newThermalPower, + isRunning, + qDotIntoGrid, + demandWrapper, ) HpState( isRunning, relevantData.currentTick, Some(relevantData.ambientTemperature), - newActivePower, - newThermalPower, + newActivePowerHp, + newThermalPowerHp, thermalGridState, maybeThreshold, ) @@ -285,7 +299,7 @@ final case class HpModel( * operating state and give back the next tick in which something will * change. * - * @param data + * @param relevantData * Relevant data for model calculation * @param lastState * The last known model state @@ -296,17 +310,27 @@ final case class HpModel( * options will change next */ override def handleControlledPowerChange( - data: HpRelevantData, + relevantData: HpRelevantData, lastState: HpState, setPower: Power, ): (HpState, FlexChangeIndicator) = { /* If the set point value is above 50 % of the electrical power, turn on the heat pump otherwise turn it off */ val turnOn = setPower > (sRated.toActivePower(cosPhiRated) * 0.5) + val ( + thermalDemandWrapper, + _, + ) = + thermalGrid.energyDemandAndUpdatedState( + relevantData, + lastState, + ) + val updatedHpState = calcState( lastState, - data, + relevantData, turnOn, + thermalDemandWrapper, ) ( @@ -378,9 +402,9 @@ object HpModel { * @param ambientTemperature * Optional ambient temperature, if available * @param activePower - * result active power + * result active power of heat pump * @param qDot - * result heat power + * result heat power of heat pump * @param thermalGridState * applicable state of the thermal grid * @param maybeThermalThreshold diff --git a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala index 1aa4377f8e..7fee381e11 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.model.participant import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ComplexPower, - PrimaryDataWithApparentPower, + PrimaryDataWithComplexPower, } import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.control.QControl @@ -44,7 +44,7 @@ import java.util.UUID */ abstract class SystemParticipant[ CD <: CalcRelevantData, - +PD <: PrimaryDataWithApparentPower[PD], + +PD <: PrimaryDataWithComplexPower[PD], MS <: ModelState, ]( uuid: UUID, diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala b/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala index 189f29887b..df97792e69 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/load/LoadReference.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.model.participant.load import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig import edu.ie3.util.StringUtils import edu.ie3.util.quantities.PowerSystemUnits.{MEGAWATT, MEGAWATTHOUR} import squants.energy.{MegawattHours, Megawatts} @@ -69,7 +69,7 @@ object LoadReference { */ def apply( inputModel: LoadInput, - modelConfig: SimonaConfig.LoadRuntimeConfig, + modelConfig: LoadRuntimeConfig, ): LoadReference = StringUtils.cleanString(modelConfig.reference).toLowerCase match { case "power" => diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala new file mode 100644 index 0000000000..221a877e73 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -0,0 +1,105 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + OperationChangeIndicator, + ModelState, + OperatingPoint, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import squants.energy.Power + +/** Trait for [[ParticipantModel]] to define methods related to flexibility. + * + * @tparam OP + * The type of operating point. + * @tparam S + * The type of model state. + */ +trait ParticipantFlexibility[ + OP <: OperatingPoint, + S <: ModelState, +] { + + this: ParticipantModel[OP, S] => + + /** Given the current state, this method determines the flexibility options + * for the current tick. This usually means that the range of possible + * operating points has be considered and subsequently distilled into a + * [[ProvideFlexOptions]] message. + * + * @param state + * The current state. + * @return + * The flexibility options. + */ + def determineFlexOptions(state: S): ProvideFlexOptions + + /** Given the current state, this method determines the operating point that + * is currently valid until the next operating point is determined, given a + * flex control power determined by EM. Also, optionally returns a tick at + * which the state will change unless the operating point changes due to + * external influences beforehand. + * + * This method should be able to handle calls at arbitrary points in + * simulation time (i.e. ticks), which have to be situated after the tick of + * the last state though. + * + * This method is only called if the participant '''is''' em-controlled. If + * the participant is '''not''' em-controlled, + * [[ParticipantModel.determineOperatingPoint]] determines the operating + * point instead. + * + * @param state + * The current state. + * @param setPower + * The power set point determined by EM. + * @return + * The operating point and optionally a next activation tick. + */ + def determineOperatingPoint( + state: S, + setPower: Power, + ): (OP, OperationChangeIndicator) + +} + +object ParticipantFlexibility { + + /** Simple trait providing flexibility implementations to + * [[ParticipantModel]]s with [[ActivePowerOperatingPoint]]. No flexibility + * is provided. + * + * @tparam S + * The type of model state. + */ + trait ParticipantSimpleFlexibility[ + S <: ModelState + ] extends ParticipantFlexibility[ActivePowerOperatingPoint, S] { + this: ParticipantModel[ActivePowerOperatingPoint, S] => + + override def determineFlexOptions( + state: S + ): ProvideFlexOptions = { + val (operatingPoint, _) = determineOperatingPoint(state) + val power = operatingPoint.activePower + + ProvideMinMaxFlexOptions.noFlexOption(uuid, power) + } + + override def determineOperatingPoint( + state: S, + setPower: Power, + ): (ActivePowerOperatingPoint, OperationChangeIndicator) = { + (ActivePowerOperatingPoint(setPower), OperationChangeIndicator()) + } + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala new file mode 100644 index 0000000000..0a23f5bacb --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -0,0 +1,318 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelInput, + ModelState, + OperatingPoint, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW +import edu.ie3.util.scala.quantities.{ApparentPower, ReactivePower} +import org.apache.pekko.actor.typed.scaladsl.ActorContext +import squants.Dimensionless +import squants.energy.Power + +import java.time.ZonedDateTime +import java.util.UUID + +/** Abstract model of a system participant that provides methods for determining + * state and operating point. + * + * @tparam OP + * The type of operating point. + * @tparam S + * The type of model state. + */ +abstract class ParticipantModel[ + OP <: OperatingPoint, + S <: ModelState, +] extends ParticipantFlexibility[OP, S] { + + /** The UUID identifying the system participant. + */ + val uuid: UUID + + /** A human-readable id identifying the system participant. + */ + val id: String + + /** The rated apparent power of the system participant. + */ + val sRated: ApparentPower + + /** The power factor of the system participant. + */ + val cosPhiRated: Double + + /** The reactive power control definition. + */ + val qControl: QControl + + /** The rated active power according to the rated apparent power and rated + * power factor. + */ + protected val pRated: Power = sRated.toActivePower(cosPhiRated) + + /** Determines the initial state given an initial model input. + */ + val initialState: ModelInput => S + + /** Determines the current state given the last state and the operating point + * that has been valid from the last state up until now. + * + * @param lastState + * The last state. + * @param operatingPoint + * The operating point valid from the simulation time of the last state up + * until now. + * @param input + * The model input data for the current tick. + * @return + * The current state. + */ + def determineState( + lastState: S, + operatingPoint: OP, + input: ModelInput, + ): S + + /** Returns a partial function that transfers the current nodal voltage and + * active power into reactive power based on the participants properties. + * + * @return + * A [[PartialFunction]] from [[Power]] and voltage ([[Dimensionless]]) to + * [[ReactivePower]]. + */ + def reactivePowerFunc: Dimensionless => Power => ReactivePower = + nodalVoltage => + qControl.activeToReactivePowerFunc( + sRated, + cosPhiRated, + nodalVoltage, + ) + + /** Given the current state, this method determines the operating point that + * is currently valid until the next operating point is determined. Also, + * optionally returns a tick at which the state will change unless the + * operating point changes due to external influences beforehand. + * + * This method should be able to handle calls at arbitrary points in + * simulation time (i.e. ticks), which have to be situated after the tick of + * the last state though. + * + * This method is only called if the participant is '''not''' em-controlled. + * If the participant '''is''' em-controlled, + * [[ParticipantFlexibility.determineOperatingPoint]] determines the + * operating point instead. + * + * @param state + * the current state. + * @return + * the operating point and optionally a next activation tick. + */ + def determineOperatingPoint(state: S): (OP, Option[Long]) + + /** Operating point used when model is out of operation, thus + * producing/consuming no power. + * + * @return + * an operating point representing zero power. + */ + def zeroPowerOperatingPoint: OP + + /** @param state + * the current state. + * @param lastOperatingPoint + * the last operating point before the current one, i.e. the one valid up + * until the last state, if applicable. + * @param currentOperatingPoint + * the operating point valid from the simulation time of the last state up + * until now. + * @param complexPower + * the total complex power derived from the current operating point. + * @param dateTime + * the current simulation date and time. + * @return + */ + def createResults( + state: S, + lastOperatingPoint: Option[OP], + currentOperatingPoint: OP, + complexPower: ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] + + def createPrimaryDataResult( + data: PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult + + /** Handling requests that are specific to the respective [[ParticipantModel]] + * and not part of the standard participant protocol. The model state can be + * updated. + * + * @param state + * The current state. + * @param ctx + * The actor context that can be used to send replies. + * @param msg + * The received request. + * @return + * An updated state, or the same state provided as parameter. + */ + def handleRequest( + state: S, + ctx: ActorContext[ParticipantAgent.Request], + msg: ParticipantRequest, + ): S = + throw new NotImplementedError(s"Method not implemented by $getClass") + + /** @return + * All secondary services required by the model. + */ + def getRequiredSecondaryServices: Iterable[ServiceType] + +} + +object ParticipantModel { + + /** Holds all potentially relevant input data for model calculation. + * + * @param receivedData + * The received primary or secondary data. + * @param nodalVoltage + * The voltage at the node that we're connected to. + * @param currentTick + * The current tick. + * @param currentSimulationTime + * The current simulation time (matches the tick). + */ + final case class ModelInput( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + currentTick: Long, + currentSimulationTime: ZonedDateTime, + ) + + trait OperatingPoint { + + val activePower: Power + + /** Reactive power can be overridden by the model itself. If this is None, + * the active-to-reactive-power function is used. + */ + val reactivePower: Option[ReactivePower] + } + + final case class ActivePowerOperatingPoint(override val activePower: Power) + extends OperatingPoint { + override val reactivePower: Option[ReactivePower] = None + } + + object ActivePowerOperatingPoint { + def zero: ActivePowerOperatingPoint = ActivePowerOperatingPoint(zeroKW) + } + + trait ModelState { + val tick: Long + } + + final case class FixedState(override val tick: Long) extends ModelState + + trait ParticipantFixedState[ + OP <: OperatingPoint + ] { + this: ParticipantModel[OP, FixedState] => + + override val initialState: ModelInput => FixedState = + input => FixedState(input.currentTick) + + override def determineState( + lastState: FixedState, + operatingPoint: OP, + input: ModelInput, + ): FixedState = FixedState(input.currentTick) + + } + + /** State that just holds the current datetime and tick. + * @param tick + * The current tick. + * @param dateTime + * The current datetime, corresponding to the current tick. + */ + final case class DateTimeState(tick: Long, dateTime: ZonedDateTime) + extends ModelState + + trait ParticipantDateTimeState[ + OP <: OperatingPoint + ] { + this: ParticipantModel[OP, DateTimeState] => + + override val initialState: ModelInput => DateTimeState = input => + DateTimeState(input.currentTick, input.currentSimulationTime) + + override def determineState( + lastState: DateTimeState, + operatingPoint: OP, + input: ModelInput, + ): DateTimeState = + DateTimeState(input.currentTick, input.currentSimulationTime) + + } + + /** Indicates when either flex options (when em-controlled) or the operating + * point are going to change (when not em-controlled). + * + * A change of flex options or operating point might occur due to various + * reasons, including expected data arrival, internal expected model changes + * and operating interval limits. + * + * @param changesAtNextActivation + * Indicates whether flex options change at the very next tick that EM is + * activated, due to e.g. storage limits being reached. Not applicable for + * not-em-controlled models. + * @param changesAtTick + * The next tick at which a change of flex options or the operating point + * is expected. + */ + final case class OperationChangeIndicator( + changesAtNextActivation: Boolean = false, + changesAtTick: Option[Long] = None, + ) { + + /** Combines two [[OperationChangeIndicator]]s by aggregating + * changesAtNextActivation via OR function and picking the earlier (or any) + * of both changesAtTick values. + * + * @param otherIndicator + * The other [[OperationChangeIndicator]] to combine with this one. + * @return + * An aggregated [[OperationChangeIndicator]]. + */ + def |( + otherIndicator: OperationChangeIndicator + ): OperationChangeIndicator = { + OperationChangeIndicator( + changesAtNextActivation || otherIndicator.changesAtNextActivation, + Seq(changesAtTick, otherIndicator.changesAtTick).flatten.minOption, + ) + } + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala new file mode 100644 index 0000000000..f71b8d9c84 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -0,0 +1,136 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.input.system.SystemParticipantInput.SystemParticipantInputCopyBuilder +import edu.ie3.datamodel.models.input.system._ +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data.{ + PrimaryData, + PrimaryDataExtra, +} +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelState, + OperatingPoint, +} +import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc +import edu.ie3.simona.model.participant2.load.LoadModel + +import java.time.ZonedDateTime +import scala.reflect.ClassTag + +/** Helper object for constructing all types of [[ParticipantModel]]s, including + * [[PrimaryDataParticipantModel]]. + */ +object ParticipantModelInit { + + /** Constructs the matching [[ParticipantModel]] for the given + * [[SystemParticipantInput]]. The given [[BaseRuntimeConfig]] has to match + * the participant input. + * + * @param participantInput + * The system participant model input. + * @param modelConfig + * The model runtime config. + * @return + * The [[ParticipantModel]]. + */ + def createPhysicalModel( + participantInput: SystemParticipantInput, + modelConfig: BaseRuntimeConfig, + ): ParticipantModel[ + _ <: OperatingPoint, + _ <: ModelState, + ] = { + + val scaledParticipantInput = + (participantInput.copy().scale(modelConfig.scaling) match { + // matching needed because Scala has trouble recognizing the Java type parameter + case copyBuilder: SystemParticipantInputCopyBuilder[_] => copyBuilder + }).build() + + (scaledParticipantInput, modelConfig) match { + case (input: LoadInput, _) => + LoadModel(input) + case (input: PvInput, _) => + PvModel(input) + case (input, config) => + throw new CriticalFailureException( + s"Handling the input model ${input.getClass.getSimpleName} or " + + "the combination of the input model with model config " + + s"${config.getClass.getSimpleName} is not implemented." + ) + } + } + + /** Constructs a [[PrimaryDataParticipantModel]] for the given + * [[SystemParticipantInput]] and the given primary data. The given + * [[BaseRuntimeConfig]] has to match the participant input. + * + * @param participantInput + * The system participant model input. + * @param modelConfig + * The model runtime config. + * @param primaryDataExtra + * Extra functionality specific to the primary data class. + * @return + * The [[PrimaryDataParticipantModel]]. + */ + def createPrimaryModel[PD <: PrimaryData: ClassTag]( + participantInput: SystemParticipantInput, + modelConfig: BaseRuntimeConfig, + primaryDataExtra: PrimaryDataExtra[PD], + ): PrimaryDataParticipantModel[PD] = { + // Create a fitting physical model to extract parameters from + val physicalModel = createPhysicalModel( + participantInput, + modelConfig, + ) + + createPrimaryModel( + physicalModel, + primaryDataExtra, + ) + } + + /** Constructs a [[PrimaryDataParticipantModel]] for the given physical + * [[ParticipantModel]] and the given primary data. The given + * [[BaseRuntimeConfig]] has to match the participant input. + * + * @param physicalModel + * The physical participant model. + * @param primaryDataExtra + * Extra functionality specific to the primary data class. + * @return + * The [[PrimaryDataParticipantModel]]. + */ + def createPrimaryModel[PD <: PrimaryData: ClassTag]( + physicalModel: ParticipantModel[_, _], + primaryDataExtra: PrimaryDataExtra[PD], + ): PrimaryDataParticipantModel[PD] = { + val primaryResultFunc = new PrimaryResultFunc { + override def createResult( + data: PrimaryData.PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + physicalModel.createPrimaryDataResult(data, dateTime) + } + + new PrimaryDataParticipantModel( + physicalModel.uuid, + physicalModel.id, + physicalModel.sRated, + physicalModel.cosPhiRated, + physicalModel.qControl, + primaryResultFunc, + primaryDataExtra, + ) + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala new file mode 100644 index 0000000000..b9c5af8c6a --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -0,0 +1,553 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.input.system.SystemParticipantInput +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower +import edu.ie3.simona.agent.participant.data.Data.{ + PrimaryData, + PrimaryDataExtra, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.SystemComponent +import edu.ie3.simona.model.em.EmTools +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelInput, + ModelState, + OperatingPoint, + OperationChangeIndicator, +} +import edu.ie3.simona.model.participant2.ParticipantModelShell.ResultsContainer +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + IssueFlexControl, + ProvideFlexOptions, +} +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.service.ServiceType +import edu.ie3.simona.util.TickUtil.TickLong +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import edu.ie3.util.scala.quantities.ReactivePower +import org.apache.pekko.actor.typed.scaladsl.ActorContext +import squants.Dimensionless +import squants.energy.Power + +import java.time.ZonedDateTime +import java.util.UUID +import scala.reflect.ClassTag + +/** A shell allowing interactions with the [[ParticipantModel]] that it holds. + * Inputs and outputs are buffered and reused where applicable. The operation + * interval is considered when determining model operating points. + * + * Model parameters include the model state and operating point. A new state is + * determined given a former state and an operating point that has been valid + * since then. A new operating point can be determined on the basis of the + * current state. + * + * @param model + * The [[ParticipantModel]] that determines operating parameters. + * @param operationInterval + * The operation interval in which the participant model is active. Outside + * the interval, no power is produced or consumed. + * @param simulationStartDate + * The date and time at which simulation started. + * @param _state + * The most recent model state, if one has been calculated already. + * @param _input + * The most recent model input used for calculation, if it has been received + * already. + * @param _flexOptions + * The most recent flex options, if they have been calculated already. + * @param _lastOperatingPoint + * The operating point valid before the current [[_operatingPoint]], if + * applicable. + * @param _operatingPoint + * The most recent operating point, if one has been calculated already. + * @param _operationChange + * The operation change indicator, which indicates until when the current + * results are valid. + * @tparam OP + * The type of operating point used by the [[ParticipantModel]]. + * @tparam S + * The type of state used by the [[ParticipantModel]]. + */ +final case class ParticipantModelShell[ + OP <: OperatingPoint, + S <: ModelState, +]( + private val model: ParticipantModel[OP, S] + with ParticipantFlexibility[OP, S], + private val operationInterval: OperationInterval, + private val simulationStartDate: ZonedDateTime, + private val _state: Option[S] = None, + private val _input: Option[ModelInput] = None, + private val _flexOptions: Option[ProvideFlexOptions] = None, + private val _lastOperatingPoint: Option[OP] = None, + private val _operatingPoint: Option[OP] = None, + private val _operationChange: OperationChangeIndicator = + OperationChangeIndicator(), +) { + + /** Returns a unique identifier for the model held by this model shell, + * including the type, UUID and id of the model, for the purpose of log or + * exception messaging. + * + * @return + * A unique identifier for the model + */ + def identifier: String = + s"${model.getClass.getSimpleName}[${model.id}/$uuid]" + + /** Returns the model UUID. + * + * @return + * The UUID of the model. + */ + def uuid: UUID = model.uuid + + /** Returns the types of required secondary services for the model to + * function. + * + * @return + * The types of secondary services required. + */ + def requiredServices: Iterable[ServiceType] = + model.getRequiredSecondaryServices + + /** Returns the current relevant data, if present, or throws a + * [[CriticalFailureException]]. Only call this if you are certain the model + * input data has been set. + * + * @return + * The model input data. + */ + private def modelInput: ModelInput = + _input.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ) + + /** Returns the current operating point, if present, or throws a + * [[CriticalFailureException]]. Only call this if you are certain the + * operating point has been set. + * + * @return + * The operating point. + */ + private def operatingPoint: OP = { + _operatingPoint + .getOrElse( + throw new CriticalFailureException("No operating point available!") + ) + } + + /** Returns the current flex options, if present, or throws a + * [[CriticalFailureException]]. Only call this if you are certain the flex + * options have been set. + * + * @return + * The flex options. + */ + def flexOptions: ProvideFlexOptions = + _flexOptions.getOrElse( + throw new CriticalFailureException( + "Flex options have not been calculated!" + ) + ) + + /** Returns the reactive power function that takes a nodal voltage value and + * an active power as input. + * + * @return + * The reactive power function. + */ + def reactivePowerFunc: Dimensionless => Power => ReactivePower = + model.reactivePowerFunc + + /** Updates the model input according to the received data, the current nodal + * voltage and the current tick. + * + * @param receivedData + * The received input data. + * @param nodalVoltage + * The current nodal voltage. + * @param tick + * The current tick. + * @return + * An updated [[ParticipantModelShell]]. + */ + def updateModelInput( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + ): ParticipantModelShell[OP, S] = { + val currentSimulationTime = tick.toDateTime(simulationStartDate) + + copy(_input = + Some( + ModelInput( + receivedData, + nodalVoltage, + tick, + currentSimulationTime, + ) + ) + ) + } + + /** Update operating point when the model is '''not''' em-controlled. + * + * @param tick + * The current tick. + * @return + * An updated [[ParticipantModelShell]]. + */ + def updateOperatingPoint( + tick: Long + ): ParticipantModelShell[OP, S] = { + val currentState = determineCurrentState(tick) + + def modelOperatingPoint(): (OP, OperationChangeIndicator) = { + val (modelOp, modelNextTick) = + model.determineOperatingPoint(currentState) + val modelIndicator = + OperationChangeIndicator(changesAtTick = modelNextTick) + (modelOp, modelIndicator) + } + + val (newOperatingPoint, newChangeIndicator) = + determineOperatingPoint(modelOperatingPoint, tick) + + copy( + _state = Some(currentState), + _lastOperatingPoint = _operatingPoint, + _operatingPoint = Some(newOperatingPoint), + _operationChange = newChangeIndicator, + ) + } + + /** Determines and returns results of the current operating point. + * + * @param tick + * The current tick. + * @param nodalVoltage + * The current nodal voltage. + * @return + * An updated [[ParticipantModelShell]]. + */ + def determineResults( + tick: Long, + nodalVoltage: Dimensionless, + ): ResultsContainer = { + val activePower = operatingPoint.activePower + val reactivePower = operatingPoint.reactivePower.getOrElse( + reactivePowerFunc(nodalVoltage)(activePower) + ) + val complexPower = ComplexPower(activePower, reactivePower) + + val participantResults = model.createResults( + determineCurrentState(tick), + _lastOperatingPoint, + operatingPoint, + complexPower, + tick.toDateTime(simulationStartDate), + ) + + ResultsContainer( + complexPower, + participantResults, + ) + } + + /** Updates the flex options on basis of the current state + * + * @param tick + * The current tick. + * @return + * An updated [[ParticipantModelShell]]. + */ + def updateFlexOptions(tick: Long): ParticipantModelShell[OP, S] = { + val currentState = determineCurrentState(tick) + + val flexOptions = + if (operationInterval.includes(tick)) { + model.determineFlexOptions(currentState) + } else { + // Out of operation, there's no way to operate besides 0 kW + ProvideMinMaxFlexOptions.noFlexOption(model.uuid, zeroKW) + } + + copy(_state = Some(currentState), _flexOptions = Some(flexOptions)) + } + + /** Update operating point on receiving [[IssueFlexControl]], i.e. when the + * model is em-controlled. + * + * @param flexControl + * The received flex control message. + * @return + * An updated [[ParticipantModelShell]]. + */ + def updateOperatingPoint( + flexControl: IssueFlexControl + ): ParticipantModelShell[OP, S] = { + val currentTick = flexControl.tick + + val currentState = determineCurrentState(currentTick) + + def modelOperatingPoint(): (OP, OperationChangeIndicator) = { + val fo = _flexOptions.getOrElse( + throw new CriticalFailureException("No flex options available!") + ) + + val setPointActivePower = EmTools.determineFlexPower( + fo, + flexControl, + ) + + model.determineOperatingPoint( + currentState, + setPointActivePower, + ) + } + + val (newOperatingPoint, newChangeIndicator) = + determineOperatingPoint(modelOperatingPoint, currentTick) + + copy( + _state = Some(currentState), + _lastOperatingPoint = _operatingPoint, + _operatingPoint = Some(newOperatingPoint), + _operationChange = newChangeIndicator, + ) + } + + /** Determines the operating point by taking into account the operation + * interval of the model. + * + * @param modelOperatingPoint + * A function determining the operating point if we're inside the operation + * interval. + * @param currentTick + * The current tick. + * @return + * A new [[OperatingPoint]] and an [[OperationChangeIndicator]]. + */ + private def determineOperatingPoint( + modelOperatingPoint: () => (OP, OperationChangeIndicator), + currentTick: Long, + ): (OP, OperationChangeIndicator) = { + if (operationInterval.includes(currentTick)) { + modelOperatingPoint() + } else { + // Current tick is outside of operation interval. + // Set operating point to "zero" + (model.zeroPowerOperatingPoint, OperationChangeIndicator()) + } + } + + /** Determines and returns the next activation tick considering the operating + * interval and given next data tick. + * + * @param currentTick + * The current tick. + * @param nextDataTick + * The next tick at which data is expected, if any. + * @return + * The [[OperationChangeIndicator]] indicating the next activation. + */ + def getChangeIndicator( + currentTick: Long, + nextDataTick: Option[Long], + ): OperationChangeIndicator = { + if (operationInterval.includes(currentTick)) { + // The next activation tick should be the earliest of + // the next tick request by the model, the next data tick and + // the end of the operation interval + val adaptedNextTick = + Seq( + _operationChange.changesAtTick, + nextDataTick, + Option(operationInterval.end), + ).flatten.minOption + + _operationChange.copy(changesAtTick = adaptedNextTick) + } else { + // If the model is not active, all activation ticks are ignored besides + // potentially the operation start + val nextTick = Option.when(operationInterval.start > currentTick)( + operationInterval.start + ) + + OperationChangeIndicator(changesAtTick = nextTick) + } + } + + /** Handles a request specific to the [[ParticipantModel]]. The model is + * allowed to send replies using the provided [[ActorContext]] and to update + * the model state, which is then stored within the shell. + * + * @param ctx + * The [[ActorContext]] used for sending replies. + * @param request + * The received request. + * @return + * An updated [[ParticipantModelShell]]. + */ + def handleRequest( + ctx: ActorContext[ParticipantAgent.Request], + request: ParticipantRequest, + ): ParticipantModelShell[OP, S] = { + val currentState = determineCurrentState(request.tick) + val updatedState = model.handleRequest(currentState, ctx, request) + + copy(_state = Some(updatedState)) + } + + /** Determines the current state (if it has not been determined before) using + * the former state, the operating point and the current tick. + * + * @param tick + * The current tick. + * @return + * The current state. + */ + private def determineCurrentState(tick: Long): S = { + // new state is only calculated if there's an old state and an operating point + val state = _state + .zip(_operatingPoint) + .flatMap { case (st, op) => + Option.when(st.tick < tick) { + model.determineState(st, op, modelInput) + } + } + .getOrElse(model.initialState(modelInput)) + + if (state.tick != tick) + throw new CriticalFailureException( + s"The current state $state is not set to current tick $tick" + ) + + state + } + +} + +object ParticipantModelShell { + + /** Container holding the resulting total complex power as well as + * [[SystemParticipantResult]] specific to the [[ParticipantModel]]. + * + * @param totalPower + * The total complex power produced or consumed. + * @param modelResults + * The model results. + */ + final case class ResultsContainer( + totalPower: ComplexPower, + modelResults: Iterable[SystemParticipantResult], + ) + + /** Creates a model shell receiving primary data using the given participant + * input. + * + * @param participantInput + * The physical participant model. + * @param config + * Runtime configuration that has to match the participant type. + * @param primaryDataExtra + * Extra functionality specific to the primary data class. + * @param simulationStart + * The simulation start date and time. + * @param simulationEnd + * The simulation end date and time. + * @tparam PD + * The type of primary data to be received. + * @return + * The constructed [[ParticipantModelShell]] with a primary data model. + */ + def createForPrimaryData[PD <: PrimaryData: ClassTag]( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + primaryDataExtra: PrimaryDataExtra[PD], + simulationStart: ZonedDateTime, + simulationEnd: ZonedDateTime, + ): ParticipantModelShell[_, _] = { + val model = ParticipantModelInit.createPrimaryModel( + participantInput, + config, + primaryDataExtra, + ) + createShell( + model, + participantInput, + simulationEnd, + simulationStart, + ) + } + + /** Creates a model shell for a physical model using the given participant + * input. + * + * @param participantInput + * The physical participant model. + * @param config + * Runtime configuration that has to match the participant type. + * @param simulationStart + * The simulation start date and time. + * @param simulationEnd + * The simulation end date and time. + * @return + * The constructed [[ParticipantModelShell]] with a physical model. + */ + def createForPhysicalModel( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + simulationStart: ZonedDateTime, + simulationEnd: ZonedDateTime, + ): ParticipantModelShell[_, _] = { + val model = ParticipantModelInit.createPhysicalModel( + participantInput, + config, + ) + createShell( + model, + participantInput, + simulationEnd, + simulationStart, + ) + } + + private def createShell[ + OP <: OperatingPoint, + S <: ModelState, + ]( + model: ParticipantModel[OP, S], + participantInput: SystemParticipantInput, + simulationEndDate: ZonedDateTime, + simulationStartDate: ZonedDateTime, + ): ParticipantModelShell[OP, S] = { + + val operationInterval: OperationInterval = + SystemComponent.determineOperationInterval( + simulationStartDate, + simulationEndDate, + participantInput.getOperationTime, + ) + + new ParticipantModelShell( + model = model, + operationInterval = operationInterval, + simulationStartDate = simulationStartDate, + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala new file mode 100644 index 0000000000..e86302dd8c --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -0,0 +1,206 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.{ + PrimaryData, + PrimaryDataExtra, +} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + EnrichableData, + PrimaryDataWithComplexPower, +} +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelInput, + ModelState, + OperatingPoint, + OperationChangeIndicator, +} +import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel._ +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.scala.quantities.{ApparentPower, ReactivePower} +import squants.Power + +import java.time.ZonedDateTime +import java.util.UUID +import scala.reflect.ClassTag + +/** A [[ParticipantModel]] that does not do any physical calculations, but just + * "replays" the primary data that it received via model input. It is used in + * place of a physical [[ParticipantModel]] and thus needs to produce the same + * type of results. + * + * @param primaryDataResultFunc + * Function that can create the typical result objects produced by the + * physical [[ParticipantModel]]. + * @param primaryDataExtra + * Extra functionality specific to the primary data class. + * @tparam PD + * The type of primary data. + */ +final case class PrimaryDataParticipantModel[PD <: PrimaryData: ClassTag]( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, + private val primaryDataResultFunc: PrimaryResultFunc, + private val primaryDataExtra: PrimaryDataExtra[PD], +) extends ParticipantModel[ + PrimaryOperatingPoint[PD], + PrimaryDataState[PD], + ] { + + override val initialState: ModelInput => PrimaryDataState[PD] = { input => + val primaryData = getPrimaryData(input.receivedData) + PrimaryDataState( + primaryData, + input.currentTick, + ) + } + + override def determineState( + lastState: PrimaryDataState[PD], + operatingPoint: PrimaryOperatingPoint[PD], + input: ModelInput, + ): PrimaryDataState[PD] = initialState(input) + + private def getPrimaryData(receivedData: Seq[Data]): PD = { + receivedData + .collectFirst { case data: PD => + data + } + .getOrElse { + throw new CriticalFailureException( + "Expected primary data of type " + + s"${implicitly[ClassTag[PD]].runtimeClass.getSimpleName}, " + + s"got $receivedData" + ) + } + } + + override def determineOperatingPoint( + state: PrimaryDataState[PD] + ): (PrimaryOperatingPoint[PD], Option[Long]) = + (PrimaryOperatingPoint(state.data), None) + + override def zeroPowerOperatingPoint: PrimaryOperatingPoint[PD] = + PrimaryOperatingPoint(primaryDataExtra.zero) + + override def createResults( + state: PrimaryDataState[PD], + lastOperatingPoint: Option[PrimaryOperatingPoint[PD]], + currentOperatingPoint: PrimaryOperatingPoint[PD], + complexPower: ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = { + val primaryDataWithApparentPower = currentOperatingPoint.data match { + case primaryDataWithApparentPower: PrimaryDataWithComplexPower[_] => + primaryDataWithApparentPower + case enrichableData: EnrichableData[_] => + enrichableData.add(complexPower.q) + } + Iterable( + primaryDataResultFunc.createResult(primaryDataWithApparentPower, dateTime) + ) + } + + override def createPrimaryDataResult( + data: PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = throw new CriticalFailureException( + "Method not implemented by this model." + ) + + override def getRequiredSecondaryServices: Iterable[ServiceType] = { + // only secondary services should be specified here + Iterable.empty + } + + override def determineFlexOptions( + state: PrimaryDataState[PD] + ): FlexibilityMessage.ProvideFlexOptions = { + val (operatingPoint, _) = determineOperatingPoint(state) + + ProvideMinMaxFlexOptions.noFlexOption(uuid, operatingPoint.activePower) + } + + override def determineOperatingPoint( + state: PrimaryDataState[PD], + setPower: Power, + ): (PrimaryOperatingPoint[PD], OperationChangeIndicator) = { + // scale the whole primary data by the same factor that + // the active power set point was scaled by + val factor = state.data.p / setPower + val scaledData: PD = primaryDataExtra.scale(state.data, factor) + + (PrimaryOperatingPoint(scaledData), OperationChangeIndicator()) + } + +} + +object PrimaryDataParticipantModel { + + final case class PrimaryDataState[+PD <: PrimaryData]( + data: PD, + override val tick: Long, + ) extends ModelState + + trait PrimaryOperatingPoint[+PD <: PrimaryData] extends OperatingPoint { + val data: PD + + override val activePower: Power = data.p + } + + private object PrimaryOperatingPoint { + def apply[PD <: PrimaryData: ClassTag]( + data: PD + ): PrimaryOperatingPoint[PD] = + data match { + case apparentPowerData: PD with PrimaryDataWithComplexPower[_] => + PrimaryApparentPowerOperatingPoint(apparentPowerData) + case other: PD with EnrichableData[_] => + PrimaryActivePowerOperatingPoint(other) + } + } + + private final case class PrimaryApparentPowerOperatingPoint[ + PD <: PrimaryDataWithComplexPower[_] + ](override val data: PD) + extends PrimaryOperatingPoint[PD] { + override val reactivePower: Option[ReactivePower] = Some(data.q) + } + + private final case class PrimaryActivePowerOperatingPoint[ + PE <: PrimaryData with EnrichableData[_]: ClassTag + ]( + override val data: PE + ) extends PrimaryOperatingPoint[PE] { + override val reactivePower: Option[ReactivePower] = None + } + + /** Trait that provides functionality that can create the same result objects + * as the corresponding physical object. + * + * The function needs to be packaged in a trait in order to be stored in a + * val. + */ + trait PrimaryResultFunc { + def createResult( + data: PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala new file mode 100644 index 0000000000..fd4c8b4515 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -0,0 +1,107 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.input.system.PvInput +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ModelInput, + ModelState, +} +import edu.ie3.simona.model.participant2.PvModel.PvState +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities._ + +import java.time.ZonedDateTime +import java.util.UUID + +class PvModel private ( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, +) extends ParticipantModel[ + ActivePowerOperatingPoint, + PvState, + ] + with ParticipantSimpleFlexibility[PvState] { + + override val initialState: ModelInput => PvState = { input => + PvState( + input.currentTick + ) + } + + override def determineState( + lastState: PvState, + operatingPoint: ActivePowerOperatingPoint, + input: ModelInput, + ): PvState = throw new NotImplementedError("Dummy implementation") + + override def determineOperatingPoint( + state: PvState + ): (ActivePowerOperatingPoint, Option[Long]) = throw new NotImplementedError( + "Dummy implementation" + ) + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + throw new NotImplementedError("Dummy implementation") + + override def createResults( + state: PvState, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = throw new NotImplementedError( + "Dummy implementation" + ) + + override def createPrimaryDataResult( + data: PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = throw new NotImplementedError( + "Dummy implementation" + ) + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable(ServiceType.WeatherService) + +} + +object PvModel { + + final case class PvState( + override val tick: Long + ) extends ModelState + + def apply( + input: PvInput + ): PvModel = + new PvModel( + input.getUuid, + input.getId, + Kilovoltamperes( + input.getsRated + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ), + input.getCosPhiRated, + QControl(input.getqCharacteristics), + ) + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala new file mode 100644 index 0000000000..730592c146 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala @@ -0,0 +1,95 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + FixedState, +} +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} + +import java.time.ZonedDateTime +import java.util.UUID + +class LoadModel private ( + override val uuid: UUID, + override val id: String, + override val sRated: ApparentPower, + override val cosPhiRated: Double, + override val qControl: QControl, +) extends ParticipantModel[ + ActivePowerOperatingPoint, + FixedState, + ] + with ParticipantSimpleFlexibility[FixedState] { + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: FixedState, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + throw new NotImplementedError("Dummy implementation") + + override def createPrimaryDataResult( + data: PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + throw new NotImplementedError("Dummy implementation") + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable.empty + + override val initialState: ParticipantModel.ModelInput => FixedState = + _ => FixedState(-1) + + override def determineState( + lastState: FixedState, + operatingPoint: ActivePowerOperatingPoint, + input: ParticipantModel.ModelInput, + ): FixedState = throw new NotImplementedError("Dummy implementation") + + override def determineOperatingPoint( + state: FixedState + ): (ActivePowerOperatingPoint, Option[Long]) = throw new NotImplementedError( + "Dummy implementation" + ) +} + +object LoadModel { + + def apply( + input: LoadInput + ): LoadModel = + new LoadModel( + input.getUuid, + input.getId, + Kilovoltamperes( + input.getsRated + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ), + input.getCosPhiRated, + QControl(input.getqCharacteristics), + ) +} diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala index 60cbd2eca9..04ce315763 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -31,6 +31,7 @@ import squants.{Energy, Power, Temperature} import java.time.ZonedDateTime import scala.jdk.CollectionConverters.SetHasAsScala +import scala.language.postfixOps /** Calculation model for a thermal grid. It is assumed, that all elements are * connected directly with exactly one thermal bus @@ -148,82 +149,345 @@ final case class ThermalGrid( * * @param relevantData * data of heat pump including state of the heat pump - * @param state - * Currently applicable state + * @param lastThermalGridState + * state of the thermalGrid until this tick * @param lastAmbientTemperature * Ambient temperature valid up until (not including) the current tick + * @param isRunning + * determines whether the heat pump is running or not * @param qDot - * Thermal energy balance + * Infeed to the grid from thermal generation (e.g. heat pump) or thermal + * storages + * @param thermalDemands + * holds the thermal demands of the thermal units (house, storage) * @return * The updated state of the grid */ def updateState( relevantData: HpRelevantData, - state: ThermalGridState, + lastThermalGridState: ThermalGridState, lastAmbientTemperature: Temperature, + isRunning: Boolean, qDot: Power, + thermalDemands: ThermalDemandWrapper, ): (ThermalGridState, Option[ThermalThreshold]) = if (qDot > zeroKW) - handleInfeed(relevantData, lastAmbientTemperature, state, qDot) + handleInfeed( + relevantData, + lastAmbientTemperature, + lastThermalGridState, + isRunning, + qDot, + thermalDemands, + ) else handleConsumption( relevantData, lastAmbientTemperature, - state, + lastThermalGridState, qDot, ) - /** Handles the case, when a grid has infeed. First, heat up all the houses to - * their maximum temperature, then fill up the storages + /** Handles the case, when a grid has infeed. Depending on which entity has + * some heat demand the house or the storage will be heated up / filled up. + * First the actions from lastState will be considered and checked if the + * behaviour should be continued. This might be the case, if we got activated + * by updated weather data. If this is not the case, all other cases will be + * handled by [[ThermalGrid.handleFinalInfeedCases]] * * @param relevantData * data of heat pump including state of the heat pump * @param lastAmbientTemperature * Ambient temperature valid up until (not including) the current tick - * @param state - * Current state of the houses + * @param lastThermalGridState + * state of the thermalGrid until this tick + * @param isRunning + * determines whether the heat pump is running or not * @param qDot - * Infeed to the grid + * Infeed to the grid from thermal generation (e.g. heat pump) or thermal + * storages + * @param thermalDemands + * holds the thermal demands of the thermal units (house, storage) * @return - * Updated thermal grid state + * Updated thermal grid state and the thermalThreshold if there is one */ private def handleInfeed( relevantData: HpRelevantData, lastAmbientTemperature: Temperature, - state: ThermalGridState, + lastThermalGridState: ThermalGridState, + isRunning: Boolean, qDot: Power, - ): (ThermalGridState, Option[ThermalThreshold]) = - house.zip(state.houseState) match { - case Some((thermalHouse, lastHouseState)) => - /* Set thermal power exchange with storage to zero */ - // TODO: We would need to issue a storage result model here... - val updatedStorageState = heatStorage.zip(state.storageState) match { - case Some((thermalStorage, storageState)) => - Some( - thermalStorage - .updateState( - relevantData.currentTick, - zeroKW, - storageState, - ) - ._1 - ) - case _ => state.storageState - } + thermalDemands: ThermalDemandWrapper, + ): (ThermalGridState, Option[ThermalThreshold]) = { + // TODO: We would need to issue a storage result model here... + + /* Consider the action in the last state */ + val qDotHouseLastState = + lastThermalGridState.houseState.map(_.qDot).getOrElse(zeroKW) + val qDotStorageLastState = + lastThermalGridState.storageState.map(_.qDot).getOrElse(zeroKW) + + // We can use the qDots from lastState to keep continuity. If... + if ( + // ... house was heated in lastState but not from Storage and has still some demand. Hp must still run for this. + ((qDotHouseLastState > zeroKW && (qDotStorageLastState >= zeroKW) && thermalDemands.houseDemand.hasAdditionalDemand) && isRunning || + // ... storage was filled up in the lastState and has still additional demand + // But only if the house not reached some requiredDemand. Hp must still run for this. + qDotStorageLastState > zeroKW && thermalDemands.heatStorageDemand.hasAdditionalDemand && !thermalDemands.houseDemand.hasRequiredDemand && isRunning) + ) { + // We can continue for the house + val (updatedHouseState, thermalHouseThreshold, remainingQDotHouse) = + handleInfeedHouse( + relevantData, + lastAmbientTemperature, + lastThermalGridState, + qDotHouseLastState, + ) - val (updatedHouseState, maybeHouseThreshold) = - thermalHouse.determineState( - relevantData, - lastHouseState, - lastAmbientTemperature, - qDot, + // ...and for the storage + val (updatedStorageState, thermalStorageThreshold) = { + // In case the ThermalHouse could not handle the infeed it will be used for the storage. + if (remainingQDotHouse > qDotStorageLastState) { + handleStorageCases( + relevantData.currentTick, + lastThermalGridState, + remainingQDotHouse, + ) + } else { + handleStorageCases( + relevantData.currentTick, + lastThermalGridState, + qDotStorageLastState, ) + } + } + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + ( + lastThermalGridState.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, + ) + } + // Handle edge case where house was heated from storage... + else if (qDotHouseLastState > zeroKW && qDotStorageLastState < zeroKW) { + // ...and HP gets activated in current tick + if (isRunning) { + handleCases( + relevantData, + lastAmbientTemperature, + lastThermalGridState, + qDot, + zeroKW, + ) + } else { + // ... or continue lastState's behaviour + handleCases( + relevantData, + lastAmbientTemperature, + lastThermalGridState, + qDotHouseLastState, + qDotStorageLastState, + ) + } + } + // Handle edge case where house should be heated from storage + else if (!isRunning && qDot > zeroKW) { + handleCases( + relevantData, + lastAmbientTemperature, + lastThermalGridState, + qDot, + -qDot, + ) + } + // or finally check for all other cases. + else + handleFinalInfeedCases( + thermalDemands, + relevantData, + lastAmbientTemperature, + lastThermalGridState, + qDot, + ) + } + + /** Handles the last cases of [[ThermalGrid.handleInfeed]], where the thermal + * infeed should be determined. + * + * | house req. demand | house add. demand | storage req. demand | storage add. demand | qDot to house | qDot to storage | + * |:------------------|:------------------|:--------------------|:--------------------|:--------------|:----------------| + * | true | true | true | true | true | false | + * | true | true | true | false | true | false | + * | true | true | false | true | true | false | + * | true | true | false | false | true | false | + * | true | false | true | true | true | false | + * | true | false | true | false | true | false | + * | true | false | false | true | true | false | + * | true | false | false | false | true | false | + * | false | true | true | true | false | true | + * | false | true | true | false | false | true | + * | false | true | false | true | false | true | + * | false | true | false | false | true | false | + * | false | false | true | true | false | true | + * | false | false | true | false | false | true | + * | false | false | false | true | false | true | + * | false | false | false | false | false | false | + * + * This can be simplified to four cases + * | No | Conditions | Result | + * |:---|:-------------------------------------|:----------| + * | 1 | if house.reqD | house | + * | 2 | else if storage.reqD OR storage.addD | storage | + * | 3 | else if house.addD | house | + * | 4 | else | no output | + * + * @param thermalDemands + * holds the thermal demands of the thermal units (house, storage) + * @param relevantData + * data of heat pump including state of the heat pump + * @param lastAmbientTemperature + * Ambient temperature valid up until (not including) the current tick + * @param gridState + * Current state of the thermalGrid + * @param qDot + * Infeed to the grid from thermal generation (e.g. heat pump) or thermal + * storages + * @return + * Updated thermal grid state and the thermalThreshold if there is one + */ + private def handleFinalInfeedCases( + thermalDemands: ThermalDemandWrapper, + relevantData: HpRelevantData, + lastAmbientTemperature: Temperature, + gridState: ThermalGridState, + qDot: Power, + ): (ThermalGridState, Option[ThermalThreshold]) = { + + if (thermalDemands.houseDemand.hasRequiredDemand) + handleCases( + relevantData, + lastAmbientTemperature, + gridState, + qDot, + zeroKW, + ) + else if ( + thermalDemands.heatStorageDemand.hasRequiredDemand || thermalDemands.heatStorageDemand.hasAdditionalDemand + ) + handleCases( + relevantData, + lastAmbientTemperature, + gridState, + zeroKW, + qDot, + ) + else if (thermalDemands.houseDemand.hasAdditionalDemand) + handleCases( + relevantData, + lastAmbientTemperature, + gridState, + qDot, + zeroKW, + ) + else + handleCases( + relevantData, + lastAmbientTemperature, + gridState, + zeroKW, + zeroKW, + ) + + } + + /** Handles the different cases, of thermal flows from and into the thermal + * grid. + * + * @param relevantData + * data of heat pump including state of the heat pump + * @param lastAmbientTemperature + * Ambient temperature until this tick + * @param state + * Current state of the thermal grid + * @param qDotHouse + * Infeed to the house + * @param qDotHeatStorage + * Infeed to the heat storage (positive: Storage is charging, negative: + * Storage is discharging) + * @return + * Updated thermal grid state and the next threshold if there is one + */ + private def handleCases( + relevantData: HpRelevantData, + lastAmbientTemperature: Temperature, + state: ThermalGridState, + qDotHouse: Power, + qDotHeatStorage: Power, + ): (ThermalGridState, Option[ThermalThreshold]) = { + val (updatedHouseState, thermalHouseThreshold, _) = + handleInfeedHouse( + relevantData, + lastAmbientTemperature, + state, + qDotHouse, + ) + + val (updatedStorageState, thermalStorageThreshold) = + handleStorageCases(relevantData.currentTick, state, qDotHeatStorage) + + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + + ( + state.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, + ) + } + + /** Handles the case, when the house has heat demand and will be heated up + * here. + * + * @param relevantData + * data of heat pump including state of the heat pump + * @param lastAmbientTemperature + * Ambient temperature until this tick + * @param state + * Current state of the houses + * @param qDotHouse + * Infeed into the house + * @return + * Updated thermal house state, a ThermalThreshold and the remaining qDot + */ + private def handleInfeedHouse( + relevantData: HpRelevantData, + lastAmbientTemperature: Temperature, + state: ThermalGridState, + qDotHouse: Power, + ): (Option[ThermalHouseState], Option[ThermalThreshold], Power) = { + (house, state.houseState) match { + case (Some(thermalHouse), Some(lastHouseState)) => + val (newState, threshold) = thermalHouse.determineState( + relevantData, + lastHouseState, + lastAmbientTemperature, + qDotHouse, + ) + /* Check if house can handle the thermal feed in */ if ( thermalHouse.isInnerTemperatureTooHigh( - updatedHouseState.innerTemperature + newState.innerTemperature ) ) { - /* The house is already heated up fully, set back the infeed and put it into storage, if available */ val (fullHouseState, maybeFullHouseThreshold) = thermalHouse.determineState( relevantData, @@ -231,63 +495,53 @@ final case class ThermalGrid( lastAmbientTemperature, zeroKW, ) - heatStorage.zip(updatedStorageState) match { - case Some((thermalStorage, storageState)) => - val (updatedStorageState, maybeStorageThreshold) = - thermalStorage.updateState( - relevantData.currentTick, - qDot, - storageState, - ) - - /* Both house and storage are updated. Determine what reaches the next threshold */ - val nextThreshold = determineMostRecentThreshold( - maybeFullHouseThreshold, - maybeStorageThreshold, - ) - - ( - state.copy( - houseState = Some(fullHouseState), - storageState = Some(updatedStorageState), - ), - nextThreshold, - ) - case None => - /* There is no storage, house determines the next activation */ - ( - state.copy(houseState = Some(fullHouseState)), - maybeFullHouseThreshold, - ) - } + (Some(fullHouseState), maybeFullHouseThreshold, qDotHouse) } else { - /* The house can handle the infeed */ - ( - state.copy(houseState = Some(updatedHouseState)), - maybeHouseThreshold, - ) + (Some(newState), threshold, zeroKW) } + case _ => (None, None, zeroKW) + } + } - case None => - heatStorage.zip(state.storageState) match { - case Some((thermalStorage, storageState)) => - val (updatedStorageState, maybeStorageThreshold) = - thermalStorage.updateState( - relevantData.currentTick, - qDot, - storageState, - ) - ( - state.copy(storageState = Some(updatedStorageState)), - maybeStorageThreshold, - ) - case None => - throw new InconsistentStateException( - "A thermal grid has to contain either at least a house or a storage." - ) - } + /** Handles the cases, when the storage has heat demand and will be filled up + * here (positive qDot) or will return its stored energy into the thermal + * grid (negative qDot). + * @param tick + * Current tick + * @param state + * Current state of the houses + * @param qDotStorage + * Infeed to the storage (positive: Storage is charging, negative: Storage + * is discharging) + * @return + * Updated thermal grid state + */ + private def handleStorageCases( + tick: Long, + state: ThermalGridState, + qDotStorage: Power, + ): (Option[ThermalStorageState], Option[ThermalThreshold]) = { + (storage, state.storageState) match { + case (Some(thermalStorage), Some(lastStorageState)) => + val (newState, threshold) = thermalStorage.updateState( + tick, + qDotStorage, + lastStorageState, + ) + (Some(newState), threshold) + case _ => (None, None) } + } + /** Determines the most recent threshold of two given input thresholds + * + * @param maybeHouseThreshold + * Option of a possible next threshold of the thermal house + * @param maybeStorageThreshold + * Option of a possible next threshold of the thermal storage + * @return + * The next threshold + */ private def determineMostRecentThreshold( maybeHouseThreshold: Option[ThermalThreshold], maybeStorageThreshold: Option[ThermalThreshold], @@ -309,34 +563,37 @@ final case class ThermalGrid( * data of heat pump including state of the heat pump * @param lastAmbientTemperature * Ambient temperature valid up until (not including) the current tick - * @param state - * Current state of the houses + * @param lastThermalGridState + * state of the thermalGrid until this tick * @param qDot - * Infeed to the grid + * Infeed to the grid from thermal generation (e.g. heat pump) or thermal + * storages * @return * Updated thermal grid state */ private def handleConsumption( relevantData: HpRelevantData, lastAmbientTemperature: Temperature, - state: ThermalGridState, + lastThermalGridState: ThermalGridState, qDot: Power, ): (ThermalGridState, Option[ThermalThreshold]) = { /* House will be left with no influx in all cases. Determine if and when a threshold is reached */ val maybeUpdatedHouseState = - house.zip(state.houseState).map { case (house, houseState) => - house.determineState( - relevantData, - houseState, - lastAmbientTemperature, - zeroMW, - ) + house.zip(lastThermalGridState.houseState).map { + case (house, houseState) => + house.determineState( + relevantData, + houseState, + lastAmbientTemperature, + zeroMW, + ) } /* Update the state of the storage */ val maybeUpdatedStorageState = - heatStorage.zip(state.storageState).map { case (storage, storageState) => - storage.updateState(relevantData.currentTick, qDot, storageState) + heatStorage.zip(lastThermalGridState.storageState).map { + case (storage, storageState) => + storage.updateState(relevantData.currentTick, qDot, storageState) } val (revisedHouseState, revisedStorageState) = @@ -344,8 +601,8 @@ final case class ThermalGrid( relevantData, maybeUpdatedHouseState, maybeUpdatedStorageState, - state.houseState, - state.storageState, + lastThermalGridState.houseState, + lastThermalGridState.storageState, lastAmbientTemperature, qDot, ) @@ -356,7 +613,7 @@ final case class ThermalGrid( ) ( - state.copy( + lastThermalGridState.copy( houseState = revisedHouseState.map(_._1), storageState = revisedStorageState.map(_._1), ), @@ -382,7 +639,8 @@ final case class ThermalGrid( * @param lastAmbientTemperature * Ambient temperature valid up until (not including) the current tick * @param qDot - * Thermal influx + * Infeed to the grid from thermal generation (e.g. heat pump) or thermal + * storages * @return * Options to revised thermal house and storage state */ @@ -562,7 +820,11 @@ object ThermalGrid { * than or equal to the absolutely required energy. Thus, this class can only * be instantiated via factory. * @param required - * The absolutely required energy to reach target state + * The absolutely required energy to reach target state. For + * [[ThermalHouse]] this would be the energy demand to reach the boundary + * or targetTemperature. For [[ThermalStorage]] this would be the amount of + * energy to get fully charged when empty. If the [[ThermalStorage]] is not + * empty, the required energy is zero. * @param possible * The maximum possible energy, that can be handled */ diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala index 22cf302028..a16840a081 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala @@ -167,8 +167,8 @@ final case class ThermalHouse( innerTemperature: Temperature, boundaryTemperature: Temperature = upperBoundaryTemperature, ): Boolean = - innerTemperature > Kelvin( - boundaryTemperature.toKelvinScale - temperatureTolerance.toKelvinScale + innerTemperature > ( + boundaryTemperature - temperatureTolerance ) /** Check if inner temperature is lower than preferred minimum temperature @@ -180,8 +180,8 @@ final case class ThermalHouse( innerTemperature: Temperature, boundaryTemperature: Temperature = lowerBoundaryTemperature, ): Boolean = - innerTemperature < Kelvin( - boundaryTemperature.toKelvinScale + temperatureTolerance.toKelvinScale + innerTemperature < ( + boundaryTemperature + temperatureTolerance ) /** Calculate the new inner temperature of the thermal house. @@ -380,7 +380,7 @@ object ThermalHouse { * @param innerTemperature * Inner temperature of the house * @param qDot - * Continuous infeed of thermal energy since the given tick + * Continuous external infeed of thermal energy since the given tick */ final case class ThermalHouseState( tick: Long, diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalStorage.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalStorage.scala index 47b079a04d..2e4de92ec1 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalStorage.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalStorage.scala @@ -69,6 +69,17 @@ abstract class ThermalStorage( } object ThermalStorage { + + /** State of a thermal storage + * + * @param tick + * Last tick of storage state change + * @param storedEnergy + * Energy stored in the storage at this tick + * @param qDot + * Infeed to the heat storage (positive: Storage is charging, negative: + * Storage is discharging) + */ final case class ThermalStorageState( tick: Long, storedEnergy: Energy, diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/flex/FlexibilityMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/flex/FlexibilityMessage.scala index 662c11e2d2..58b95e3e1d 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/flex/FlexibilityMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/flex/FlexibilityMessage.scala @@ -22,7 +22,7 @@ import java.util.UUID object FlexibilityMessage { /** Trait that is extended by all messages that are supposed to be received by - * a flex options provider, which could be any + * a controlled asset model, which could be any * [[edu.ie3.simona.agent.participant.ParticipantAgent]] or * [[edu.ie3.simona.agent.em.EmAgent]], if it is EM-controlled. */ @@ -30,48 +30,47 @@ object FlexibilityMessage { val tick: Long } - /** Trait that is extended by all messages that are supposed to be received by + /** Trait that is extended by all messages that are received by * [[edu.ie3.simona.agent.em.EmAgent]]s. */ sealed trait FlexResponse extends EmAgent.Request { val modelUuid: UUID } - /** Message that registers a flex options provider with an + /** Message that registers a controlled asset model with an * [[edu.ie3.simona.agent.em.EmAgent]]. * - * @param modelUuid - * The UUID of the flex options provider asset model * @param participant - * The actor reference to the flex options provider + * The actor reference to the controlled asset model * @param inputModel - * The asset input model of the flex options provider + * The asset input model of the controlled asset model */ - final case class RegisterParticipant( - override val modelUuid: UUID, + final case class RegisterControlledAsset( participant: ActorRef[FlexRequest], inputModel: AssetInput, - ) extends FlexResponse + ) extends FlexResponse { + override val modelUuid: UUID = inputModel.getUuid + } - /** Message that schedules a flex request for a flex options provider at given - * tick. + /** Message that schedules a flex activation for a controlled asset model at + * given tick. * * @param modelUuid - * The UUID of the flex options provider asset model + * The UUID of the controlled asset model * @param tick - * The tick to schedule the flex options provider for + * The tick to schedule the controlled asset model for * @param scheduleKey * Optionally a schedule key that unlocks the scheduler once the scheduling * chain is completed */ - final case class ScheduleFlexRequest( + final case class ScheduleFlexActivation( override val modelUuid: UUID, tick: Long, scheduleKey: Option[ScheduleKey] = None, ) extends FlexResponse - /** Message that activates a connected agent, usually in order to requests - * flex options for given tick. During initialization, no flex option + /** Message that activates a controlled asset agent, usually in order to + * request flex options for given tick. During initialization, no flex option * provision is expected. * * @param tick @@ -85,8 +84,8 @@ object FlexibilityMessage { */ trait ProvideFlexOptions extends FlexResponse - /** Message that issues flexibility control to a flex options provider, i.e. a - * feasible set point is delivered that the flex options provider should + /** Message that issues flexibility control to a controlled asset model, i.e. + * a feasible set point is delivered that the controlled asset model should * adhere to */ trait IssueFlexControl extends FlexRequest @@ -115,15 +114,15 @@ object FlexibilityMessage { final case class IssueNoControl(override val tick: Long) extends IssueFlexControl - /** Message sent by flex options providers that transports the result after + /** Message sent by controlled asset models that transports the result after * flex control has been handled. Has to be sent before [[FlexCompletion]], * but is not required during initialization. * * @param modelUuid - * The UUID of the flex options provider asset model + * The UUID of the controlled asset model * @param result - * The apparent power that is produced/consumed by the flex options - * provider, which can deviate from the set point communicated by a + * The apparent power that is produced/consumed by the controlled asset + * model, which can deviate from the set point communicated by a * [[IssueFlexControl]] message if it is not feasible. */ final case class FlexResult( @@ -131,19 +130,19 @@ object FlexibilityMessage { result: ComplexPower, ) extends FlexResponse - /** Message sent by flex options providers indicating that the + /** Message sent by controlled asset models indicating that the * [[IssueFlexControl]] message has been handled and the flex communication * for the current tick is completed. * * @param modelUuid - * The UUID of the flex options provider asset model + * The UUID of the controlled asset model * @param requestAtNextActivation * Whether to request flex options at the very next activation of the * receiving EM agent. This is the case if flex options change the very * next second after the current tick. * @param requestAtTick * Optionally the tick at which flex options are foreseen to have changed, - * i.e. the tick at which the flex options provider would like to be + * i.e. the tick at which the controlled asset model would like to be * activated at the latest. */ final case class FlexCompletion( diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala index 1e8ae341a9..0c1cca56ba 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala @@ -8,11 +8,7 @@ package edu.ie3.simona.ontology.messages.services import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.model.participant.evcs.EvModelWrapper -import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ - ProvisionMessage, - ServiceRegistrationMessage, -} -import org.apache.pekko.actor.ActorRef +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ServiceRegistrationMessage import java.util.UUID @@ -33,24 +29,6 @@ object EvMessage { trait EvData extends SecondaryData - /** Provide EV movements for the requested tick - * - * @param tick - * The tick, for which the data is requested for - * @param data - * Actual information - * @param nextDataTick - * Foreseen next tick, where data is available. Usually, no ticks can be - * foreseen within evs - */ - final case class ProvideEvDataMessage( - override val tick: Long, - override val serviceRef: ActorRef, - override val data: EvData, - override val nextDataTick: Option[Long], - ) extends EvMessage - with ProvisionMessage[EvData] - /** Requests number of free lots from evcs * * @param tick diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/PrimaryDataMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/PrimaryDataMessage.scala deleted file mode 100644 index f78e1f5fe4..0000000000 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/PrimaryDataMessage.scala +++ /dev/null @@ -1,34 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.ontology.messages.services - -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage -import org.apache.pekko.actor.ActorRef - -sealed trait PrimaryDataMessage - -object PrimaryDataMessage { - - /** Provides primary data in the form of [[ComplexPower]] - * - * @param tick - * Tick, the data belongs to - * @param data - * The actual payload - * @param nextDataTick - * Option to the next tick, when data is available - */ - @deprecated - final case class ApparentPowerProvisionMessage( - override val tick: Long, - override val serviceRef: ActorRef, - override val data: ComplexPower, - override val nextDataTick: Option[Long], - ) extends ProvisionMessage[ComplexPower] - with PrimaryDataMessage -} diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala index d7444454fd..43ba7240cd 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala @@ -28,8 +28,10 @@ object ServiceMessage { * @param inputModelUuid * Identifier of the input model */ - final case class PrimaryServiceRegistrationMessage(inputModelUuid: UUID) - extends ServiceRegistrationMessage + final case class PrimaryServiceRegistrationMessage( + requestingActor: ActorRef, + inputModelUuid: UUID, + ) extends ServiceRegistrationMessage /** This message can be sent from a proxy to a subordinate worker in order to * forward the original registration request. This message may only be used, @@ -41,44 +43,9 @@ object ServiceMessage { final case class WorkerRegistrationMessage(requestingActor: ActorRef) extends ServiceRegistrationMessage - sealed trait RegistrationResponseMessage extends ServiceMessage { - val serviceRef: ActorRef - } - - object RegistrationResponseMessage { - - /** Message, that is used to confirm a successful registration - */ - final case class RegistrationSuccessfulMessage( - override val serviceRef: ActorRef, - nextDataTick: Option[Long], - ) extends RegistrationResponseMessage - - /** Message, that is used to announce a failed registration - */ - final case class RegistrationFailedMessage( - override val serviceRef: ActorRef - ) extends RegistrationResponseMessage - - final case class ScheduleServiceActivation( - tick: Long, - unlockKey: ScheduleKey, - ) - } - - /** Actual provision of data - * - * @tparam D - * type of data that is delivered - */ - trait ProvisionMessage[D <: Data] extends ServiceMessage { - val tick: Long - val serviceRef: ActorRef - val data: D + final case class ScheduleServiceActivation( + tick: Long, + unlockKey: ScheduleKey, + ) - /** Next tick at which data could arrive. If None, no data is expected for - * the rest of the simulation - */ - val nextDataTick: Option[Long] - } } diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala index cb9e8349ba..a06ed97512 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala @@ -7,12 +7,8 @@ package edu.ie3.simona.ontology.messages.services import edu.ie3.simona.agent.participant.data.Data.SecondaryData -import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ - ProvisionMessage, - ServiceRegistrationMessage, -} +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ServiceRegistrationMessage import edu.ie3.util.scala.quantities.Irradiance -import org.apache.pekko.actor.ActorRef import squants.{Temperature, Velocity} sealed trait WeatherMessage @@ -39,23 +35,6 @@ object WeatherMessage { ) extends WeatherMessage with ServiceRegistrationMessage - /** Provide weather for the requested tick - * - * @param tick - * The tick, for which the data is requested for - * @param data - * Actual information - * @param nextDataTick - * Foreseen next tick, where data is available - */ - final case class ProvideWeatherMessage( - override val tick: Long, - override val serviceRef: ActorRef, - override val data: WeatherData, - override val nextDataTick: Option[Long], - ) extends WeatherMessage - with ProvisionMessage[WeatherData] - /** Container class for the entirety of weather information at a certain point * in time and at a certain coordinate * diff --git a/src/main/scala/edu/ie3/simona/service/ServiceType.scala b/src/main/scala/edu/ie3/simona/service/ServiceType.scala new file mode 100644 index 0000000000..a1796cea89 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/service/ServiceType.scala @@ -0,0 +1,18 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.service + +sealed trait ServiceType + +object ServiceType { + + case object WeatherService extends ServiceType + + case object PriceService extends ServiceType + + case object EvMovementService extends ServiceType +} diff --git a/src/main/scala/edu/ie3/simona/service/SimonaService.scala b/src/main/scala/edu/ie3/simona/service/SimonaService.scala index 310c7f0b32..3b8099f5d1 100644 --- a/src/main/scala/edu/ie3/simona/service/SimonaService.scala +++ b/src/main/scala/edu/ie3/simona/service/SimonaService.scala @@ -6,16 +6,16 @@ package edu.ie3.simona.service -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{Actor, ActorContext, ActorRef, Stash} import edu.ie3.simona.logging.SimonaActorLogging import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.ScheduleServiceActivation -import edu.ie3.simona.ontology.messages.services.ServiceMessage.ServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ + ScheduleServiceActivation, + ServiceRegistrationMessage, +} import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.service.ServiceStateData.{ InitializeServiceStateData, @@ -23,6 +23,8 @@ import edu.ie3.simona.service.ServiceStateData.{ } import edu.ie3.simona.service.SimonaService.Create import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.{Actor, ActorContext, ActorRef, Stash} import scala.util.{Failure, Success, Try} diff --git a/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala b/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala index 7f27121552..a777cdfc07 100644 --- a/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala +++ b/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala @@ -6,6 +6,10 @@ package edu.ie3.simona.service.ev +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationSuccessfulMessage, +} import edu.ie3.simona.api.data.ev.ExtEvDataConnection import edu.ie3.simona.api.data.ev.model.EvModel import edu.ie3.simona.api.data.ev.ontology._ @@ -18,7 +22,6 @@ import edu.ie3.simona.exceptions.{ } import edu.ie3.simona.model.participant.evcs.EvModelWrapper import edu.ie3.simona.ontology.messages.services.EvMessage._ -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.ServiceRegistrationMessage import edu.ie3.simona.service.ServiceStateData.{ InitializeServiceStateData, @@ -298,8 +301,10 @@ class ExtEvDataService(override val scheduler: ActorRef) ): (ExtEvStateData, Option[Long]) = { if (tick == INIT_SIM_TICK) { + // During initialization, an empty ProvideArrivingEvs message + // is sent, which includes the first relevant tick - maybeNextTick.getOrElse( + val nextTick = maybeNextTick.getOrElse( throw new CriticalFailureException( s"After initialization, a first simulation tick needs to be provided by the external mobility simulation." ) @@ -308,7 +313,7 @@ class ExtEvDataService(override val scheduler: ActorRef) serviceStateData.uuidToActorRef.foreach { case (_, actor) => actor ! RegistrationSuccessfulMessage( self, - maybeNextTick, + nextTick, ) } @@ -317,7 +322,7 @@ class ExtEvDataService(override val scheduler: ActorRef) val evs = allArrivingEvs.getOrElse(evcs, Seq.empty) - actor ! ProvideEvDataMessage( + actor ! DataProvision( tick, self, ArrivingEvs(evs.map(EvModelWrapper.apply)), diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index 45de3bae63..67b8ddbffb 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -29,6 +29,7 @@ import edu.ie3.datamodel.io.source.{ TimeSeriesMetaInformationSource, } import edu.ie3.datamodel.models.value.Value +import edu.ie3.simona.agent.participant2.ParticipantAgent.RegistrationFailedMessage import edu.ie3.simona.config.SimonaConfig.PrimaryDataCsvParams import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams import edu.ie3.simona.config.SimonaConfig.Simona.Input.{ @@ -41,7 +42,6 @@ import edu.ie3.simona.exceptions.{ import edu.ie3.simona.logging.SimonaActorLogging import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ PrimaryServiceRegistrationMessage, WorkerRegistrationMessage, @@ -244,7 +244,7 @@ case class PrimaryServiceProxy( * Message handling routine */ private def onMessage(stateData: PrimaryServiceStateData): Receive = { - case PrimaryServiceRegistrationMessage(modelUuid) => + case PrimaryServiceRegistrationMessage(requestingActor, modelUuid) => /* Try to register for this model */ stateData.modelToTimeSeries.get(modelUuid) match { case Some(timeSeriesUuid) => @@ -253,14 +253,14 @@ case class PrimaryServiceProxy( modelUuid, timeSeriesUuid, stateData, - sender(), + requestingActor, ) case None => log.debug( s"There is no time series apparent for the model with uuid '{}'.", modelUuid, ) - sender() ! RegistrationFailedMessage(self) + requestingActor ! RegistrationFailedMessage(self) } case x => log.error( diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index f61974392d..7c98b67070 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.service.primary -import org.apache.pekko.actor.{ActorContext, ActorRef, Props} import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme @@ -17,23 +16,27 @@ import edu.ie3.datamodel.io.source.sql.SqlTimeSeriesSource import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.agent.participant.data.Data.PrimaryData.RichValue +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + PrimaryRegistrationSuccessfulMessage, +} import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams -import edu.ie3.simona.exceptions.InitializationException import edu.ie3.simona.exceptions.WeatherServiceException.InvalidRegistrationRequestException import edu.ie3.simona.exceptions.agent.ServiceRegistrationException +import edu.ie3.simona.exceptions.{ + CriticalFailureException, + InitializationException, +} import edu.ie3.simona.ontology.messages.services.ServiceMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage import edu.ie3.simona.service.ServiceStateData.{ InitializeServiceStateData, ServiceActivationBaseStateData, } -import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ - PrimaryServiceInitializedStateData, - ProvidePrimaryDataMessage, -} +import edu.ie3.simona.service.primary.PrimaryServiceWorker.PrimaryServiceInitializedStateData import edu.ie3.simona.service.{ServiceStateData, SimonaService} import edu.ie3.simona.util.TickUtil.{RichZonedDateTime, TickLong} import edu.ie3.util.scala.collection.immutable.SortedDistinctSeq +import org.apache.pekko.actor.{ActorContext, ActorRef, Props} import java.nio.file.Path import java.time.ZonedDateTime @@ -192,9 +195,14 @@ final case class PrimaryServiceWorker[V <: Value]( serviceStateData: PrimaryServiceInitializedStateData[V] ): Try[PrimaryServiceInitializedStateData[V]] = registrationMessage match { case ServiceMessage.WorkerRegistrationMessage(requestingActor) => - requestingActor ! RegistrationSuccessfulMessage( + requestingActor ! PrimaryRegistrationSuccessfulMessage( self, - serviceStateData.maybeNextActivationTick, + serviceStateData.maybeNextActivationTick.getOrElse( + throw new CriticalFailureException( + s"There is no primary data for $requestingActor" + ) + ), + PrimaryData.getPrimaryDataExtra(valueClass), ) val subscribers = serviceStateData.subscribers :+ requestingActor Success(serviceStateData.copy(subscribers = subscribers)) @@ -330,7 +338,7 @@ final case class PrimaryServiceWorker[V <: Value]( ) val provisionMessage = - ProvidePrimaryDataMessage(tick, self, primaryData, maybeNextTick) + DataProvision(tick, self, primaryData, maybeNextTick) serviceBaseStateData.subscribers.foreach(_ ! provisionMessage) (updatedStateData, maybeNextTick) } @@ -438,19 +446,4 @@ object PrimaryServiceWorker { subscribers: Vector[ActorRef] = Vector.empty[ActorRef], ) extends ServiceActivationBaseStateData - /** Provide primary data to subscribes - * - * @param tick - * Current tick - * @param data - * The payload - * @param nextDataTick - * The next tick, when data is available - */ - final case class ProvidePrimaryDataMessage( - override val tick: Long, - override val serviceRef: ActorRef, - override val data: PrimaryData, - override val nextDataTick: Option[Long], - ) extends ServiceMessage.ProvisionMessage[PrimaryData] } 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 a31a217eb0..d1838ab56d 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala @@ -6,14 +6,18 @@ package edu.ie3.simona.service.weather -import org.apache.pekko.actor.{ActorContext, ActorRef, Props} -import edu.ie3.simona.exceptions.InitializationException -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.exceptions.WeatherServiceException.InvalidRegistrationRequestException -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, RegistrationFailedMessage, RegistrationSuccessfulMessage, } +import org.apache.pekko.actor.{ActorContext, ActorRef, Props} +import edu.ie3.simona.exceptions.{ + CriticalFailureException, + InitializationException, +} +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.exceptions.WeatherServiceException.InvalidRegistrationRequestException import edu.ie3.simona.ontology.messages.services.ServiceMessage.ServiceRegistrationMessage import edu.ie3.simona.ontology.messages.services.WeatherMessage._ import edu.ie3.simona.service.SimonaService @@ -226,7 +230,11 @@ final case class WeatherService( case Success(weightedCoordinates) => agentToBeRegistered ! RegistrationSuccessfulMessage( self, - serviceStateData.maybeNextActivationTick, + serviceStateData.maybeNextActivationTick.getOrElse( + throw new CriticalFailureException( + "No first data tick for weather service" + ) + ), ) /* Enhance the mapping from agent coordinate to requesting actor's ActorRef as well as the necessary @@ -252,7 +260,11 @@ final case class WeatherService( // coordinate is already known (= we have data for it), but this actor is not registered yet agentToBeRegistered ! RegistrationSuccessfulMessage( self, - serviceStateData.maybeNextActivationTick, + serviceStateData.maybeNextActivationTick.getOrElse( + throw new CriticalFailureException( + "No first data tick for weather service" + ) + ), ) serviceStateData.copy( @@ -311,7 +323,7 @@ final case class WeatherService( .get(coordinate) .foreach(recipients => recipients.foreach( - _ ! ProvideWeatherMessage( + _ ! DataProvision( tick, self, weatherResult, diff --git a/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala b/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala index 9b06f604da..72df0e49fc 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SetupHelper.scala @@ -6,7 +6,6 @@ package edu.ie3.simona.sim.setup -import com.typesafe.config.{Config => TypesafeConfig} import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.graph.SubGridGate import edu.ie3.datamodel.models.input.container.{SubGridContainer, ThermalGrid} @@ -15,13 +14,16 @@ import edu.ie3.datamodel.models.result.system.FlexOptionsResult import edu.ie3.datamodel.utils.ContainerUtils import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.GridAgentData.GridAgentInitData -import edu.ie3.simona.config.RefSystemParser.ConfigRefSystems +import edu.ie3.simona.config.GridConfigParser.{ + ConfigRefSystems, + ConfigVoltageLimits, +} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.exceptions.InitializationException import edu.ie3.simona.exceptions.agent.GridAgentInitializationException import edu.ie3.simona.io.result.ResultSinkType import edu.ie3.simona.logging.logback.LogbackConfiguration -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.util.ConfigUtil.{GridOutputConfigUtil, OutputConfigUtil} import edu.ie3.simona.util.ResultFileHierarchy.ResultEntityPathConfig import edu.ie3.simona.util.{EntityMapperUtil, ResultFileHierarchy} @@ -62,6 +64,7 @@ trait SetupHelper extends LazyLogging { subGridToActorRef: Map[Int, ActorRef[GridAgent.Request]], gridGates: Set[SubGridGate], configRefSystems: ConfigRefSystems, + configVoltageLimits: ConfigVoltageLimits, thermalGrids: Seq[ThermalGrid], ): GridAgentInitData = { val subGridGateToActorRef = buildGateToActorRef( @@ -74,6 +77,8 @@ trait SetupHelper extends LazyLogging { val refSystem = getRefSystem(configRefSystems, subGridContainer) + val voltageLimits = getVoltageLimits(configVoltageLimits, subGridContainer) + /* Prepare the subgrid container for the agents by adapting the transformer high voltage nodes to be slacks */ val updatedSubGridContainer = ContainerUtils.withTrafoNodeAsSlack(subGridContainer) @@ -84,6 +89,7 @@ trait SetupHelper extends LazyLogging { thermalGrids, subGridGateToActorRef, refSystem, + voltageLimits, ) } @@ -200,18 +206,33 @@ trait SetupHelper extends LazyLogging { refSystem } + def getVoltageLimits( + configVoltageLimits: ConfigVoltageLimits, + subGridContainer: SubGridContainer, + ): VoltageLimits = configVoltageLimits + .find( + subGridContainer.getSubnet, + Some(subGridContainer.getPredominantVoltageLevel), + ) + .getOrElse( + throw new InitializationException( + s"Unable to determine voltage limits for grid with id ${subGridContainer.getSubnet} @ " + + s"volt level ${subGridContainer.getPredominantVoltageLevel}. Please either provide voltage limits for the grid id or the whole volt level!" + ) + ) + /** Build the result file hierarchy based on the provided configuration file. * The provided type safe config must be able to be parsed as * [[SimonaConfig]], otherwise an exception is thrown * - * @param config + * @param simonaConfig * the configuration file * @return * the resulting result file hierarchy */ - def buildResultFileHierarchy(config: TypesafeConfig): ResultFileHierarchy = { - - val simonaConfig = SimonaConfig(config) + def buildResultFileHierarchy( + simonaConfig: SimonaConfig + ): ResultFileHierarchy = { /* Determine the result models to write */ val modelsToWrite = @@ -229,7 +250,7 @@ trait SetupHelper extends LazyLogging { ), configureLogger = LogbackConfiguration.default(simonaConfig.simona.output.log.level), - config = Some(config), + config = Some(simonaConfig), addTimeStampToOutputDir = simonaConfig.simona.output.base.addTimestampToOutputDir, ) 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 0894ad3978..672a5969dd 100644 --- a/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala +++ b/src/main/scala/edu/ie3/simona/sim/setup/SimonaStandaloneSetup.scala @@ -16,10 +16,9 @@ import edu.ie3.simona.agent.EnvironmentRefs import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.GridAgentMessages.CreateGridAgent import edu.ie3.simona.api.ExtSimAdapter -import edu.ie3.simona.api.data.ExtDataConnection import edu.ie3.simona.api.data.ev.ExtEvDataConnection import edu.ie3.simona.api.simulation.ExtSimAdapterData -import edu.ie3.simona.config.{ArgsParser, RefSystemParser, SimonaConfig} +import edu.ie3.simona.config.{ArgsParser, GridConfigParser, SimonaConfig} import edu.ie3.simona.event.listener.{ResultEventListener, RuntimeEventListener} import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} import edu.ie3.simona.exceptions.agent.GridAgentInitializationException @@ -89,8 +88,8 @@ class SimonaStandaloneSetup( ) /* extract and prepare refSystem information from config */ - val configRefSystems = - RefSystemParser.parse(simonaConfig.simona.gridConfig.refSystems) + val (configRefSystems, configVoltageLimits) = + GridConfigParser.parse(simonaConfig.simona.gridConfig) /* Create all agents and map the sub grid id to their actor references */ val subGridToActorRefMap = buildSubGridToActorRefMap( @@ -141,6 +140,7 @@ class SimonaStandaloneSetup( subGridToActorRefMap, subGridGates, configRefSystems, + configVoltageLimits, thermalGrids, ) @@ -388,13 +388,14 @@ object SimonaStandaloneSetup extends LazyLogging with SetupHelper { def apply( typeSafeConfig: Config, + simonaConfig: SimonaConfig, resultFileHierarchy: ResultFileHierarchy, runtimeEventQueue: Option[LinkedBlockingQueue[RuntimeEvent]] = None, mainArgs: Array[String] = Array.empty[String], ): SimonaStandaloneSetup = new SimonaStandaloneSetup( typeSafeConfig, - SimonaConfig(typeSafeConfig), + simonaConfig, resultFileHierarchy, runtimeEventQueue, mainArgs, diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index a91ff99924..e6937991e9 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -19,7 +19,8 @@ import edu.ie3.datamodel.models.result.connector.{ Transformer3WResult, } import edu.ie3.datamodel.models.result.{NodeResult, ResultEntity} -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig +import edu.ie3.simona.config.{RuntimeConfig, SimonaConfig} import edu.ie3.simona.config.SimonaConfig._ import edu.ie3.simona.event.notifier.{Notifier, NotifierConfig} import edu.ie3.simona.exceptions.InvalidConfigParameterException @@ -37,7 +38,7 @@ import scala.util.{Failure, Success, Try, Using} object ConfigUtil { final case class ParticipantConfigUtil private ( - private val configs: Map[UUID, SimonaConfig.BaseRuntimeConfig], + private val configs: Map[UUID, BaseRuntimeConfig], private val defaultConfigs: Map[Class[_], BaseRuntimeConfig], ) { @@ -78,7 +79,7 @@ object ConfigUtil { * a matching config utility */ def apply( - subConfig: SimonaConfig.Simona.Runtime.Participant + subConfig: RuntimeConfig.Participant ): ParticipantConfigUtil = { ParticipantConfigUtil( buildUuidMapping( diff --git a/src/main/scala/edu/ie3/simona/util/ResultFileHierarchy.scala b/src/main/scala/edu/ie3/simona/util/ResultFileHierarchy.scala index bb371308a6..0bfd4a0176 100644 --- a/src/main/scala/edu/ie3/simona/util/ResultFileHierarchy.scala +++ b/src/main/scala/edu/ie3/simona/util/ResultFileHierarchy.scala @@ -9,13 +9,14 @@ package edu.ie3.simona.util import java.io.{BufferedWriter, File, FileWriter} import java.nio.file.{Files, Path, Paths} import java.text.SimpleDateFormat -import com.typesafe.config.{ConfigRenderOptions, Config => TypesafeConfig} +import com.typesafe.config.ConfigRenderOptions import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.io.naming.{ EntityPersistenceNamingStrategy, FileNamingStrategy, } import edu.ie3.datamodel.models.result.ResultEntity +import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.exceptions.FileHierarchyException import edu.ie3.simona.io.result.ResultSinkType import edu.ie3.simona.io.result.ResultSinkType.Csv @@ -46,7 +47,7 @@ object ResultFileHierarchy extends LazyLogging { simulationName: String, resultEntityPathConfig: ResultEntityPathConfig, configureLogger: Path => Unit = LogbackConfiguration.default("INFO"), - config: Option[TypesafeConfig] = None, + config: Option[SimonaConfig] = None, addTimeStampToOutputDir: Boolean = true, ): ResultFileHierarchy = { @@ -214,7 +215,7 @@ object ResultFileHierarchy extends LazyLogging { baseOutputDir: Path, dirsToBeCreated: Seq[Path], resultFileHierarchy: ResultFileHierarchy, - maybeConfig: Option[TypesafeConfig], + maybeConfig: Option[SimonaConfig], ): Unit = { // create output directories if they are not present yet if (!runOutputDirExists(resultFileHierarchy)) @@ -227,7 +228,7 @@ object ResultFileHierarchy extends LazyLogging { maybeConfig.foreach { config => logger.info( "Processing configs for simulation: {}.", - config.getString("simona.simulationName"), + config.simona.simulationName, ) val outFile = @@ -235,7 +236,6 @@ object ResultFileHierarchy extends LazyLogging { val bw = new BufferedWriter(new FileWriter(outFile)) bw.write( config - .root() .render( ConfigRenderOptions .defaults() diff --git a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala index 880f441460..fe80870ac0 100644 --- a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala +++ b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala @@ -6,34 +6,15 @@ package edu.ie3.util.scala -import edu.ie3.util.interval.ClosedInterval +import edu.ie3.util.interval.RightOpenInterval /** Wrapper class for an operation interval, as the superclass - * [[ClosedInterval]] only accepts [[java.lang.Long]] as type parameter + * [[RightOpenInterval]] only accepts [[java.lang.Long]] as type parameter * * @param start * Start of operation period (included) * @param end * End of operation period (included) */ -final case class OperationInterval(start: java.lang.Long, end: java.lang.Long) - extends ClosedInterval[java.lang.Long](start, end) { - - /** Get the first tick, in which the operation starts - * - * @return - * Tick, in which operation starts - */ - def getStart: Long = getLower - - /** Get the last tick, in which the operation end - * - * @return - * Tick, in which operation end - */ - def getEnd: Long = getUpper -} - -object OperationInterval { - def apply(start: Long, end: Long) = new OperationInterval(start, end) -} +final case class OperationInterval(start: Long, end: Long) + extends RightOpenInterval[java.lang.Long](start, end) 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 630f17575d..9efa2145a9 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala @@ -13,7 +13,12 @@ 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.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, +} +import edu.ie3.simona.config.RuntimeConfig._ import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.event.notifier.NotifierConfig @@ -23,12 +28,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ } import edu.ie3.simona.ontology.messages.services.ServiceMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ - RegistrationFailedMessage, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.ontology.messages.services.WeatherMessage.{ - ProvideWeatherMessage, RegisterForWeatherMessage, WeatherData, } @@ -62,7 +62,6 @@ class EmAgentIT with should.Matchers with EmInputTestData with MockitoSugar { - // start a bit later so the sun is up protected implicit val simulationStartDate: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T10:00:00Z") @@ -85,11 +84,8 @@ class EmAgentIT ) override protected val modelConfig: EmRuntimeConfig = EmRuntimeConfig( - calculateMissingReactivePowerWithModel = false, - scaling = 1d, uuids = List("default"), aggregateFlex = "SELF_OPT", - curtailRegenerative = false, ) private implicit val quantityTolerance: Double = 1e-10d @@ -124,11 +120,7 @@ class EmAgentIT initStateData = ParticipantInitializeStateData( loadInput, LoadRuntimeConfig( - calculateMissingReactivePowerWithModel = true, - scaling = 1d, - modelBehaviour = "fix", - reference = "power", - uuids = List.empty, + calculateMissingReactivePowerWithModel = true ), primaryServiceProxy.ref.toClassic, None, @@ -150,7 +142,6 @@ class EmAgentIT pvInput, PvRuntimeConfig( calculateMissingReactivePowerWithModel = true, - scaling = 2d, uuids = List.empty, ), primaryServiceProxy.ref.toClassic, @@ -173,9 +164,7 @@ class EmAgentIT householdStorageInput, StorageRuntimeConfig( calculateMissingReactivePowerWithModel = true, - scaling = 1d, uuids = List.empty, - initialSoc = 0d, targetSoc = None, ), primaryServiceProxy.ref.toClassic, @@ -200,7 +189,7 @@ class EmAgentIT loadAgent ! Activation(INIT_SIM_TICK) primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(loadInput.getUuid) + PrimaryServiceRegistrationMessage(loadAgent.ref, loadInput.getUuid) ) loadAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) @@ -227,7 +216,7 @@ class EmAgentIT pvAgent ! Activation(INIT_SIM_TICK) primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(pvInput.getUuid) + PrimaryServiceRegistrationMessage(pvAgent.ref, pvInput.getUuid) ) pvAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) @@ -241,7 +230,7 @@ class EmAgentIT pvAgent ! RegistrationSuccessfulMessage( weatherService.ref.toClassic, - Some(0L), + 0L, ) scheduler.expectMessage(Completion(pvAgent)) @@ -250,7 +239,10 @@ class EmAgentIT storageAgent ! Activation(INIT_SIM_TICK) primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(householdStorageInput.getUuid) + PrimaryServiceRegistrationMessage( + storageAgent.ref, + householdStorageInput.getUuid, + ) ) storageAgent ! RegistrationFailedMessage( primaryServiceProxy.ref.toClassic @@ -260,20 +252,20 @@ class EmAgentIT /* TICK 0 LOAD: 0.269 kW - PV: -5.617 kW + PV: -5.842 kW STORAGE: SOC 0 % -> charge with 5 kW - -> remaining -0.348 kW + -> remaining -0.573 kW */ emAgentActivation ! Activation(0) - pvAgent ! ProvideWeatherMessage( + pvAgent ! DataProvision( 0, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(540d), WattsPerSquareMeter(200d), + WattsPerSquareMeter(100d), Celsius(0d), MetersPerSecond(0d), ), @@ -285,29 +277,31 @@ class EmAgentIT emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 0L.toDateTime emResult.getP should equalWithTolerance( - -0.00034885012.asMegaWatt + -0.00057340027059.asMegaWatt + ) + emResult.getQ should equalWithTolerance( + 0.0000882855367033.asMegaVar ) - emResult.getQ should equalWithTolerance(0.00008828554.asMegaVar) } scheduler.expectMessage(Completion(emAgentActivation, Some(7200))) /* TICK 7200 LOAD: 0.269 kW (unchanged) - PV: -3.651 kW + PV: -3.791 kW STORAGE: SOC 63.3 % - -> charge with 3.382 kW + -> charge with 3.522 kW -> remaining 0 kW */ emAgentActivation ! Activation(7200) - pvAgent ! ProvideWeatherMessage( + pvAgent ! DataProvision( 7200, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(300d), - WattsPerSquareMeter(500d), + WattsPerSquareMeter(50d), + WattsPerSquareMeter(150d), Celsius(0d), MetersPerSecond(0d), ), @@ -318,28 +312,30 @@ class EmAgentIT case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 7200.toDateTime - emResult.getP should equalWithTolerance(0.asMegaWatt) + emResult.getP should equalWithTolerance( + 0.0.asMegaWatt + ) emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } - scheduler.expectMessage(Completion(emAgentActivation, Some(13362))) + scheduler.expectMessage(Completion(emAgentActivation, Some(13115))) - /* TICK 13362 + /* TICK 13115 LOAD: 0.269 kW (unchanged) - PV: -3.651 kW (unchanged) + PV: -3.791 kW (unchanged) STORAGE: SOC 100 % -> charge with 0 kW - -> remaining -3.382 kW + -> remaining -3.522 kW */ - emAgentActivation ! Activation(13362) + emAgentActivation ! Activation(13115) resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 13362L.toDateTime + emResult.getTime shouldBe 13115L.toDateTime emResult.getP should equalWithTolerance( - -0.003382375474.asMegaWatt + -0.0035233186089842434.asMegaWatt ) emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } @@ -348,20 +344,20 @@ class EmAgentIT /* TICK 14400 LOAD: 0.269 kW (unchanged) - PV: -0.066 kW + PV: -0.069 kW STORAGE: SOC 100 % - -> charge with -0.202956 kW - -> remaining 0 kW + -> discharge with 0.2 kW + -> remaining 0.0 kW */ // send weather data before activation, which can happen // it got cloudy now... - pvAgent ! ProvideWeatherMessage( + pvAgent ! DataProvision( 14400, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(5d), - WattsPerSquareMeter(5d), + WattsPerSquareMeter(0.5d), + WattsPerSquareMeter(2d), Celsius(0d), MetersPerSecond(0d), ), @@ -374,8 +370,10 @@ class EmAgentIT 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) + emResult.getP should equalWithTolerance( + 0.0.asMegaWatt + ) + emResult.getQ should equalWithTolerance(0.000088285536.asMegaVar) } scheduler.expectMessage(Completion(emAgentActivation, Some(21600))) @@ -410,11 +408,7 @@ class EmAgentIT initStateData = ParticipantInitializeStateData( loadInput, LoadRuntimeConfig( - calculateMissingReactivePowerWithModel = true, - scaling = 1d, - modelBehaviour = "fix", - reference = "power", - uuids = List.empty, + calculateMissingReactivePowerWithModel = true ), primaryServiceProxy.ref.toClassic, None, @@ -436,7 +430,6 @@ class EmAgentIT pvInput, PvRuntimeConfig( calculateMissingReactivePowerWithModel = true, - scaling = 2d, uuids = List.empty, ), primaryServiceProxy.ref.toClassic, @@ -485,7 +478,7 @@ class EmAgentIT loadAgent ! Activation(INIT_SIM_TICK) primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(loadInput.getUuid) + PrimaryServiceRegistrationMessage(loadAgent.ref, loadInput.getUuid) ) loadAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) @@ -512,7 +505,7 @@ class EmAgentIT pvAgent ! Activation(INIT_SIM_TICK) primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(pvInput.getUuid) + PrimaryServiceRegistrationMessage(pvAgent.ref, pvInput.getUuid) ) pvAgent ! RegistrationFailedMessage(primaryServiceProxy.ref.toClassic) @@ -526,7 +519,7 @@ class EmAgentIT pvAgent ! RegistrationSuccessfulMessage( weatherService.ref.toClassic, - Some(0L), + 0L, ) scheduler.expectMessage(Completion(pvAgent)) @@ -535,7 +528,10 @@ class EmAgentIT heatPumpAgent ! Activation(INIT_SIM_TICK) primaryServiceProxy.expectMessage( - PrimaryServiceRegistrationMessage(adaptedHpInputModel.getUuid) + PrimaryServiceRegistrationMessage( + heatPumpAgent.ref, + adaptedHpInputModel.getUuid, + ) ) heatPumpAgent ! RegistrationFailedMessage( primaryServiceProxy.ref.toClassic @@ -543,14 +539,14 @@ class EmAgentIT weatherService.expectMessage( RegisterForWeatherMessage( - hpInputModel.getNode.getGeoPosition.getY, - hpInputModel.getNode.getGeoPosition.getX, + adaptedHpInputModel.getNode.getGeoPosition.getY, + adaptedHpInputModel.getNode.getGeoPosition.getX, ) ) heatPumpAgent ! RegistrationSuccessfulMessage( weatherService.ref.toClassic, - Some(0L), + 0L, ) scheduler.expectMessage(Completion(heatPumpAgent)) @@ -559,21 +555,21 @@ class EmAgentIT /* TICK 0 LOAD: 0.269 kW - PV: -5.617 kW + PV: -5.842 kW Heat pump: off, can be turned on or stay off -> set point ~3.5 kW (bigger than 50 % rated apparent power): turned on - -> remaining -0.499 kW + -> remaining -0.723 kW */ emAgentActivation ! Activation(0) weatherDependentAgents.foreach { - _ ! ProvideWeatherMessage( + _ ! DataProvision( 0, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(540d), WattsPerSquareMeter(200d), + WattsPerSquareMeter(100d), Celsius(0d), MetersPerSecond(0d), ), @@ -586,30 +582,32 @@ class EmAgentIT emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 0.toDateTime emResult.getP should equalWithTolerance( - -0.000498850118.asMegaWatt + -0.0007234002705905523.asMegaWatt + ) + emResult.getQ should equalWithTolerance( + 0.0010731200407782782.asMegaVar ) - emResult.getQ should equalWithTolerance(0.001073120041.asMegaVar) } scheduler.expectMessage(Completion(emAgentActivation, Some(7200))) /* TICK 7200 LOAD: 0.269 kW (unchanged) - PV: -3.651 kW + PV: -3.791 kW Heat pump: running (turned on from last request), can also be turned off -> set point ~3.5 kW (bigger than 50 % rated apparent power): stays turned on with unchanged state - -> remaining 1.468 kW + -> remaining 1.327 kW */ emAgentActivation ! Activation(7200) weatherDependentAgents.foreach { - _ ! ProvideWeatherMessage( + _ ! DataProvision( 7200, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(300d), - WattsPerSquareMeter(500d), + WattsPerSquareMeter(50d), + WattsPerSquareMeter(150d), Celsius(0d), MetersPerSecond(0d), ), @@ -621,30 +619,34 @@ class EmAgentIT case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 7200.toDateTime - emResult.getP should equalWithTolerance(0.001467624526.asMegaWatt) - emResult.getQ should equalWithTolerance(0.001073120041.asMegaVar) + emResult.getP should equalWithTolerance( + 0.0013266813910157566.asMegaWatt + ) + emResult.getQ should equalWithTolerance( + 0.0010731200407782782.asMegaVar + ) } scheduler.expectMessage(Completion(emAgentActivation, Some(14400))) /* TICK 14400 LOAD: 0.269 kW (unchanged) - PV: -0.066 kW + PV: -0.07 kW Heat pump: Is still running, can still be turned off -> flex signal is 0 MW: Heat pump is turned off - -> remaining 0.203 kW + -> remaining 0.199 kW */ emAgentActivation ! Activation(14400) // it got cloudy now... weatherDependentAgents.foreach { - _ ! ProvideWeatherMessage( + _ ! DataProvision( 14400, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(5d), - WattsPerSquareMeter(5d), + WattsPerSquareMeter(0.5d), + WattsPerSquareMeter(2d), Celsius(0d), MetersPerSecond(0d), ), @@ -656,30 +658,34 @@ class EmAgentIT case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 14400L.toDateTime - emResult.getP should equalWithTolerance(0.000202956264.asMegaWatt) - emResult.getQ should equalWithTolerance(0.000088285537.asMegaVar) + emResult.getP should equalWithTolerance( + 0.00019892577822992104.asMegaWatt + ) + emResult.getQ should equalWithTolerance( + 0.0000882855367033582.asMegaVar + ) } scheduler.expectMessage(Completion(emAgentActivation, Some(21600))) /* TICK 21600 LOAD: 0.269 kW (unchanged) - PV: -0.026 kW + PV: -0.023 kW Heat pump: Is not running, can run or stay off -> flex signal is 0 MW: Heat pump is turned off - -> remaining 0.242 kW + -> remaining 0.245 kW */ emAgentActivation ! Activation(21600) weatherDependentAgents.foreach { - _ ! ProvideWeatherMessage( + _ ! DataProvision( 21600, weatherService.ref.toClassic, WeatherData( // Same irradiation, but different angle of the sun - WattsPerSquareMeter(5d), - WattsPerSquareMeter(5d), + WattsPerSquareMeter(2d), + WattsPerSquareMeter(4d), Celsius(0d), MetersPerSecond(0d), ), @@ -691,28 +697,12 @@ class EmAgentIT case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 21600.toDateTime - emResult.getP should equalWithTolerance(0.000242284024.asMegaWatt) - emResult.getQ should equalWithTolerance(0.000088285537.asMegaVar) - } - - scheduler.expectMessage(Completion(emAgentActivation, Some(28665))) - - /* TICK 28666 - LOAD: 0.269 kW (unchanged) - PV: -0.026 kW (unchanged) - Heat pump: Is turned on again and cannot be turned off - -> flex signal is no control -> 0.00485 MW - -> remaining 5.092 kW - */ - - emAgentActivation ! Activation(28665) - - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(emResult: EmResult) => - emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 28665.toDateTime - emResult.getP should equalWithTolerance(0.005092284024.asMegaWatt) - emResult.getQ should equalWithTolerance(0.001073120040.asMegaVar) + emResult.getP should equalWithTolerance( + 0.0002450436827011999.asMegaWatt + ) + emResult.getQ should equalWithTolerance( + 0.0000882855367033582.asMegaVar + ) } scheduler.expectMessage(Completion(emAgentActivation, Some(28800))) diff --git a/src/test/scala/edu/ie3/simona/agent/em/EmAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/em/EmAgentSpec.scala index a770817277..ce0948cbcd 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentSpec.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.em import edu.ie3.datamodel.models.result.system.EmResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.config.SimonaConfig.EmRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.EmRuntimeConfig import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.{ FlexOptionsResultEvent, @@ -88,8 +88,8 @@ class EmAgentSpec ) val pvAgent = TestProbe[FlexRequest]("PvAgent") - emAgent ! RegisterParticipant(pvInput.getUuid, pvAgent.ref, pvInput) - emAgent ! ScheduleFlexRequest(pvInput.getUuid, INIT_SIM_TICK) + emAgent ! RegisterControlledAsset(pvAgent.ref, pvInput) + emAgent ! ScheduleFlexActivation(pvInput.getUuid, INIT_SIM_TICK) val sa1 = scheduler.expectMessageType[ScheduleActivation] sa1.tick shouldBe INIT_SIM_TICK @@ -97,8 +97,8 @@ class EmAgentSpec val emAgentActivation = sa1.actor val evcsAgent = TestProbe[FlexRequest]("EvcsAgent") - emAgent ! RegisterParticipant(evcsInput.getUuid, evcsAgent.ref, evcsInput) - emAgent ! ScheduleFlexRequest(evcsInput.getUuid, INIT_SIM_TICK) + emAgent ! RegisterControlledAsset(evcsAgent.ref, evcsInput) + emAgent ! ScheduleFlexActivation(evcsInput.getUuid, INIT_SIM_TICK) // no additional scheduling message, since tick -1 has already been scheduled scheduler.expectNoMessage() @@ -273,8 +273,8 @@ class EmAgentSpec ) val pvAgent = TestProbe[FlexRequest]("PvAgent") - emAgent ! RegisterParticipant(pvInput.getUuid, pvAgent.ref, pvInput) - emAgent ! ScheduleFlexRequest(pvInput.getUuid, 0) + emAgent ! RegisterControlledAsset(pvAgent.ref, pvInput) + emAgent ! ScheduleFlexActivation(pvInput.getUuid, 0) val sa1 = scheduler.expectMessageType[ScheduleActivation] sa1.tick shouldBe 0 @@ -282,8 +282,8 @@ class EmAgentSpec val emAgentActivation = sa1.actor val evcsAgent = TestProbe[FlexRequest]("EvcsAgent") - emAgent ! RegisterParticipant(evcsInput.getUuid, evcsAgent.ref, evcsInput) - emAgent ! ScheduleFlexRequest(evcsInput.getUuid, 0) + emAgent ! RegisterControlledAsset(evcsAgent.ref, evcsInput) + emAgent ! ScheduleFlexActivation(evcsInput.getUuid, 0) // no additional scheduling message, since tick 0 has already been scheduled scheduler.expectNoMessage() @@ -453,8 +453,8 @@ class EmAgentSpec ) val pvAgent = TestProbe[FlexRequest]("PvAgent") - emAgent ! RegisterParticipant(pvInput.getUuid, pvAgent.ref, pvInput) - emAgent ! ScheduleFlexRequest(pvInput.getUuid, 0) + emAgent ! RegisterControlledAsset(pvAgent.ref, pvInput) + emAgent ! ScheduleFlexActivation(pvInput.getUuid, 0) val sa1 = scheduler.expectMessageType[ScheduleActivation] sa1.tick shouldBe 0 @@ -462,8 +462,8 @@ class EmAgentSpec val emAgentActivation = sa1.actor val evcsAgent = TestProbe[FlexRequest]("EvcsAgent") - emAgent ! RegisterParticipant(evcsInput.getUuid, evcsAgent.ref, evcsInput) - emAgent ! ScheduleFlexRequest(evcsInput.getUuid, 0) + emAgent ! RegisterControlledAsset(evcsAgent.ref, evcsInput) + emAgent ! ScheduleFlexActivation(evcsInput.getUuid, 0) // no additional scheduling message, since tick 0 has already been scheduled scheduler.expectNoMessage() @@ -643,23 +643,22 @@ class EmAgentSpec ) val pvAgent = TestProbe[FlexRequest]("PvAgent") - emAgent ! RegisterParticipant(pvInput.getUuid, pvAgent.ref, pvInput) - emAgent ! ScheduleFlexRequest(pvInput.getUuid, INIT_SIM_TICK) + emAgent ! RegisterControlledAsset(pvAgent.ref, pvInput) + emAgent ! ScheduleFlexActivation(pvInput.getUuid, INIT_SIM_TICK) val emAgentFlex = - parentEmAgent.expectMessageType[RegisterParticipant] match { - case RegisterParticipant(modelUuid, participant, inputModel) => - modelUuid shouldBe emInput.getUuid + parentEmAgent.expectMessageType[RegisterControlledAsset] match { + case RegisterControlledAsset(participant, inputModel) => inputModel shouldBe emInput participant } parentEmAgent.expectMessage( - ScheduleFlexRequest(emInput.getUuid, INIT_SIM_TICK) + ScheduleFlexActivation(emInput.getUuid, INIT_SIM_TICK) ) val evcsAgent = TestProbe[FlexRequest]("EvcsAgent") - emAgent ! RegisterParticipant(evcsInput.getUuid, evcsAgent.ref, evcsInput) - emAgent ! ScheduleFlexRequest(evcsInput.getUuid, INIT_SIM_TICK) + emAgent ! RegisterControlledAsset(evcsAgent.ref, evcsInput) + emAgent ! ScheduleFlexActivation(evcsInput.getUuid, INIT_SIM_TICK) // no additional scheduling message, since tick -1 has already been scheduled parentEmAgent.expectNoMessage() diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala index 8d23af9c46..6ab5ee808e 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmCenGridSpec.scala @@ -16,7 +16,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -116,6 +116,7 @@ class DBFSAlgorithmCenGridSpec Seq.empty[ThermalGrid], subGridGateToActorRef, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), ) val key = ScheduleLock.singleKey(TSpawner, scheduler.ref, INIT_SIM_TICK) diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala index a1047edeff..d5ece47f75 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmFailedPowerFlowSpec.scala @@ -15,7 +15,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ } import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -100,6 +100,7 @@ class DBFSAlgorithmFailedPowerFlowSpec Seq.empty[ThermalGrid], subGridGateToActorRef, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), ) val key = @@ -312,6 +313,7 @@ class DBFSAlgorithmFailedPowerFlowSpec Seq.empty[ThermalGrid], subnetGatesToActorRef, RefSystem("5000 MVA", "380 kV"), + VoltageLimits(0.9, 1.1), ) val key = diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala index fa43ee2d1e..2fe1f0fbb5 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmParticipantSpec.scala @@ -14,15 +14,15 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ ExchangeVoltage, } import edu.ie3.simona.agent.grid.GridAgentMessages._ +import edu.ie3.simona.agent.participant2.ParticipantAgent.RegistrationFailedMessage import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } import edu.ie3.simona.ontology.messages.services.ServiceMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.test.common.model.grid.DbfsTestGridWithParticipants @@ -92,6 +92,7 @@ class DBFSAlgorithmParticipantSpec Seq.empty, subGridGateToActorRef, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), ) val key = @@ -118,11 +119,11 @@ class DBFSAlgorithmParticipantSpec loadAgent ! Activation(INIT_SIM_TICK) - primaryService.expectMessage( - PrimaryServiceRegistrationMessage(load1.getUuid) - ) + val serviceRegistrationMsg = primaryService + .expectMessageType[PrimaryServiceRegistrationMessage] + serviceRegistrationMsg.inputModelUuid shouldBe load1.getUuid - loadAgent.toClassic ! RegistrationFailedMessage( + serviceRegistrationMsg.requestingActor ! RegistrationFailedMessage( primaryService.ref.toClassic ) diff --git a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala index beb1408270..f81b92ea51 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/DBFSAlgorithmSupGridSpec.scala @@ -14,7 +14,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.ExchangePower import edu.ie3.simona.agent.grid.GridAgentMessages._ import edu.ie3.simona.event.ResultEvent.PowerFlowResultEvent import edu.ie3.simona.event.{ResultEvent, RuntimeEvent} -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, @@ -87,6 +87,7 @@ class DBFSAlgorithmSupGridSpec Seq.empty[ThermalGrid], subnetGatesToActorRef, RefSystem("5000 MVA", "380 kV"), + VoltageLimits(0.9, 1.1), ) val key = diff --git a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala index 5d7eea87c5..60f216668f 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/GridAgentSetupSpec.scala @@ -34,6 +34,7 @@ class GridAgentSetupSpec val testKit = BehaviorTestKit(Behaviors.setup[AnyRef] { ctx => SimonaStandaloneSetup( typesafeConfig, + simonaConfig, mock[ResultFileHierarchy], ).buildSubGridToActorRefMap( gridContainer.getSubGridTopologyGraph, @@ -60,6 +61,7 @@ class GridAgentSetupSpec val testKit = BehaviorTestKit(Behaviors.setup[AnyRef] { ctx => SimonaStandaloneSetup( typesafeConfig, + simonaConfig, mock[ResultFileHierarchy], ).buildSubGridToActorRefMap( threeWindingTestGrid.getSubGridTopologyGraph, diff --git a/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala index d909ec4d61..89614f68e2 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/PowerFlowSupportSpec.scala @@ -23,7 +23,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.Responses.{ ExchangeVoltage, } import edu.ie3.simona.config.SimonaConfig.Simona -import edu.ie3.simona.model.grid.{GridModel, RefSystem} +import edu.ie3.simona.model.grid.{GridModel, RefSystem, VoltageLimits} import edu.ie3.simona.test.common.model.grid.{ BasicGridWithSwitches, DbfsTestGrid, @@ -469,6 +469,7 @@ class PowerFlowSupportSpec GridModel( subGridContainer, RefSystem("2000 MVA", "110 kV"), + VoltageLimits(0.9, 1.1), time.startDateTime, time.endDateTime, simonaConfig, diff --git a/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala b/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala index 9cdeb2f6f5..fc349066a8 100644 --- a/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/grid/ReceivedValuesStoreSpec.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.agent.grid import edu.ie3.datamodel.graph.SubGridGate -import edu.ie3.simona.agent.participant.ParticipantAgent.ParticipantMessage +import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.model.grid.SubGridGateMokka import org.apache.pekko.actor.testkit.typed.scaladsl.{ @@ -24,24 +24,25 @@ class ReceivedValuesStoreSpec with SubGridGateMokka { // test actorRefs - val participant1: TestProbe[ParticipantMessage] = - TestProbe[ParticipantMessage]() - val participant2: TestProbe[ParticipantMessage] = - TestProbe[ParticipantMessage]() - val participant3: TestProbe[ParticipantMessage] = - TestProbe[ParticipantMessage]() + val participant1: TestProbe[ParticipantAgent.Request] = + TestProbe[ParticipantAgent.Request]() + val participant2: TestProbe[ParticipantAgent.Request] = + TestProbe[ParticipantAgent.Request]() + val participant3: TestProbe[ParticipantAgent.Request] = + TestProbe[ParticipantAgent.Request]() val gridAgent: TestProbe[GridAgent.Request] = TestProbe[GridAgent.Request]() // test data used by almost all tests // / node to asset agents mapping - val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef[ParticipantMessage]]] = Map( - UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") -> Set( - participant1.ref - ), - UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") -> Set( - participant2.ref - ), - ) + val nodeToAssetAgentsMap: Map[UUID, Set[ActorRef[ParticipantAgent.Request]]] = + Map( + UUID.fromString("dd9a5b54-94bb-4201-9108-2b1b7d689546") -> Set( + participant1.ref + ), + UUID.fromString("34e807f1-c62b-4968-b0f6-980ce500ff97") -> Set( + participant2.ref + ), + ) // / subnet gate mapping for inferior grids val inferiorSubGridGateToActorRefMap @@ -64,7 +65,7 @@ class ReceivedValuesStoreSpec "initialize an empty store correctly when everything is empty" in { val nodeToAssetAgentsMap = - Map.empty[UUID, Set[ActorRef[ParticipantMessage]]] + Map.empty[UUID, Set[ActorRef[ParticipantAgent.Request]]] val inferiorSubGridGateToActorRefMap = Map.empty[SubGridGate, ActorRef[GridAgent.Request]] val superiorGridNodeUuids = Vector.empty[UUID] @@ -176,7 +177,7 @@ class ReceivedValuesStoreSpec "initialize an empty store correctly when only information on the superior grid slack nodes are provided" in { val nodeToAssetAgentsMap = - Map.empty[UUID, Set[ActorRef[ParticipantMessage]]] + Map.empty[UUID, Set[ActorRef[ParticipantAgent.Request]]] val inferiorSubGridGateToActorRefMap = Map.empty[SubGridGate, ActorRef[GridAgent.Request]] diff --git a/src/test/scala/edu/ie3/simona/agent/grid/ThermalGridIT.scala b/src/test/scala/edu/ie3/simona/agent/grid/ThermalGridIT.scala new file mode 100644 index 0000000000..a6549d63be --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/grid/ThermalGridIT.scala @@ -0,0 +1,827 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.grid + +import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService +import edu.ie3.simona.agent.participant.hp.HpAgent +import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, +} +import edu.ie3.simona.config.RuntimeConfig.HpRuntimeConfig +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.event.ResultEvent.{ + CylindricalThermalStorageResult, + ParticipantResultEvent, + ThermalHouseResult, +} +import edu.ie3.simona.event.notifier.NotifierConfig +import edu.ie3.simona.model.thermal.ThermalHouseTestData +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.services.ServiceMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.WeatherMessage.{ + RegisterForWeatherMessage, + WeatherData, +} +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.test.common.DefaultTestData +import edu.ie3.simona.test.common.input.EmInputTestData +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.QuantityMatchers.equalWithTolerance +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.WattsPerSquareMeter +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.actor.testkit.typed.scaladsl.{ + ScalaTestWithActorTestKit, + TestProbe, +} +import org.apache.pekko.actor.typed.scaladsl.adapter.{TypedActorRefOps, _} +import org.apache.pekko.testkit.TestActorRef +import org.scalatest.matchers.should +import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatestplus.mockito.MockitoSugar +import squants.motion.MetersPerSecond +import squants.thermal.Celsius + +import java.time.ZonedDateTime +import scala.language.postfixOps + +/** Test to ensure the functions that a thermal grid and its connected assets is + * capable. + */ +class ThermalGridIT + extends ScalaTestWithActorTestKit + with ThermalHouseTestData + with AnyWordSpecLike + with should.Matchers + with EmInputTestData + with MockitoSugar + with DefaultTestData { + private implicit val classicSystem: ActorSystem = system.toClassic + protected implicit val simulationStartDate: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") + protected val simulationEndDate: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-02T02:00:00Z") + + private val resolution = + simonaConfig.simona.powerflow.resolution.getSeconds + + private val outputConfigOn = NotifierConfig( + simulationResultInfo = true, + powerRequestReply = false, + flexResult = false, + ) + + "A Thermal Grid with thermal house, storage and heat pump not under the control of energy management" should { + "be initialized correctly and run through some activations" in { + val scheduler: TestProbe[SchedulerMessage] = TestProbe("scheduler") + val primaryServiceProxy = + TestProbe[ServiceMessage]("PrimaryServiceProxy") + + val weatherService = TestProbe[ServiceMessage]("WeatherService") + + val resultListener: TestProbe[ResultEvent] = TestProbe("resultListener") + + val heatPumpAgent = TestActorRef( + new HpAgent( + scheduler = scheduler.ref.toClassic, + initStateData = ParticipantInitializeStateData( + typicalHpInputModel, + typicalThermalGrid, + HpRuntimeConfig( + calculateMissingReactivePowerWithModel = true, + 1.0, + List.empty[String], + ), + primaryServiceProxy.ref.toClassic, + Iterable(ActorWeatherService(weatherService.ref.toClassic)), + simulationStartDate, + simulationEndDate, + resolution, + simonaConfig.simona.runtime.participant.requestVoltageDeviationThreshold, + outputConfigOn, + None, + ), + listener = Iterable(resultListener.ref.toClassic), + ), + "HeatPumpAgent1", + ) + + val pRunningHp = 0.0038.asMegaWatt + val qRunningHp = 0.0012489995996796802.asMegaVar + + scheduler.expectNoMessage() + + /* INIT */ + // heat pump + heatPumpAgent ! Activation(INIT_SIM_TICK) + + primaryServiceProxy.expectMessage( + PrimaryServiceRegistrationMessage( + heatPumpAgent.ref, + typicalHpInputModel.getUuid, + ) + ) + heatPumpAgent ! RegistrationFailedMessage( + primaryServiceProxy.ref.toClassic + ) + + weatherService.expectMessage( + RegisterForWeatherMessage( + typicalHpInputModel.getNode.getGeoPosition.getY, + typicalHpInputModel.getNode.getGeoPosition.getX, + ) + ) + + heatPumpAgent ! RegistrationSuccessfulMessage( + weatherService.ref.toClassic, + 0, + ) + val weatherDependentAgents = Seq(heatPumpAgent) + + scheduler.expectMessage(Completion(heatPumpAgent, Some(0))) + + /* TICK 0 + Start of Simulation + House demand heating : requiredDemand = 0.0 kWh, possibleDemand ~ 15 kWh + ThermalStorage : requiredDemand = 10.44 kWh, possibleDemand = 10.44 kWh + Heat pump: turned on - to serve the storage demand + */ + + heatPumpAgent ! Activation(0) + + weatherDependentAgents.foreach { + _ ! DataProvision( + 0, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(0d), + WattsPerSquareMeter(0d), + Celsius(-5d), + MetersPerSecond(0d), + ), + Some(7200), + ) + } + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 0.toDateTime + hpResult.getP should equalWithTolerance(pRunningHp) + hpResult.getQ should equalWithTolerance(qRunningHp) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 0.toDateTime + qDot should equalWithTolerance(0.0.asMegaWatt) + indoorTemperature should equalWithTolerance( + 19.9999074074074.asDegreeCelsius + ) + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 0.toDateTime + qDot should equalWithTolerance(0.011.asMegaWatt) + energy should equalWithTolerance(0.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(3417))) + + /* TICK 3417 + Storage is fully heated up + House demand heating : requiredDemand = 0.0 kWh, possibleDemand = 17.37 kWh + ThermalStorage : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + Heat pump: stays on since it was on and the house has possible demand + */ + + heatPumpAgent ! Activation(3417) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 3417.toDateTime + hpResult.getP should equalWithTolerance(pRunningHp) + hpResult.getQ should equalWithTolerance( + qRunningHp + ) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 3417.toDateTime + qDot should equalWithTolerance(0.011.asMegaWatt) + indoorTemperature should equalWithTolerance( + 19.6835196903292.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 3417.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + energy should equalWithTolerance(0.01044.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(7200))) + + /* TICK 7200 + New weather data (unchanged) incoming + House demand heating : requiredDemand = 0.0 kWh, possibleDemand = 8.41 kWh + ThermalStorage : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + Heat pump: stays on, we got triggered by incoming weather data. So we continue with same behaviour as before + */ + + heatPumpAgent ! Activation(7200) + + weatherDependentAgents.foreach { + _ ! DataProvision( + 7200, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(1d), + WattsPerSquareMeter(1d), + Celsius(-5d), + MetersPerSecond(0d), + ), + Some(28800), + ) + } + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 7200.toDateTime + hpResult.getP should equalWithTolerance(pRunningHp) + hpResult.getQ should equalWithTolerance( + qRunningHp + ) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 7200.toDateTime + qDot should equalWithTolerance(0.011.asMegaWatt) + indoorTemperature should equalWithTolerance( + 20.8788983755569.asDegreeCelsius + ) + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 7200.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + energy should equalWithTolerance(0.01044.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(10798))) + + /* TICK 10798 + House reaches upper temperature boundary + House demand heating : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + ThermalStorage : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + Heat pump: turned off + */ + + heatPumpAgent ! Activation(10798) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 10798.toDateTime + hpResult.getP should equalWithTolerance(0.asMegaWatt) + hpResult.getQ should equalWithTolerance(0.asMegaVar) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 10798.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + indoorTemperature should equalWithTolerance( + 21.9998899446115.asDegreeCelsius + ) + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 10798.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + energy should equalWithTolerance(0.01044.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(28800))) + + /* TICK 28800 + House would reach lowerTempBoundary at tick 50797 + but now it's getting colder which should decrease inner temp of house faster + House demand heating : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + ThermalStorage : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + Heat pump: stays off + */ + + heatPumpAgent ! Activation(28800) + + weatherDependentAgents.foreach { + _ ! DataProvision( + 28800, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(2d), + WattsPerSquareMeter(2d), + Celsius(-25d), + MetersPerSecond(0d), + ), + Some(45000), + ) + } + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 28800.toDateTime + hpResult.getP should equalWithTolerance(0.0.asMegaWatt) + hpResult.getQ should equalWithTolerance(0.0.asMegaVar) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 28800.toDateTime + qDot should equalWithTolerance(0.0.asMegaWatt) + indoorTemperature should equalWithTolerance( + 20.19969728245267.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 28800.toDateTime + qDot should equalWithTolerance(0.0.asMegaWatt) + energy should equalWithTolerance(0.01044.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(41940))) + + /* TICK 41940 + House reach lowerTemperatureBoundary + House demand heating : requiredDemand = 15.0 kWh, possibleDemand = 30.00 kWh + ThermalStorage : requiredDemand = 0.0 kWh, possibleDemand = 0.0 kWh + Heat pump: stays off, demand should be covered by storage + */ + + heatPumpAgent ! Activation(41940) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 41940.toDateTime + hpResult.getP should equalWithTolerance(0.0.asMegaWatt) + hpResult.getQ should equalWithTolerance(0.0.asMegaVar) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 41940.toDateTime + qDot should equalWithTolerance(0.01044.asMegaWatt) + indoorTemperature should equalWithTolerance( + 17.9999786813733.asDegreeCelsius + ) + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 41940.toDateTime + qDot should equalWithTolerance(-0.01044.asMegaWatt) + energy should equalWithTolerance(0.01044.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(45000))) + + /* TICK 45000 + Storage will be empty at tick 45540 + Additional trigger caused by (unchanged) weather data should not change this + House demand heating : requiredDemand = 9.78 kWh, possibleDemand = 24.78 kWh + ThermalStorage : requiredDemand = 0.0 kWh, possibleDemand = 8.87 kWh + Heat pump: stays off + */ + + heatPumpAgent ! Activation(45000) + + weatherDependentAgents.foreach { + _ ! DataProvision( + 45000, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(3d), + WattsPerSquareMeter(3d), + Celsius(-25d), + MetersPerSecond(0d), + ), + Some(57600), + ) + } + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 45000.toDateTime + hpResult.getP should equalWithTolerance(0.0.asMegaWatt) + hpResult.getQ should equalWithTolerance(0.0.asMegaVar) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 45000.toDateTime + qDot should equalWithTolerance(0.01044.asMegaWatt) + indoorTemperature should equalWithTolerance( + 18.69584558965105.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 45000.toDateTime + qDot should equalWithTolerance(-0.01044.asMegaWatt) + energy should equalWithTolerance( + 0.00156599999999999.asMegaWattHour + ) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(45540))) + + /* TICK 45540 + Storage will be empty + House demand heating : requiredDemand = 8.87kWh, possibleDemand = 23.87 kWh + ThermalStorage : requiredDemand = 10.44 kWh, possibleDemand = 10.44 kWh + DomesticWaterStorage : tba + Heat pump: will be turned on - to serve the remaining heat demand of house (and refill storage later) + */ + + heatPumpAgent ! Activation(45540) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 45540.toDateTime + hpResult.getP should equalWithTolerance(pRunningHp) + hpResult.getQ should equalWithTolerance(qRunningHp) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 45540.toDateTime + qDot should equalWithTolerance(0.011.asMegaWatt) + indoorTemperature should equalWithTolerance( + 18.81725389847177.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 45540.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + energy should equalWithTolerance(0.asMegaWattHour) + case _ => + fail( + "Expected a ThermalHouseResult and a ThermalStorageResult but got something else" + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(57600))) + + /* TICK 57600 + New weather data: it's getting warmer again + House demand heating : requiredDemand = 0.00 kWh, possibleDemand = 1.70 kWh + ThermalStorage : requiredDemand = 10.44 kWh, possibleDemand = 10.44 kWh + Heat pump: stays on + */ + + heatPumpAgent ! Activation(57600) + + weatherDependentAgents.foreach { + _ ! DataProvision( + 57600, + weatherService.ref.toClassic, + WeatherData( + WattsPerSquareMeter(4d), + WattsPerSquareMeter(4d), + Celsius(5d), + MetersPerSecond(0d), + ), + Some(151200), + ) + } + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 57600.toDateTime + hpResult.getP should equalWithTolerance(pRunningHp) + hpResult.getQ should equalWithTolerance(qRunningHp) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 57600.toDateTime + qDot should equalWithTolerance(0.011.asMegaWatt) + indoorTemperature should equalWithTolerance( + 21.77341655767336.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 57600.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + energy should equalWithTolerance(0.asMegaWattHour) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(58256))) + + /* TICK 58256 + House will reach the upperTemperatureBoundary + House demand heating : requiredDemand = 0.00 kWh, possibleDemand = 0.00 kWh + ThermalStorage : requiredDemand = 10.44 kWh, possibleDemand = 10.44 kWh + Heat pump: stays on to refill the storage now + */ + + heatPumpAgent ! Activation(58256) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 58256.toDateTime + hpResult.getP should equalWithTolerance(pRunningHp) + hpResult.getQ should equalWithTolerance(qRunningHp) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 58256.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + indoorTemperature should equalWithTolerance( + 21.999922627074.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 58256.toDateTime + qDot should equalWithTolerance(0.011.asMegaWatt) + energy should equalWithTolerance( + 0.asMegaWattHour + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(61673))) + + /* TICK 61673 + Storage will be fully charged + House demand heating : requiredDemand = 0.00 kWh, possibleDemand = 0.00 kWh + ThermalStorage : requiredDemand = 0.00 kWh, possibleDemand = 0.00 kWh + Heat pump: turned off + */ + + heatPumpAgent ! Activation(61673) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(hpResult) => + hpResult.getInputModel shouldBe typicalHpInputModel.getUuid + hpResult.getTime shouldBe 61673.toDateTime + hpResult.getP should equalWithTolerance(0.asMegaWatt) + hpResult.getQ should equalWithTolerance(0.asMegaVar) + } + + Range(0, 2) + .map { _ => + resultListener.expectMessageType[ResultEvent] + } + .foreach { case ResultEvent.ThermalResultEvent(thermalUnitResult) => + thermalUnitResult match { + case ThermalHouseResult( + time, + inputModel, + qDot, + indoorTemperature, + ) => + inputModel shouldBe typicalThermalHouse.getUuid + time shouldBe 61673.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + indoorTemperature should equalWithTolerance( + 21.7847791618269.asDegreeCelsius + ) + + case CylindricalThermalStorageResult( + time, + inputModel, + qDot, + energy, + ) => + inputModel shouldBe typicalThermalStorage.getUuid + time shouldBe 61673.toDateTime + qDot should equalWithTolerance(0.asMegaWatt) + energy should equalWithTolerance( + 0.01044.asMegaWattHour + ) + } + } + + scheduler.expectMessage(Completion(heatPumpAgent, Some(122555))) + + } + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala index 5af145f60a..1f225a7878 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -19,16 +19,21 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorExtEvDataService import edu.ie3.simona.agent.participant.evcs.EvcsAgent import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData import edu.ie3.simona.agent.participant.statedata.DataCollectionStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData._ +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation -import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.EvcsRuntimeConfig import edu.ie3.simona.event.ResultEvent.{ FlexOptionsResultEvent, ParticipantResultEvent, @@ -45,10 +50,6 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.ontology.messages.services.EvMessage._ import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ - RegistrationFailedMessage, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.test.ParticipantAgentSpec import edu.ie3.simona.test.common.input.EvcsInputTestData import edu.ie3.simona.test.common.{EvTestData, TestSpawnerClassic} @@ -92,14 +93,7 @@ class EvcsAgentModelCalculationSpec flexResult = false, ) - private val modelConfig = - EvcsRuntimeConfig.apply( - calculateMissingReactivePowerWithModel = false, - scaling = 1.0, - uuids = List("default"), - chargingStrategy = "maxPower", - lowestEvSoc = 0.2, - ) + private val modelConfig = EvcsRuntimeConfig() protected implicit val simulationStartDate: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") @@ -234,7 +228,7 @@ class EvcsAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(evcsInputModel.getUuid) + PrimaryServiceRegistrationMessage(evcsAgent.ref, evcsInputModel.getUuid) ) /* State should be information handling and having correct state data */ evcsAgent.stateName shouldBe HandleInformation @@ -329,18 +323,18 @@ class EvcsAgentModelCalculationSpec /* Reply, that registration was successful */ evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, None), + RegistrationSuccessfulMessage(evService.ref, 0), ) /* Expect a completion message */ - scheduler.expectMsg(Completion(evcsAgent.toTyped, None)) + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(0))) /* ... as well as corresponding state and state data */ evcsAgent.stateName shouldBe Idle evcsAgent.stateData match { case baseStateData: ParticipantModelBaseStateData[_, _, _, _] => /* Only check the awaited next data ticks, as the rest has yet been checked */ - baseStateData.foreseenDataTicks shouldBe Map(evService.ref -> None) + baseStateData.foreseenDataTicks shouldBe Map(evService.ref -> Some(0)) case _ => fail( s"Did not find expected state data $ParticipantModelBaseStateData, but ${evcsAgent.stateData}" @@ -370,7 +364,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsg(RegisterForEvDataMessage(evcsInputModel.getUuid)) evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(900L)), + RegistrationSuccessfulMessage(evService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -383,6 +377,7 @@ class EvcsAgentModelCalculationSpec 0L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -436,7 +431,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsgType[RegisterForEvDataMessage] evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) /* I'm not interested in the content of the Completion */ @@ -450,7 +445,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 0L, evService.ref, arrivingEvsData, @@ -570,7 +565,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsgType[RegisterForEvDataMessage] evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) /* I'm not interested in the content of the Completion */ @@ -612,7 +607,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 0L, evService.ref, arrivingEvsData, @@ -701,7 +696,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsgType[RegisterForEvDataMessage] evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(10800)), + RegistrationSuccessfulMessage(evService.ref, 10800), ) /* I'm not interested in the content of the Completion */ @@ -712,6 +707,7 @@ class EvcsAgentModelCalculationSpec 7200, Each(1.0), Each(0.0), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -746,7 +742,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsgType[RegisterForEvDataMessage] evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) /* I'm not interested in the content of the CompletionM */ @@ -771,7 +767,7 @@ class EvcsAgentModelCalculationSpec /* Send ev for this tick */ evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 0, evService.ref, ArrivingEvs(Seq(EvModelWrapper(evA))), @@ -827,7 +823,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsgType[RegisterForEvDataMessage] evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) /* I'm not interested in the content of the Completion */ @@ -837,7 +833,7 @@ class EvcsAgentModelCalculationSpec /* Send ev for this tick */ evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 0, evService.ref, ArrivingEvs(Seq(EvModelWrapper(evA))), @@ -854,7 +850,7 @@ class EvcsAgentModelCalculationSpec /* Send empty EV list for this tick */ evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 900, evService.ref, ArrivingEvs(Seq.empty), @@ -905,7 +901,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsgType[RegisterForEvDataMessage] evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) /* I'm not interested in the content of the Completion */ @@ -916,7 +912,7 @@ class EvcsAgentModelCalculationSpec /* ... for tick 0 */ evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 0, evService.ref, ArrivingEvs( @@ -950,7 +946,7 @@ class EvcsAgentModelCalculationSpec // arrivals second evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 3600, evService.ref, ArrivingEvs( @@ -984,7 +980,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 7200, evService.ref, ArrivingEvs( @@ -1003,6 +999,7 @@ class EvcsAgentModelCalculationSpec 7500L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -1020,6 +1017,7 @@ class EvcsAgentModelCalculationSpec 7500L, Each(1.000000000000001d), Each(0.0), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -1036,6 +1034,7 @@ class EvcsAgentModelCalculationSpec 7500L, Each(0.98), Each(0.0), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ @@ -1078,7 +1077,10 @@ class EvcsAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(evcsInputModelQv.getUuid) + PrimaryServiceRegistrationMessage( + evcsAgent.ref, + evcsInputModelQv.getUuid, + ) ) /* State should be information handling and having correct state data */ evcsAgent.stateName shouldBe HandleInformation @@ -1116,11 +1118,7 @@ class EvcsAgentModelCalculationSpec ) emAgent.expectMsg( - RegisterParticipant( - evcsInputModelQv.getUuid, - evcsAgent.toTyped, - evcsInputModelQv, - ) + RegisterControlledAsset(evcsAgent.toTyped, evcsInputModelQv) ) // only receive registration message. ScheduleFlexRequest after secondary service initialized emAgent.expectNoMessage() @@ -1128,11 +1126,11 @@ class EvcsAgentModelCalculationSpec evService.expectMsg(RegisterForEvDataMessage(evcsInputModelQv.getUuid)) evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) emAgent.expectMsg( - ScheduleFlexRequest(evcsInputModelQv.getUuid, 0) + ScheduleFlexActivation(evcsInputModelQv.getUuid, 0) ) scheduler.expectMsg(Completion(evcsAgent.toTyped)) @@ -1216,7 +1214,10 @@ class EvcsAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(evcsInputModelQv.getUuid) + PrimaryServiceRegistrationMessage( + evcsAgent.ref, + evcsInputModelQv.getUuid, + ) ) /* State should be information handling and having correct state data */ evcsAgent.stateName shouldBe HandleInformation @@ -1258,22 +1259,18 @@ class EvcsAgentModelCalculationSpec ) emAgent.expectMsg( - RegisterParticipant( - evcsInputModelQv.getUuid, - evcsAgent.toTyped, - evcsInputModelQv, - ) + RegisterControlledAsset(evcsAgent.toTyped, evcsInputModelQv) ) emAgent.expectNoMessage() evService.expectMsg(RegisterForEvDataMessage(evcsInputModelQv.getUuid)) evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(900)), + RegistrationSuccessfulMessage(evService.ref, 900), ) emAgent.expectMsg( - ScheduleFlexRequest(evcsInputModelQv.getUuid, 900) + ScheduleFlexActivation(evcsInputModelQv.getUuid, 900) ) scheduler.expectMsg(Completion(evcsAgent.toTyped)) @@ -1337,7 +1334,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 900, evService.ref, ArrivingEvs(Seq(ev900)), @@ -1454,7 +1451,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 4500, evService.ref, ArrivingEvs(Seq(ev4500)), @@ -1584,7 +1581,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 11700, evService.ref, ArrivingEvs(Seq(ev11700)), @@ -1808,13 +1805,13 @@ class EvcsAgentModelCalculationSpec case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev4500.uuid => result.getTime shouldBe 18000.toDateTime - result.getP should beEquivalentTo((-10d).asKiloWatt) + result.getP should beEquivalentTo(-10d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(44.3194d.asPercent, 1e-2) case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev11700.uuid => result.getTime shouldBe 18000.toDateTime - result.getP should beEquivalentTo((-10d).asKiloWatt) + result.getP should beEquivalentTo(-10d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(44.137931034d.asPercent, 1e-6) } @@ -1823,7 +1820,7 @@ class EvcsAgentModelCalculationSpec case ParticipantResultEvent(result: EvcsResult) => result.getInputModel shouldBe evcsInputModelQv.getUuid result.getTime shouldBe 18000.toDateTime - result.getP should beEquivalentTo((-20d).asKiloWatt) + result.getP should beEquivalentTo(-20d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) } @@ -1880,7 +1877,7 @@ class EvcsAgentModelCalculationSpec case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev4500.uuid => result.getTime shouldBe 23040.toDateTime - result.getP should beEquivalentTo((-10d).asKiloWatt) + result.getP should beEquivalentTo(-10d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(26.819445d.asPercent, 1e-2) case ParticipantResultEvent(result: EvResult) @@ -1895,7 +1892,7 @@ class EvcsAgentModelCalculationSpec case ParticipantResultEvent(result: EvcsResult) => result.getInputModel shouldBe evcsInputModelQv.getUuid result.getTime shouldBe 23040.toDateTime - result.getP should beEquivalentTo((-10d).asKiloWatt) + result.getP should beEquivalentTo(-10d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) } @@ -2048,7 +2045,7 @@ class EvcsAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(inputModelUuid) + PrimaryServiceRegistrationMessage(evcsAgent.ref, inputModelUuid) ) /* State should be information handling and having correct state data */ evcsAgent.stateName shouldBe HandleInformation @@ -2092,7 +2089,7 @@ class EvcsAgentModelCalculationSpec evService.expectMsg(RegisterForEvDataMessage(evcsInputModel.getUuid)) evService.send( evcsAgent, - RegistrationSuccessfulMessage(evService.ref, Some(0)), + RegistrationSuccessfulMessage(evService.ref, 0), ) scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(0))) @@ -2104,7 +2101,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 0, evService.ref, ArrivingEvs(Seq.empty), @@ -2124,7 +2121,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 900, evService.ref, ArrivingEvs(Seq(ev900)), @@ -2152,7 +2149,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 1800, evService.ref, ArrivingEvs(Seq(ev1800)), @@ -2191,7 +2188,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - ProvideEvDataMessage( + DataProvision( 2700, evService.ref, ArrivingEvs(Seq(ev2700)), diff --git a/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala index c8bcdd13b5..0b12d19506 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/FixedFeedInAgentModelCalculationSpec.scala @@ -14,7 +14,6 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.fixedfeedin.FixedFeedInAgent import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData @@ -24,16 +23,19 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ ParticipantUninitializedStateData, SimpleInputContainer, } +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + RegistrationFailedMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.FixedFeedInRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig 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.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.FixedFeedInputTestData import edu.ie3.simona.util.ConfigUtil @@ -152,7 +154,10 @@ class FixedFeedInAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(voltageSensitiveInput.getUuid) + PrimaryServiceRegistrationMessage( + fixedFeedAgent.ref, + voltageSensitiveInput.getUuid, + ) ) /* State should be information handling and having correct state data */ fixedFeedAgent.stateName shouldBe HandleInformation @@ -259,6 +264,7 @@ class FixedFeedInAgentModelCalculationSpec 0L, Each(1d), Each(0d), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -371,6 +377,7 @@ class FixedFeedInAgentModelCalculationSpec 3000L, Each(1d), Each(0d), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -412,6 +419,7 @@ class FixedFeedInAgentModelCalculationSpec 3000L, Each(1d), Each(0d), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -429,6 +437,7 @@ class FixedFeedInAgentModelCalculationSpec 3000L, Each(1.000000000000001d), Each(0d), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -445,6 +454,7 @@ class FixedFeedInAgentModelCalculationSpec 3000L, Each(0.98), Each(0d), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ diff --git a/src/test/scala/edu/ie3/simona/agent/participant/HpAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/HpAgentModelCalculationSpec.scala index 68ff601380..4c3ec24f18 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/HpAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/HpAgentModelCalculationSpec.scala @@ -13,17 +13,22 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPowerAndHeat import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.hp.HpAgent import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData import edu.ie3.simona.agent.participant.statedata.DataCollectionStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData._ +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.HpRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.HpRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.integration.common.IntegrationSpecCommon import edu.ie3.simona.model.participant.HpModel.HpState @@ -31,12 +36,7 @@ import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseState import edu.ie3.simona.ontology.messages.Activation 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, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.ontology.messages.services.WeatherMessage.{ - ProvideWeatherMessage, RegisterForWeatherMessage, WeatherData, } @@ -235,7 +235,7 @@ class HpAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(hpInput.getUuid) + PrimaryServiceRegistrationMessage(hpAgent.ref, hpInput.getUuid) ) /* State should be information handling and having correct state data */ hpAgent.stateName shouldBe HandleInformation @@ -334,7 +334,7 @@ class HpAgentModelCalculationSpec /* Reply, that registration was successful */ weatherService.send( hpAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(4711L)), + RegistrationSuccessfulMessage(weatherService.ref, 4711L), ) /* Expect a completion message */ @@ -379,7 +379,7 @@ class HpAgentModelCalculationSpec ) weatherService.send( hpAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -392,6 +392,7 @@ class HpAgentModelCalculationSpec 0L, Dimensionless.primaryUnit(1.0), Dimensionless.primaryUnit(0.0), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -443,7 +444,7 @@ class HpAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( hpAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(0L)), + RegistrationSuccessfulMessage(weatherService.ref, 0L), ) /* I'm not interested in the content of the Completion */ @@ -461,7 +462,7 @@ class HpAgentModelCalculationSpec weatherService.send( hpAgent, - ProvideWeatherMessage(0L, weatherService.ref, weatherData, Some(3600L)), + DataProvision(0L, weatherService.ref, weatherData, Some(3600L)), ) /* Find yourself in corresponding state and state data */ @@ -574,7 +575,7 @@ class HpAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( hpAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(0L)), + RegistrationSuccessfulMessage(weatherService.ref, 0L), ) /* I'm not interested in the content of the Completion */ @@ -618,7 +619,7 @@ class HpAgentModelCalculationSpec weatherService.send( hpAgent, - ProvideWeatherMessage(0L, weatherService.ref, weatherData, Some(3600L)), + DataProvision(0L, weatherService.ref, weatherData, Some(3600L)), ) /* Expect confirmation */ @@ -703,7 +704,7 @@ class HpAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( hpAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(3600L)), + RegistrationSuccessfulMessage(weatherService.ref, 3600L), ) /* I'm not interested in the content of the Completion */ @@ -715,6 +716,7 @@ class HpAgentModelCalculationSpec 7200L, Each(1.0), Each(0.0), + self.toTyped, ) expectNoMessage(noReceiveTimeOut.duration) awaitAssert(hpAgent.stateName == Idle) @@ -728,7 +730,7 @@ class HpAgentModelCalculationSpec ) weatherService.send( hpAgent, - ProvideWeatherMessage( + DataProvision( 3600L, weatherService.ref, weatherData, @@ -774,7 +776,7 @@ class HpAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( hpAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(0L)), + RegistrationSuccessfulMessage(weatherService.ref, 0L), ) /* I'm not interested in the content of the Completion */ @@ -785,7 +787,7 @@ class HpAgentModelCalculationSpec /* ... for tick 0 */ weatherService.send( hpAgent, - ProvideWeatherMessage( + DataProvision( 0L, weatherService.ref, WeatherData( @@ -803,7 +805,7 @@ class HpAgentModelCalculationSpec /* ... for tick 3600 */ weatherService.send( hpAgent, - ProvideWeatherMessage( + DataProvision( 3600L, weatherService.ref, WeatherData( @@ -821,7 +823,7 @@ class HpAgentModelCalculationSpec /* ... for tick 7200 */ weatherService.send( hpAgent, - ProvideWeatherMessage( + DataProvision( 7200L, weatherService.ref, WeatherData( @@ -841,6 +843,7 @@ class HpAgentModelCalculationSpec 7500L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -858,6 +861,7 @@ class HpAgentModelCalculationSpec 7500L, Each(1.000000000000001d), Each(0.0), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -874,6 +878,7 @@ class HpAgentModelCalculationSpec 7500L, Each(0.98), Each(0.0), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ diff --git a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala index ddb97cd27b..640eee1164 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentFixedModelCalculationSpec.scala @@ -14,7 +14,6 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.load.LoadAgent.FixedLoadAgent import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData @@ -24,16 +23,19 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ ParticipantUninitializedStateData, SimpleInputContainer, } +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + RegistrationFailedMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig 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.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.model.participant.LoadTestData import edu.ie3.simona.util.ConfigUtil @@ -146,7 +148,10 @@ class LoadAgentFixedModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(voltageSensitiveInput.getUuid) + PrimaryServiceRegistrationMessage( + loadAgent.ref, + voltageSensitiveInput.getUuid, + ) ) /* State should be information handling and having correct state data */ loadAgent.stateName shouldBe HandleInformation @@ -253,6 +258,7 @@ class LoadAgentFixedModelCalculationSpec 0L, Each(1d), Each(0d), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -365,6 +371,7 @@ class LoadAgentFixedModelCalculationSpec 3000L, Each(1d), Each(0d), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -406,6 +413,7 @@ class LoadAgentFixedModelCalculationSpec 3000L, Each(1d), Each(0d), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -423,6 +431,7 @@ class LoadAgentFixedModelCalculationSpec 3000L, Each(1.000000000000001d), Each(0d), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -439,6 +448,7 @@ class LoadAgentFixedModelCalculationSpec 3000L, Each(0.98), Each(0d), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ diff --git a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala index 6615308552..885f93916a 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/LoadAgentProfileModelCalculationSpec.scala @@ -14,7 +14,6 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.load.LoadAgent.ProfileLoadAgent import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData @@ -24,16 +23,19 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ ParticipantUninitializedStateData, SimpleInputContainer, } +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + RegistrationFailedMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.LoadRuntimeConfig 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.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.model.participant.LoadTestData import edu.ie3.simona.util.ConfigUtil @@ -253,6 +255,7 @@ class LoadAgentProfileModelCalculationSpec 0L, Each(1d), Each(0d), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -367,6 +370,7 @@ class LoadAgentProfileModelCalculationSpec 1800L, Each(1d), Each(0d), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -383,6 +387,7 @@ class LoadAgentProfileModelCalculationSpec 1800L, Each(1.000000000000001d), Each(0d), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -399,6 +404,7 @@ class LoadAgentProfileModelCalculationSpec 1800L, Each(0.98), Each(0), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala index dcfa3b2cb1..7a06287d09 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgent2ListenerSpec.scala @@ -13,21 +13,21 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.{ - FinishParticipantSimulation, - RequestAssetPowerMessage, -} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + GridSimulationFinished, + RegistrationFailedMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.ResultEvent.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.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.DefaultTestData import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK @@ -232,6 +232,7 @@ class ParticipantAgent2ListenerSpec 3000L, Each(1d), Each(0d), + self.toTyped, ) /* Wait for original reply (this is the querying agent) */ @@ -243,7 +244,7 @@ class ParticipantAgent2ListenerSpec case unknownMsg => fail(s"Received unexpected message: $unknownMsg") } - scheduler.send(mockAgent, FinishParticipantSimulation(3000L)) + scheduler.send(mockAgent, GridSimulationFinished(3000L, 6000L)) /* Wait for the result event (this is the event listener) */ logger.warn( @@ -294,6 +295,7 @@ class ParticipantAgent2ListenerSpec 3000L, Each(1d), Each(0d), + self.toTyped, ) /* Wait for original reply (this is the querying agent) */ @@ -305,7 +307,7 @@ class ParticipantAgent2ListenerSpec case unknownMsg => fail(s"Received unexpected message: $unknownMsg") } - scheduler.send(mockAgent, FinishParticipantSimulation(3000L)) + scheduler.send(mockAgent, GridSimulationFinished(3000L, 6000L)) /* Make sure nothing else is sent */ expectNoMessage(noReceiveTimeOut.duration) diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala index 041052ee5f..48f2c09f93 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentExternalSourceSpec.scala @@ -19,12 +19,13 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ActivePower, ActivePowerAndHeat, + ActivePowerExtra, ComplexPower, ComplexPowerAndHeat, + ComplexPowerExtra, } import edu.ie3.simona.agent.participant.statedata.BaseStateData.FromOutsideBaseStateData import edu.ie3.simona.agent.participant.statedata.DataCollectionStateData @@ -34,10 +35,15 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ ParticipantUninitializedStateData, SimpleInputContainer, } +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + PrimaryRegistrationSuccessfulMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.model.participant.CalcRelevantData.FixedRelevantData import edu.ie3.simona.model.participant.ModelState.ConstantState @@ -46,8 +52,6 @@ import edu.ie3.simona.model.participant.{CalcRelevantData, SystemParticipant} import edu.ie3.simona.ontology.messages.Activation 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.RegistrationSuccessfulMessage -import edu.ie3.simona.service.primary.PrimaryServiceWorker.ProvidePrimaryDataMessage import edu.ie3.simona.test.ParticipantAgentSpec import edu.ie3.simona.test.common.DefaultTestData import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK @@ -173,7 +177,7 @@ class ParticipantAgentExternalSourceSpec /* Expect a registration message */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(testUUID) + PrimaryServiceRegistrationMessage(mockAgent.ref, testUUID) ) /* ... as well as corresponding state and state data */ @@ -206,7 +210,11 @@ class ParticipantAgentExternalSourceSpec /* Reply, that registration was successful */ primaryServiceProxy.send( mockAgent, - RegistrationSuccessfulMessage(primaryServiceProxy.ref, Some(4711L)), + PrimaryRegistrationSuccessfulMessage( + primaryServiceProxy.ref, + 4711L, + ActivePowerExtra, + ), ) scheduler.expectMsg(Completion(mockAgent.toTyped, Some(4711L))) @@ -244,7 +252,11 @@ class ParticipantAgentExternalSourceSpec primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] primaryServiceProxy.send( mockAgent, - RegistrationSuccessfulMessage(primaryServiceProxy.ref, Some(900L)), + PrimaryRegistrationSuccessfulMessage( + primaryServiceProxy.ref, + 900L, + ActivePowerExtra, + ), ) /* I'm not interested in the content of the Completion */ @@ -257,6 +269,7 @@ class ParticipantAgentExternalSourceSpec 0L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -309,7 +322,11 @@ class ParticipantAgentExternalSourceSpec primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] primaryServiceProxy.send( mockAgent, - RegistrationSuccessfulMessage(primaryServiceProxy.ref, Some(900L)), + PrimaryRegistrationSuccessfulMessage( + primaryServiceProxy.ref, + 900L, + ComplexPowerExtra, + ), ) /* I'm not interested in the content of the Completion */ @@ -319,7 +336,7 @@ class ParticipantAgentExternalSourceSpec /* Send out new data */ primaryServiceProxy.send( mockAgent, - ProvidePrimaryDataMessage( + DataProvision( 900L, primaryServiceProxy.ref, ComplexPower( @@ -410,7 +427,11 @@ class ParticipantAgentExternalSourceSpec primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] primaryServiceProxy.send( mockAgent, - RegistrationSuccessfulMessage(primaryServiceProxy.ref, Some(900L)), + PrimaryRegistrationSuccessfulMessage( + primaryServiceProxy.ref, + 900L, + ComplexPowerExtra, + ), ) /* I'm not interested in the content of the Completion */ @@ -451,7 +472,7 @@ class ParticipantAgentExternalSourceSpec /* Providing the awaited data will lead to the foreseen transitions */ primaryServiceProxy.send( mockAgent, - ProvidePrimaryDataMessage( + DataProvision( 900L, primaryServiceProxy.ref, ComplexPower( @@ -505,7 +526,11 @@ class ParticipantAgentExternalSourceSpec primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] primaryServiceProxy.send( mockAgent, - RegistrationSuccessfulMessage(primaryServiceProxy.ref, Some(900L)), + PrimaryRegistrationSuccessfulMessage( + primaryServiceProxy.ref, + 900L, + ComplexPowerExtra, + ), ) /* I'm not interested in the content of the Completion */ @@ -517,6 +542,7 @@ class ParticipantAgentExternalSourceSpec 1800L, Each(1.0), Each(0.0), + self.toTyped, ) expectNoMessage(noReceiveTimeOut.duration) awaitAssert(mockAgent.stateName == Idle) @@ -524,7 +550,7 @@ class ParticipantAgentExternalSourceSpec /* Send out the expected data and wait for the reply */ primaryServiceProxy.send( mockAgent, - ProvidePrimaryDataMessage( + DataProvision( 900L, primaryServiceProxy.ref, ComplexPower( @@ -621,7 +647,11 @@ class ParticipantAgentExternalSourceSpec primaryServiceProxy.expectMsgType[PrimaryServiceRegistrationMessage] primaryServiceProxy.send( mockAgent, - RegistrationSuccessfulMessage(primaryServiceProxy.ref, Some(900L)), + PrimaryRegistrationSuccessfulMessage( + primaryServiceProxy.ref, + 900L, + ComplexPowerExtra, + ), ) /* I'm not interested in the content of the Completion */ @@ -632,7 +662,7 @@ class ParticipantAgentExternalSourceSpec /* ... for tick 900 */ primaryServiceProxy.send( mockAgent, - ProvidePrimaryDataMessage( + DataProvision( 900L, primaryServiceProxy.ref, ComplexPower( @@ -648,7 +678,7 @@ class ParticipantAgentExternalSourceSpec /* ... for tick 1800 */ primaryServiceProxy.send( mockAgent, - ProvidePrimaryDataMessage( + DataProvision( 1800L, primaryServiceProxy.ref, ComplexPower( @@ -664,7 +694,7 @@ class ParticipantAgentExternalSourceSpec /* ... for tick 2700 */ primaryServiceProxy.send( mockAgent, - ProvidePrimaryDataMessage( + DataProvision( 2700L, primaryServiceProxy.ref, ComplexPower( @@ -684,6 +714,7 @@ class ParticipantAgentExternalSourceSpec 3000L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -700,6 +731,7 @@ class ParticipantAgentExternalSourceSpec 3000L, Each(1.000000000000001d), Each(0.0), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -717,6 +749,7 @@ class ParticipantAgentExternalSourceSpec 3000L, Each(0.98d), Each(0.0), + self.toTyped, ) /* Expect, that nothing has changed, as this model is meant to forward information from outside */ diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala index 26058683b3..50c60d246a 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentalsSpec.scala @@ -16,7 +16,7 @@ import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModel import edu.ie3.simona.agent.participant.statedata.ParticipantStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.ParticipantInitializeStateData import edu.ie3.simona.agent.state.AgentState -import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.{ AgentInitializationException, diff --git a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentMock.scala b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentMock.scala index 3e17e76ccb..9d63f3a625 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentMock.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/ParticipantAgentMock.scala @@ -28,7 +28,7 @@ import edu.ie3.simona.agent.participant.statedata.{ import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.BaseRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.exceptions.agent.InvalidRequestException import edu.ie3.simona.io.result.AccompaniedSimulationResult @@ -72,7 +72,7 @@ class ParticipantAgentMock( scheduler: ActorRef, initStateData: ParticipantInitializeStateData[ SystemParticipantInput, - SimonaConfig.BaseRuntimeConfig, + BaseRuntimeConfig, ComplexPower, ], override val listener: Iterable[ActorRef] = Iterable.empty[ActorRef], @@ -82,7 +82,7 @@ class ParticipantAgentMock( ConstantState.type, ParticipantStateData[ComplexPower], SystemParticipantInput, - SimonaConfig.BaseRuntimeConfig, + BaseRuntimeConfig, SystemParticipant[ FixedRelevantData.type, ComplexPower, @@ -95,7 +95,7 @@ class ParticipantAgentMock( ConstantState.type, ParticipantStateData[ComplexPower], SystemParticipantInput, - SimonaConfig.BaseRuntimeConfig, + BaseRuntimeConfig, SystemParticipant[ FixedRelevantData.type, ComplexPower, @@ -192,7 +192,7 @@ class ParticipantAgentMock( */ override def determineModelBaseStateData( inputModel: InputModelContainer[SystemParticipantInput], - modelConfig: SimonaConfig.BaseRuntimeConfig, + modelConfig: BaseRuntimeConfig, services: Iterable[SecondaryDataService[_ <: SecondaryData]], simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, @@ -462,7 +462,7 @@ object ParticipantAgentMock { scheduler: ActorRef, initStateData: ParticipantInitializeStateData[ SystemParticipantInput, - SimonaConfig.BaseRuntimeConfig, + BaseRuntimeConfig, ComplexPower, ], ): Props = diff --git a/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala index 071a5fa2c7..0d742a6ba4 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/PvAgentModelCalculationSpec.scala @@ -14,28 +14,28 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.pv.PvAgent import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData import edu.ie3.simona.agent.participant.statedata.DataCollectionStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData._ +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.PvRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.PvRuntimeConfig 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.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ - RegistrationFailedMessage, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.ontology.messages.services.WeatherMessage.{ - ProvideWeatherMessage, RegisterForWeatherMessage, WeatherData, } @@ -233,7 +233,10 @@ class PvAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(voltageSensitiveInput.getUuid) + PrimaryServiceRegistrationMessage( + pvAgent.ref, + voltageSensitiveInput.getUuid, + ) ) /* State should be information handling and having correct state data */ pvAgent.stateName shouldBe HandleInformation @@ -328,7 +331,7 @@ class PvAgentModelCalculationSpec /* Reply, that registration was successful */ weatherService.send( pvAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(4711L)), + RegistrationSuccessfulMessage(weatherService.ref, 4711L), ) /* Expect a completion message */ @@ -373,7 +376,7 @@ class PvAgentModelCalculationSpec ) weatherService.send( pvAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -386,6 +389,7 @@ class PvAgentModelCalculationSpec 0L, Each(1d), Each(0d), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -436,7 +440,7 @@ class PvAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( pvAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(0L)), + RegistrationSuccessfulMessage(weatherService.ref, 0L), ) /* I'm not interested in the content of the Completion */ @@ -454,7 +458,7 @@ class PvAgentModelCalculationSpec weatherService.send( pvAgent, - ProvideWeatherMessage(0L, weatherService.ref, weatherData, Some(3600L)), + DataProvision(0L, weatherService.ref, weatherData, Some(3600L)), ) /* Find yourself in corresponding state and state data */ @@ -543,7 +547,7 @@ class PvAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( pvAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(0L)), + RegistrationSuccessfulMessage(weatherService.ref, 0), ) /* I'm not interested in the content of the Completion */ @@ -587,7 +591,7 @@ class PvAgentModelCalculationSpec weatherService.send( pvAgent, - ProvideWeatherMessage(0L, weatherService.ref, weatherData, Some(3600L)), + DataProvision(0L, weatherService.ref, weatherData, Some(3600L)), ) /* Expect confirmation */ @@ -648,7 +652,7 @@ class PvAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( pvAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(3600L)), + RegistrationSuccessfulMessage(weatherService.ref, 3600L), ) /* I'm not interested in the content of the Completion */ @@ -660,6 +664,7 @@ class PvAgentModelCalculationSpec 7200L, Each(1d), Each(0d), + self.toTyped, ) expectNoMessage(noReceiveTimeOut.duration) awaitAssert(pvAgent.stateName == Idle) @@ -673,7 +678,7 @@ class PvAgentModelCalculationSpec ) weatherService.send( pvAgent, - ProvideWeatherMessage( + DataProvision( 3600L, weatherService.ref, weatherData, @@ -719,7 +724,7 @@ class PvAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( pvAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(0L)), + RegistrationSuccessfulMessage(weatherService.ref, 0L), ) /* I'm not interested in the content of the Completion */ @@ -730,7 +735,7 @@ class PvAgentModelCalculationSpec /* ... for tick 0 */ weatherService.send( pvAgent, - ProvideWeatherMessage( + DataProvision( 0L, weatherService.ref, WeatherData( @@ -748,7 +753,7 @@ class PvAgentModelCalculationSpec /* ... for tick 3600 */ weatherService.send( pvAgent, - ProvideWeatherMessage( + DataProvision( 3600L, weatherService.ref, WeatherData( @@ -766,7 +771,7 @@ class PvAgentModelCalculationSpec /* ... for tick 7200 */ weatherService.send( pvAgent, - ProvideWeatherMessage( + DataProvision( 7200L, weatherService.ref, WeatherData( @@ -786,6 +791,7 @@ class PvAgentModelCalculationSpec 7500L, Each(1d), Each(0d), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -803,6 +809,7 @@ class PvAgentModelCalculationSpec 7500L, Each(1.000000000000001d), Each(0d), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -819,6 +826,7 @@ class PvAgentModelCalculationSpec 7500L, Each(0.98), Each(0d), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ 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 aa4d1984e5..df1541c8c9 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/StorageAgentModelCalculationSpec.scala @@ -12,7 +12,6 @@ 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.ComplexPower import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ @@ -21,10 +20,14 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ SimpleInputContainer, } import edu.ie3.simona.agent.participant.storage.StorageAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + RegistrationFailedMessage, + RequestAssetPowerMessage, +} 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.config.RuntimeConfig.StorageRuntimeConfig import edu.ie3.simona.event.ResultEvent.{ FlexOptionsResultEvent, ParticipantResultEvent, @@ -36,7 +39,6 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions 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 @@ -138,7 +140,10 @@ class StorageAgentModelCalculationSpec /* Actor should ask for registration with primary service */ primaryServiceProxy.expectMsg( - PrimaryServiceRegistrationMessage(storageInputQv.getUuid) + PrimaryServiceRegistrationMessage( + storageAgent.ref, + storageInputQv.getUuid, + ) ) /* State should be information handling and having correct state data */ storageAgent.stateName shouldBe HandleInformation @@ -174,14 +179,10 @@ class StorageAgentModelCalculationSpec ) emAgent.expectMsg( - RegisterParticipant( - storageInputQv.getUuid, - storageAgent.toTyped, - storageInputQv, - ) + RegisterControlledAsset(storageAgent.toTyped, storageInputQv) ) emAgent.expectMsg( - ScheduleFlexRequest(storageInputQv.getUuid, 0) + ScheduleFlexActivation(storageInputQv.getUuid, 0) ) scheduler.expectMsg(Completion(storageAgent.toTyped)) @@ -247,8 +248,8 @@ class StorageAgentModelCalculationSpec RegistrationFailedMessage(primaryServiceProxy.ref), ) - emAgent.expectMsgType[RegisterParticipant] - emAgent.expectMsg(ScheduleFlexRequest(storageInputQv.getUuid, 0)) + emAgent.expectMsgType[RegisterControlledAsset] + emAgent.expectMsg(ScheduleFlexActivation(storageInputQv.getUuid, 0)) /* I'm not interested in the content of the Completion */ scheduler.expectMsgType[Completion] @@ -260,6 +261,7 @@ class StorageAgentModelCalculationSpec 0, Each(1d), Each(0d), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -308,8 +310,8 @@ class StorageAgentModelCalculationSpec RegistrationFailedMessage(primaryServiceProxy.ref), ) - emAgent.expectMsgType[RegisterParticipant] - emAgent.expectMsg(ScheduleFlexRequest(storageInputQv.getUuid, 0)) + emAgent.expectMsgType[RegisterControlledAsset] + emAgent.expectMsg(ScheduleFlexActivation(storageInputQv.getUuid, 0)) /* I am not interested in the Completion */ scheduler.expectMsgType[Completion] @@ -570,7 +572,7 @@ class StorageAgentModelCalculationSpec case ParticipantResultEvent(result: StorageResult) => result.getInputModel shouldBe storageInputQv.getUuid result.getTime shouldBe 81099.toDateTime(simulationStartDate) - result.getP should beEquivalentTo((-12d).asKiloWatt) + result.getP should beEquivalentTo(-12d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(100d.asPercent) } diff --git a/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala index 01d8e24c29..15a9407985 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/WecAgentModelCalculationSpec.scala @@ -14,7 +14,6 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.ActorWeatherService import edu.ie3.simona.agent.participant.statedata.BaseStateData.ParticipantModelBaseStateData @@ -25,10 +24,16 @@ import edu.ie3.simona.agent.participant.statedata.ParticipantStateData.{ ParticipantUninitializedStateData, } import edu.ie3.simona.agent.participant.wec.WecAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, + RequestAssetPowerMessage, +} import edu.ie3.simona.agent.state.AgentState.{Idle, Uninitialized} import edu.ie3.simona.agent.state.ParticipantAgentState.HandleInformation +import edu.ie3.simona.config.RuntimeConfig.WecRuntimeConfig import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.WecRuntimeConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.model.participant.ModelState.ConstantState import edu.ie3.simona.model.participant.WecModel @@ -37,12 +42,7 @@ import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.ontology.messages.Activation 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, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.ontology.messages.services.WeatherMessage.{ - ProvideWeatherMessage, RegisterForWeatherMessage, WeatherData, } @@ -305,7 +305,7 @@ class WecAgentModelCalculationSpec /* Reply, that registration was successful */ weatherService.send( wecAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(4711L)), + RegistrationSuccessfulMessage(weatherService.ref, 4711L), ) /* Expect a completion message */ @@ -353,7 +353,7 @@ class WecAgentModelCalculationSpec weatherService.expectMsg(RegisterForWeatherMessage(51.4843281, 7.4116482)) weatherService.send( wecAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -366,6 +366,7 @@ class WecAgentModelCalculationSpec 0L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsg( AssetPowerChangedMessage( @@ -421,7 +422,7 @@ class WecAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( wecAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -439,7 +440,7 @@ class WecAgentModelCalculationSpec weatherService.send( wecAgent, - ProvideWeatherMessage( + DataProvision( 900L, weatherService.ref, weatherData, @@ -543,7 +544,7 @@ class WecAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( wecAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -592,7 +593,7 @@ class WecAgentModelCalculationSpec weatherService.send( wecAgent, - ProvideWeatherMessage( + DataProvision( 900L, weatherService.ref, weatherData, @@ -663,7 +664,7 @@ class WecAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( wecAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -675,6 +676,7 @@ class WecAgentModelCalculationSpec 1800L, Each(1.0), Each(0.0), + self.toTyped, ) expectNoMessage(noReceiveTimeOut.duration) awaitAssert(wecAgent.stateName == Idle) @@ -688,7 +690,7 @@ class WecAgentModelCalculationSpec ) weatherService.send( wecAgent, - ProvideWeatherMessage( + DataProvision( 900L, weatherService.ref, weatherData, @@ -734,7 +736,7 @@ class WecAgentModelCalculationSpec weatherService.expectMsgType[RegisterForWeatherMessage] weatherService.send( wecAgent, - RegistrationSuccessfulMessage(weatherService.ref, Some(900L)), + RegistrationSuccessfulMessage(weatherService.ref, 900L), ) /* I'm not interested in the content of the Completion */ @@ -745,7 +747,7 @@ class WecAgentModelCalculationSpec /* ... for tick 900 */ weatherService.send( wecAgent, - ProvideWeatherMessage( + DataProvision( 900L, weatherService.ref, WeatherData( @@ -763,7 +765,7 @@ class WecAgentModelCalculationSpec /* ... for tick 1800 */ weatherService.send( wecAgent, - ProvideWeatherMessage( + DataProvision( 1800L, weatherService.ref, WeatherData( @@ -781,7 +783,7 @@ class WecAgentModelCalculationSpec /* ... for tick 2700 */ weatherService.send( wecAgent, - ProvideWeatherMessage( + DataProvision( 2700L, weatherService.ref, WeatherData( @@ -801,6 +803,7 @@ class WecAgentModelCalculationSpec 3000L, Each(1.0), Each(0.0), + self.toTyped, ) expectMsgType[AssetPowerChangedMessage] match { @@ -818,6 +821,7 @@ class WecAgentModelCalculationSpec 3000L, Each(1.000000000000001d), Each(0.0), + self.toTyped, ) /* Expect, that nothing has changed */ @@ -834,6 +838,7 @@ class WecAgentModelCalculationSpec 3000L, Each(0.98d), Each(0.0), + self.toTyped, ) /* Expect, the correct values (this model has fixed power factor) */ diff --git a/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala new file mode 100644 index 0000000000..2190f7b461 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala @@ -0,0 +1,54 @@ +/* + * © 2024. 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.data + +import edu.ie3.simona.agent.participant.data.Data.PrimaryData._ +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import edu.ie3.util.scala.quantities.Kilovars +import squants.energy.Kilowatts + +class DataSpec extends UnitSpec { + + "Meta functions for active power should work as expected" in { + ActivePowerExtra.zero shouldBe ActivePower(zeroKW) + + ActivePowerExtra.scale( + ActivePower(Kilowatts(5)), + 2.5, + ) shouldBe ActivePower(Kilowatts(12.5)) + } + + "Meta functions for complex power should work as expected" in { + ComplexPowerExtra.zero shouldBe ComplexPower(zeroKW, zeroKVAr) + + ComplexPowerExtra.scale( + ComplexPower(Kilowatts(5), Kilovars(2)), + 1.5, + ) shouldBe ComplexPower(Kilowatts(7.5), Kilovars(3)) + } + + "Meta functions for active power and heat should work as expected" in { + ActivePowerAndHeatExtra.zero shouldBe ActivePowerAndHeat(zeroKW, zeroKW) + + ActivePowerAndHeatExtra.scale( + ActivePowerAndHeat(Kilowatts(5), Kilowatts(2)), + 2, + ) shouldBe ActivePowerAndHeat(Kilowatts(10), Kilowatts(4)) + } + + "Meta functions for complex power and heat should work as expected" in { + ComplexPowerAndHeatExtra.zero shouldBe + ComplexPowerAndHeat(zeroKW, zeroKVAr, zeroKW) + + ComplexPowerAndHeatExtra.scale( + ComplexPowerAndHeat(Kilowatts(5), Kilovars(1), Kilowatts(2)), + 3, + ) shouldBe ComplexPowerAndHeat(Kilowatts(15), Kilovars(3), Kilowatts(6)) + } + +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala new file mode 100644 index 0000000000..42ea376447 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -0,0 +1,186 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} +import edu.ie3.simona.agent.participant2.MockParticipantModel._ +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel._ +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.ActorContext +import squants.energy.{Kilowatts, Power} +import tech.units.indriya.ComparableQuantity + +import java.time.ZonedDateTime +import java.util.UUID +import javax.measure.quantity.{Power => QuantPower} + +/** Mock [[ParticipantModel]] to test various functionality of + * [[edu.ie3.simona.model.participant2.ParticipantModelShell]] and + * [[ParticipantAgent]]. + * + * @param mockActivationTicks + * Map where a current tick maps to the next activation tick + * @param mockChangeAtNext + * Set of current ticks when an activation at the next tick is desired (used + * with flexibility) + */ +class MockParticipantModel( + override val uuid: UUID = UUID.fromString("0-0-0-0-1"), + override val id: String = "MockParticipant 1", + override val sRated: ApparentPower = Kilovoltamperes(10), + override val cosPhiRated: Double = 0.9, + override val qControl: QControl = CosPhiFixed(0.9), + mockActivationTicks: Map[Long, Long] = Map.empty, + mockChangeAtNext: Set[Long] = Set.empty, +) extends ParticipantModel[ + ActivePowerOperatingPoint, + MockState, + ] { + + override val initialState: ModelInput => MockState = { input => + val maybeAdditionalPower = input.receivedData.collectFirst { + case data: MockSecondaryData => + data.additionalP + } + + MockState( + maybeAdditionalPower, + input.currentTick, + ) + } + + override def determineState( + lastState: MockState, + operatingPoint: ActivePowerOperatingPoint, + input: ModelInput, + ): MockState = initialState(input) + + override def determineOperatingPoint( + state: MockState + ): (ActivePowerOperatingPoint, Option[Long]) = { + ( + ActivePowerOperatingPoint( + Kilowatts(6) + state.additionalP.getOrElse(zeroKW) + ), + mockActivationTicks.get(state.tick), + ) + } + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: MockState, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: PrimaryData.ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + Iterable( + MockResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = { + MockResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + } + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + throw new NotImplementedError() // Not tested + + override def determineFlexOptions( + state: MockState + ): FlexibilityMessage.ProvideFlexOptions = { + val additionalP = state.additionalP.getOrElse(zeroKW) + ProvideMinMaxFlexOptions( + uuid, + Kilowatts(1) + additionalP, + Kilowatts(-1) + additionalP, + Kilowatts(3) + additionalP, + ) + } + + override def determineOperatingPoint( + state: MockState, + setPower: Power, + ): (ActivePowerOperatingPoint, OperationChangeIndicator) = + ( + ActivePowerOperatingPoint(setPower), + OperationChangeIndicator( + changesAtNextActivation = mockChangeAtNext.contains(state.tick), + changesAtTick = mockActivationTicks.get(state.tick), + ), + ) + + override def handleRequest( + state: MockState, + ctx: ActorContext[ParticipantAgent.Request], + msg: ParticipantRequest, + ): MockState = { + msg match { + case MockRequestMessage(_, replyTo) => + replyTo ! MockResponseMessage + } + + state + } + +} + +object MockParticipantModel { + + /** Simple [[ModelState]] to test its usage in operation point calculations + * + * @param additionalP + * Power value that is added to the power or flex options power for testing + * purposes + */ + final case class MockState( + additionalP: Option[Power], + override val tick: Long, + ) extends ModelState + + final case class MockResult( + time: ZonedDateTime, + inputModel: UUID, + p: ComparableQuantity[QuantPower], + q: ComparableQuantity[QuantPower], + ) extends SystemParticipantResult(time, inputModel, p, q) + + final case class MockRequestMessage( + override val tick: Long, + replyTo: ActorRef[MockResponseMessage.type], + ) extends ParticipantRequest + + case object MockResponseMessage + + final case class MockSecondaryData(additionalP: Power) extends SecondaryData + +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInitSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInitSpec.scala new file mode 100644 index 0000000000..364cd92104 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInitSpec.scala @@ -0,0 +1,556 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ActivePowerExtra +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + PrimaryRegistrationSuccessfulMessage, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, +} +import edu.ie3.simona.agent.participant2.ParticipantAgentInit.{ + ParticipantRefs, + SimulationParameters, +} +import edu.ie3.simona.config.RuntimeConfig.{LoadRuntimeConfig, PvRuntimeConfig} +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + Completion, + ScheduleActivation, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexActivation, + FlexCompletion, + FlexResponse, + RegisterControlledAsset, + ScheduleFlexActivation, +} +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.WeatherMessage.RegisterForWeatherMessage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.service.ServiceType +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.{LoadInputTestData, PvInputTestData} +import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import edu.ie3.simona.util.TickUtil.TickLong +import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps +import squants.Each + +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +/** Testing [[ParticipantAgentInit]], which means testing the complete + * initialization process of [[ParticipantAgent]] up until the first tick + */ +class ParticipantAgentInitSpec + extends ScalaTestWithActorTestKit + with UnitSpec + with LoadInputTestData + with PvInputTestData { + + private implicit val simulationStart: ZonedDateTime = defaultSimulationStart + + private val simulationParams = SimulationParameters( + 3600, + Each(1e-14), + simulationStart, + defaultSimulationStart.plus(2, ChronoUnit.DAYS), + ) + + "A ParticipantAgent that is not depending on external services" when { + + val config = LoadRuntimeConfig() + + val operationStart = 10 * 3600L + + val mockInput = loadInput.copy + .operationTime( + OperationTime.builder + .withStart(operationStart.toDateTime) + .build() + ) + .build() + + "not controlled by EM" should { + + "initialize correctly when not replaying primary data" in { + + val scheduler = createTestProbe[SchedulerMessage]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map.empty, + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Left(scheduler.ref), + ) + ) + + val scheduleMsg = scheduler.expectMessageType[ScheduleActivation] + scheduleMsg.tick shouldBe INIT_SIM_TICK + val activationRef = scheduleMsg.actor + + activationRef ! Activation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! RegistrationFailedMessage( + primaryService.ref.toClassic + ) + + scheduler.expectMessage(Completion(activationRef, Some(operationStart))) + + } + + "initialize correctly when replaying primary data" in { + + val scheduler = createTestProbe[SchedulerMessage]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map.empty, + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Left(scheduler.ref), + ) + ) + + val scheduleMsg = scheduler.expectMessageType[ScheduleActivation] + scheduleMsg.tick shouldBe INIT_SIM_TICK + val activationRef = scheduleMsg.actor + + activationRef ! Activation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! PrimaryRegistrationSuccessfulMessage( + primaryService.ref.toClassic, + 15 * 3600L, + ActivePowerExtra, + ) + + scheduler.expectMessage(Completion(activationRef, Some(15 * 3600L))) + + } + + } + + "controlled by EM" should { + + "initialize correctly when not replaying primary data" in { + + val em = createTestProbe[FlexResponse]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map.empty, + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Right(em.ref), + ) + ) + + val emRegistrationMsg = em.expectMessageType[RegisterControlledAsset] + emRegistrationMsg.modelUuid shouldBe mockInput.getUuid + emRegistrationMsg.inputModel shouldBe mockInput + val activationRef = emRegistrationMsg.participant + + em.expectMessage( + ScheduleFlexActivation(mockInput.getUuid, INIT_SIM_TICK) + ) + + activationRef ! FlexActivation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! RegistrationFailedMessage( + primaryService.ref.toClassic + ) + + em.expectMessage( + FlexCompletion( + mockInput.getUuid, + requestAtTick = Some(operationStart), + ) + ) + + } + + "initialize correctly when replaying primary data" in { + + val em = createTestProbe[FlexResponse]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map.empty, + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Right(em.ref), + ) + ) + + val emRegistrationMsg = em.expectMessageType[RegisterControlledAsset] + emRegistrationMsg.modelUuid shouldBe mockInput.getUuid + emRegistrationMsg.inputModel shouldBe mockInput + val activationRef = emRegistrationMsg.participant + + em.expectMessage( + ScheduleFlexActivation(mockInput.getUuid, INIT_SIM_TICK) + ) + + activationRef ! FlexActivation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! PrimaryRegistrationSuccessfulMessage( + primaryService.ref.toClassic, + 15 * 3600L, + ActivePowerExtra, + ) + + em.expectMessage( + FlexCompletion(mockInput.getUuid, requestAtTick = Some(15 * 3600L)) + ) + } + + } + + } + + "A ParticipantAgent that is depending on an external service" when { + + val operationStart = 10 * 3600L + + val mockInput = pvInput.copy + .operationTime( + OperationTime.builder + .withStart(operationStart.toDateTime) + .build() + ) + .build() + + val config = PvRuntimeConfig() + + "not controlled by EM" should { + + "initialize correctly when not replaying primary data" in { + + val scheduler = createTestProbe[SchedulerMessage]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe[Any]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map(ServiceType.WeatherService -> service.ref.toClassic), + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Left(scheduler.ref), + ) + ) + + val scheduleMsg = scheduler.expectMessageType[ScheduleActivation] + scheduleMsg.tick shouldBe INIT_SIM_TICK + val activationRef = scheduleMsg.actor + + activationRef ! Activation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! RegistrationFailedMessage( + primaryService.ref.toClassic + ) + + service.expectMessage( + RegisterForWeatherMessage( + mockInput.getNode.getGeoPosition.getY, + mockInput.getNode.getGeoPosition.getX, + ) + ) + + participantAgent ! RegistrationSuccessfulMessage( + service.ref.toClassic, + 12 * 3600L, + ) + + scheduler.expectMessage(Completion(activationRef, Some(12 * 3600L))) + + } + + "initialize correctly when replaying primary data" in { + + val scheduler = createTestProbe[SchedulerMessage]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe[Any]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map(ServiceType.WeatherService -> service.ref.toClassic), + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Left(scheduler.ref), + ) + ) + + val scheduleMsg = scheduler.expectMessageType[ScheduleActivation] + scheduleMsg.tick shouldBe INIT_SIM_TICK + val activationRef = scheduleMsg.actor + + activationRef ! Activation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! PrimaryRegistrationSuccessfulMessage( + primaryService.ref.toClassic, + // no activation expected for this tick, since it is + // outside the operation interval + 15 * 3600L, + ActivePowerExtra, + ) + + scheduler.expectMessage(Completion(activationRef, Some(15 * 3600L))) + + // service should not be called at all + service.expectNoMessage() + } + + } + + "controlled by EM" should { + + "initialize correctly when not replaying primary data" in { + + val em = createTestProbe[FlexResponse]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe[Any]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map(ServiceType.WeatherService -> service.ref.toClassic), + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Right(em.ref), + ) + ) + + val emRegistrationMsg = em.expectMessageType[RegisterControlledAsset] + emRegistrationMsg.modelUuid shouldBe mockInput.getUuid + emRegistrationMsg.inputModel shouldBe mockInput + val activationRef = emRegistrationMsg.participant + + em.expectMessage( + ScheduleFlexActivation(mockInput.getUuid, INIT_SIM_TICK) + ) + + activationRef ! FlexActivation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! RegistrationFailedMessage( + primaryService.ref.toClassic + ) + + service.expectMessage( + RegisterForWeatherMessage( + mockInput.getNode.getGeoPosition.getY, + mockInput.getNode.getGeoPosition.getX, + ) + ) + + participantAgent ! RegistrationSuccessfulMessage( + service.ref.toClassic, + 12 * 3600L, + ) + + em.expectMessage( + FlexCompletion(mockInput.getUuid, requestAtTick = Some(12 * 3600L)) + ) + } + + "initialize correctly when replaying primary data" in { + + val em = createTestProbe[FlexResponse]() + + val gridAgent = createTestProbe[GridAgent.Request]() + val primaryService = createTestProbe[Any]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe[Any]() + + val refs = ParticipantRefs( + gridAgent = gridAgent.ref, + primaryServiceProxy = primaryService.ref.toClassic, + services = Map(ServiceType.WeatherService -> service.ref.toClassic), + resultListener = Iterable(resultListener.ref), + ) + + val participantAgent = spawn( + ParticipantAgentInit( + mockInput, + config, + refs, + simulationParams, + Right(em.ref), + ) + ) + + val emRegistrationMsg = em.expectMessageType[RegisterControlledAsset] + emRegistrationMsg.modelUuid shouldBe mockInput.getUuid + emRegistrationMsg.inputModel shouldBe mockInput + val activationRef = emRegistrationMsg.participant + + em.expectMessage( + ScheduleFlexActivation(mockInput.getUuid, INIT_SIM_TICK) + ) + + activationRef ! FlexActivation(INIT_SIM_TICK) + + primaryService.expectMessage( + PrimaryServiceRegistrationMessage( + participantAgent.ref.toClassic, + mockInput.getUuid, + ) + ) + + participantAgent ! PrimaryRegistrationSuccessfulMessage( + primaryService.ref.toClassic, + // no activation expected for this tick, since it is + // outside the operation interval + 15 * 3600L, + ActivePowerExtra, + ) + + em.expectMessage( + FlexCompletion(mockInput.getUuid, requestAtTick = Some(15 * 3600L)) + ) + + // service should not be called at all + service.expectNoMessage() + } + + } + + } + +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala new file mode 100644 index 0000000000..b1950a0164 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala @@ -0,0 +1,65 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + Flex, + FlexControlledData, + ParticipantActivation, + Request, + SchedulerData, +} +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.model.participant2.ParticipantModelShell +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexRequest, + FlexResponse, +} +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import org.apache.pekko.actor.typed.scaladsl.Behaviors + +object ParticipantAgentMockFactory { + + /** Creates a [[ParticipantAgent]] behavior with given parameters. This detour + * is needed because normally, [[ParticipantAgentInit]] creates adapters that + * are handed over to [[ParticipantAgent]]. + */ + def create( + modelShell: ParticipantModelShell[_, _], + inputHandler: ParticipantInputHandler, + gridAdapter: ParticipantGridAdapter, + resultListener: Iterable[ActorRef[ResultEvent]], + parent: Either[ + (ActorRef[SchedulerMessage], ActorRef[ActorRef[Activation]]), + (ActorRef[FlexResponse], ActorRef[ActorRef[FlexRequest]]), + ], + ): Behavior[Request] = Behaviors.setup { ctx => + val parentData = parent + .map { case (em, adapterReply) => + val flexAdapter = ctx.messageAdapter[FlexRequest](Flex) + adapterReply ! flexAdapter + FlexControlledData(em, flexAdapter) + } + .left + .map { case (scheduler, adapterReply) => + val activationAdapter = ctx.messageAdapter[Activation] { msg => + ParticipantActivation(msg.tick) + } + adapterReply ! activationAdapter + SchedulerData(scheduler, activationAdapter) + } + + ParticipantAgent( + modelShell, + inputHandler, + gridAdapter, + resultListener, + parentData, + ) + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala new file mode 100644 index 0000000000..5395917d4b --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -0,0 +1,1654 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.GridAgentMessages.{ + AssetPowerChangedMessage, + AssetPowerUnchangedMessage, +} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ActivePower, + ActivePowerExtra, +} +import edu.ie3.simona.agent.participant2.MockParticipantModel.{ + MockRequestMessage, + MockResponseMessage, + MockResult, + MockSecondaryData, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + GridSimulationFinished, + NoDataProvision, + RequestAssetPowerMessage, +} +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent +import edu.ie3.simona.model.participant2.{ + ParticipantModelInit, + ParticipantModelShell, +} +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.util.TickUtil.TickLong +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.{Kilovars, ReactivePower} +import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.adapter._ +import squants.energy.Kilowatts +import squants.{Each, Power} + +import java.time.ZonedDateTime + +/** Test for [[ParticipantAgent]] and [[ParticipantModelShell]] using a mock + * participant [[MockParticipantModel]]. Primary and secondary data handling is + * tested here. + */ +class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { + + private implicit val simulationStartDate: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") + + private implicit val activePowerTolerance: Power = Kilowatts(1e-10) + private implicit val reactivePowerTolerance: ReactivePower = Kilovars(1e-10) + + "A ParticipantAgent that is not controlled by EM" when { + + "not depending on external services" should { + + "calculate operating point and results correctly with no additional model activations" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val responseReceiver = createTestProbe[MockResponseMessage.type]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val model = new MockParticipantModel() + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 8 * 3600: Start of operation interval + + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593262.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 12 * 3600: GridAgent requests power + + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + + // first request + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 6 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.968644209676)) + } + + // second request with same voltage + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + gridAgent.expectMessageType[AssetPowerUnchangedMessage] match { + case AssetPowerUnchangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.968644209676)) + } + + // third request with different voltage + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(0.98), + Each(0), + gridAgent.ref, + ) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + // not voltage dependent + q should approximate(Kilovars(0.968644209676)) + } + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(4)) + q should approximate(Kilovars(1.93728841935)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + } + + "calculate operating point and results correctly with additional model activations" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // with additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 22 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593262.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Inside of operation interval and GridAgent requests power + + activationRef ! Activation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 6 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.968644209676)) + } + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593262.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(4)) + q should approximate(Kilovars(1.93728841935)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + } + + } + + "depending on secondary data" should { + + "calculate operating point and results correctly with additional model activations" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // with additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 22 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 0: Outside of operation interval + + activationRef ! Activation(0) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! DataProvision( + 0, + service.ref.toClassic, + MockSecondaryData(Kilowatts(1)), + Some(6 * 3600), + ) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! DataProvision( + 6 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(3)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.009.asMegaWatt) + result.getQ should equalWithTolerance(0.00435889893.asMegaVar) + } + + // next model tick and next data tick are both hour 12 + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Inside of operation interval, secondary data and GridAgent requests power + + activationRef ! Activation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 6+3=9 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(3)) + q should approximate(Kilovars(1.452966314514)) + } + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! DataProvision( + 12 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(6)), + Some(15 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.012.asMegaWatt) + result.getQ should equalWithTolerance(0.005811865258.asMegaVar) + } + + // new data is expected at 18 hours + scheduler.expectMessage( + Completion(activationRef, Some(15 * 3600)) + ) + + // TICK 15 * 3600: Inside of operation interval, but empty input data received + + activationRef ! Activation(15 * 3600) + + // nothing should happen, still waiting for secondary data... + scheduler.expectNoMessage() + + participantAgent ! NoDataProvision( + 15 * 3600, + service.ref.toClassic, + Some(18 * 3600), + ) + + // no-op activation, thus no result expected + resultListener.expectNoMessage() + + // new data is expected at 18 hours + scheduler.expectMessage( + Completion(activationRef, Some(18 * 3600)) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected secondary data + + activationRef ! Activation(18 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! DataProvision( + 18 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(9)), + Some(24 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.015.asMegaWatt) + result.getQ should equalWithTolerance(0.00726483157257.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 6 hours of 6+6=12 kW, 2 hours of 6+9=15 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(8.5)) + q should approximate(Kilovars(4.116737891123)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + } + + } + + "depending on primary data" should { + + "calculate operating point and results correctly" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val physicalModel = new MockParticipantModel() + + val model = ParticipantModelInit.createPrimaryModel( + physicalModel, + ActivePowerExtra, + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 0: Outside of operation interval + + activationRef ! Activation(0) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! DataProvision( + 0, + service.ref.toClassic, + ActivePower(Kilowatts(1)), + Some(6 * 3600), + ) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! DataProvision( + 6 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + // next data tick is hour 12 + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + activationRef ! Activation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 3 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210484)) + } + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! DataProvision( + 12 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(6)), + Some(18 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593263.asMegaVar) + } + + // new data is expected at 18 hours + scheduler.expectMessage( + Completion(activationRef, Some(18 * 3600)) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected primary data + + activationRef ! Activation(18 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! DataProvision( + 18 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(24 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 6 hours of 6 kW, 2 hours of 3 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(3.5)) + q should approximate(Kilovars(1.695127366932)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + } + + } + + } + + "A ParticipantAgent that is controlled by EM" when { + + "not depending on external services" should { + + "calculate operating point and results correctly with no additional model activations" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // no additional activation ticks + val model = new MockParticipantModel() + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(1)) + min should approximate(Kilowatts(-1)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 12 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 3 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 3 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.96864420966)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + } + + "calculate operating point and results correctly with additional model activations" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // with additional activation ticks + val model = new MockParticipantModel( + mockActivationTicks = Map( + 0 * 3600L -> 4 * 3600L, // out of operation, is ignored + 8 * 3600L -> 12 * 3600L, // in operation + 12 * 3600L -> 22 * 3600L, // out of operation, is ignored + ), + mockChangeAtNext = Set( + 0, // out of operation, is ignored + 12 * 3600, // in operation + 20 * 3600, // out of operation, is ignored + ), + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(1)) + min should approximate(Kilowatts(-1)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(12 * 3600), + ) + ) + + // TICK 12 * 3600: Inside of operation interval and GridAgent requests power + + flexRef ! FlexActivation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(1)) + min should approximate(Kilowatts(-1)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssueNoControl(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.001.asMegaWatt) + result.getQ should equalWithTolerance(0.0004843221.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtNextActivation = true, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 1 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(0.6666666667)) + q should approximate(Kilovars(0.32288140322)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + } + + } + + "depending on secondary data" should { + + "calculate operating point and results correctly with additional model activations" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // with additional activation ticks + val model = new MockParticipantModel( + mockActivationTicks = Map( + 0 * 3600L -> 4 * 3600L, // out of operation, is ignored + 8 * 3600L -> 12 * 3600L, // in operation + 12 * 3600L -> 22 * 3600L, // out of operation, is ignored + ), + mockChangeAtNext = Set( + 0, // out of operation, is ignored + 18 * 3600, // in operation + 20 * 3600, // out of operation, is ignored + ), + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 0: Outside of operation interval + + flexRef ! FlexActivation(0) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! DataProvision( + 0, + service.ref.toClassic, + MockSecondaryData(Kilowatts(1)), + Some(6 * 3600), + ) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(0) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.start), + ) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! DataProvision( + 6 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(1)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + em.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(2)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(4)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + // next model tick and next data tick are both hour 12 + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(12 * 3600), + ) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + flexRef ! FlexActivation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 3 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! DataProvision( + 12 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(2)), + Some(18 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(3)) + min should approximate(Kilowatts(1)) + max should approximate(Kilowatts(5)) + } + + flexRef ! IssueNoControl(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(18 * 3600), + ) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected secondary data + + flexRef ! FlexActivation(18 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! DataProvision( + 18 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(5)), + Some(24 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(6)) + min should approximate(Kilowatts(4)) + max should approximate(Kilowatts(8)) + } + + flexRef ! IssueNoControl(18 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.002905932629.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtNextActivation = true, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 6 hours of 3 kW, 2 hours of 6 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2.5)) + q should approximate(Kilovars(1.210805262)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + em.expectNoMessage() + + } + + } + + "depending on primary data" should { + + "calculate operating point and results correctly" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // no additional activation ticks + val physicalModel = new MockParticipantModel() + + val model = ParticipantModelInit.createPrimaryModel( + physicalModel, + ActivePowerExtra, + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + expectedRequestTick = 12 * 3600, + requestVoltageDeviationTolerance = Each(1e-14), + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 0: Outside of operation interval + + flexRef ! FlexActivation(0) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! DataProvision( + 0, + service.ref.toClassic, + ActivePower(Kilowatts(1)), + Some(6 * 3600), + ) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(0) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.start), + ) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! DataProvision( + 6 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + em.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(3)) + min should approximate(Kilowatts(3)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + // next data tick is hour 12 + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(12 * 3600), + ) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + flexRef ! FlexActivation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 8 hours of 0 kW, 4 hours of 3 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + participantAgent ! GridSimulationFinished(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! DataProvision( + 12 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(6)), + Some(18 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(6)) + min should approximate(Kilowatts(6)) + max should approximate(Kilowatts(6)) + } + + flexRef ! IssueNoControl(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593263.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(18 * 3600), + ) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected primary data + + flexRef ! FlexActivation(18 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! DataProvision( + 18 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(24 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(3)) + min should approximate(Kilowatts(3)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssueNoControl(18 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage( + 24 * 3600, + Each(1), + Each(0), + gridAgent.ref, + ) + + // 6 hours of 6 kW, 2 hours of 3 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(3.5)) + q should approximate(Kilovars(1.695127366932)) + } + + participantAgent ! GridSimulationFinished(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + em.expectNoMessage() + + } + + } + + } + +} diff --git a/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala b/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala index 0a4dab2d51..b1d5d34028 100644 --- a/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala +++ b/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala @@ -21,7 +21,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.ScheduleServiceActivation +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ScheduleServiceActivation import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.test.common.{TestKitWithShutdown, TestSpawnerClassic} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK diff --git a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala index f86b0b47c0..b035fd3279 100644 --- a/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/ConfigFailFastSpec.scala @@ -7,6 +7,7 @@ package edu.ie3.simona.config import com.typesafe.config.ConfigFactory +import edu.ie3.simona.config.RuntimeConfig.StorageRuntimeConfig import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource import edu.ie3.simona.config.SimonaConfig.Simona.Input.Weather.Datasource.{ CoordinateSource, @@ -38,7 +39,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "let valid input pass" in { noException shouldBe thrownBy { ConfigFailFast invokePrivate checkTimeConfig( - new Time( + Time( "2020-06-18T13:41:00Z", None, "2020-05-18T13:41:00Z", @@ -50,7 +51,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "identify invalid date or time configuration" in { intercept[InvalidConfigParameterException] { ConfigFailFast invokePrivate checkTimeConfig( - new Time( + Time( "2020-06-18T13:41:00Z", None, "2020-07-18T13:41:00Z", @@ -94,7 +95,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ConfigFailFast invokePrivate checkPowerFlowResolutionConfiguration( new Powerflow( 10, - new Newtonraphson( + Newtonraphson( List(10, 30), 100, ), @@ -111,7 +112,7 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { ConfigFailFast invokePrivate checkPowerFlowResolutionConfiguration( new Powerflow( 10, - new Newtonraphson( + Newtonraphson( List(10, 30), 100, ), @@ -283,6 +284,138 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } } + "A configuration with faulty voltage limit parameters" should { + val checkVoltageLimits = + PrivateMethod[Unit](Symbol("checkVoltageLimits")) + + "throw an InvalidConfigParametersException when gridIds and voltLvls are empty" in { + + val voltageLimitsConfigAllEmpty = ConfigFactory.parseString( + "simona.gridConfig.voltageLimits = [{vMin=\"0.9\", vMax=\"1.1\"}]" + ) + val faultyConfig = + voltageLimitsConfigAllEmpty.withFallback(typesafeConfig).resolve() + val faultySimonaConfig = SimonaConfig(faultyConfig) + + intercept[InvalidConfigParameterException] { + ConfigFailFast.check(faultySimonaConfig) + }.getMessage shouldBe "The provided values for voltLvls and gridIds are empty! " + + "At least one of these optional parameters has to be provided for a valid voltage limit! " + + "Provided voltage limit is: VoltageLimitsConfig(None,1.1,0.9,None)." + + } + + "throw an InvalidConfigParametersException when the gridId is malformed" in { + + val malformedGridIds = List("10--100", "MV", "10..100") + + malformedGridIds.foreach(malformedGridId => { + + val voltageLimitsConfig = + ConfigFactory.parseString(s"""simona.gridConfig.voltageLimits = [ + | { + | vMin="0.9", + | vMax="1.1", + | gridIds = [$malformedGridId] + | } + |]""".stripMargin) + val faultyConfig = + voltageLimitsConfig.withFallback(typesafeConfig).resolve() + val faultySimonaConfig: List[SimonaConfig] = + List(SimonaConfig(faultyConfig)) + + intercept[InvalidConfigParameterException] { + faultySimonaConfig.foreach(conf => + conf.simona.gridConfig.voltageLimits.foreach(refSystem => + ConfigFailFast invokePrivate checkVoltageLimits(refSystem) + ) + ) + }.getMessage shouldBe s"The provided gridId $malformedGridId is malformed!" + + }) + } + + "throw an InvalidConfigParameterException if the nominal voltage of the voltage level is malformed" in { + + val voltageLimitsConfig = + ConfigFactory.parseString("""simona.gridConfig.voltageLimits = [ + | { + | vMin="0.9", + | vMax="1.1", + | voltLvls = [{id = "1", vNom = "foo"}] + | } + |]""".stripMargin) + val faultyConfig = + voltageLimitsConfig.withFallback(typesafeConfig).resolve() + val faultySimonaConfig: List[SimonaConfig] = + List(SimonaConfig(faultyConfig)) + + intercept[InvalidConfigParameterException] { + faultySimonaConfig.foreach(conf => + conf.simona.gridConfig.voltageLimits.foreach(refSystem => + ConfigFailFast invokePrivate checkVoltageLimits(refSystem) + ) + ) + }.getMessage shouldBe "The given nominal voltage 'foo' cannot be parsed to a quantity. Did you provide the volt level with it's unit (e.g. \"20 kV\")?" + + } + + "throw an InvalidConfigParametersException when vMin is greater than vMax" in { + val voltageLimitsConfig = + ConfigFactory.parseString( + """simona.gridConfig.voltageLimits = [ + | { + | vMin="1.1", + | vMax="1.0", + | voltLvls = [{id = "MV", vNom = "10 kV"},{id = "HV", vNom = "110 kV"}] + | } + |]""".stripMargin + ) + val faultyConfig = + voltageLimitsConfig.withFallback(typesafeConfig).resolve() + val faultySimonaConfig: List[SimonaConfig] = + List(SimonaConfig(faultyConfig)) + + intercept[InvalidConfigParameterException] { + faultySimonaConfig.foreach(conf => + conf.simona.gridConfig.voltageLimits.foreach(refSystem => + ConfigFailFast invokePrivate checkVoltageLimits(refSystem) + ) + ) + }.getMessage shouldBe "Invalid value for vMin and vMax from provided voltage limit VoltageLimitsConfig(None,1.0,1.1,Some(List(VoltLvlConfig(MV,10 kV), VoltLvlConfig(HV,110 kV)))). Is vMin smaller than vMax?" + } + + "work as expected for correctly provided data" in { + val voltageLimitsConfig = + ConfigFactory.parseString( + """simona.gridConfig.voltageLimits = [ + | { + | vMin="0.9", + | vMax="1.1", + | voltLvls = [{id = "MV", vNom = "10 kV"},{id = "HV", vNom = "110 kV"}] + | gridIds = ["1","1-10","10...100"] + | }, + | { + | vMin="0.9", + | vMax="1.1", + | voltLvls = [{id = "HV", vNom = "110 kV"},{id = "EHV", vNom = "380 kV"}] + | gridIds = ["1-3","3...6","10...100"] + | } + |]""".stripMargin + ) + val config = + voltageLimitsConfig.withFallback(typesafeConfig).resolve() + val simonaConfig = List(SimonaConfig(config)) + + simonaConfig.foreach(conf => + conf.simona.gridConfig.voltageLimits.foreach(refSystem => { + ConfigFailFast invokePrivate checkVoltageLimits(refSystem) + }) + ) + + } + } + "Checking a participant model config" should { val checkParticipantRuntimeConfiguration = PrivateMethod[Unit](Symbol("checkParticipantRuntimeConfiguration")) @@ -1197,16 +1330,16 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "throw exception if default initial SOC is negative" in { - val defaultConfig: SimonaConfig.StorageRuntimeConfig = - SimonaConfig.StorageRuntimeConfig( + val defaultConfig: StorageRuntimeConfig = + 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) + val storageConfig = + RuntimeConfig.ParticipantRuntimeConfigs(defaultConfig, List.empty) intercept[RuntimeException] { ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) @@ -1214,16 +1347,16 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } "throw exception if default target SOC is negative" in { - val defaultConfig: SimonaConfig.StorageRuntimeConfig = - SimonaConfig.StorageRuntimeConfig( + val defaultConfig: StorageRuntimeConfig = + 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) + val storageConfig = + RuntimeConfig.ParticipantRuntimeConfigs(defaultConfig, List.empty) intercept[RuntimeException] { ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) @@ -1232,16 +1365,16 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "throw exception if individual initial SOC is negative" in { val uuid = java.util.UUID.randomUUID().toString - val defaultConfig: SimonaConfig.StorageRuntimeConfig = - SimonaConfig.StorageRuntimeConfig( + val defaultConfig: StorageRuntimeConfig = + StorageRuntimeConfig( calculateMissingReactivePowerWithModel = false, 1.0, List(java.util.UUID.randomUUID().toString), 0.5, Some(0.8), ) - val individualConfig: List[SimonaConfig.StorageRuntimeConfig] = List( - SimonaConfig.StorageRuntimeConfig( + val individualConfig: List[StorageRuntimeConfig] = List( + StorageRuntimeConfig( calculateMissingReactivePowerWithModel = false, 1.0, List(uuid), @@ -1249,8 +1382,10 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { Some(0.8), ) ) - val storageConfig = SimonaConfig.Simona.Runtime.Participant - .Storage(defaultConfig, individualConfig) + val storageConfig = RuntimeConfig.ParticipantRuntimeConfigs( + defaultConfig, + individualConfig, + ) intercept[RuntimeException] { ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) @@ -1259,16 +1394,16 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { "throw exception if individual target SOC is negative" in { val uuid = java.util.UUID.randomUUID().toString - val defaultConfig: SimonaConfig.StorageRuntimeConfig = - SimonaConfig.StorageRuntimeConfig( + val defaultConfig: StorageRuntimeConfig = + StorageRuntimeConfig( calculateMissingReactivePowerWithModel = false, 1.0, List(java.util.UUID.randomUUID().toString), 0.5, Some(0.8), ) - val individualConfig: List[SimonaConfig.StorageRuntimeConfig] = List( - SimonaConfig.StorageRuntimeConfig( + val individualConfig: List[StorageRuntimeConfig] = List( + StorageRuntimeConfig( calculateMissingReactivePowerWithModel = false, 1.0, List(uuid), @@ -1276,8 +1411,10 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { Some(-0.8), ) ) - val storageConfig = SimonaConfig.Simona.Runtime.Participant - .Storage(defaultConfig, individualConfig) + val storageConfig = RuntimeConfig.ParticipantRuntimeConfigs( + defaultConfig, + individualConfig, + ) intercept[RuntimeException] { ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) @@ -1285,16 +1422,16 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { } "not throw exception if all parameters are in parameter range" in { - val defaultConfig: SimonaConfig.StorageRuntimeConfig = - SimonaConfig.StorageRuntimeConfig( + val defaultConfig: StorageRuntimeConfig = + StorageRuntimeConfig( calculateMissingReactivePowerWithModel = false, 1.0, List(java.util.UUID.randomUUID().toString), 0.5, Some(0.8), ) - val individualConfig: List[SimonaConfig.StorageRuntimeConfig] = List( - SimonaConfig.StorageRuntimeConfig( + val individualConfig: List[StorageRuntimeConfig] = List( + StorageRuntimeConfig( calculateMissingReactivePowerWithModel = false, 1.0, List(java.util.UUID.randomUUID().toString), @@ -1302,8 +1439,10 @@ class ConfigFailFastSpec extends UnitSpec with ConfigTestData { Some(0.8), ) ) - val storageConfig = SimonaConfig.Simona.Runtime.Participant - .Storage(defaultConfig, individualConfig) + val storageConfig = RuntimeConfig.ParticipantRuntimeConfigs( + defaultConfig, + individualConfig, + ) noException should be thrownBy { ConfigFailFast invokePrivate checkStorageConfigs(storageConfig) diff --git a/src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala b/src/test/scala/edu/ie3/simona/config/GridConfigParserSpec.scala similarity index 52% rename from src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala rename to src/test/scala/edu/ie3/simona/config/GridConfigParserSpec.scala index 13ff81a1a3..9bac374c49 100644 --- a/src/test/scala/edu/ie3/simona/config/RefSystemParserSpec.scala +++ b/src/test/scala/edu/ie3/simona/config/GridConfigParserSpec.scala @@ -7,16 +7,20 @@ package edu.ie3.simona.config import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.simona.config.SimonaConfig.{RefSystemConfig, VoltLvlConfig} +import edu.ie3.simona.config.SimonaConfig.{ + RefSystemConfig, + VoltLvlConfig, + VoltageLimitsConfig, +} import edu.ie3.simona.exceptions.InvalidConfigParameterException -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.test.common.UnitSpec import squants.electro.{Kilovolts, Volts} import squants.energy.{Kilowatts, Megawatts} -class RefSystemParserSpec extends UnitSpec { +class GridConfigParserSpec extends UnitSpec { - "A RefSystemParser" must { + "A GridConfigParser.parseRefSystems" must { // check internal gridIdRefSystems val gridIdRefSystems = PrivateMethod[Map[Int, RefSystem]](Symbol("gridIdRefSystems")) @@ -26,7 +30,7 @@ class RefSystemParserSpec extends UnitSpec { PrivateMethod[Map[String, RefSystem]](Symbol("voltLvLRefSystems")) "return the default ref systems if no config was provided" in { - val defaults = RefSystemParser.parse(Some(List.empty)) + val defaults = GridConfigParser.parseRefSystems(Some(List.empty)) defaults invokePrivate gridIdRefSystems() shouldBe Map.empty @@ -60,7 +64,7 @@ class RefSystemParserSpec extends UnitSpec { val validRefSystems: Option[List[SimonaConfig.RefSystemConfig]] = Some( List( - new RefSystemConfig( + RefSystemConfig( gridIds = Some(List("1", "2-10", "15...20")), sNom = "100 MVA", vNom = "10 kV", @@ -68,7 +72,7 @@ class RefSystemParserSpec extends UnitSpec { List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) ), ), - new RefSystemConfig( + RefSystemConfig( gridIds = Some(List("100")), sNom = "5000 MVA", vNom = "110 kV", @@ -79,7 +83,7 @@ class RefSystemParserSpec extends UnitSpec { ) ), ), - new RefSystemConfig( + RefSystemConfig( gridIds = None, sNom = "5000 MVA", vNom = "110 kV", @@ -88,7 +92,7 @@ class RefSystemParserSpec extends UnitSpec { ) ) - val configRefSystems = RefSystemParser.parse(validRefSystems) + val configRefSystems = GridConfigParser.parseRefSystems(validRefSystems) // prepare internal value check val configRefSystemOne = RefSystem("100 MVA", "10 kV") @@ -128,7 +132,7 @@ class RefSystemParserSpec extends UnitSpec { val validRefSystems: Option[List[SimonaConfig.RefSystemConfig]] = Some( List( - new RefSystemConfig( + RefSystemConfig( gridIds = Some(List("1", "2", "2", "2-10", "15...20")), sNom = "100 MVA", vNom = "10 kV", @@ -139,7 +143,7 @@ class RefSystemParserSpec extends UnitSpec { ) ) intercept[InvalidConfigParameterException] { - RefSystemParser.parse(validRefSystems) + GridConfigParser.parseRefSystems(validRefSystems) }.getMessage shouldBe s"The provided gridIds in simona.gridConfig.refSystems contain duplicates. Please check if there are either duplicate entries or overlapping ranges!" } @@ -149,7 +153,7 @@ class RefSystemParserSpec extends UnitSpec { val validRefSystems: Option[List[SimonaConfig.RefSystemConfig]] = Some( List( - new RefSystemConfig( + RefSystemConfig( gridIds = None, sNom = "100 MVA", vNom = "10 kV", @@ -157,7 +161,7 @@ class RefSystemParserSpec extends UnitSpec { List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) ), ), - new RefSystemConfig( + RefSystemConfig( gridIds = None, sNom = "100 MVA", vNom = "10 kV", @@ -168,7 +172,7 @@ class RefSystemParserSpec extends UnitSpec { ) ) intercept[InvalidConfigParameterException] { - RefSystemParser.parse(validRefSystems) + GridConfigParser.parseRefSystems(validRefSystems) }.getMessage shouldBe s"The provided voltLvls in simona.gridConfig.refSystems contain duplicates. Please check your configuration for duplicates in voltLvl entries!" } @@ -178,7 +182,7 @@ class RefSystemParserSpec extends UnitSpec { val validRefSystems: Option[List[SimonaConfig.RefSystemConfig]] = Some( List( - new RefSystemConfig( + RefSystemConfig( gridIds = Some(List("asd")), sNom = "100 MVA", vNom = "10 kV", @@ -186,7 +190,7 @@ class RefSystemParserSpec extends UnitSpec { List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) ), ), - new RefSystemConfig( + RefSystemConfig( gridIds = None, sNom = "100 MVA", vNom = "10 kV", @@ -197,8 +201,184 @@ class RefSystemParserSpec extends UnitSpec { ) ) intercept[InvalidConfigParameterException] { - RefSystemParser.parse(validRefSystems) - }.getMessage shouldBe "Unknown gridId format asd provided for refSystem RefSystemConfig(Some(List(asd)),100 MVA,10 kV,Some(List(VoltLvlConfig(MV,10 kV), VoltLvlConfig(MV,20 kV))))" + GridConfigParser.parseRefSystems(validRefSystems) + }.getMessage shouldBe "Unknown gridId format asd provided for grid config: RefSystemConfig(Some(List(asd)),100 MVA,10 kV,Some(List(VoltLvlConfig(MV,10 kV), VoltLvlConfig(MV,20 kV))))" + + } + + } + + "A GridConfigParser.parseVoltageLimits" must { + // check internal gridIdRefSystems + val gridIdVoltageLimits = + PrivateMethod[Map[Int, RefSystem]](Symbol("gridIdVoltageLimits")) + + // check internal voltLvLRefSystems + val voltLvLVoltageLimits = + PrivateMethod[Map[String, RefSystem]](Symbol("voltLvLVoltageLimits")) + + "return the default voltage limits if no config was provided" in { + val defaults = GridConfigParser.parseVoltageLimits(Some(List.empty)) + + defaults invokePrivate gridIdVoltageLimits() shouldBe Map.empty + + val distributionVoltageLimits = VoltageLimits(0.9, 1.1) + + defaults invokePrivate voltLvLVoltageLimits() shouldBe Map( + GermanVoltageLevelUtils.LV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_10KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_20KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.MV_30KV -> distributionVoltageLimits, + GermanVoltageLevelUtils.HV -> distributionVoltageLimits, + GermanVoltageLevelUtils.EHV_220KV -> VoltageLimits(0.9, 1.118), + GermanVoltageLevelUtils.EHV_380KV -> VoltageLimits(0.9, 1.05), + ) + } + + "parse provided valid simona config voltage limits correctly" in { + val validVoltageLimits: Option[List[VoltageLimitsConfig]] = + Some( + List( + VoltageLimitsConfig( + gridIds = Some(List("1", "2-10", "15...20")), + vMax = 1.1, + vMin = 0.9, + voltLvls = Some( + List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) + ), + ), + VoltageLimitsConfig( + gridIds = Some(List("100")), + vMax = 1.05, + vMin = 0.9, + voltLvls = Some( + List( + VoltLvlConfig("HV", "110 kV"), + VoltLvlConfig("EHV", "380 kV"), + ) + ), + ), + VoltageLimitsConfig( + gridIds = None, + vMax = 1.1, + vMin = 0.9, + voltLvls = None, + ), + ) + ) + + val configVoltageLimits = + GridConfigParser.parseVoltageLimits(validVoltageLimits) + + // prepare internal value check + val configVoltageLimitsOne = VoltageLimits(0.9, 1.1) + val configVoltageLimitsTwo = VoltageLimits(0.9, 1.05) + + configVoltageLimits invokePrivate gridIdVoltageLimits() shouldBe Map( + 1 -> configVoltageLimitsOne, + 2 -> configVoltageLimitsOne, + 3 -> configVoltageLimitsOne, + 4 -> configVoltageLimitsOne, + 5 -> configVoltageLimitsOne, + 6 -> configVoltageLimitsOne, + 7 -> configVoltageLimitsOne, + 8 -> configVoltageLimitsOne, + 9 -> configVoltageLimitsOne, + 10 -> configVoltageLimitsOne, + 15 -> configVoltageLimitsOne, + 16 -> configVoltageLimitsOne, + 17 -> configVoltageLimitsOne, + 18 -> configVoltageLimitsOne, + 19 -> configVoltageLimitsOne, + 20 -> configVoltageLimitsOne, + 100 -> configVoltageLimitsTwo, + ) + + configVoltageLimits invokePrivate voltLvLVoltageLimits() shouldBe Map( + GermanVoltageLevelUtils.MV_10KV -> configVoltageLimitsOne, + GermanVoltageLevelUtils.MV_20KV -> configVoltageLimitsOne, + GermanVoltageLevelUtils.HV -> configVoltageLimitsTwo, + GermanVoltageLevelUtils.EHV_380KV -> configVoltageLimitsTwo, + ) + + } + + "throw an InvalidConfigParameterException when provided gridIds contain duplicate entries" in { + + val validVoltageLimits: Option[List[VoltageLimitsConfig]] = + Some( + List( + VoltageLimitsConfig( + gridIds = Some(List("1", "2", "2", "2-10", "15...20")), + vMax = 1.1, + vMin = 0.9, + voltLvls = Some( + List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) + ), + ) + ) + ) + intercept[InvalidConfigParameterException] { + GridConfigParser.parseVoltageLimits(validVoltageLimits) + }.getMessage shouldBe s"The provided gridIds in simona.gridConfig.voltageLimits contain duplicates. Please check if there are either duplicate entries or overlapping ranges!" + + } + + "throw an InvalidConfigParameterException when provided voltLvls contain duplicate entries" in { + + val validVoltageLimits: Option[List[VoltageLimitsConfig]] = + Some( + List( + VoltageLimitsConfig( + gridIds = None, + vMax = 1.1, + vMin = 0.9, + voltLvls = Some( + List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) + ), + ), + VoltageLimitsConfig( + gridIds = None, + vMax = 1.1, + vMin = 0.9, + voltLvls = Some( + List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) + ), + ), + ) + ) + intercept[InvalidConfigParameterException] { + GridConfigParser.parseVoltageLimits(validVoltageLimits) + }.getMessage shouldBe s"The provided voltLvls in simona.gridConfig.voltageLimits contain duplicates. Please check your configuration for duplicates in voltLvl entries!" + + } + + "throw an InvalidConfigParameterException when the provided gridId format is unknown" in { + + val validVoltageLimits: Option[List[VoltageLimitsConfig]] = + Some( + List( + VoltageLimitsConfig( + gridIds = Some(List("asd")), + vMax = 1.1, + vMin = 0.9, + voltLvls = Some( + List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) + ), + ), + VoltageLimitsConfig( + gridIds = None, + vMax = 1.1, + vMin = 0.9, + voltLvls = Some( + List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) + ), + ), + ) + ) + intercept[InvalidConfigParameterException] { + GridConfigParser.parseVoltageLimits(validVoltageLimits) + }.getMessage shouldBe "Unknown gridId format asd provided for grid config: VoltageLimitsConfig(Some(List(asd)),1.1,0.9,Some(List(VoltLvlConfig(MV,10 kV), VoltLvlConfig(MV,20 kV))))" } @@ -209,7 +389,7 @@ class RefSystemParserSpec extends UnitSpec { val validRefSystems: Option[List[SimonaConfig.RefSystemConfig]] = Some( List( - new RefSystemConfig( + RefSystemConfig( gridIds = Some(List("1", "2-10", "15...20")), sNom = "100 MVA", vNom = "10 kV", @@ -217,7 +397,7 @@ class RefSystemParserSpec extends UnitSpec { List(VoltLvlConfig("MV", "10 kV"), VoltLvlConfig("MV", "20 kV")) ), ), - new RefSystemConfig( + RefSystemConfig( gridIds = Some(List("100")), sNom = "5000 MVA", vNom = "110 kV", @@ -231,7 +411,7 @@ class RefSystemParserSpec extends UnitSpec { ) ) - val configRefSystems = RefSystemParser.parse(validRefSystems) + val configRefSystems = GridConfigParser.parseRefSystems(validRefSystems) // prepare expected RefSystems val configRefSystemOne = RefSystem("100 MVA", "10 kV") diff --git a/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala b/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala index 68caa1706b..33ebe9caaa 100644 --- a/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/listener/ResultEventListenerSpec.scala @@ -402,6 +402,8 @@ class ResultEventListenerSpec max = timeoutDuration, ) + assert(outputFile.exists(), "Output file does not exist") + // stopping the actor should wait until existing messages within an actor are fully processed // otherwise it might happen, that the shutdown is triggered even before the just send ParticipantResultEvent // reached the listener @@ -425,18 +427,19 @@ class ResultEventListenerSpec timeoutDuration, ) + val compressedFile = specificOutputFileHierarchy.rawOutputDataFilePaths + .getOrElse( + classOf[PvResult], + fail( + s"Cannot get filepath for raw result file of class '${classOf[PvResult].getSimpleName}' from outputFileHierarchy!'" + ), + ) + .toFile + assert(compressedFile.exists(), "Compressed file does not exist") + val resultFileSource = Source.fromInputStream( new GZIPInputStream( - new FileInputStream( - specificOutputFileHierarchy.rawOutputDataFilePaths - .getOrElse( - classOf[PvResult], - fail( - s"Cannot get filepath for raw result file of class '${classOf[PvResult].getSimpleName}' from outputFileHierarchy!'" - ), - ) - .toFile - ) + new FileInputStream(compressedFile) ) ) diff --git a/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerKafkaSpec.scala b/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerKafkaSpec.scala index e60be2acd3..1259d4395c 100644 --- a/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerKafkaSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerKafkaSpec.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.event.listener import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import com.sksamuel.avro4s.RecordFormat -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.{RuntimeConfig, SimonaConfig} import edu.ie3.simona.event.RuntimeEvent.{Done, Error, PowerFlowFailed} import edu.ie3.simona.io.runtime.RuntimeEventKafkaSink.SimonaEndMessage import edu.ie3.simona.test.KafkaSpecLike @@ -84,7 +84,7 @@ class RuntimeEventListenerKafkaSpec // build the listener val listenerRef = spawn( RuntimeEventListener( - SimonaConfig.Simona.Runtime.Listener( + RuntimeConfig.Listener( None, Some( SimonaConfig.RuntimeKafkaParams( diff --git a/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerLoggingSpec.scala b/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerLoggingSpec.scala index 7f77f7ca77..c231461e41 100644 --- a/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerLoggingSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerLoggingSpec.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.event.listener import com.typesafe.config.ConfigValueFactory -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.{RuntimeConfig, SimonaConfig} import edu.ie3.simona.event.RuntimeEvent.{ CheckWindowPassed, Done, @@ -48,7 +48,7 @@ class RuntimeEventListenerLoggingSpec val listenerRef = spawn( RuntimeEventListener( - SimonaConfig.Simona.Runtime.Listener( + RuntimeConfig.Listener( None, None, ), diff --git a/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerSpec.scala b/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerSpec.scala index 7171882b5c..342d21aebc 100644 --- a/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerSpec.scala +++ b/src/test/scala/edu/ie3/simona/event/listener/RuntimeEventListenerSpec.scala @@ -6,7 +6,7 @@ package edu.ie3.simona.event.listener -import edu.ie3.simona.config.SimonaConfig +import edu.ie3.simona.config.{RuntimeConfig, SimonaConfig} import edu.ie3.simona.event.RuntimeEvent import edu.ie3.simona.event.RuntimeEvent.{ Done, @@ -31,7 +31,7 @@ class RuntimeEventListenerSpec val listenerRef = spawn( RuntimeEventListener( - SimonaConfig.Simona.Runtime.Listener( + RuntimeConfig.Listener( None, None, ), diff --git a/src/test/scala/edu/ie3/simona/integration/RunSimonaStandaloneIT.scala b/src/test/scala/edu/ie3/simona/integration/RunSimonaStandaloneIT.scala index 083b9681e3..f1a56078ee 100644 --- a/src/test/scala/edu/ie3/simona/integration/RunSimonaStandaloneIT.scala +++ b/src/test/scala/edu/ie3/simona/integration/RunSimonaStandaloneIT.scala @@ -71,12 +71,13 @@ class RunSimonaStandaloneIT ConfigFailFast.check(simonaConfig) val resultFileHierarchy = - SimonaStandaloneSetup.buildResultFileHierarchy(parsedConfig) + SimonaStandaloneSetup.buildResultFileHierarchy(simonaConfig) val runtimeEventQueue = new LinkedBlockingQueue[RuntimeEvent]() val simonaStandaloneSetup = SimonaStandaloneSetup( parsedConfig, + simonaConfig, resultFileHierarchy, Some(runtimeEventQueue), ) diff --git a/src/test/scala/edu/ie3/simona/model/em/EmAggregatePowerOptSpec.scala b/src/test/scala/edu/ie3/simona/model/em/EmAggregatePowerOptSpec.scala new file mode 100644 index 0000000000..ad9dd0852e --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/em/EmAggregatePowerOptSpec.scala @@ -0,0 +1,688 @@ +/* + * © 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.em + +import edu.ie3.datamodel.models.input.system.{PvInput, SystemParticipantInput} +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.test.common.UnitSpec +import org.scalatestplus.mockito.MockitoSugar +import squants.energy.Kilowatts + +import java.util.UUID + +class EmAggregatePowerOptSpec extends UnitSpec with MockitoSugar { + "The aggregating strategy overall" should { + val strat = EmAggregatePowerOpt(curtailRegenerative = true) + "work with single flex options" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(2.0), + min = Kilowatts(-1.0), + max = Kilowatts(4.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1) + ) + ) + + actualResult shouldBe ( + Kilowatts(0.0), + Kilowatts(-1.0), + Kilowatts(4.0) + ) + } + "work as expected at zero flexibility" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(5.0), + min = Kilowatts(5.0), + max = Kilowatts(5.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1) + ) + ) + + actualResult shouldBe ( + Kilowatts(5.0), + Kilowatts(5.0), + Kilowatts(5.0) + ) + } + } + + "The self-optimizing aggregating strategy with PV flex" should { + val strat = EmAggregatePowerOpt(curtailRegenerative = true) + + "pick 0kW if possible" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(2.0), + min = Kilowatts(-1.0), + max = Kilowatts(4.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[SystemParticipantInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(0.0), + Kilowatts(-7.0), + Kilowatts(4.0) + ) + } + + "pick minSum if minSum > 0kW" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(6.0), + min = Kilowatts(4.0), + max = Kilowatts(12.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(2.0), + min = Kilowatts(-2.0), + max = Kilowatts(2.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[SystemParticipantInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(2.0), + Kilowatts(2.0), + Kilowatts(14.0) + ) + } + + "pick maxSum if maxSum < 0kW" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-1.0), + min = Kilowatts(-10.0), + max = Kilowatts(-1.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[SystemParticipantInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-1.0), + Kilowatts(-16.0), + Kilowatts(-1.0) + ) + } + } + + "The self-optimizing aggregating strategy without PV flex" should { + val strat = EmAggregatePowerOpt(curtailRegenerative = false) + + "exclude PV max power when normally picking 0kW as target" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(2.0), + min = Kilowatts(-1.0), + max = Kilowatts(4.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-2.0), + Kilowatts(-7.0), + Kilowatts(4.0) + ) + } + + "exclude PV max power when normally picking maxSum as target" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-1.0), + min = Kilowatts(-10.0), + max = Kilowatts(-1.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-7.0), + Kilowatts(-16.0), + Kilowatts(-1.0) + ) + } + } + + "The power target aggregating strategy with PV flex" should { + val powerTarget = Kilowatts(3d) + val strat = EmAggregatePowerOpt(powerTarget, curtailRegenerative = true) + + "pick closed possible power if power target is not possible" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(5.0), + min = Kilowatts(4.0), + max = Kilowatts(6.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(0.0), + min = Kilowatts(0.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(4.0), + Kilowatts(4.0), + Kilowatts(6.0) + ) + } + + "use min flex of to stay inside inside power target limits" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(10.0), + min = Kilowatts(9.0), + max = Kilowatts(11.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(3.0), + Kilowatts(3.0), + Kilowatts(11.0) + ) + } + + "use ref and stay inside inside power target limits" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(8.0), + min = Kilowatts(7.0), + max = Kilowatts(11.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(2.0), + Kilowatts(1.0), + Kilowatts(11.0) + ) + } + + "pick power target if possible" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(9.0), + min = Kilowatts(8.0), + max = Kilowatts(10.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(3.0), + Kilowatts(2.0), + Kilowatts(10.0) + ) + } + + "pick reference inside power target limits if possible" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(9.0), + min = Kilowatts(8.0), + max = Kilowatts(10.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-7.0), + min = Kilowatts(-7.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(2.0), + Kilowatts(1.0), + Kilowatts(10.0) + ) + } + + "stay inside inside power target limits and not reduce renewable generation" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(9.0), + min = Kilowatts(8.0), + max = Kilowatts(10.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-12.0), + min = Kilowatts(-12.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-3.0), + Kilowatts(-4.0), + Kilowatts(10.0) + ) + } + + "use max flex of to stay inside inside power target limits and not reduce renewable generation" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(1.0), + min = Kilowatts(0.0), + max = Kilowatts(5.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-8.0), + min = Kilowatts(-8.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-3.0), + Kilowatts(-8.0), + Kilowatts(5.0) + ) + } + + "pick 3kW if possible" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(10.0), + min = Kilowatts(-1.0), + max = Kilowatts(12.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(3.0), + Kilowatts(-7.0), + Kilowatts(12.0) + ) + } + + "pick -3kW if possible" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(2.0), + min = Kilowatts(-1.0), + max = Kilowatts(4.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-3.0), + Kilowatts(-7.0), + Kilowatts(4.0) + ) + } + + "pick reference power (positive) if inside the power target limits" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(8.0), + min = Kilowatts(-1.0), + max = Kilowatts(10.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(2.0), + Kilowatts(-7.0), + Kilowatts(10.0) + ) + } + + "pick (negative) reference power if inside the power target limits" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(4.0), + min = Kilowatts(-1.0), + max = Kilowatts(4.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-2.0), + Kilowatts(-7.0), + Kilowatts(4.0) + ) + } + + "pick minSum if minSum > 0kW" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(6.0), + min = Kilowatts(4.0), + max = Kilowatts(12.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(2.0), + min = Kilowatts(-2.0), + max = Kilowatts(2.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(3.0), + Kilowatts(2.0), + Kilowatts(14.0) + ) + } + + "pick maxSum if maxSum < 0kW" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-1.0), + min = Kilowatts(-10.0), + max = Kilowatts(-1.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-3.0), + Kilowatts(-16.0), + Kilowatts(-1.0) + ) + } + } + + "The power target aggregating strategy without PV flex" should { + val powerTarget = Kilowatts(3d) + val strat = EmAggregatePowerOpt(powerTarget, curtailRegenerative = false) + + "pick min power to get closed possible to power target if it cannot reached" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(11.0), + min = Kilowatts(10.0), + max = Kilowatts(12.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(4.0), + Kilowatts(4.0), + Kilowatts(12.0) + ) + } + + "pick reference power if power target can be reached" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(9.0), + min = Kilowatts(8.0), + max = Kilowatts(12.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(3.0), + Kilowatts(2.0), + Kilowatts(12.0) + ) + } + + "pick max power to get closed possible to power target if it cannot reached" in { + val flexOptions1 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(1.0), + min = Kilowatts(0.0), + max = Kilowatts(2.0), + ) + + val flexOptions2 = ProvideMinMaxFlexOptions( + modelUuid = UUID.randomUUID(), + ref = Kilowatts(-6.0), + min = Kilowatts(-6.0), + max = Kilowatts(0.0), + ) + + val actualResult = strat.aggregateFlexOptions( + Iterable( + (mock[SystemParticipantInput], flexOptions1), + (mock[PvInput], flexOptions2), + ) + ) + + actualResult shouldBe ( + Kilowatts(-4.0), + Kilowatts(-6.0), + Kilowatts(2.0) + ) + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/em/EmAggregateSelfOptSpec.scala b/src/test/scala/edu/ie3/simona/model/em/EmAggregateSelfOptSpec.scala deleted file mode 100644 index 9ae0ee63c1..0000000000 --- a/src/test/scala/edu/ie3/simona/model/em/EmAggregateSelfOptSpec.scala +++ /dev/null @@ -1,171 +0,0 @@ -/* - * © 2022. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.model.em - -import edu.ie3.datamodel.models.input.system.{PvInput, SystemParticipantInput} -import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions -import edu.ie3.simona.test.common.UnitSpec -import org.scalatestplus.mockito.MockitoSugar -import squants.energy.Kilowatts - -import java.util.UUID - -class EmAggregateSelfOptSpec extends UnitSpec with MockitoSugar { - - "The self-optimizing aggregating strategy with PV flex" should { - val strat = EmAggregateSelfOpt(curtailRegenerative = true) - - "pick 0kW if possible" in { - val flexOptions1 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(2.0), - min = Kilowatts(-1.0), - max = Kilowatts(4.0), - ) - - val flexOptions2 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(-6.0), - min = Kilowatts(-6.0), - max = Kilowatts(0.0), - ) - - val actualResult = strat.aggregateFlexOptions( - Iterable( - (mock[SystemParticipantInput], flexOptions1), - (mock[SystemParticipantInput], flexOptions2), - ) - ) - - actualResult shouldBe ( - Kilowatts(0.0), - Kilowatts(-7.0), - Kilowatts(4.0) - ) - } - - "pick minSum if minSum > 0kW" in { - val flexOptions1 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(6.0), - min = Kilowatts(4.0), - max = Kilowatts(12.0), - ) - - val flexOptions2 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(2.0), - min = Kilowatts(-2.0), - max = Kilowatts(2.0), - ) - - val actualResult = strat.aggregateFlexOptions( - Iterable( - (mock[SystemParticipantInput], flexOptions1), - (mock[SystemParticipantInput], flexOptions2), - ) - ) - - actualResult shouldBe ( - Kilowatts(2.0), - Kilowatts(2.0), - Kilowatts(14.0) - ) - } - - "pick maxSum if maxSum < 0kW" in { - val flexOptions1 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(-1.0), - min = Kilowatts(-10.0), - max = Kilowatts(-1.0), - ) - - val flexOptions2 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(-6.0), - min = Kilowatts(-6.0), - max = Kilowatts(0.0), - ) - - val actualResult = strat.aggregateFlexOptions( - Iterable( - (mock[SystemParticipantInput], flexOptions1), - (mock[SystemParticipantInput], flexOptions2), - ) - ) - - actualResult shouldBe ( - Kilowatts(-1.0), - Kilowatts(-16.0), - Kilowatts(-1.0) - ) - } - } - - "The self-optimizing aggregating strategy without PV flex" should { - val strat = EmAggregateSelfOpt(curtailRegenerative = false) - - "exclude PV max power when normally picking 0kW as target" in { - val flexOptions1 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(2.0), - min = Kilowatts(-1.0), - max = Kilowatts(4.0), - ) - - val flexOptions2 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(-6.0), - min = Kilowatts(-6.0), - max = Kilowatts(0.0), - ) - - val actualResult = strat.aggregateFlexOptions( - Iterable( - (mock[SystemParticipantInput], flexOptions1), - (mock[PvInput], flexOptions2), - ) - ) - - actualResult shouldBe ( - Kilowatts(-2.0), - Kilowatts(-7.0), - Kilowatts(4.0) - ) - } - - "exclude PV max power when normally picking maxSum as target" in { - val flexOptions1 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(-1.0), - min = Kilowatts(-10.0), - max = Kilowatts(-1.0), - ) - - val flexOptions2 = ProvideMinMaxFlexOptions( - modelUuid = UUID.randomUUID(), - ref = Kilowatts(-6.0), - min = Kilowatts(-6.0), - max = Kilowatts(0.0), - ) - - val actualResult = strat.aggregateFlexOptions( - Iterable( - (mock[SystemParticipantInput], flexOptions1), - (mock[PvInput], flexOptions2), - ) - ) - - actualResult shouldBe ( - Kilowatts(-7.0), - Kilowatts(-16.0), - Kilowatts(-1.0) - ) - } - } -} diff --git a/src/test/scala/edu/ie3/simona/model/em/EmModelShellSpec.scala b/src/test/scala/edu/ie3/simona/model/em/EmModelShellSpec.scala new file mode 100644 index 0000000000..fab04fe595 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/em/EmModelShellSpec.scala @@ -0,0 +1,181 @@ +/* + * © 2025. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.em + +import edu.ie3.simona.config.RuntimeConfig.EmRuntimeConfig +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.EmInputTestData +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW +import org.scalatestplus.mockito.MockitoSugar +import squants.energy.Kilowatts + +import java.util.UUID + +class EmModelShellSpec extends UnitSpec with MockitoSugar with EmInputTestData { + + "EmModelShell" should { + "apply PROPORTIONAL strategy correctly" in { + val result = EmModelShell.apply( + UUID.randomUUID(), + "TestID1", + "PROPORTIONAL", + modelConfig, + ) + + result.modelStrategy shouldBe ProportionalFlexStrat + result.aggregateFlex shouldBe EmAggregatePowerOpt( + zeroKW, + false, + ) + } + + "apply PRIORITIZED strategy with curtailRegenerative correctly" in { + val model = EmRuntimeConfig.apply(false, 1.0, null, "SELF_OPT", true) + val result = EmModelShell.apply( + UUID.randomUUID(), + "TestID2", + "PRIORITIZED", + model, + ) + + result.modelStrategy shouldBe PrioritizedFlexStrat(true) + result.aggregateFlex shouldBe EmAggregatePowerOpt( + zeroKW, + true, + ) + } + + "throw CriticalFailureException for unknown model strategy" in { + val exception = intercept[CriticalFailureException] { + EmModelShell.apply( + UUID.randomUUID(), + "TestID3", + "UNKNOWN_STRATEGY", + modelConfig, + ) + } + + exception.getMessage should include( + "Unknown model strategy UNKNOWN_STRATEGY" + ) + } + + "apply SIMPLE_SUM strategy correctly" in { + val model = EmRuntimeConfig.apply(false, 1.0, null, "SIMPLE_SUM", true) + val result = EmModelShell.apply( + UUID.randomUUID(), + "TestID4", + "PROPORTIONAL", + model, + ) + + result.aggregateFlex shouldBe EmAggregateSimpleSum + } + + "apply SELF_POWER_* strategy with correct power limit" in { + val model = + EmRuntimeConfig.apply(false, 1.0, null, "SELF_POWER_100.5", true) + val result = EmModelShell.apply( + UUID.randomUUID(), + "TestID5", + "PROPORTIONAL", + model, + ) + + result.aggregateFlex shouldBe EmAggregatePowerOpt(Kilowatts(100.5), true) + } + + "apply SELF_POWER_*_EXCL_REG strategy correctly" in { + val model = EmRuntimeConfig.apply( + false, + 1.0, + null, + "SELF_POWER_200_EXCL_REG", + false, + ) + val result = EmModelShell.apply( + UUID.randomUUID(), + "TestID6", + "PROPORTIONAL", + model, + ) + + result.aggregateFlex shouldBe EmAggregatePowerOpt(Kilowatts(200), false) + } + + "throw CriticalFailureException for invalid power limit format" in { + val model = + EmRuntimeConfig.apply(false, 1.0, null, "SELF_POWER_100.100.100", false) + + val exception = intercept[CriticalFailureException] { + EmModelShell.apply( + UUID.randomUUID(), + "TestID7", + "PROPORTIONAL", + model, + ) + } + + exception.getMessage should include( + "Invalid numeric value in aggregate flex strategy: SELF_POWER_100.100.100" + ) + } + + "throw CriticalFailureException for non numeric power limit format" in { + val model = + EmRuntimeConfig.apply(false, 1.0, null, "SELF_POWER_abc", false) + + val exception = intercept[CriticalFailureException] { + EmModelShell.apply( + UUID.randomUUID(), + "TestID7", + "PROPORTIONAL", + model, + ) + } + + exception.getMessage should include( + "Invalid format for aggregate flex strategy: SELF_POWER_abc" + ) + } + + "throw CriticalFailureException for invalid format in SELF_POWER strategy" in { + val model = EmRuntimeConfig.apply(false, 1.0, null, "SELF_POWER_", false) + + val exception = intercept[CriticalFailureException] { + EmModelShell.apply( + UUID.randomUUID(), + "TestID10", + "PROPORTIONAL", + model, + ) + } + + exception.getMessage should include( + "Invalid format for aggregate flex strategy: SELF_POWER_" + ) + } + + "throw CriticalFailureException for unknown aggregate flex strategy" in { + val model = EmRuntimeConfig.apply(false, 1.0, null, "UNKNOWN_FLEX", false) + + val exception = intercept[CriticalFailureException] { + EmModelShell.apply( + UUID.randomUUID(), + "TestID11", + "PROPORTIONAL", + model, + ) + } + + exception.getMessage should include( + "Unknown aggregate flex strategy UNKNOWN_FLEX" + ) + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala index 45b0e5a837..3d7d81d6c8 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala @@ -215,6 +215,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) // get the private method for validation @@ -251,6 +252,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -355,6 +357,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -407,6 +410,7 @@ class GridSpec Set.empty[Transformer3wModel], Set.empty[SwitchModel], ), + defaultVoltageLimits, GridControls.empty, ) @@ -460,6 +464,7 @@ class GridSpec Set.empty[Transformer3wModel], switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -540,6 +545,7 @@ class GridSpec Set.empty, switches, ), + defaultVoltageLimits, GridControls.empty, ) @@ -643,6 +649,7 @@ class GridSpec GridModel( validTestGridInputModel, gridInputModelTestDataRefSystem, + defaultVoltageLimits, defaultSimulationStart, defaultSimulationEnd, simonaConfig, diff --git a/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala index 880ca12116..5f7becad11 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/TransformerModelSpec.scala @@ -379,6 +379,7 @@ class TransformerModelSpec val gridModel = GridModel( grid, refSystem, + defaultVoltageLimits, defaultSimulationStart, defaultSimulationEnd, simonaConfig, diff --git a/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala index be5dc6ac69..0b6a80ea52 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/FixedFeedInModelSpec.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.model.participant import edu.ie3.simona.config.SimonaConfig -import edu.ie3.simona.config.SimonaConfig.FixedFeedInRuntimeConfig +import edu.ie3.simona.config.RuntimeConfig.FixedFeedInRuntimeConfig import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.simona.test.common.input.FixedFeedInputTestData diff --git a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala index 0479798610..9964f7af3f 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala @@ -265,7 +265,7 @@ class HpModelSpec (95.0, 95.0, 95.0), ), // 2. Same as before but heat storage is NOT empty - // should be possible to keep hp off + // should be possible to turn hp on ( ThermalGridState( Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala index b80bb12c30..073b5aeb37 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -241,7 +241,9 @@ class ThermalGridWithHouseAndStorageSpec updatedGridState match { case ThermalGridState( - Some(ThermalHouseState(houseTick, innerTemperature, qDotHouse)), + Some( + ThermalHouseState(houseTick, innerTemperature, qDotHouse) + ), Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), @@ -532,12 +534,16 @@ class ThermalGridWithHouseAndStorageSpec relevantData, testGridAmbientTemperature, initialGridState, + isRunning, externalQDot, + onlyThermalDemandOfHouse, ) updatedGridState match { case ThermalGridState( - Some(ThermalHouseState(houseTick, innerTemperature, qDotHouse)), + Some( + ThermalHouseState(houseTick, innerTemperature, qDotHouse) + ), Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), @@ -546,7 +552,7 @@ class ThermalGridWithHouseAndStorageSpec innerTemperature should approximate(Celsius(18.9999d)) qDotHouse should approximate(externalQDot) - storageTick shouldBe -1L + storageTick shouldBe 0L storedEnergy should approximate( initialGridState.storageState .map(_.storedEnergy) @@ -580,12 +586,16 @@ class ThermalGridWithHouseAndStorageSpec relevantData, testGridAmbientTemperature, gridState, + isRunning, externalQDot, + onlyThermalDemandOfHeatStorage, ) updatedGridState match { case ThermalGridState( - Some(ThermalHouseState(houseTick, innerTemperature, qDotHouse)), + Some( + ThermalHouseState(houseTick, innerTemperature, qDotHouse) + ), Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala index 6f7cf8fe31..47c8631999 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -202,7 +202,9 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { relevantData, testGridAmbientTemperature, gridState, + isNotRunning, testGridQDotInfeed, + onlyThermalDemandOfHouse, ) updatedGridState match { @@ -229,7 +231,9 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { relevantData, ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, + isRunning, testGridQDotInfeed, + onlyThermalDemandOfHouse, ) match { case ( ThermalGridState( @@ -251,7 +255,9 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { relevantData, ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, + isNotRunning, testGridQDotConsumption, + onlyThermalDemandOfHouse, ) match { case ( ThermalGridState( @@ -273,7 +279,9 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { relevantData, ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, + isNotRunning, zeroKW, + onlyThermalDemandOfHouse, ) match { case ( ThermalGridState( diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala index a1013bd1f0..f2dce6be07 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -214,7 +214,9 @@ class ThermalGridWithStorageOnlySpec relevantData, testGridAmbientTemperature, gridState, + isRunning, testGridQDotInfeed, + onlyThermalDemandOfHeatStorage, ) updatedGridState match { @@ -229,6 +231,44 @@ class ThermalGridWithStorageOnlySpec } reachedThreshold shouldBe Some(StorageFull(276000L)) } + + "properly take energy from storage" in { + val relevantData = HpRelevantData(0, testGridAmbientTemperature) + val gridState = ThermalGrid + .startingState(thermalGrid) + .copy(storageState = + Some( + ThermalStorageState( + 0L, + KilowattHours(150d), + zeroKW, + ) + ) + ) + + val (updatedGridState, reachedThreshold) = + thermalGrid invokePrivate handleInfeed( + relevantData, + testGridAmbientTemperature, + gridState, + isNotRunning, + testGridQDotInfeed, + onlyThermalDemandOfHeatStorage, + ) + + updatedGridState match { + case ThermalGridState( + None, + Some(ThermalStorageState(tick, storedEnergy, qDot)), + ) => + tick shouldBe 0L + storedEnergy should approximate(KilowattHours(150d)) + qDot should approximate(testGridQDotInfeed * (-1)) + case _ => fail("Thermal grid state has been calculated wrong.") + } + reachedThreshold shouldBe Some(StorageEmpty(36000L)) + } + } "updating the grid state dependent on the given thermal infeed" should { @@ -238,7 +278,9 @@ class ThermalGridWithStorageOnlySpec relevantData, ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, + isRunning, testGridQDotInfeed, + onlyThermalDemandOfHeatStorage, ) nextThreshold shouldBe Some(StorageFull(276000L)) @@ -270,7 +312,9 @@ class ThermalGridWithStorageOnlySpec ) ), testGridAmbientTemperature, + isRunning, testGridQDotConsumptionHigh, + onlyThermalDemandOfHouse, ) match { case ( ThermalGridState( @@ -292,7 +336,9 @@ class ThermalGridWithStorageOnlySpec relevantData, ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, + isRunning, zeroKW, + noThermalDemand, ) updatedState match { case ( diff --git a/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala b/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala index a1846a39a1..06d9ef07c9 100644 --- a/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala @@ -7,6 +7,10 @@ package edu.ie3.simona.service.ev import com.typesafe.config.ConfigFactory +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationSuccessfulMessage, +} import edu.ie3.simona.api.data.ev.ExtEvDataConnection import edu.ie3.simona.api.data.ev.model.EvModel import edu.ie3.simona.api.data.ev.ontology._ @@ -19,7 +23,6 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ ScheduleActivation, } import edu.ie3.simona.ontology.messages.services.EvMessage._ -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.service.SimonaService import edu.ie3.simona.service.ev.ExtEvDataService.InitExtEvData @@ -165,8 +168,8 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(INIT_SIM_TICK)) scheduler.expectMsg(Completion(evService.toTyped)) - evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) - evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) + evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) + evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) } "fail when activated without having received ExtEvMessage" in { @@ -243,8 +246,8 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(INIT_SIM_TICK)) scheduler.expectMsg(Completion(evService.toTyped)) - evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) - evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) + evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) + evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) extEvData.sendExtMsg( new RequestEvcsFreeLots() @@ -345,8 +348,8 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(INIT_SIM_TICK)) scheduler.expectMsg(Completion(evService.toTyped)) - evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) - evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) + evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) + evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) extEvData.sendExtMsg(new RequestCurrentPrices()) @@ -465,8 +468,8 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(INIT_SIM_TICK)) scheduler.expectMsg(Completion(evService.toTyped)) - evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) - evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) + evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) + evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) val departures = Map( evcs1UUID -> List(evA.getUuid).asJava, @@ -618,8 +621,8 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(INIT_SIM_TICK)) scheduler.expectMsg(Completion(evService.toTyped)) - evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) - evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) + evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) + evcs2.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) val arrivals = Map( evcs1UUID -> List[EvModel](evA).asJava, @@ -639,13 +642,13 @@ class ExtEvDataServiceSpec // we trigger ev service scheduler.send(evService, Activation(tick)) - val evsMessage1 = evcs1.expectMsgType[ProvideEvDataMessage] + val evsMessage1 = evcs1.expectMsgType[DataProvision[_]] evsMessage1.tick shouldBe tick evsMessage1.data shouldBe ArrivingEvs( Seq(EvModelWrapper(evA)) ) - val evsMessage2 = evcs2.expectMsgType[ProvideEvDataMessage] + val evsMessage2 = evcs2.expectMsgType[DataProvision[_]] evsMessage2.tick shouldBe tick evsMessage2.data shouldBe ArrivingEvs( Seq(EvModelWrapper(evB)) @@ -693,7 +696,7 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(INIT_SIM_TICK)) scheduler.expectMsg(Completion(evService.toTyped)) - evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, Some(0L))) + evcs1.expectMsg(RegistrationSuccessfulMessage(evService.ref, 0L)) val arrivals = Map( evcs1UUID -> List[EvModel](evA).asJava, @@ -713,7 +716,7 @@ class ExtEvDataServiceSpec // we trigger ev service scheduler.send(evService, Activation(tick)) - val evsMessage1 = evcs1.expectMsgType[ProvideEvDataMessage] + val evsMessage1 = evcs1.expectMsgType[DataProvision[_]] evsMessage1.tick shouldBe tick evsMessage1.data shouldBe ArrivingEvs( Seq(EvModelWrapper(evA)) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index be522fbc9c..8debb0cbd8 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -17,6 +17,7 @@ import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformatio import edu.ie3.datamodel.io.source.TimeSeriesMappingSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource import edu.ie3.datamodel.models.value.{SValue, Value} +import edu.ie3.simona.agent.participant2.ParticipantAgent.RegistrationFailedMessage import edu.ie3.simona.config.SimonaConfig.PrimaryDataCsvParams import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.{ CouchbaseParams, @@ -34,7 +35,6 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ PrimaryServiceRegistrationMessage, WorkerRegistrationMessage, @@ -673,7 +673,8 @@ class PrimaryServiceProxySpec "Trying to register with a proxy" should { "fail, if there is no information for the requested model" in { val request = PrimaryServiceRegistrationMessage( - UUID.fromString("2850a2d6-4b70-43c9-b5cc-cd823a72d860") + self, + UUID.fromString("2850a2d6-4b70-43c9-b5cc-cd823a72d860"), ) proxyRef ! request @@ -706,7 +707,7 @@ class PrimaryServiceProxySpec scheduler.expectMsg(Completion(fakeProxyRef.toTyped)) /* Try to register with fake proxy */ - fakeProxyRef ! PrimaryServiceRegistrationMessage(modelUuid) + fakeProxyRef ! PrimaryServiceRegistrationMessage(self, modelUuid) worker.expectMsg(WorkerRegistrationMessage(self)) } } diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala index fee341c869..d946ea8ee8 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySqlIT.scala @@ -6,14 +6,13 @@ package edu.ie3.simona.service.primary -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.typed.scaladsl.adapter.{ - ClassicActorRefOps, - TypedActorRefOps, -} -import org.apache.pekko.testkit.{TestActorRef, TestProbe} import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} import com.typesafe.config.ConfigFactory +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ActivePowerExtra +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + PrimaryRegistrationSuccessfulMessage, + RegistrationFailedMessage, +} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams import edu.ie3.simona.ontology.messages.Activation @@ -22,15 +21,17 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ ScheduleActivation, } import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ - RegistrationFailedMessage, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.service.primary.PrimaryServiceProxy.InitPrimaryServiceProxyStateData import edu.ie3.simona.test.common.{AgentSpec, TestSpawnerClassic} import edu.ie3.simona.test.helper.TestContainerHelper import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import edu.ie3.util.TimeUtil +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.actor.typed.scaladsl.adapter.{ + ClassicActorRefOps, + TypedActorRefOps, +} +import org.apache.pekko.testkit.{TestActorRef, TestProbe} import org.scalatest.BeforeAndAfterAll import org.testcontainers.utility.DockerImageName @@ -131,7 +132,8 @@ class PrimaryServiceProxySqlIT systemParticipantProbe.send( proxyRef, PrimaryServiceRegistrationMessage( - UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") + systemParticipantProbe.ref, + UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409"), ), ) scheduler.expectMsgType[ScheduleActivation] // lock activation scheduled @@ -150,7 +152,11 @@ class PrimaryServiceProxySqlIT scheduler.expectMsg(Completion(workerRef, Some(0))) systemParticipantProbe.expectMsg( - RegistrationSuccessfulMessage(workerRef.toClassic, Some(0L)) + PrimaryRegistrationSuccessfulMessage( + workerRef.toClassic, + 0L, + ActivePowerExtra, + ) ) } @@ -165,7 +171,8 @@ class PrimaryServiceProxySqlIT systemParticipantProbe.send( proxyRef, PrimaryServiceRegistrationMessage( - UUID.fromString("db958617-e49d-44d3-b546-5f7b62776afd") + systemParticipantProbe.ref, + UUID.fromString("db958617-e49d-44d3-b546-5f7b62776afd"), ), ) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala index c08f189b79..3885edcf79 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala @@ -15,13 +15,19 @@ import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesSource import edu.ie3.datamodel.models.StandardUnits import edu.ie3.datamodel.models.value.{HeatDemandValue, PValue} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ActivePower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ActivePower, + ActivePowerExtra, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + PrimaryRegistrationSuccessfulMessage, +} import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.WorkerRegistrationMessage import edu.ie3.simona.ontology.messages.services.WeatherMessage.RegisterForWeatherMessage import edu.ie3.simona.scheduler.ScheduleLock @@ -30,7 +36,6 @@ import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ CsvInitPrimaryServiceStateData, InitPrimaryServiceStateData, PrimaryServiceInitializedStateData, - ProvidePrimaryDataMessage, } import edu.ie3.simona.service.primary.PrimaryServiceWorkerSpec.WrongInitPrimaryServiceStateData import edu.ie3.simona.test.common.{AgentSpec, TestSpawnerClassic} @@ -210,14 +215,14 @@ class PrimaryServiceWorkerSpec /* Wait for request approval */ systemParticipant.expectMsg( - RegistrationSuccessfulMessage(serviceRef, Some(0L)) + PrimaryRegistrationSuccessfulMessage(serviceRef, 0L, ActivePowerExtra) ) /* We cannot directly check, if the requesting actor is among the subscribers, therefore we ask the actor to * provide data to all subscribed actors and check, if the subscribed probe gets one */ scheduler.send(serviceRef, Activation(0)) scheduler.expectMsgType[Completion] - systemParticipant.expectMsgAllClassOf(classOf[ProvidePrimaryDataMessage]) + systemParticipant.expectMsgAllClassOf(classOf[DataProvision[_]]) } /* At this point, the test (self) is registered with the service */ @@ -271,8 +276,8 @@ class PrimaryServiceWorkerSpec maybeNextTick shouldBe Some(900L) } /* Check, if correct message is sent */ - expectMsgClass(classOf[ProvidePrimaryDataMessage]) match { - case ProvidePrimaryDataMessage( + expectMsgClass(classOf[DataProvision[_]]) match { + case DataProvision( actualTick, actualServiceRef, actualData, @@ -351,7 +356,7 @@ class PrimaryServiceWorkerSpec } expectMsg( - ProvidePrimaryDataMessage( + DataProvision( tick, serviceRef, ActivePower(Kilowatts(50.0)), @@ -373,9 +378,9 @@ class PrimaryServiceWorkerSpec scheduler.expectMsg(Completion(serviceRef.toTyped)) inside( - systemParticipant.expectMsgClass(classOf[ProvidePrimaryDataMessage]) + systemParticipant.expectMsgClass(classOf[DataProvision[_]]) ) { - case ProvidePrimaryDataMessage( + case DataProvision( tick, actualServiceRef, data, diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala index 9a50b4fce9..73077b0c08 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -15,7 +15,13 @@ import edu.ie3.datamodel.io.naming.DatabaseNamingStrategy import edu.ie3.datamodel.models.value.{HeatAndSValue, PValue} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ActivePower, + ActivePowerExtra, ComplexPowerAndHeat, + ComplexPowerAndHeatExtra, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + PrimaryRegistrationSuccessfulMessage, } import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams import edu.ie3.simona.ontology.messages.Activation @@ -23,14 +29,10 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.WorkerRegistrationMessage import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.service.SimonaService -import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ - ProvidePrimaryDataMessage, - SqlInitPrimaryServiceStateData, -} +import edu.ie3.simona.service.primary.PrimaryServiceWorker.SqlInitPrimaryServiceStateData import edu.ie3.simona.test.common.input.TimeSeriesTestData import edu.ie3.simona.test.common.{AgentSpec, TestSpawnerClassic} import edu.ie3.simona.test.helper.TestContainerHelper @@ -98,6 +100,7 @@ class PrimaryServiceWorkerSqlIT "uuid", "firstTick", "firstData", + "primaryDataExtra", "maybeNextTick", ), ( @@ -112,6 +115,7 @@ class PrimaryServiceWorkerSqlIT Kilovars(329.0), Kilowatts(8000.0), ), + ComplexPowerAndHeatExtra, Some(900L), ), ( @@ -124,6 +128,7 @@ class PrimaryServiceWorkerSqlIT ActivePower( Kilowatts(1000.0) ), + ActivePowerExtra, Some(900L), ), ) @@ -134,6 +139,7 @@ class PrimaryServiceWorkerSqlIT uuid, firstTick, firstData, + primaryDataExtra, maybeNextTick, ) => val serviceRef = TestActorRef(service) @@ -170,13 +176,17 @@ class PrimaryServiceWorkerSqlIT WorkerRegistrationMessage(participant.ref), ) participant.expectMsg( - RegistrationSuccessfulMessage(serviceRef, Some(firstTick)) + PrimaryRegistrationSuccessfulMessage( + serviceRef, + firstTick, + primaryDataExtra, + ) ) scheduler.send(serviceRef, Activation(firstTick)) scheduler.expectMsg(Completion(serviceRef.toTyped, maybeNextTick)) - val dataMsg = participant.expectMsgType[ProvidePrimaryDataMessage] + val dataMsg = participant.expectMsgType[DataProvision[_]] dataMsg.tick shouldBe firstTick dataMsg.data shouldBe firstData dataMsg.nextDataTick shouldBe maybeNextTick diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherServiceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherServiceSpec.scala index cff17ec153..04fedac624 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherServiceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherServiceSpec.scala @@ -8,16 +8,17 @@ package edu.ie3.simona.service.weather import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + DataProvision, + RegistrationFailedMessage, + RegistrationSuccessfulMessage, +} import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.{ - RegistrationFailedMessage, - RegistrationSuccessfulMessage, -} import edu.ie3.simona.ontology.messages.services.WeatherMessage._ import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.service.SimonaService @@ -154,7 +155,7 @@ class WeatherServiceSpec validCoordinate.longitude, ) - expectMsg(RegistrationSuccessfulMessage(weatherActor.ref, Some(0L))) + expectMsg(RegistrationSuccessfulMessage(weatherActor.ref, 0L)) } "recognize, that a valid coordinate yet is registered" in { @@ -180,7 +181,7 @@ class WeatherServiceSpec scheduler.expectMsg(Completion(weatherActor.toTyped, Some(3600))) expectMsg( - ProvideWeatherMessage( + DataProvision( 0, weatherActor, WeatherData( @@ -202,7 +203,7 @@ class WeatherServiceSpec scheduler.expectMsg(Completion(weatherActor.toTyped)) expectMsg( - ProvideWeatherMessage( + DataProvision( 3600, weatherActor, WeatherData( 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 b2ddb887a7..0c026e2f66 100644 --- a/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/DefaultTestData.scala @@ -10,7 +10,7 @@ import com.typesafe.config.{Config, ConfigFactory} import edu.ie3.datamodel.models.OperationTime import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.model.SystemComponent -import edu.ie3.simona.model.grid.RefSystem +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} import edu.ie3.util.scala.OperationInterval import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} @@ -63,6 +63,8 @@ trait DefaultTestData { Kilovolts(10d), ) + protected val defaultVoltageLimits: VoltageLimits = VoltageLimits(0.9, 1.1) + /** Creates a [[SimonaConfig]], that provides the desired participant model * configurations * 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 d235203639..a9b673f775 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 @@ -24,6 +24,7 @@ import edu.ie3.datamodel.models.input.thermal.{ } import edu.ie3.datamodel.models.input.{EmInput, OperatorInput} import edu.ie3.datamodel.models.{OperationTime, StandardUnits} +import edu.ie3.simona.config.RuntimeConfig.EmRuntimeConfig import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.event.notifier.NotifierConfig import edu.ie3.simona.model.participant.load.{LoadModelBehaviour, LoadReference} @@ -108,8 +109,8 @@ trait EmInputTestData simonaConfig.simona.output.participant.defaultConfig.flexResult, ) - protected val modelConfig: SimonaConfig.EmRuntimeConfig = - configUtil.getOrDefault[SimonaConfig.EmRuntimeConfig]( + protected val modelConfig: EmRuntimeConfig = + configUtil.getOrDefault[EmRuntimeConfig]( emInput.getUuid ) @@ -140,7 +141,7 @@ trait EmInputTestData UUID.fromString("91940626-bdd0-41cf-96dd-47c94c86b20e"), "thermal house", thermalBusInput, - Quantities.getQuantity(0.325, StandardUnits.THERMAL_TRANSMISSION), + Quantities.getQuantity(0.15, StandardUnits.THERMAL_TRANSMISSION), Quantities.getQuantity(75, StandardUnits.HEAT_CAPACITY), Quantities.getQuantity(20.3, StandardUnits.TEMPERATURE), Quantities.getQuantity(22.0, StandardUnits.TEMPERATURE), diff --git a/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala index 312b3515d0..815e8b5fca 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala @@ -10,6 +10,7 @@ import edu.ie3.datamodel.models.input.system.HpInput import edu.ie3.datamodel.models.input.system.`type`.HpTypeInput import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed import edu.ie3.datamodel.models.input.thermal.{ + CylindricalStorageInput, DomesticHotWaterStorageInput, ThermalHouseInput, ThermalStorageInput, @@ -32,7 +33,7 @@ import tech.units.indriya.quantity.Quantities import tech.units.indriya.unit.Units import java.util.UUID -import scala.jdk.CollectionConverters.SeqHasAsJava +import scala.jdk.CollectionConverters._ trait HpInputTestData extends NodeInputTestData with ThermalGridTestData { @@ -88,6 +89,58 @@ trait HpInputTestData extends NodeInputTestData with ThermalGridTestData { Seq.empty[ThermalStorageInput].asJava, ) + protected val typicalThermalHouse = new ThermalHouseInput( + UUID.fromString("74ac67b4-4743-416a-b731-1b5fe4a0a4e7"), + "thermal house", + thermalBusInput, + Quantities.getQuantity(0.1, StandardUnits.THERMAL_TRANSMISSION), + Quantities.getQuantity(7.5, StandardUnits.HEAT_CAPACITY), + Quantities.getQuantity(20.0, StandardUnits.TEMPERATURE), + Quantities.getQuantity(22.0, StandardUnits.TEMPERATURE), + Quantities.getQuantity(18.0, StandardUnits.TEMPERATURE), + ) + + protected val typicalThermalStorage: CylindricalStorageInput = + new CylindricalStorageInput( + UUID.fromString("4b8933dc-aeb6-4573-b8aa-59d577214150"), + "thermal storage", + thermalBusInput, + Quantities.getQuantity(300.0, Units.LITRE), + Quantities.getQuantity(0.0, Units.LITRE), + Quantities.getQuantity(60.0, StandardUnits.TEMPERATURE), + Quantities.getQuantity(30.0, StandardUnits.TEMPERATURE), + Quantities.getQuantity(1.16, StandardUnits.SPECIFIC_HEAT_CAPACITY), + ) + + protected val typicalThermalGrid = new container.ThermalGrid( + thermalBusInput, + Seq(typicalThermalHouse).asJava, + Set[ThermalStorageInput](typicalThermalStorage).asJava, + // Set.empty[ThermalStorageInput].asJava, + ) + + protected val typicalHpTypeInput = new HpTypeInput( + UUID.fromString("2829d5eb-352b-40df-a07f-735b65a0a7bd"), + "TypicalHpTypeInput", + Quantities.getQuantity(7500d, PowerSystemUnits.EURO), + Quantities.getQuantity(200d, PowerSystemUnits.EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(4, PowerSystemUnits.KILOVOLTAMPERE), + 0.95, + Quantities.getQuantity(11, PowerSystemUnits.KILOWATT), + ) + + protected val typicalHpInputModel = new HpInput( + UUID.fromString("1b5e928e-65a3-444c-b7f2-6a48af092224"), + "TypicalHpInput", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + nodeInputNoSlackNs04KvA, + thermalBusInput, + new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), + null, + typicalHpTypeInput, + ) + protected def thermalGrid( thermalHouse: ThermalHouse, thermalStorage: Option[ThermalStorage] = None, diff --git a/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala index c7d18b2474..2acc0bdbdb 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/PvInputTestData.scala @@ -44,12 +44,12 @@ trait PvInputTestData nodeInputNoSlackNs04KvA, CosPhiFixed.CONSTANT_CHARACTERISTIC, null, - 1, + 0.2, Quantities.getQuantity(12, StandardUnits.AZIMUTH), - Quantities.getQuantity(10, StandardUnits.EFFICIENCY), - Quantities.getQuantity(100, StandardUnits.SOLAR_ELEVATION_ANGLE), - 12, - 11, + Quantities.getQuantity(90, StandardUnits.EFFICIENCY), + Quantities.getQuantity(45, StandardUnits.SOLAR_ELEVATION_ANGLE), + 0.9, + 1.0, false, Quantities.getQuantity(10, StandardUnits.S_RATED), 0.95, diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala index 5781848e1b..3b0af96454 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/BasicGridWithSwitches.scala @@ -231,6 +231,7 @@ trait BasicGridWithSwitches extends BasicGrid { Set.empty[Transformer3wModel], gridSwitches, ), + defaultVoltageLimits, GridControls.empty, ) } diff --git a/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala b/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala index 26f26cc141..c4dddcf25e 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/grid/TransformerTestData.scala @@ -8,8 +8,7 @@ package edu.ie3.simona.test.common.model.grid import breeze.math.Complex import edu.ie3.datamodel.models.input.connector.ConnectorPort -import edu.ie3.simona.model.grid.RefSystem -import edu.ie3.util.quantities.PowerSystemUnits._ +import edu.ie3.simona.model.grid.{RefSystem, VoltageLimits} import org.scalatest.prop.TableDrivenPropertyChecks.Table import org.scalatest.prop.{TableFor5, TableFor9} import squants.electro.Kilovolts @@ -28,11 +27,13 @@ trait TransformerTestData extends TransformerTestGrid { /* Define some parameters for the power flow calculation */ val epsilon = 1e-10 val maxIterations = 50 - val refSystem: RefSystem = + val refSystem: RefSystem = { RefSystem( Kilowatts(400d), Kilovolts(0.4d), ) + } + val defaultVoltageLimits: VoltageLimits = VoltageLimits(0.9, 1.1) val nodeUuidToIndexMap: Map[UUID, Int] = gridTapHv.getRawGrid.getNodes.asScala .map(node => node.getUuid -> node.getSubnet) diff --git a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala index a19cca5be3..7699ecf35a 100644 --- a/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala +++ b/src/test/scala/edu/ie3/simona/util/ConfigUtilSpec.scala @@ -15,6 +15,7 @@ import edu.ie3.datamodel.models.result.connector.{ } import edu.ie3.datamodel.models.result.system.{ChpResult, LoadResult} import edu.ie3.datamodel.models.result.{NodeResult, ResultEntity} +import edu.ie3.simona.config.RuntimeConfig._ import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig.{apply => _, _} import edu.ie3.simona.event.notifier.NotifierConfig @@ -57,7 +58,7 @@ class ConfigUtilSpec ) inside(actual) { case ParticipantConfigUtil(configs, defaultConfigs) => - configs shouldBe Map.empty[UUID, SimonaConfig.LoadRuntimeConfig] + configs shouldBe Map.empty[UUID, LoadRuntimeConfig] defaultConfigs.size shouldBe 8 inside(defaultConfigs.get(classOf[LoadRuntimeConfig])) { @@ -278,8 +279,7 @@ class ConfigUtilSpec ) inside(actual) { case ParticipantConfigUtil(configs, defaultConfigs) => - configs shouldBe Map - .empty[UUID, SimonaConfig.FixedFeedInRuntimeConfig] + configs shouldBe Map.empty[UUID, FixedFeedInRuntimeConfig] inside(defaultConfigs.get(classOf[FixedFeedInRuntimeConfig])) { case Some(