From 1c8c08f37a7214d6b6787b3747a2606d17089550 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 8 Jul 2024 13:19:57 +0200 Subject: [PATCH 1/6] Adding conversion of pv plants into PvInputs. --- .gitignore | 2 +- CHANGELOG.md | 4 + build.gradle | 6 +- gradle/scripts/tscfg.gradle | 2 +- .../config/allNoSwitches.conf | 0 .../config/multiVoltLvlNoSwitches.conf | 0 .../config/mvLvNoSwitches.conf | 0 .../config/mvWithSwitches.conf | 0 .../config/singleVoltLvlNoSwitches.conf | 0 {inputData => input}/config/smallGrid.conf | 0 src/main/resources/config-template.conf | 5 +- .../ie3/simbench/config/SimbenchConfig.scala | 15 +-- .../ie3/simbench/convert/GridConverter.scala | 39 ++++++-- .../ie3/simbench/convert/ResConverter.scala | 99 +++++++++++++++++-- .../convert/profiles/PvProfileConverter.scala | 40 ++++++++ .../ie3/simbench/io/ParticipantToInput.scala | 16 +++ .../edu/ie3/simbench/main/RunSimbench.scala | 15 ++- 17 files changed, 209 insertions(+), 34 deletions(-) rename {inputData => input}/config/allNoSwitches.conf (100%) rename {inputData => input}/config/multiVoltLvlNoSwitches.conf (100%) rename {inputData => input}/config/mvLvNoSwitches.conf (100%) rename {inputData => input}/config/mvWithSwitches.conf (100%) rename {inputData => input}/config/singleVoltLvlNoSwitches.conf (100%) rename {inputData => input}/config/smallGrid.conf (100%) create mode 100644 src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala create mode 100644 src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala diff --git a/.gitignore b/.gitignore index 2b424aba..c2cd097a 100644 --- a/.gitignore +++ b/.gitignore @@ -153,4 +153,4 @@ testData/download !testData/download/targetFolderExistsAndIsFile.zip # Downloader productive folder -inputData/download \ No newline at end of file +input/download \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7f37e5..dd6a79f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Conversion of pv plants into actual PvInputs [#253](https://github.com/ie3-institute/simBench2psdm/issues/253) + + ## [1.0.0] - 2021-08-03 ### Added - Basic functionality to convert SimBench data sets to [PowerSystemDataModel](https://github.com/ie3-institute/powersystemdatamodel) diff --git a/build.gradle b/build.gradle index d329e196..5c9f83a6 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ ext { scalaVersion = '2.13' scalaBinaryVersion = '2.13.14' - tscfgVersion = '0.9.986' + tscfgVersion = '1.0.0' slf4jVersion = '2.0.13' scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator //location of script plugins @@ -87,8 +87,8 @@ dependencies { implementation 'org.mockito:mockito-core:5.12.0' // mocking framework // config // - implementation 'com.typesafe:config:+' - implementation "com.github.carueda:tscfg_2.13:${tscfgVersion}" + implementation 'com.typesafe:config:1.4.3' + implementation "com.github.carueda:tscfg_2.13:$tscfgVersion" // cmd args parser // implementation "com.github.scopt:scopt_${scalaVersion}:+" diff --git a/gradle/scripts/tscfg.gradle b/gradle/scripts/tscfg.gradle index fc2026a6..90ac4283 100644 --- a/gradle/scripts/tscfg.gradle +++ b/gradle/scripts/tscfg.gradle @@ -5,7 +5,7 @@ task genConfigClass { doLast { def tscfgJarFile = project.file('build/tscfg-' + tscfgVersion + '.jar') if (!tscfgJarFile.exists() || !tscfgJarFile.isFile()) { - download { + download.run { src 'https://github.com/carueda/tscfg/releases/download/v' + tscfgVersion + '/tscfg-' + tscfgVersion + '.jar' dest buildDir } diff --git a/inputData/config/allNoSwitches.conf b/input/config/allNoSwitches.conf similarity index 100% rename from inputData/config/allNoSwitches.conf rename to input/config/allNoSwitches.conf diff --git a/inputData/config/multiVoltLvlNoSwitches.conf b/input/config/multiVoltLvlNoSwitches.conf similarity index 100% rename from inputData/config/multiVoltLvlNoSwitches.conf rename to input/config/multiVoltLvlNoSwitches.conf diff --git a/inputData/config/mvLvNoSwitches.conf b/input/config/mvLvNoSwitches.conf similarity index 100% rename from inputData/config/mvLvNoSwitches.conf rename to input/config/mvLvNoSwitches.conf diff --git a/inputData/config/mvWithSwitches.conf b/input/config/mvWithSwitches.conf similarity index 100% rename from inputData/config/mvWithSwitches.conf rename to input/config/mvWithSwitches.conf diff --git a/inputData/config/singleVoltLvlNoSwitches.conf b/input/config/singleVoltLvlNoSwitches.conf similarity index 100% rename from inputData/config/singleVoltLvlNoSwitches.conf rename to input/config/singleVoltLvlNoSwitches.conf diff --git a/inputData/config/smallGrid.conf b/input/config/smallGrid.conf similarity index 100% rename from inputData/config/smallGrid.conf rename to input/config/smallGrid.conf diff --git a/src/main/resources/config-template.conf b/src/main/resources/config-template.conf index 55a408d4..77e9fc4d 100644 --- a/src/main/resources/config-template.conf +++ b/src/main/resources/config-template.conf @@ -10,16 +10,17 @@ io { simbenchCodes = ["String"] input { download.baseUrl = "String" | "http://141.51.193.167/simbench/gui/usecase/download" - download.folder = "String" | "inputData/download/" + download.folder = "String" | "input/download/" download.failOnExistingFiles = "Boolean" | false csv = CsvConfig } output { csv = CsvConfig - targetFolder = "String" | "convertedData" + targetFolder = "String" | "output" compress = "Boolean" | true } } conversion { removeSwitches = "Boolean" | false + convertPv = "Boolean" | false } \ No newline at end of file diff --git a/src/main/scala/edu/ie3/simbench/config/SimbenchConfig.scala b/src/main/scala/edu/ie3/simbench/config/SimbenchConfig.scala index 8f9caba7..5f5b9474 100644 --- a/src/main/scala/edu/ie3/simbench/config/SimbenchConfig.scala +++ b/src/main/scala/edu/ie3/simbench/config/SimbenchConfig.scala @@ -1,4 +1,4 @@ -// generated by tscfg 0.9.986 on Mon Aug 09 20:12:24 CEST 2021 +// generated by tscfg 1.0.0 on Mon Jul 08 11:40:43 CEST 2024 // source: src/main/resources/config-template.conf package edu.ie3.simbench.config @@ -38,6 +38,7 @@ object SimbenchConfig { } final case class Conversion( + convertPv: scala.Boolean, removeSwitches: scala.Boolean ) object Conversion { @@ -47,9 +48,9 @@ object SimbenchConfig { $tsCfgValidator: $TsCfgValidator ): SimbenchConfig.Conversion = { SimbenchConfig.Conversion( - removeSwitches = c.hasPathOrNull("removeSwitches") && c.getBoolean( - "removeSwitches" - ) + convertPv = c.hasPathOrNull("convertPv") && c.getBoolean("convertPv"), + removeSwitches = + c.hasPathOrNull("removeSwitches") && c.getBoolean("removeSwitches") ) } } @@ -86,7 +87,7 @@ object SimbenchConfig { ), folder = if (c.hasPathOrNull("folder")) c.getString("folder") - else "inputData/download/" + else "input/download/" ) } } @@ -134,7 +135,7 @@ object SimbenchConfig { ), targetFolder = if (c.hasPathOrNull("targetFolder")) c.getString("targetFolder") - else "convertedData" + else "output" ) } } @@ -208,7 +209,7 @@ object SimbenchConfig { java.lang.String.valueOf(cv.unwrapped()) } - private final class $TsCfgValidator { + final class $TsCfgValidator { private val badPaths = scala.collection.mutable.ArrayBuffer[java.lang.String]() diff --git a/src/main/scala/edu/ie3/simbench/convert/GridConverter.scala b/src/main/scala/edu/ie3/simbench/convert/GridConverter.scala index e1c25b5c..190c3b90 100644 --- a/src/main/scala/edu/ie3/simbench/convert/GridConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/GridConverter.scala @@ -28,11 +28,13 @@ import edu.ie3.simbench.convert.NodeConverter.AttributeOverride.{ JoinOverride, SubnetOverride } +import edu.ie3.simbench.convert.ResConverter.ConvertedRes import edu.ie3.simbench.convert.types.{ LineTypeConverter, Transformer2wTypeConverter } import edu.ie3.simbench.exception.ConversionException +import edu.ie3.simbench.io.ParticipantToInput import edu.ie3.simbench.model.datamodel._ import scala.annotation.tailrec @@ -51,6 +53,9 @@ case object GridConverter extends LazyLogging { * Total grid input model to be converted * @param removeSwitches * Whether or not to remove switches from the grid structure + * @param participantToInput + * Whether or not to convert a given type of participant into actual input + * models * @return * A converted [[JointGridContainer]], a [[Vector]] of * [[IndividualTimeSeries]] as well as a [[Vector]] of [[NodeResult]]s @@ -58,7 +63,8 @@ case object GridConverter extends LazyLogging { def convert( simbenchCode: String, gridInput: GridModel, - removeSwitches: Boolean + removeSwitches: Boolean, + participantToInput: ParticipantToInput ): ( JointGridContainer, Vector[IndividualTimeSeries[_ <: PValue]], @@ -73,7 +79,7 @@ case object GridConverter extends LazyLogging { s"Converting system participants and their time series of '${gridInput.simbenchCode}'" ) val (systemParticipants, timeSeries, timeSeriesMapping) = - convertParticipants(gridInput, nodeConversion) + convertParticipants(gridInput, nodeConversion, participantToInput) logger.debug( s"Converting power flow results of '${gridInput.simbenchCode}'" @@ -601,13 +607,17 @@ case object GridConverter extends LazyLogging { * Total grid input model to convert * @param nodeConversion * Already known conversion mapping of nodes + * @param participantToInput + * Whether or not to convert a given type of participant into actual input + * models * @return * A collection of converted system participants and their individual time * series */ def convertParticipants( gridInput: GridModel, - nodeConversion: Map[Node, NodeInput] + nodeConversion: Map[Node, NodeInput], + participantToInput: ParticipantToInput ): ( SystemParticipants, Vector[IndividualTimeSeries[_ <: PValue]], @@ -626,14 +636,14 @@ case object GridConverter extends LazyLogging { logger.debug( s"Done converting ${gridInput.powerPlants.size} power plants including time series" ) - val resToTimeSeries = convertRes(gridInput, nodeConversion) + val res = convertRes(gridInput, nodeConversion, participantToInput) logger.debug( s"Done converting ${gridInput.res.size} RES including time series" ) /* Map participant uuid onto time series */ val participantsToTimeSeries = - loadsToTimeSeries ++ powerPlantsToTimeSeries ++ resToTimeSeries + loadsToTimeSeries ++ powerPlantsToTimeSeries ++ res.fixedFeedInInput val mapping = participantsToTimeSeries.map { case (model, timeSeries) => new TimeSeriesMappingSource.MappingEntry( model.getUuid, @@ -649,10 +659,10 @@ case object GridConverter extends LazyLogging { Set.empty[ChpInput].asJava, Set.empty[EvcsInput].asJava, Set.empty[EvInput].asJava, - (powerPlantsToTimeSeries.keySet ++ resToTimeSeries.keySet).asJava, + (powerPlantsToTimeSeries.keySet ++ res.fixedFeedInInput.keySet).asJava, Set.empty[HpInput].asJava, loadsToTimeSeries.keySet.asJava, - Set.empty[PvInput].asJava, + res.pvInput.asJava, Set.empty[StorageInput].asJava, Set.empty[WecInput].asJava ), @@ -709,16 +719,25 @@ case object GridConverter extends LazyLogging { * Total grid input model to convert * @param nodeConversion * Already known conversion mapping of nodes + * @param participantToInput + * Whether or not to convert a given type of participant into actual input + * models * @return * A mapping from renewable energy source system to their assigned, * specific time series */ def convertRes( gridInput: GridModel, - nodeConversion: Map[Node, NodeInput] - ): Map[FixedFeedInInput, IndividualTimeSeries[PValue]] = { + nodeConversion: Map[Node, NodeInput], + participantToInput: ParticipantToInput + ): ConvertedRes = { val resProfiles = gridInput.resProfiles.map(profile => profile.profileType -> profile).toMap - ResConverter.convert(gridInput.res, nodeConversion, resProfiles) + ResConverter.convert( + gridInput.res, + nodeConversion, + resProfiles, + participantToInput + ) } } diff --git a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala index 4b254396..5861eabc 100644 --- a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala @@ -1,14 +1,22 @@ package edu.ie3.simbench.convert import java.util.{Locale, UUID} - import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.input.system.FixedFeedInInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.system.{FixedFeedInInput, PvInput} +import edu.ie3.datamodel.models.input.system.characteristic.{ + CosPhiFixed, + ReactivePowerCharacteristic +} import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries import edu.ie3.datamodel.models.value.PValue -import edu.ie3.simbench.convert.profiles.PowerProfileConverter +import edu.ie3.simbench.convert.profiles.{ + PowerProfileConverter, + PvProfileConverter +} +import edu.ie3.simbench.io.ParticipantToInput +import edu.ie3.simbench.model.datamodel.enums.ResType +import edu.ie3.simbench.model.datamodel.enums.ResType.{PV, PvMv} import edu.ie3.simbench.model.datamodel.profiles.{ResProfile, ResProfileType} import edu.ie3.simbench.model.datamodel.{Node, RES} import edu.ie3.util.quantities.PowerSystemUnits.{ @@ -16,12 +24,21 @@ import edu.ie3.util.quantities.PowerSystemUnits.{ MEGAVOLTAMPERE, MEGAWATT } +import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities +import java.util +import javax.measure.quantity.{Angle, Dimensionless, Power} +import scala.Option.when import scala.collection.parallel.CollectionConverters._ case object ResConverter extends ShuntConverter { + final case class ConvertedRes( + fixedFeedInInput: Map[FixedFeedInInput, IndividualTimeSeries[PValue]], + pvInput: Set[PvInput] + ) + /** Convert a full set of renewable energy source system * * @param res @@ -30,6 +47,9 @@ case object ResConverter extends ShuntConverter { * Mapping from Simbench to power system data model node * @param profiles * Collection of [[ResProfile]]s + * @param participantToInput + * Whether or not to convert a given type of participant into actual input + * models * @return * A mapping from converted renewable energy source system to equivalent * individual time series @@ -37,9 +57,30 @@ case object ResConverter extends ShuntConverter { def convert( res: Vector[RES], nodes: Map[Node, NodeInput], - profiles: Map[ResProfileType, ResProfile] - ): Map[FixedFeedInInput, IndividualTimeSeries[PValue]] = - res.par + profiles: Map[ResProfileType, ResProfile], + participantToInput: ParticipantToInput + ): ConvertedRes = { + + /* check whether to convert pv plants into PvInputs */ + val (pvs, pvInputs) = if (participantToInput.pvInput) { + val plants = + res.filter(input => input.resType == PV || input.resType == PvMv) + val inputs = plants.par + .map { plant => + val node = NodeConverter.getNode(plant.node, nodes) + val profile = + PowerProfileConverter.getProfile(plant.profile, profiles) + convertPv(plant, node, profile) + } + .seq + .toSet + + (plants, inputs) + } else (Vector.empty, Set.empty[PvInput]) + + /* convert all other plants into a FixedFeedInInputs */ + val fixedFeedIns = res.diff(pvs) + val fixedFeedInInput = fixedFeedIns.par .map { plant => val node = NodeConverter.getNode(plant.node, nodes) val profile = @@ -49,6 +90,50 @@ case object ResConverter extends ShuntConverter { .seq .toMap + /* return the converted models */ + ConvertedRes(fixedFeedInInput, pvInputs) + } + + def convertPv( + input: RES, + node: NodeInput, + profile: ResProfile, + uuid: Option[UUID] = None + ): PvInput = { + val p = Quantities.getQuantity(input.p, MEGAWATT) + val q = Quantities.getQuantity(input.q, MEGAVAR) + val cosphi = cosPhi(p.getValue.doubleValue(), q.getValue.doubleValue()) + val varCharacteristicString = + "cosPhiFixed:{(0.0,%#.2f)}".formatLocal(Locale.ENGLISH, cosphi) + val sRated = Quantities.getQuantity(input.sR, MEGAVOLTAMPERE) + + /* Flip the sign, as infeed is negative in PowerSystemDataModel */ + val timeSeries = PowerProfileConverter.convert(profile, p.multiply(-1)) + + /* build pv parameter from time series */ + val (albedo, azimuth, etaConv, elevationAngle, kG, kT) = + PvProfileConverter.convert(timeSeries) + + new PvInput( + uuid.getOrElse(UUID.randomUUID()), + input.id + "_" + input.resType.toString, + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + node, + new CosPhiFixed(varCharacteristicString), + null, + albedo, + azimuth, + etaConv, + elevationAngle, + kG, + kT, + false, + sRated, + cosphi + ) + } + /** Converts a single renewable energy source system to a fixed feed in model * due to lacking information to sophistically guess typical types of assets. * Different voltage regulation strategies are not covered, yet. diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala new file mode 100644 index 00000000..7172170c --- /dev/null +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala @@ -0,0 +1,40 @@ +package edu.ie3.simbench.convert.profiles + +import edu.ie3.datamodel.models.StandardUnits +import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries +import edu.ie3.datamodel.models.value.PValue +import tech.units.indriya.ComparableQuantity +import tech.units.indriya.quantity.Quantities + +import javax.measure.quantity.{Angle, Dimensionless} + +object PvProfileConverter { + + def convert( + timeSeries: IndividualTimeSeries[PValue] + ): ( + Double, + ComparableQuantity[Angle], + ComparableQuantity[Dimensionless], + ComparableQuantity[Angle], + Double, + Double + ) = { + + val albedo: Double = 0.0 + val azimuth = Quantities.getQuantity(0.0, StandardUnits.AZIMUTH) + val elevationAngle = + Quantities.getQuantity(0.0, StandardUnits.SOLAR_ELEVATION_ANGLE) + + // TODO: Check these default values + val etaConv = Quantities.getQuantity( + 95, + StandardUnits.EFFICIENCY + ) // efficiency of the converter + val kG: Double = 0.8999999761581421 // see vn_simona + val kT: Double = 1.0 // see vn_simona + + (albedo, azimuth, etaConv, elevationAngle, kG, kT) + } + +} diff --git a/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala b/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala new file mode 100644 index 00000000..bc5a8b4a --- /dev/null +++ b/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala @@ -0,0 +1,16 @@ +package edu.ie3.simbench.io + +import edu.ie3.simbench.config.SimbenchConfig + +final case class ParticipantToInput( + pvInput: Boolean, + wecInput: Boolean = false +) + +object ParticipantToInput { + + def apply(cfg: SimbenchConfig.Conversion): ParticipantToInput = + ParticipantToInput( + cfg.convertPv + ) +} diff --git a/src/main/scala/edu/ie3/simbench/main/RunSimbench.scala b/src/main/scala/edu/ie3/simbench/main/RunSimbench.scala index 6cfa4af6..d9eff689 100644 --- a/src/main/scala/edu/ie3/simbench/main/RunSimbench.scala +++ b/src/main/scala/edu/ie3/simbench/main/RunSimbench.scala @@ -10,7 +10,13 @@ import edu.ie3.datamodel.io.sink.CsvFileSink import edu.ie3.simbench.config.{ConfigValidator, SimbenchConfig} import edu.ie3.simbench.convert.GridConverter import edu.ie3.simbench.exception.CodeValidationException -import edu.ie3.simbench.io.{Downloader, IoUtils, SimbenchReader, Zipper} +import edu.ie3.simbench.io.{ + Downloader, + IoUtils, + ParticipantToInput, + SimbenchReader, + Zipper +} import edu.ie3.simbench.model.SimbenchCode import edu.ie3.util.io.FileIOUtils import org.apache.commons.io.FilenameUtils @@ -36,6 +42,8 @@ object RunSimbench extends SimbenchHelper { /* Validate the config */ ConfigValidator.checkValidity(simbenchConfig) + val participantToInput = ParticipantToInput(simbenchConfig.conversion) + simbenchConfig.io.simbenchCodes.foreach { simbenchCode => logger.info(s"$simbenchCode - Downloading data set from SimBench website") val downloader = @@ -80,7 +88,8 @@ object RunSimbench extends SimbenchHelper { GridConverter.convert( simbenchCode, simbenchModel, - simbenchConfig.conversion.removeSwitches + simbenchConfig.conversion.removeSwitches, + participantToInput ) logger.info(s"$simbenchCode - Writing converted data set to files") @@ -110,7 +119,7 @@ object RunSimbench extends SimbenchHelper { } csvSink.persistJointGrid(jointGridContainer) - timeSeries.foreach(csvSink.persistTimeSeries(_)) + // timeSeries.foreach(csvSink.persistTimeSeries(_)) csvSink.persistAllIgnoreNested(timeSeriesMapping.asJava) csvSink.persistAll(powerFlowResults.asJava) From d6357e1c6d2962039d0f18b9bbe45dd704538f73 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Tue, 9 Jul 2024 15:05:48 +0200 Subject: [PATCH 2/6] Adding calculation for power values before the converter. --- .../ie3/simbench/convert/ResConverter.scala | 36 ++++++--- .../convert/profiles/PvProfileConverter.scala | 78 +++++++++++++------ 2 files changed, 77 insertions(+), 37 deletions(-) diff --git a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala index 5861eabc..7bbc8a61 100644 --- a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala @@ -1,12 +1,8 @@ package edu.ie3.simbench.convert -import java.util.{Locale, UUID} -import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.{OperationTime, StandardUnits} +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed import edu.ie3.datamodel.models.input.system.{FixedFeedInInput, PvInput} -import edu.ie3.datamodel.models.input.system.characteristic.{ - CosPhiFixed, - ReactivePowerCharacteristic -} import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries import edu.ie3.datamodel.models.value.PValue @@ -15,7 +11,6 @@ import edu.ie3.simbench.convert.profiles.{ PvProfileConverter } import edu.ie3.simbench.io.ParticipantToInput -import edu.ie3.simbench.model.datamodel.enums.ResType import edu.ie3.simbench.model.datamodel.enums.ResType.{PV, PvMv} import edu.ie3.simbench.model.datamodel.profiles.{ResProfile, ResProfileType} import edu.ie3.simbench.model.datamodel.{Node, RES} @@ -27,9 +22,8 @@ import edu.ie3.util.quantities.PowerSystemUnits.{ import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities -import java.util -import javax.measure.quantity.{Angle, Dimensionless, Power} -import scala.Option.when +import java.util.{Locale, UUID} +import javax.measure.quantity.Dimensionless import scala.collection.parallel.CollectionConverters._ case object ResConverter extends ShuntConverter { @@ -111,8 +105,26 @@ case object ResConverter extends ShuntConverter { val timeSeries = PowerProfileConverter.convert(profile, p.multiply(-1)) /* build pv parameter from time series */ - val (albedo, azimuth, etaConv, elevationAngle, kG, kT) = - PvProfileConverter.convert(timeSeries) + // TODO: Check these values, that vn_simona defaults to + val etaConv: ComparableQuantity[Dimensionless] = Quantities.getQuantity( + 97, + StandardUnits.EFFICIENCY + ) // vn_simona uses 95%, 97% and 98% + val albedo: Double = 0.20000000298023224 // see vn_simona + val kG: Double = 0.8999999761581421 // see vn_simona + val kT: Double = 1.0 // see vn_simona + + /* calculate the power values before the converter */ + val powerBeforeConverter = + PvProfileConverter.calculatePowerBeforeConverter(timeSeries, etaConv) + + /* calculate the angles of the pv input */ + val (azimuth, elevationAngle) = + PvProfileConverter.calculateAngles( + powerBeforeConverter, + sRated, + profile.profileType + ) new PvInput( uuid.getOrElse(UUID.randomUUID()), diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala index 7172170c..4a0083f6 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala @@ -1,40 +1,68 @@ package edu.ie3.simbench.convert.profiles import edu.ie3.datamodel.models.StandardUnits -import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries +import edu.ie3.datamodel.models.input.system.PvInput +import edu.ie3.datamodel.models.timeseries.individual.{ + IndividualTimeSeries, + TimeBasedValue +} import edu.ie3.datamodel.models.value.PValue +import edu.ie3.simbench.model.datamodel.profiles.ResProfileType import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities -import javax.measure.quantity.{Angle, Dimensionless} +import javax.measure.quantity.{Angle, Dimensionless, Power} +import scala.jdk.CollectionConverters._ object PvProfileConverter { - def convert( - timeSeries: IndividualTimeSeries[PValue] - ): ( - Double, - ComparableQuantity[Angle], - ComparableQuantity[Dimensionless], - ComparableQuantity[Angle], - Double, - Double - ) = { + /** Calculates the power before the converter using the efficiency of the used + * converter + * @param timeSeries + * of power values + * @param efficiency + * of the converter + * @return + */ + def calculatePowerBeforeConverter( + timeSeries: IndividualTimeSeries[PValue], + efficiency: ComparableQuantity[Dimensionless] + ): IndividualTimeSeries[PValue] = new IndividualTimeSeries( + timeSeries.getEntries.asScala.map { timeBasedValue => + // get the power value + val powerOption = timeBasedValue.getValue.getP - val albedo: Double = 0.0 - val azimuth = Quantities.getQuantity(0.0, StandardUnits.AZIMUTH) - val elevationAngle = - Quantities.getQuantity(0.0, StandardUnits.SOLAR_ELEVATION_ANGLE) + val pValue = if (powerOption.isPresent) { + // if the power value is present, we divide it by the efficiency of the converter + new PValue( + powerOption.get().divide(efficiency.getValue.doubleValue()) + ) + } else timeBasedValue.getValue // else we just return the given value - // TODO: Check these default values - val etaConv = Quantities.getQuantity( - 95, - StandardUnits.EFFICIENCY - ) // efficiency of the converter - val kG: Double = 0.8999999761581421 // see vn_simona - val kT: Double = 1.0 // see vn_simona + new TimeBasedValue(timeBasedValue.getTime, pValue) + }.asJava + ) - (albedo, azimuth, etaConv, elevationAngle, kG, kT) + /** Calculates the azimuth and the elevation angle of a [[PvInput]]. + * @param timeSeries + * with power values + * @param sRated + * the rated maximum power of the model + * @param profile + * the profile of the pv model that can be used to retrieve the latitude of + * location the pv model is installed + * @return + * the azimuth and the elevation angle in radians + */ + def calculateAngles( + timeSeries: IndividualTimeSeries[PValue], + sRated: ComparableQuantity[Power], + profile: ResProfileType + ): (ComparableQuantity[Angle], ComparableQuantity[Angle]) = { + // azimuth of 0° -> pv is facing south + ( + Quantities.getQuantity(0.0, StandardUnits.AZIMUTH), + Quantities.getQuantity(45.0, StandardUnits.SOLAR_ELEVATION_ANGLE) + ) } - } From 41b2f99d7ade9bc4af2a592375c2d477ccf36196 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Thu, 11 Jul 2024 15:10:47 +0200 Subject: [PATCH 3/6] Adding determination of --- .../ie3/simbench/convert/ResConverter.scala | 16 ++- .../convert/profiles/PvProfileConverter.scala | 116 ++++++++++++++---- .../profiles/ResProfileConverter.scala | 66 ++++++++++ 3 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala diff --git a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala index 7bbc8a61..9dd6575b 100644 --- a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala @@ -116,14 +116,22 @@ case object ResConverter extends ShuntConverter { /* calculate the power values before the converter */ val powerBeforeConverter = - PvProfileConverter.calculatePowerBeforeConverter(timeSeries, etaConv) + PvProfileConverter.calculatePowerBeforeConverter( + timeSeries, + etaConv, + kG, + kT + ) /* calculate the angles of the pv input */ - val (azimuth, elevationAngle) = - PvProfileConverter.calculateAngles( + val azimuth = PvProfileConverter.getAzimuth(profile.profileType) + + val elevationAngle = + PvProfileConverter.calculateElevationAngle( powerBeforeConverter, sRated, - profile.profileType + profile.profileType, + azimuth ) new PvInput( diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala index 4a0083f6..14db1d9a 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala @@ -7,43 +7,84 @@ import edu.ie3.datamodel.models.timeseries.individual.{ TimeBasedValue } import edu.ie3.datamodel.models.value.PValue +import edu.ie3.simbench.convert.profiles.ResProfileConverter.CompassDirection.{ + East, + North, + South, + West +} import edu.ie3.simbench.model.datamodel.profiles.ResProfileType +import org.locationtech.jts.geom.Coordinate import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities +import java.time.ZonedDateTime import javax.measure.quantity.{Angle, Dimensionless, Power} import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters._ object PvProfileConverter { /** Calculates the power before the converter using the efficiency of the used - * converter + * converter and the technical influence. + * * @param timeSeries * of power values * @param efficiency - * of the converter + * efficiency of converter (typically in %) + * @param kG + * generator correction factor merging different technical influences + * @param kT + * generator correction factor merging different technical influences * @return */ def calculatePowerBeforeConverter( timeSeries: IndividualTimeSeries[PValue], - efficiency: ComparableQuantity[Dimensionless] - ): IndividualTimeSeries[PValue] = new IndividualTimeSeries( - timeSeries.getEntries.asScala.map { timeBasedValue => - // get the power value - val powerOption = timeBasedValue.getValue.getP + efficiency: ComparableQuantity[Dimensionless], + kG: Double, + kT: Double + ): IndividualTimeSeries[PValue] = { + val correctionFactor = efficiency.getValue.doubleValue() / 100 * kG * kT - val pValue = if (powerOption.isPresent) { - // if the power value is present, we divide it by the efficiency of the converter - new PValue( - powerOption.get().divide(efficiency.getValue.doubleValue()) - ) - } else timeBasedValue.getValue // else we just return the given value + new IndividualTimeSeries( + timeSeries.getEntries.asScala.map { timeBasedValue => + // get the power value + val powerOption = timeBasedValue.getValue.getP + + val pValue = if (powerOption.isPresent) { + // if the power value is present, we divide it by the efficiency of the converter + new PValue( + powerOption.get().divide(correctionFactor) + ) + } else timeBasedValue.getValue // else we just return the given value + + new TimeBasedValue(timeBasedValue.getTime, pValue) + }.asJava + ) + } - new TimeBasedValue(timeBasedValue.getTime, pValue) - }.asJava - ) + /** Determine the azimuth (inclination in a compass direction) of the + * [[PvInput]] using the [[ResProfileType]]. + * @param profile + * of the plant + * @return + * the azimuth of the pv input + */ + def getAzimuth( + profile: ResProfileType + ): ComparableQuantity[Angle] = { + /* extracting some useful information from the given profile type */ + val orientation = ResProfileConverter.getCompassDirection(profile) + /* find azimuth using the given orientation */ + orientation match { + case North => Quantities.getQuantity(180d, StandardUnits.AZIMUTH) + case East => Quantities.getQuantity(-90d, StandardUnits.AZIMUTH) + case South => Quantities.getQuantity(0d, StandardUnits.AZIMUTH) + case West => Quantities.getQuantity(90d, StandardUnits.AZIMUTH) + } + } - /** Calculates the azimuth and the elevation angle of a [[PvInput]]. + /** Calculates the elevation angle of a [[PvInput]]. * @param timeSeries * with power values * @param sRated @@ -51,18 +92,45 @@ object PvProfileConverter { * @param profile * the profile of the pv model that can be used to retrieve the latitude of * location the pv model is installed + * @param azimuth + * of the [[PvInput]] * @return * the azimuth and the elevation angle in radians */ - def calculateAngles( + def calculateElevationAngle( timeSeries: IndividualTimeSeries[PValue], sRated: ComparableQuantity[Power], - profile: ResProfileType - ): (ComparableQuantity[Angle], ComparableQuantity[Angle]) = { - // azimuth of 0° -> pv is facing south - ( - Quantities.getQuantity(0.0, StandardUnits.AZIMUTH), - Quantities.getQuantity(45.0, StandardUnits.SOLAR_ELEVATION_ANGLE) + profile: ResProfileType, + azimuth: ComparableQuantity[Angle] + ): ComparableQuantity[Angle] = { + val ratedPower = sRated.to(StandardUnits.S_RATED).getValue.doubleValue() + + val values = timeSeries.getEntries.asScala.toList.flatMap { + timeBasedValue => + timeBasedValue.getValue.getP.toScala.map(value => + (timeBasedValue.getTime, value) + ) + } + + val position: Coordinate = ResProfileConverter.getCoordinate(profile) + + val march21 = getMaxFeedIn(values, 3) + val june21 = getMaxFeedIn(values, 6) + val september21 = getMaxFeedIn(values, 9) + val dezember21 = getMaxFeedIn(values, 12) + + Quantities.getQuantity(35d, StandardUnits.SOLAR_ELEVATION_ANGLE) + } + + private def getMaxFeedIn( + timeSeriesEntries: Seq[(ZonedDateTime, ComparableQuantity[Power])], + month: Int, + dayOfTheMonth: Int = 21 + ): Option[(ZonedDateTime, ComparableQuantity[Power])] = { + val powerValues = timeSeriesEntries.filter(value => + value._1.getMonth.getValue == month && value._1.getDayOfMonth == dayOfTheMonth ) + + powerValues.minByOption(_._2.getValue.doubleValue()) } } diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala new file mode 100644 index 00000000..91b33748 --- /dev/null +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala @@ -0,0 +1,66 @@ +package edu.ie3.simbench.convert.profiles + +import edu.ie3.simbench.exception.ConversionException +import edu.ie3.simbench.model.datamodel.profiles.ResProfileType +import edu.ie3.util.geo.GeoUtils +import org.locationtech.jts.geom.Coordinate + +object ResProfileConverter { + // default coordinates for two locations + private val hanoverCoordinate: Coordinate = + GeoUtils.buildCoordinate(52.366667, 9.733333) + private val luebeckCoordinate: Coordinate = + GeoUtils.buildCoordinate(53.866667, 10.683333) + + /** Determines the [[Coordinate]] of a + * [[edu.ie3.simbench.model.datamodel.RES]] based on its [[ResProfileType]] + * @param profileType + * that can be used to determine the position + * @return + * the coordinate + */ + def getCoordinate(profileType: ResProfileType): Coordinate = + profileType match { + case ResProfileType.PV1 => hanoverCoordinate // hanover (first place) + case ResProfileType.PV2 => luebeckCoordinate // lübeck (first place) + case ResProfileType.PV3 => hanoverCoordinate // hanover (first place) + case ResProfileType.PV4 => hanoverCoordinate // hanover (second place) + case ResProfileType.PV5 => luebeckCoordinate // lübeck (second place) + case ResProfileType.PV6 => luebeckCoordinate // lübeck (third place) + case ResProfileType.PV7 => hanoverCoordinate // hanover (second place) + case ResProfileType.PV8 => hanoverCoordinate // hanover (second place) + case other => + throw ConversionException( + s"There are no coordinates for the profile type $other." + ) + } + + /** Determines the [[CompassDirection]] of a + * [[edu.ie3.simbench.model.datamodel.RES]] based on its [[ResProfileType]] + * @param profileType + * that can be used to determine the orientation + * @return + * the orientation + */ + def getCompassDirection(profileType: ResProfileType): CompassDirection.Value = + profileType match { + case ResProfileType.PV1 | ResProfileType.PV2 => CompassDirection.East + case ResProfileType.PV3 | ResProfileType.PV4 | ResProfileType.PV5 | + ResProfileType.PV6 => + CompassDirection.South + case ResProfileType.PV7 | ResProfileType.PV8 => CompassDirection.West + case other => + throw ConversionException( + s"There are no coordinates for the profile type $other." + ) + } + + /** Orientation that can be north, east, south and west. + */ + final case object CompassDirection extends Enumeration { + val North: Value = Value("north") + val East: Value = Value("east") + val South: Value = Value("south") + val West: Value = Value("west") + } +} From a0028b6a9ab942e4b6cd9b2a47fb1dc7acc7827f Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Fri, 12 Jul 2024 12:52:57 +0200 Subject: [PATCH 4/6] Adding basic elevation angle calculation. Adapting tests. --- .../ie3/simbench/convert/ResConverter.scala | 8 ++- .../convert/profiles/PvProfileConverter.scala | 37 +++-------- .../profiles/ResProfileConverter.scala | 17 +++++ .../ie3/simbench/io/ParticipantToInput.scala | 3 +- .../simbench/convert/GridConverterSpec.scala | 66 ++++++++++++++++++- .../edu/ie3/test/common/ConfigTestData.scala | 5 +- 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala index 9dd6575b..55f81c78 100644 --- a/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/ResConverter.scala @@ -8,7 +8,8 @@ import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries import edu.ie3.datamodel.models.value.PValue import edu.ie3.simbench.convert.profiles.{ PowerProfileConverter, - PvProfileConverter + PvProfileConverter, + ResProfileConverter } import edu.ie3.simbench.io.ParticipantToInput import edu.ie3.simbench.model.datamodel.enums.ResType.{PV, PvMv} @@ -123,12 +124,15 @@ case object ResConverter extends ShuntConverter { kT ) + val maxFeedIn = ResProfileConverter.findMaxFeedIn(powerBeforeConverter) + /* calculate the angles of the pv input */ val azimuth = PvProfileConverter.getAzimuth(profile.profileType) + /* calculate the elevation angle */ val elevationAngle = PvProfileConverter.calculateElevationAngle( - powerBeforeConverter, + maxFeedIn, sRated, profile.profileType, azimuth diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala index 14db1d9a..851a9b73 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala @@ -85,8 +85,8 @@ object PvProfileConverter { } /** Calculates the elevation angle of a [[PvInput]]. - * @param timeSeries - * with power values + * @param maxFeedIn + * option for the maximum occurred feed in * @param sRated * the rated maximum power of the model * @param profile @@ -98,39 +98,20 @@ object PvProfileConverter { * the azimuth and the elevation angle in radians */ def calculateElevationAngle( - timeSeries: IndividualTimeSeries[PValue], + maxFeedIn: Option[TimeBasedValue[PValue]], sRated: ComparableQuantity[Power], profile: ResProfileType, azimuth: ComparableQuantity[Angle] ): ComparableQuantity[Angle] = { - val ratedPower = sRated.to(StandardUnits.S_RATED).getValue.doubleValue() - - val values = timeSeries.getEntries.asScala.toList.flatMap { - timeBasedValue => - timeBasedValue.getValue.getP.toScala.map(value => - (timeBasedValue.getTime, value) - ) - } - val position: Coordinate = ResProfileConverter.getCoordinate(profile) - val march21 = getMaxFeedIn(values, 3) - val june21 = getMaxFeedIn(values, 6) - val september21 = getMaxFeedIn(values, 9) - val dezember21 = getMaxFeedIn(values, 12) - - Quantities.getQuantity(35d, StandardUnits.SOLAR_ELEVATION_ANGLE) - } - - private def getMaxFeedIn( - timeSeriesEntries: Seq[(ZonedDateTime, ComparableQuantity[Power])], - month: Int, - dayOfTheMonth: Int = 21 - ): Option[(ZonedDateTime, ComparableQuantity[Power])] = { - val powerValues = timeSeriesEntries.filter(value => - value._1.getMonth.getValue == month && value._1.getDayOfMonth == dayOfTheMonth + // TODO: Adjust angle with power value and rated power + // angle for horizontal irradiance + val angleInDegrees = position.getY - 15 + Quantities.getQuantity( + Math.toRadians(angleInDegrees), + StandardUnits.SOLAR_ELEVATION_ANGLE ) - powerValues.minByOption(_._2.getValue.doubleValue()) } } diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala index 91b33748..0373d273 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala @@ -1,10 +1,20 @@ package edu.ie3.simbench.convert.profiles +import edu.ie3.datamodel.models.timeseries.individual.{ + IndividualTimeSeries, + TimeBasedValue +} +import edu.ie3.datamodel.models.value.PValue import edu.ie3.simbench.exception.ConversionException import edu.ie3.simbench.model.datamodel.profiles.ResProfileType import edu.ie3.util.geo.GeoUtils import org.locationtech.jts.geom.Coordinate +import edu.ie3.util.quantities.QuantityUtils._ + +import scala.jdk.CollectionConverters.SetHasAsScala +import scala.jdk.OptionConverters.RichOptional + object ResProfileConverter { // default coordinates for two locations private val hanoverCoordinate: Coordinate = @@ -12,6 +22,13 @@ object ResProfileConverter { private val luebeckCoordinate: Coordinate = GeoUtils.buildCoordinate(53.866667, 10.683333) + def findMaxFeedIn( + timeSeries: IndividualTimeSeries[PValue] + ): Option[TimeBasedValue[PValue]] = + timeSeries.getEntries.asScala.minByOption { value => + value.getValue.getP.toScala.getOrElse(0.asKiloWatt).getValue.doubleValue() + } + /** Determines the [[Coordinate]] of a * [[edu.ie3.simbench.model.datamodel.RES]] based on its [[ResProfileType]] * @param profileType diff --git a/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala b/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala index bc5a8b4a..3988e9be 100644 --- a/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala +++ b/src/main/scala/edu/ie3/simbench/io/ParticipantToInput.scala @@ -3,8 +3,7 @@ package edu.ie3.simbench.io import edu.ie3.simbench.config.SimbenchConfig final case class ParticipantToInput( - pvInput: Boolean, - wecInput: Boolean = false + pvInput: Boolean ) object ParticipantToInput { diff --git a/src/test/scala/edu/ie3/simbench/convert/GridConverterSpec.scala b/src/test/scala/edu/ie3/simbench/convert/GridConverterSpec.scala index 5bf1cc3f..5957ce2b 100644 --- a/src/test/scala/edu/ie3/simbench/convert/GridConverterSpec.scala +++ b/src/test/scala/edu/ie3/simbench/convert/GridConverterSpec.scala @@ -5,9 +5,13 @@ import java.util import edu.ie3.datamodel.models.UniqueEntity import edu.ie3.datamodel.models.input.NodeInput import edu.ie3.datamodel.models.input.connector.{LineInput, Transformer2WInput} -import edu.ie3.datamodel.models.input.system.{FixedFeedInInput, LoadInput} +import edu.ie3.datamodel.models.input.system.{ + FixedFeedInInput, + LoadInput, + PvInput +} import edu.ie3.simbench.convert.NodeConverter.AttributeOverride.JoinOverride -import edu.ie3.simbench.io.SimbenchReader +import edu.ie3.simbench.io.{ParticipantToInput, SimbenchReader} import edu.ie3.simbench.model.datamodel.{GridModel, Node, Switch} import edu.ie3.test.common.{SwitchTestingData, UnitSpec} import org.scalatest.Inside._ @@ -145,7 +149,8 @@ class GridConverterSpec extends UnitSpec with SwitchTestingData { val actual = GridConverter.convert( "1-LV-rural1--0-no_sw", input, - removeSwitches = false + removeSwitches = false, + ParticipantToInput(pvInput = false) ) inside(actual) { case ( @@ -194,6 +199,61 @@ class GridConverterSpec extends UnitSpec with SwitchTestingData { powerFlowResults.size shouldBe 15 } } + + "bring the correct amount of converted models with participant conversion enabled" in { + val actual = GridConverter.convert( + "1-LV-rural1--0-no_sw", + input, + removeSwitches = false, + ParticipantToInput(pvInput = true) + ) + inside(actual) { + case ( + gridContainer, + timeSeries, + timeSeriesMapping, + powerFlowResults + ) => + /* Evaluate the correctness of the container by counting the occurrence of models (the correct conversion is + * tested in separate unit tests */ + gridContainer.getGridName shouldBe "1-LV-rural1--0-no_sw" + countClassOccurrences( + gridContainer.getRawGrid.allEntitiesAsList() + ) shouldBe Map( + classOf[NodeInput] -> 15, + classOf[LineInput] -> 13, + classOf[Transformer2WInput] -> 1 + ) + countClassOccurrences( + gridContainer.getSystemParticipants.allEntitiesAsList() + ) shouldBe Map( + classOf[PvInput] -> 4, + classOf[LoadInput] -> 13 + ) + countClassOccurrences( + gridContainer.getGraphics.allEntitiesAsList() + ) shouldBe Map + .empty[Class[_ <: UniqueEntity], Int] + + /* Evaluate the correctness of the time series by counting the occurrence of models */ + timeSeries.size shouldBe 13 + + /* Evaluate the existence of time series mappings for all participants */ + timeSeriesMapping.size shouldBe 13 + val participantUuids = gridContainer.getSystemParticipants + .allEntitiesAsList() + .asScala + .map(_.getUuid) + .toVector + /* There is no participant uuid in mapping, that is not among participants */ + timeSeriesMapping.exists(entry => + !participantUuids.contains(entry.participant()) + ) shouldBe false + + /* Evaluate the amount of converted power flow results */ + powerFlowResults.size shouldBe 15 + } + } } } diff --git a/src/test/scala/edu/ie3/test/common/ConfigTestData.scala b/src/test/scala/edu/ie3/test/common/ConfigTestData.scala index ae9f0162..56fae1e1 100644 --- a/src/test/scala/edu/ie3/test/common/ConfigTestData.scala +++ b/src/test/scala/edu/ie3/test/common/ConfigTestData.scala @@ -32,7 +32,10 @@ trait ConfigTestData { List("1-LV-urban6--0-sw", "blabla", "1-EHVHV-mixed-2-0-sw") ) - val validConversionConfig: Conversion = Conversion(removeSwitches = false) + val validConversionConfig: Conversion = Conversion( + removeSwitches = false, + convertPv = false + ) val validConfig = new SimbenchConfig(validConversionConfig, validIo) } From 17b122312c53897d60452964c6a30ac0f01b1b65 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 29 Jul 2024 10:37:01 +0200 Subject: [PATCH 5/6] Adding some test. --- .../convert/profiles/PvProfileConverter.scala | 6 +- .../profiles/ResProfileConverter.scala | 30 ++++- .../simbench/convert/ResConverterSpec.scala | 64 +++++++++- .../profiles/PvProfileConverterSpec.scala | 83 ++++++++++++ .../profiles/ResProfileConverterSpec.scala | 120 ++++++++++++++++++ .../ie3/test/common/ConverterTestData.scala | 65 +++++++++- 6 files changed, 348 insertions(+), 20 deletions(-) create mode 100644 src/test/scala/edu/ie3/simbench/convert/profiles/PvProfileConverterSpec.scala create mode 100644 src/test/scala/edu/ie3/simbench/convert/profiles/ResProfileConverterSpec.scala diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala index 851a9b73..c84c6431 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/PvProfileConverter.scala @@ -18,10 +18,8 @@ import org.locationtech.jts.geom.Coordinate import tech.units.indriya.ComparableQuantity import tech.units.indriya.quantity.Quantities -import java.time.ZonedDateTime import javax.measure.quantity.{Angle, Dimensionless, Power} import scala.jdk.CollectionConverters._ -import scala.jdk.OptionConverters._ object PvProfileConverter { @@ -95,7 +93,7 @@ object PvProfileConverter { * @param azimuth * of the [[PvInput]] * @return - * the azimuth and the elevation angle in radians + * the elevation angle in radians */ def calculateElevationAngle( maxFeedIn: Option[TimeBasedValue[PValue]], @@ -105,7 +103,7 @@ object PvProfileConverter { ): ComparableQuantity[Angle] = { val position: Coordinate = ResProfileConverter.getCoordinate(profile) - // TODO: Adjust angle with power value and rated power + // TODO: Adjust angle with power value, rated power and azimuth // angle for horizontal irradiance val angleInDegrees = position.getY - 15 Quantities.getQuantity( diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala index 0373d273..b0ce662c 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala @@ -17,17 +17,33 @@ import scala.jdk.OptionConverters.RichOptional object ResProfileConverter { // default coordinates for two locations - private val hanoverCoordinate: Coordinate = + val hanoverCoordinate: Coordinate = GeoUtils.buildCoordinate(52.366667, 9.733333) - private val luebeckCoordinate: Coordinate = + val luebeckCoordinate: Coordinate = GeoUtils.buildCoordinate(53.866667, 10.683333) + /** Determines the maximum feed in of the time series. + * @param timeSeries + * given time series + * @return + * time based value with the maximum feed in + */ def findMaxFeedIn( timeSeries: IndividualTimeSeries[PValue] ): Option[TimeBasedValue[PValue]] = - timeSeries.getEntries.asScala.minByOption { value => - value.getValue.getP.toScala.getOrElse(0.asKiloWatt).getValue.doubleValue() - } + timeSeries.getEntries.asScala + .filter { value => + value.getValue.getP.toScala + .getOrElse(1.asKiloWatt) + .getValue + .doubleValue() < 0 + } + .minByOption { value => + value.getValue.getP.toScala + .getOrElse(0.asKiloWatt) + .getValue + .doubleValue() + } /** Determines the [[Coordinate]] of a * [[edu.ie3.simbench.model.datamodel.RES]] based on its [[ResProfileType]] @@ -48,7 +64,7 @@ object ResProfileConverter { case ResProfileType.PV8 => hanoverCoordinate // hanover (second place) case other => throw ConversionException( - s"There are no coordinates for the profile type $other." + s"There is no coordinate for the profile type $other." ) } @@ -68,7 +84,7 @@ object ResProfileConverter { case ResProfileType.PV7 | ResProfileType.PV8 => CompassDirection.West case other => throw ConversionException( - s"There are no coordinates for the profile type $other." + s"There is no compass direction for the profile type $other." ) } diff --git a/src/test/scala/edu/ie3/simbench/convert/ResConverterSpec.scala b/src/test/scala/edu/ie3/simbench/convert/ResConverterSpec.scala index a6b69349..97e5c5ae 100644 --- a/src/test/scala/edu/ie3/simbench/convert/ResConverterSpec.scala +++ b/src/test/scala/edu/ie3/simbench/convert/ResConverterSpec.scala @@ -1,11 +1,14 @@ package edu.ie3.simbench.convert -import edu.ie3.datamodel.models.StandardUnits +import edu.ie3.datamodel.models.{OperationTime, StandardUnits} +import edu.ie3.datamodel.models.input.OperatorInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import java.util.Objects +import java.util.{Locale, Objects} import edu.ie3.simbench.model.datamodel.profiles.{ResProfile, ResProfileType} import edu.ie3.test.common.{ConverterTestData, TestTimeUtils, UnitSpec} import edu.ie3.test.matchers.QuantityMatchers +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import tech.units.indriya.quantity.Quantities import scala.jdk.OptionConverters.RichOptional @@ -86,5 +89,62 @@ class ResConverterSpec } } } + + "convert pv plants correctly" in { + + val pvPlant = pvRes + val pvNode = getNodePair("LV4.101 Bus 19")._2 + + val pvProfile: ResProfile = ResProfile( + "PV profile", + ResProfileType.PV3, + Map( + TestTimeUtils.simbench.toZonedDateTime( + "01.01.1990 00:00" + ) -> BigDecimal( + "0.75" + ), + TestTimeUtils.simbench.toZonedDateTime( + "01.01.1990 00:15" + ) -> BigDecimal( + "0.55" + ), + TestTimeUtils.simbench.toZonedDateTime( + "01.01.1990 00:30" + ) -> BigDecimal( + "0.35" + ), + TestTimeUtils.simbench.toZonedDateTime( + "01.01.1990 00:45" + ) -> BigDecimal( + "0.15" + ) + ) + ) + + val result = ResConverter.convertPv( + pvPlant, + pvNode, + pvProfile + ) + + result.getId shouldBe "LV4.101 SGen 1_pv" + result.getOperator shouldBe OperatorInput.NO_OPERATOR_ASSIGNED + result.getOperationTime shouldBe OperationTime.notLimited() + result.getNode shouldBe pvNode + result.getqCharacteristics() shouldBe new CosPhiFixed( + "cosPhiFixed:{(0.0,1.0)}" + ) + result.getControllingEm.toScala shouldBe None + result.getAlbedo shouldBe 0.20000000298023224 + result.getAzimuth shouldBe 0.asDegreeGeom + result.getEtaConv shouldBe 97.asPercent + result.getElevationAngle shouldBe 0.652171369646312.asDegreeGeom + result.getkG() shouldBe 0.8999999761581421 + result.getkT() shouldBe 1.0 + result.isMarketReaction shouldBe false + result.getsRated() shouldBe 6.48.asKiloVoltAmpere + result.getCosPhiRated shouldBe 1.0 + } } } diff --git a/src/test/scala/edu/ie3/simbench/convert/profiles/PvProfileConverterSpec.scala b/src/test/scala/edu/ie3/simbench/convert/profiles/PvProfileConverterSpec.scala new file mode 100644 index 00000000..c49af8f0 --- /dev/null +++ b/src/test/scala/edu/ie3/simbench/convert/profiles/PvProfileConverterSpec.scala @@ -0,0 +1,83 @@ +package edu.ie3.simbench.convert.profiles + +import edu.ie3.datamodel.models.timeseries.individual.{ + IndividualTimeSeries, + TimeBasedValue +} +import edu.ie3.datamodel.models.value.PValue +import edu.ie3.simbench.model.datamodel.profiles.ResProfileType +import edu.ie3.test.common.UnitSpec +import edu.ie3.util.quantities.QuantityUtils._ +import org.scalatest.prop.TableDrivenPropertyChecks + +import java.time.ZonedDateTime +import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters.RichOptional + +class PvProfileConverterSpec extends UnitSpec with TableDrivenPropertyChecks { + + "The pv profile converter" should { + + "calculate the power before the converter correctly" in { + val time = ZonedDateTime.now() + + val timeSeries = new IndividualTimeSeries( + Set( + new TimeBasedValue(time, new PValue((-1).asKiloWatt)), + new TimeBasedValue(time.plusHours(1), new PValue((-3).asKiloWatt)), + new TimeBasedValue(time.plusHours(2), new PValue((-2).asKiloWatt)) + ).asJava + ) + + val efficiency = 97.asPercent + val kG: Double = 0.8999999761581421 + val kT: Double = 1.0 + + val result = PvProfileConverter.calculatePowerBeforeConverter( + timeSeries, + efficiency, + kG, + kT + ) + + val entries = result.getEntries.asScala.toList.map(value => + value.getValue.getP.toScala + .getOrElse(fail("This test should not fail!")) + ) + + entries(0).getValue.doubleValue() shouldBe -1.1454754026242313 + entries(1).getValue.doubleValue() shouldBe -3.436426207872694 + entries(2).getValue.doubleValue() shouldBe -2.2909508052484626 + } + + "return the correct azimuth for a given pv profile type" in { + val cases = Table( + ("profileType", "expectedAzimuth"), + (ResProfileType.PV1, (-90).asDegreeGeom), + (ResProfileType.PV2, (-90).asDegreeGeom), + (ResProfileType.PV3, 0.asDegreeGeom), + (ResProfileType.PV4, 0.asDegreeGeom), + (ResProfileType.PV5, 0.asDegreeGeom), + (ResProfileType.PV6, 0.asDegreeGeom), + (ResProfileType.PV7, 90.asDegreeGeom), + (ResProfileType.PV8, 90.asDegreeGeom) + ) + + forAll(cases) { (profileType, expectedAzimuth) => + PvProfileConverter.getAzimuth(profileType) shouldBe expectedAzimuth + } + } + + "calculate the elevation angle correctly" in { + val result = PvProfileConverter.calculateElevationAngle( + None, + 5.asKiloVoltAmpere, + ResProfileType.PV3, + 0.asDegreeGeom + ) + + result shouldBe 0.652171369646312.asDegreeGeom + } + + } +} diff --git a/src/test/scala/edu/ie3/simbench/convert/profiles/ResProfileConverterSpec.scala b/src/test/scala/edu/ie3/simbench/convert/profiles/ResProfileConverterSpec.scala new file mode 100644 index 00000000..1658f32e --- /dev/null +++ b/src/test/scala/edu/ie3/simbench/convert/profiles/ResProfileConverterSpec.scala @@ -0,0 +1,120 @@ +package edu.ie3.simbench.convert.profiles + +import edu.ie3.datamodel.models.timeseries.individual.{ + IndividualTimeSeries, + TimeBasedValue +} +import edu.ie3.datamodel.models.value.PValue +import edu.ie3.simbench.convert.profiles.ResProfileConverter.{ + CompassDirection, + findMaxFeedIn, + hanoverCoordinate, + luebeckCoordinate +} +import edu.ie3.simbench.model.datamodel.profiles.ResProfileType +import edu.ie3.test.common.UnitSpec +import edu.ie3.util.quantities.QuantityUtils._ +import org.scalatest.prop.TableDrivenPropertyChecks + +import java.time.ZonedDateTime +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +class ResProfileConverterSpec extends UnitSpec with TableDrivenPropertyChecks { + + "The res profile converter" should { + "find the maximum feed in of a time series" in { + val time = ZonedDateTime.now() + + val value0 = new TimeBasedValue(time, new PValue(1.asKiloWatt)) + val value1 = + new TimeBasedValue(time.plusHours(1), new PValue((-1).asKiloWatt)) + val value2 = new TimeBasedValue(time.plusHours(2), new PValue(null)) + val value3 = + new TimeBasedValue(time.plusHours(3), new PValue((-3).asKiloWatt)) + val value4 = + new TimeBasedValue(time.plusHours(4), new PValue((-2).asKiloWatt)) + + val cases = Table( + ("timeSeries", "expectedValue"), + (new IndividualTimeSeries(Set(value0).asJava), None), + (new IndividualTimeSeries(Set(value0, value2).asJava), None), + ( + new IndividualTimeSeries(Set(value0, value1, value2).asJava), + Some(value1) + ), + ( + new IndividualTimeSeries(Set(value1, value2, value4).asJava), + Some(value4) + ), + ( + new IndividualTimeSeries(Set(value1, value2, value3, value4).asJava), + Some(value3) + ) + ) + + forAll(cases) { (timeSeries, expectedValue) => + val maxFeedIn = findMaxFeedIn(timeSeries) + maxFeedIn shouldBe expectedValue + } + + } + + "return the correct coordinate" in { + val cases = Table( + ("profileType", "expectedCoordinate"), + (ResProfileType.PV1, hanoverCoordinate), + (ResProfileType.PV2, luebeckCoordinate), + (ResProfileType.PV3, hanoverCoordinate), + (ResProfileType.PV4, hanoverCoordinate), + (ResProfileType.PV5, luebeckCoordinate), + (ResProfileType.PV6, luebeckCoordinate), + (ResProfileType.PV7, hanoverCoordinate), + (ResProfileType.PV8, hanoverCoordinate) + ) + + forAll(cases) { (profileType, expectedCoordinate) => + ResProfileConverter.getCoordinate( + profileType + ) shouldBe expectedCoordinate + } + } + + "throw an exception if unknown profile type is given for coordinate" in { + Try(ResProfileConverter.getCoordinate(ResProfileType.WP1)) match { + case Success(_) => fail("This test should not pass!") + case Failure(exception) => + exception.getMessage shouldBe s"There is no coordinate for the profile type WP1." + } + } + + "return the correct compass direction" in { + val cases = Table( + ("profileType", "expectedDirection"), + (ResProfileType.PV1, CompassDirection.East), + (ResProfileType.PV2, CompassDirection.East), + (ResProfileType.PV3, CompassDirection.South), + (ResProfileType.PV4, CompassDirection.South), + (ResProfileType.PV5, CompassDirection.South), + (ResProfileType.PV6, CompassDirection.South), + (ResProfileType.PV7, CompassDirection.West), + (ResProfileType.PV8, CompassDirection.West) + ) + + forAll(cases) { (profileType, expectedDirection) => + ResProfileConverter.getCompassDirection( + profileType + ) shouldBe expectedDirection + } + } + + "throw an exception if unknown profile type is given for compass direction" in { + Try(ResProfileConverter.getCompassDirection(ResProfileType.WP1)) match { + case Success(_) => fail("This test should not pass!") + case Failure(exception) => + exception.getMessage shouldBe s"There is no compass direction for the profile type WP1." + } + } + + } +} diff --git a/src/test/scala/edu/ie3/test/common/ConverterTestData.scala b/src/test/scala/edu/ie3/test/common/ConverterTestData.scala index c425bf58..7b0989d4 100644 --- a/src/test/scala/edu/ie3/test/common/ConverterTestData.scala +++ b/src/test/scala/edu/ie3/test/common/ConverterTestData.scala @@ -113,14 +113,26 @@ trait ConverterTestData { ), geometryFactory.createPoint(new JTSCoordinate(11.4097, 53.6413)) ), - "coordinate_2" -> (Coordinate( - "coordinate_2", - BigDecimal("11.411"), - BigDecimal("53.6407"), - "LV1.101", - 7 + "coordinate_2" -> ( + Coordinate( + "coordinate_2", + BigDecimal("11.411"), + BigDecimal("53.6407"), + "LV1.101", + 7 + ), + geometryFactory.createPoint(new JTSCoordinate(11.411, 53.6407)) ), - geometryFactory.createPoint(new JTSCoordinate(11.411, 53.6407))) + "coord_32" -> ( + Coordinate( + "coord_32", + BigDecimal("11.3942"), + BigDecimal("53.6381"), + "LV4.101", + 7 + ), + geometryFactory.createPoint(new JTSCoordinate(11.3942, 53.6381)) + ) ) def getCoordinatePair(key: String): (Coordinate, Point) = @@ -340,6 +352,32 @@ trait ConverterTestData { LV, 2 ) + ), + "LV4.101 Bus 19" -> ConversionPair( + Node( + "LV4.101 Bus 19", + NodeType.BusBar, + None, + None, + BigDecimal("0.4"), + BigDecimal("0.9"), + BigDecimal("1.1"), + None, + Some(getCoordinatePair("coord_32")._1), + "LV4.101", + 7 + ), + new NodeInput( + UUID.randomUUID(), + "LV4.101 Bus 19", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1d, PU), + false, + getCoordinatePair("coord_32")._2, + LV, + 2 + ) ) ) @@ -911,6 +949,19 @@ trait ConverterTestData { ) .getPair + val pvRes = RES( + "LV4.101 SGen 1", + getNodePair("LV4.101 Bus 19")._1, + ResType.PV, + ResProfileType.PV5, + CalculationType.PQ, + BigDecimal("0.00648"), + BigDecimal("0"), + BigDecimal("0.00648"), + "LV4.101", + 7 + ) + val res = Map( "MV1.101 SGen 2" -> ConversionPair( RES( From 6ae144cd06466f19c852cba0fa3afd458bfe2154 Mon Sep 17 00:00:00 2001 From: staudtMarius Date: Mon, 29 Jul 2024 10:44:02 +0200 Subject: [PATCH 6/6] Fixing some `codacy` issues. --- .../ie3/simbench/convert/profiles/ResProfileConverter.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala index b0ce662c..b3257480 100644 --- a/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala +++ b/src/main/scala/edu/ie3/simbench/convert/profiles/ResProfileConverter.scala @@ -55,11 +55,11 @@ object ResProfileConverter { def getCoordinate(profileType: ResProfileType): Coordinate = profileType match { case ResProfileType.PV1 => hanoverCoordinate // hanover (first place) - case ResProfileType.PV2 => luebeckCoordinate // lübeck (first place) + case ResProfileType.PV2 => luebeckCoordinate // luebeck (first place) case ResProfileType.PV3 => hanoverCoordinate // hanover (first place) case ResProfileType.PV4 => hanoverCoordinate // hanover (second place) - case ResProfileType.PV5 => luebeckCoordinate // lübeck (second place) - case ResProfileType.PV6 => luebeckCoordinate // lübeck (third place) + case ResProfileType.PV5 => luebeckCoordinate // luebeck (second place) + case ResProfileType.PV6 => luebeckCoordinate // luebeck (third place) case ResProfileType.PV7 => hanoverCoordinate // hanover (second place) case ResProfileType.PV8 => hanoverCoordinate // hanover (second place) case other =>