diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 00000000..834f2d20 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1 @@ +version = 2.7.5 \ No newline at end of file diff --git a/src/main/scala/edu/ie3/powerFactory2psdm/converter/ConversionHelper.scala b/src/main/scala/edu/ie3/powerFactory2psdm/converter/ConversionHelper.scala index 83104dd0..652887e2 100644 --- a/src/main/scala/edu/ie3/powerFactory2psdm/converter/ConversionHelper.scala +++ b/src/main/scala/edu/ie3/powerFactory2psdm/converter/ConversionHelper.scala @@ -6,6 +6,7 @@ package edu.ie3.powerFactory2psdm.converter +import edu.ie3.datamodel.models.StandardUnits import edu.ie3.datamodel.models.input.system.characteristic.ReactivePowerCharacteristic import edu.ie3.powerFactory2psdm.config.ConversionConfigUtils.{ DependentQCharacteristic, @@ -14,7 +15,10 @@ import edu.ie3.powerFactory2psdm.config.ConversionConfigUtils.{ } import edu.ie3.powerFactory2psdm.exception.pf.ElementConfigurationException import edu.ie3.powerFactory2psdm.model.entity.StaticGenerator +import edu.ie3.util.quantities.PowerSystemUnits +import tech.units.indriya.ComparableQuantity +import javax.measure.quantity.Power import scala.util.{Failure, Success, Try} /** Utility object to hold utility functions for model converison @@ -43,6 +47,24 @@ object ConversionHelper { ReactivePowerCharacteristic.parse(characteristic) } + /** Specified cosinus phi with respect to the grid regulations (VDE-AR-N 4105) + * + * @param sRated + * rated power of the infeed system + * @return + * cosinus phi for the infeed system + */ + def lvGenerationCosPhi(sRated: ComparableQuantity[Power]): Double = { + val power = + sRated.to(PowerSystemUnits.KILOVOLTAMPERE).getValue.doubleValue() + if (power <= 3.86) + 1d + else if ((3.86 < power) && (power <= 13.8)) + 0.95 + else + 0.9 + } + /** Determines the cos phi rated of a static generator * * @param input diff --git a/src/main/scala/edu/ie3/powerFactory2psdm/converter/LoadConverter.scala b/src/main/scala/edu/ie3/powerFactory2psdm/converter/LoadConverter.scala index 20dd2f22..17bb91d5 100644 --- a/src/main/scala/edu/ie3/powerFactory2psdm/converter/LoadConverter.scala +++ b/src/main/scala/edu/ie3/powerFactory2psdm/converter/LoadConverter.scala @@ -8,17 +8,12 @@ package edu.ie3.powerFactory2psdm.converter import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} import edu.ie3.datamodel.models.input.system.LoadInput import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.profile.{ - BdewStandardLoadProfile, - LoadProfile, - NbwTemperatureDependantLoadProfile -} -import edu.ie3.powerFactory2psdm.exception.pf.ElementConfigurationException +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.profile.{BdewStandardLoadProfile, LoadProfile, NbwTemperatureDependantLoadProfile} import edu.ie3.powerFactory2psdm.converter.NodeConverter.getNode -import edu.ie3.powerFactory2psdm.exception.pf.ConversionException +import edu.ie3.powerFactory2psdm.exception.pf.{ConversionException, ElementConfigurationException} import edu.ie3.powerFactory2psdm.model.entity.Load import edu.ie3.powerFactory2psdm.util.QuantityUtils.RichQuantityDouble diff --git a/src/main/scala/edu/ie3/powerFactory2psdm/generator/PvInputGenerator.scala b/src/main/scala/edu/ie3/powerFactory2psdm/generator/PvInputGenerator.scala index 519bcbae..00b5772b 100644 --- a/src/main/scala/edu/ie3/powerFactory2psdm/generator/PvInputGenerator.scala +++ b/src/main/scala/edu/ie3/powerFactory2psdm/generator/PvInputGenerator.scala @@ -8,22 +8,18 @@ package edu.ie3.powerFactory2psdm.generator import edu.ie3.datamodel.models.input.NodeInput import edu.ie3.datamodel.models.input.system.PvInput -import edu.ie3.datamodel.models.input.system.characteristic.ReactivePowerCharacteristic import edu.ie3.powerFactory2psdm.config.model.PvConversionConfig.PvModelGeneration import edu.ie3.powerFactory2psdm.converter.ConversionHelper.{ convertQCharacteristic, determineCosPhiRated } import edu.ie3.powerFactory2psdm.model.entity.StaticGenerator -import edu.ie3.powerFactory2psdm.util.QuantityUtils.RichQuantityDouble import edu.ie3.powerFactory2psdm.util.RandomSampler.sample -import edu.ie3.util.quantities.PowerSystemUnits.{DEGREE_GEOM, MEGAVOLTAMPERE} +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import tech.units.indriya.ComparableQuantity -import tech.units.indriya.quantity.Quantities -import tech.units.indriya.unit.Units.PERCENT import java.util.UUID -import javax.measure.quantity.{Angle, Dimensionless, Power} +import javax.measure.quantity.Power object PvInputGenerator { @@ -44,6 +40,37 @@ object PvInputGenerator { input: StaticGenerator, node: NodeInput, params: PvModelGeneration + ): PvInput = { + generate( + node, + input.id, + input.sRated.asKiloVoltAmpere, + determineCosPhiRated(input), + params + ) + } + + /** Generates a [[PvInput]] + * + * @param id + * name of the pv plant + * @param power + * rated power of the pv plant + * @param cosPhi + * cosPhi of the pv plant + * @param node + * the node the input is connected to + * @param params + * parameters for generating missing parameters + * @return + * a [[PvInput]] + */ + def generate( + node: NodeInput, + id: String, + power: ComparableQuantity[Power], + cosPhi: Double, + params: PvModelGeneration ): PvInput = { val albedo = sample(params.albedo) val azimuth = sample(params.azimuth).asDegreeGeom @@ -51,14 +78,14 @@ object PvInputGenerator { val height = sample(params.elevationAngle).asDegreeGeom val kG = sample(params.kG) val kT = sample(params.kT) - val sRated = input.sRated.asKiloVoltAmpere - val cosPhiRated = determineCosPhiRated(input) + val sRated = power + val cosPhiRated = cosPhi val qCharacteristics = convertQCharacteristic(params.qCharacteristic, cosPhiRated) new PvInput( UUID.randomUUID(), - input.id, + id, node, qCharacteristics, albedo, @@ -72,5 +99,4 @@ object PvInputGenerator { cosPhiRated ) } - } diff --git a/src/main/scala/edu/ie3/powerFactory2psdm/util/ParticipantAdjustments.scala b/src/main/scala/edu/ie3/powerFactory2psdm/util/ParticipantAdjustments.scala new file mode 100644 index 00000000..583cecd6 --- /dev/null +++ b/src/main/scala/edu/ie3/powerFactory2psdm/util/ParticipantAdjustments.scala @@ -0,0 +1,130 @@ +/* + * © 2023. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.powerFactory2psdm.util + +import com.typesafe.scalalogging.LazyLogging +import edu.ie3.datamodel.models.input.container.{ + JointGridContainer, + SystemParticipants +} +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile +import edu.ie3.powerFactory2psdm.config.model.PvConversionConfig.PvModelGeneration +import edu.ie3.powerFactory2psdm.converter.ConversionHelper.lvGenerationCosPhi +import edu.ie3.powerFactory2psdm.generator.PvInputGenerator +import edu.ie3.powerFactory2psdm.util.ParticipantInformation.Participants +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble + +import java.util.{Locale, UUID} +import scala.jdk.CollectionConverters.{IterableHasAsScala, SetHasAsJava} + +object ParticipantAdjustments extends LazyLogging { + + def adjust( + jgc: JointGridContainer, + nodalParticipantInfo: Map[ + String, + Map[Participants.Value, ParticipantInformation] + ], + pvModelGeneration: PvModelGeneration + ): JointGridContainer = { + val nodes = jgc.getRawGrid.getNodes.asScala + val nodesMap = nodes.map(node => node.getId -> node).toMap + val oldSystemParticipants = jgc.getSystemParticipants + val updatedParticipants = nodalParticipantInfo.foldLeft( + oldSystemParticipants + )((systemParticipants, mapEntry) => { + val (id, participantInfos) = mapEntry + val node = nodesMap.getOrElse( + id, + throw new NoSuchElementException(s"Node with id $id not found") + ) + participantInfos.foldLeft(systemParticipants)((sp, info) => { + val (participant, participantInfo) = info + participant match { + case Participants.LOAD => + val oldLoads = sp.getLoads.asScala.filter(_.getNode == node) + val cosPhi = oldLoads.map(_.getCosPhiRated).sum / oldLoads.size + val oldSRated = + oldLoads.map(_.getsRated()).reduce((a, b) => a.add(b)) + val newLoads = (0 until participantInfo.count).map(count => { + val energy = 3500d.asKiloWattHour + val varCharacteristicString = + "cosPhiFixed:{(0.0,%#.2f)}".formatLocal(Locale.ENGLISH, cosPhi) + val sRated = oldSRated.divide(participantInfo.count) + new LoadInput( + UUID.randomUUID(), + id + f"-Load-$count", + node, + new CosPhiFixed(varCharacteristicString), + BdewStandardLoadProfile.H0, + false, + energy, + sRated, + cosPhi + ) + }) + val updatedLoads = + sp.getLoads.asScala.filter(_.getNode != node) ++ newLoads + new SystemParticipants( + sp.getBmPlants, + sp.getChpPlants, + sp.getEvCS, + sp.getEvs, + sp.getFixedFeedIns, + sp.getHeatPumps, + updatedLoads.toSet.asJava, + sp.getPvPlants, + sp.getStorages, + sp.getWecPlants, + sp.getEmSystems + ) + + case Participants.PV => + val newPvs = (0 until participantInfo.count).map(count => { + val power = participantInfo.power.divide(participantInfo.count) + PvInputGenerator.generate( + node, + id + s"-PV-$count", + power, + lvGenerationCosPhi(power), + pvModelGeneration + ) + }) + val updatedPvs = sp.getPvPlants.asScala + .filter(_.getNode != node) ++ newPvs + new SystemParticipants( + sp.getBmPlants, + sp.getChpPlants, + sp.getEvCS, + sp.getEvs, + sp.getFixedFeedIns, + sp.getHeatPumps, + sp.getLoads, + updatedPvs.toSet.asJava, + sp.getStorages, + sp.getWecPlants, + sp.getEmSystems + ) + case participant: Participants.Value => + logger.warn( + s"Handling $participant adjustment information not supported yet. Skipping ..." + ) + sp + } + }) + }) + new JointGridContainer( + jgc.getGridName, + jgc.getRawGrid, + updatedParticipants, + jgc.getGraphics, + jgc.getSubGridTopologyGraph + ) + } +} diff --git a/src/main/scala/edu/ie3/powerFactory2psdm/util/ParticipantInformation.scala b/src/main/scala/edu/ie3/powerFactory2psdm/util/ParticipantInformation.scala new file mode 100644 index 00000000..b3b061bb --- /dev/null +++ b/src/main/scala/edu/ie3/powerFactory2psdm/util/ParticipantInformation.scala @@ -0,0 +1,116 @@ +/* + * © 2023. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.powerFactory2psdm.util + +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import io.circe._ +import io.circe.parser._ +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Power +import scala.io.Source +import scala.util.{Failure, Success, Using} + +final case class ParticipantInformation( + power: ComparableQuantity[Power], + count: Int +) + +object ParticipantInformation { + + object Participants extends Enumeration { + val LOAD: Participants.Value = Value("load") + val PV: Participants.Value = Value("pv") + val EVCS: Participants.Value = Value("evcs") + val STORAGE: Participants.Value = Value("storage") + val WEC: Participants.Value = Value("wec") + val HP: Participants.Value = Value("hp") + val NSH: Participants.Value = Value("nsh") + val DIRECT_HEATING: Participants.Value = Value("direct_heating") + val WATER_HEATER: Participants.Value = Value("water_heater") + val CHP: Participants.Value = Value("chp") + val BM: Participants.Value = Value("bm") + val MISC: Participants.Value = Value("misc") + } + + implicit val participantInformationDecoder: Decoder[ParticipantInformation] = + cursor => + for { + power <- cursor.get[Double]("power") + count <- cursor.get[Int]("count") + } yield ParticipantInformation(power.asKiloWatt, count) + + implicit val participantInformationMapDecoder + : Decoder[Map[Participants.Value, ParticipantInformation]] = cursor => { + val map = Participants.values.toSeq + .flatMap(participant => { + cursor.get[ParticipantInformation](participant.toString) match { + case Left(_) => None + case Right(info) => Some(participant -> info) + } + }) + .toMap + Right(map) + } + + implicit val nodalParticipantInformationDecoder: Decoder[ + Map[String, Map[Participants.Value, ParticipantInformation]] + ] = cursor => { + cursor.keys match { + case Some(keys) => + val (lefts, rights) = keys.foldLeft( + Seq.empty[(String, DecodingFailure)], + Seq.empty[(String, Map[Participants.Value, ParticipantInformation])] + )((lr, key) => { + val (left, right) = lr + val res = + cursor.get[Map[Participants.Value, ParticipantInformation]](key) + res match { + case Left(decodingFailure) => + (left :+ (key, decodingFailure), right) + case Right(participantInfoMap) => + (left, right :+ (key, participantInfoMap)) + } + }) + if (lefts.isEmpty) { + Right(rights.toMap) + } else { + val history = lefts.flatMap { case (_, failure) => failure.history } + val keys = lefts.map(_._1).mkString("-") + Left( + DecodingFailure( + s"Could not retrieve information for keys: $keys", + history.toList + ) + ) + } + case None => Right(Map.empty) + } + } + + def fromJson( + filePath: String + ): Map[String, Map[Participants.Value, ParticipantInformation]] = { + Using(Source.fromFile(filePath)) { src => + src.getLines().reduce((a, b) => a + " " + b) + } match { + case Failure(exception) => throw exception + case Success(jsonString) => + parse(jsonString) match { + case Left(failure) => throw failure + case Right(json) => + json.as[ + Map[String, Map[Participants.Value, ParticipantInformation]] + ] match { + case Left(decodingFailure: DecodingFailure) => + throw decodingFailure + case Right(result) => result + } + } + } + } +} diff --git a/src/test/resources/nodal_participants_info.json b/src/test/resources/nodal_participants_info.json new file mode 100644 index 00000000..4792a96d --- /dev/null +++ b/src/test/resources/nodal_participants_info.json @@ -0,0 +1,50 @@ +{ + "Node-A-ID": { + "nsh": { + "power": 39.05, + "count": 4.0 + }, + "direct_heating": { + "power": 16, + "count": 5.0 + }, + "misc": { + "power": 5, + "count": 3.0 + }, + "pv": { + "power": 241.94, + "count": 11.0 + } + }, + "Node-B-ID": { + "water_heater": { + "power": 48, + "count": 2.0 + }, + "nsh": { + "power": 43, + "count": 3.0 + }, + "evcs": { + "power": 11, + "count": 1.0 + }, + "direct_heating": { + "power": 7, + "count": 1.0 + }, + "storage": { + "power": 2.5, + "count": 1.0 + }, + "misc": { + "power": 2.5, + "count": 3.0 + }, + "pv": { + "power": 202.96, + "count": 14.0 + } + } +} \ No newline at end of file diff --git a/src/test/scala/edu/ie3/powerFactory2psdm/util/ParticipantInformationSpec.scala b/src/test/scala/edu/ie3/powerFactory2psdm/util/ParticipantInformationSpec.scala new file mode 100644 index 00000000..d64980cb --- /dev/null +++ b/src/test/scala/edu/ie3/powerFactory2psdm/util/ParticipantInformationSpec.scala @@ -0,0 +1,28 @@ +/* + * © 2023. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.powerFactory2psdm.util + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike + +import java.io.File + +class ParticipantInformationSpec extends Matchers with AnyWordSpecLike { + + "ParticipantAdjustments" should { + + "be parsed from Json" in { + + val map = ParticipantInformation.fromJson( + s"${new File(".").getCanonicalPath}/src/test/resources/nodal_participants_info.json" + ) + + map.keySet should contain allOf ("Node-A-ID", "Node-B-ID") + } + } + +}