diff --git a/build.gradle b/build.gradle index baf70611..921996ea 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ apply from: scriptsLocation + 'tscfg.gradle' repositories { mavenCentral() //searches in bintray's repository 'jCenter', which contains Maven Central maven { url 'https://www.jitpack.io' } // allows github repos as dependencies + maven { url 'https://oss.sonatype.org/service/local/repositories/snapshots/content' } } dependencies { @@ -76,13 +77,16 @@ dependencies { // todo CLEANUP following old dependencies // ie³ power system utils - implementation('com.github.ie3-institute:PowerSystemUtils:1.5.3') { + implementation('com.github.ie3-institute:PowerSystemUtils:1.6-SNAPSHOT') { exclude group: 'org.slf4j' exclude group: 'org.apache.logging.log4j' exclude group: 'com.github.ie3-institute' exclude group: 'com.github.johanneshiry', module: 'OSMonaut' } + // indriya + implementation 'tech.units:indriya:2.1.2' + // ie³ power system data model implementation('com.github.ie3-institute:PowerSystemDataModel:2.0.1') { exclude group: 'org.slf4j' @@ -90,6 +94,8 @@ dependencies { exclude group: 'com.github.ie3-institute' } + implementation 'org.scalanlp:breeze_3:2.0' + // Graphs implementation 'org.jgrapht:jgrapht-core:1.5.1' diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 21e5f74f..72a1050a 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -23,6 +23,8 @@ generation: { lv: { amountOfGridGenerators: Int | 10 # Amount of actors actually building the grids amountOfRegionCoordinators: Int | 5 # Amount of actors coordinating generation per municipality + loadDensity: Double # Estimation for energy usage of a building in W / m² + restrictSubgridsToLanduseAreas: Boolean | false # If sub grids should be bound to landuse areas distinctHouseConnections: Boolean | false # If there shall be distinct lines for house connection } } diff --git a/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala b/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala index 5c422184..b3b92de0 100644 --- a/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala +++ b/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala @@ -38,7 +38,8 @@ object ConfigFailFast { case Lv( amountOfGridGenerators, amountOfRegionCoordinators, - distinctHouseConnections + distinctHouseConnections, + loadDensity ) => if (amountOfGridGenerators < 1) throw IllegalConfigException( @@ -48,6 +49,10 @@ object ConfigFailFast { throw IllegalConfigException( s"The amount of lv region coordination actors needs to be at least 1 (provided: $amountOfRegionCoordinators)." ) + if (loadDensity < 0) + throw IllegalConfigException( + s"The load density of a building in an lv grid needs to be positive (provided: $loadDensity)." + ) } private def checkInputConfig(input: OsmoGridConfig.Input): Unit = diff --git a/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala b/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala index 650e6c53..9b61dcc9 100644 --- a/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala +++ b/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala @@ -1,5 +1,5 @@ /* - * © 2021. TU Dortmund University, + * © 2022. TU Dortmund University, * Institute of Energy Systems, Energy Efficiency and Energy Economics, * Research group Distribution grid planning and operation */ @@ -19,7 +19,9 @@ object OsmoGridConfig { final case class Lv( amountOfGridGenerators: scala.Int, amountOfRegionCoordinators: scala.Int, - distinctHouseConnections: scala.Boolean + distinctHouseConnections: scala.Boolean, + loadDensity: scala.Double, + restrictSubgridsToLanduseAreas: scala.Boolean ) object Lv { def apply( @@ -35,12 +37,32 @@ object OsmoGridConfig { amountOfRegionCoordinators = if (c.hasPathOrNull("amountOfRegionCoordinators")) c.getInt("amountOfRegionCoordinators") - else 50, + else 5, distinctHouseConnections = c.hasPathOrNull( "distinctHouseConnections" - ) && c.getBoolean("distinctHouseConnections") + ) && c.getBoolean("distinctHouseConnections"), + loadDensity = $_reqDbl(parentPath, c, "loadDensity", $tsCfgValidator), + restrictSubgridsToLanduseAreas = c.hasPathOrNull( + "restrictSubgridsToLanduseAreas" + ) && c.getBoolean("restrictSubgridsToLanduseAreas") ) } + 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 apply( diff --git a/src/main/scala/edu/ie3/osmogrid/exception/IllegalCalculationException.scala b/src/main/scala/edu/ie3/osmogrid/exception/IllegalCalculationException.scala new file mode 100644 index 00000000..1aaae77e --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/exception/IllegalCalculationException.scala @@ -0,0 +1,12 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.exception + +final case class IllegalCalculationException( + msg: String = "", + cause: Throwable = None.orNull +) extends Exception(msg, cause) diff --git a/src/main/scala/edu/ie3/osmogrid/exception/TestPreparationFailedException.scala b/src/main/scala/edu/ie3/osmogrid/exception/TestPreparationFailedException.scala new file mode 100644 index 00000000..98be93a3 --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/exception/TestPreparationFailedException.scala @@ -0,0 +1,12 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.exception + +class TestPreparationFailedException( + msg: String = "", + cause: Throwable = None.orNull +) extends Exception(msg, cause) diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LoadCalculation.scala b/src/main/scala/edu/ie3/osmogrid/lv/LoadCalculation.scala new file mode 100644 index 00000000..247a8787 --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/lv/LoadCalculation.scala @@ -0,0 +1,110 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.lv + +import de.osmogrid.util.quantities.PowerDensity +import edu.ie3.osmogrid.exception.IllegalCalculationException +import edu.ie3.osmogrid.model.LoadLocation +import edu.ie3.osmogrid.model.WayUtils.RichClosedWay +import edu.ie3.util.osm.OsmEntities.Way +import edu.ie3.util.osm.{OsmEntities, OsmModel} +import org.slf4j.Logger +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Power +import scala.collection.generic.DefaultSerializable +import scala.collection.immutable.{AbstractSeq, StrictOptimizedSeqOps} +import scala.util.{Failure, Success, Try} + +trait LoadCalculation { + + /** Determine the locations and the estimated consumed power of loads + * + * @param osmContainer + * Container with all available osm entities + * @param loadDensity + * Load density per square metre + * @param logger + * Logger to document the results + * @return + * A [[List]] of [[LoadLocation]]s + */ + protected def determineLoadLocations( + osmContainer: OsmModel, + loadDensity: ComparableQuantity[PowerDensity] + )(implicit logger: Logger): Seq[LoadLocation[_]] = { + val results = + loadLocationsFromOsmContainer(osmContainer, loadDensity).groupBy( + _._2.isSuccess + ) + + /* Report on failed entities */ + results.get(false).map(_.map(_._1)) match { + case Some(failedEntityIds) => + logger.warn( + s"Determination of load locations failed for the following ${failedEntityIds.length} entities:\n\t${failedEntityIds + .mkString("\n\t")}" + ) + case None => + logger.debug("No error during determination of load locations.") + } + + /* Extract the valid results */ + results + .get(true) + .map { + _.map { + case (_, Success(loadLocation)) => loadLocation + case (id, Failure(_)) => + throw RuntimeException( + "Filtering of successful load location determination obviously went wrong." + ) + } + } + .getOrElse(List.empty[LoadLocation[_]]) + } + + private def loadLocationsFromOsmContainer( + osmContainer: OsmModel, + loadDensity: ComparableQuantity[PowerDensity] + ) = { + OsmModel + .extractBuildings(osmContainer.ways) + .map(way => way.osmId -> loadFromWay(way, loadDensity)) + } + + /** Determine the load of a building, if it is modeled as a way + * + * @param way + * Way, describing the building + * @param loadDensity + * Assumed load density + * @return + */ + private def loadFromWay( + way: Way, + loadDensity: ComparableQuantity[PowerDensity] + ): Try[LoadLocation[Way]] = way match { + case closedWay @ OsmEntities.ClosedWay( + uuid, + osmId, + lastEdited, + tags, + nodes + ) => + closedWay.area.map { area => + val load = loadDensity.multiply(area).asType(classOf[Power]) + LoadLocation(closedWay.center, load, closedWay) + } + case OsmEntities.OpenWay(_, osmId, _, _, _) => + Failure( + IllegalCalculationException( + s"Cannot determine the area of a building with id '$osmId' from an open way." + ) + ) + } +} diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LoadClustering.scala b/src/main/scala/edu/ie3/osmogrid/lv/LoadClustering.scala new file mode 100644 index 00000000..3d528e02 --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/lv/LoadClustering.scala @@ -0,0 +1,52 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.lv + +import edu.ie3.osmogrid.model.LoadLocation +import edu.ie3.util.geo.GeoUtils +import edu.ie3.util.osm.OsmModel +import net.morbz.osmonaut.osm.LatLon + +import scala.jdk.CollectionConverters._ + +trait LoadClustering { + + def clusterLoads( + loadLocations: Seq[LoadLocation[_]], + osmContainer: OsmModel, + restrictSubgridsToLanduseAreas: Boolean + ): Unit = { + /* Build sub groups if necessary */ + val subGroups = if (restrictSubgridsToLanduseAreas) { + /* There shall be sub groups per land use area */ + OsmModel + .extractLandUses(osmContainer.ways) + .map(landUseArea => + loadLocations.filter(loadLocation => + // TODO: Check if coordinate is inside landuse, whenever the PowerSystemUtils are ready + true + ) + ) + } else { + Seq(loadLocations) + } + + /** TODO + * 1) Go through all sub groups and check, if they are fully connected. If not: Split them up. + * 2) Check for minimum cluster size and ignore those, that don't fulfill this requirement + * For each sub group: + * 3) Calculate the number of needed clusters + * 4) Determine clusters + * 5) Rebuild a graph per cluster + * 6) Possibly assign a unique number to the cluster + * + * Needed data + * - Targeted power of a substation (maybe as a list?) + * - Minimum power of a substation + */ + } +} diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala b/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala index 1889207e..74669396 100644 --- a/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala +++ b/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala @@ -8,14 +8,17 @@ package edu.ie3.osmogrid.lv import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} import akka.actor.typed.scaladsl.{Behaviors, Routers} +import de.osmogrid.util.quantities.OsmoGridUnits import edu.ie3.datamodel.models.input.container.SubGridContainer import edu.ie3.osmogrid.cfg.OsmoGridConfig import edu.ie3.osmogrid.cfg.OsmoGridConfig.Generation.Lv +import edu.ie3.osmogrid.guardian.OsmoGridGuardian import edu.ie3.osmogrid.guardian.OsmoGridGuardian.{ OsmoGridGuardianEvent, RepLvGrids } import edu.ie3.osmogrid.lv.LvGenerator +import tech.units.indriya.quantity.Quantities object LvCoordinator { sealed trait LvCoordinatorEvent @@ -23,6 +26,8 @@ object LvCoordinator { cfg: OsmoGridConfig.Generation.Lv, replyTo: ActorRef[OsmoGridGuardianEvent] ) extends LvCoordinatorEvent + final case class RepLvGrids(grids: Vector[SubGridContainer]) + extends LvCoordinatorEvent def apply(): Behavior[LvCoordinatorEvent] = idle @@ -33,16 +38,18 @@ object LvCoordinator { Lv( amountOfGridGenerators, amountOfRegionCoordinators, - distinctHouseConnections + distinctHouseConnections, + loadDensity, + restrictSubgridsToLanduseAreas ), - replyTo + guardian ) => ctx.log.info("Starting generation of low voltage grids!") /* TODO: 1) Ask for OSM data 2) Ask for asset data 3) Split up osm data at municipality boundaries - 4) start generation */ + 4) start generation (register, how many requests have been sent!) */ /* Spawn a pool of workers to build grids from sub-graphs */ val lvGeneratorPool = @@ -59,17 +66,51 @@ object LvCoordinator { Routers.pool(poolSize = amountOfRegionCoordinators) { // Restart workers on failure Behaviors - .supervise(LvRegionCoordinator(lvGeneratorProxy)) + .supervise( + LvRegionCoordinator( + lvGeneratorProxy, + Quantities.getQuantity( + loadDensity, + OsmoGridUnits.WATT_PER_SQUARE_METRE + ), + restrictSubgridsToLanduseAreas + ) + ) .onFailure(SupervisorStrategy.restart) } val lvRegionCoordinatorProxy = ctx.spawn(lvRegionCoordinatorPool, "LvRegionCoordinatorPool") - replyTo ! RepLvGrids(Vector.empty[SubGridContainer]) - Behaviors.stopped + /* Wait for the incoming data and check, if all replies are received. */ + awaitReplies(0, guardian) case unsupported => ctx.log.error(s"Received unsupported message: $unsupported") Behaviors.stopped } } + + private def awaitReplies( + awaitedReplies: Int, + guardian: ActorRef[OsmoGridGuardianEvent], + collectedGrids: Vector[SubGridContainer] = Vector.empty + ): Behaviors.Receive[LvCoordinatorEvent] = Behaviors.receive { + case (ctx, RepLvGrids(grids)) => + val stillAwaited = awaitedReplies - 1 + ctx.log.debug( + s"Received another ${grids.length} sub grids. ${if (stillAwaited == 0) "All requests are answered." + else s"Still awaiting $stillAwaited replies."}." + ) + val updatedGrids = collectedGrids ++ grids + if (stillAwaited == 0) { + ctx.log.info( + s"Received ${updatedGrids.length} sub grid containers in total. Join and send them to the guardian." + ) + guardian ! OsmoGridGuardian.RepLvGrids(updatedGrids) + Behaviors.stopped + } else + awaitReplies(stillAwaited, guardian, updatedGrids) + case (ctx, unsupported) => + ctx.log.error(s"Received unsupported message: $unsupported") + Behaviors.stopped + } } diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala b/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala index 55da4bed..228e0c0c 100644 --- a/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala +++ b/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala @@ -7,20 +7,61 @@ package edu.ie3.osmogrid.lv import akka.actor.typed.scaladsl.Behaviors - import akka.actor.typed.ActorRef +import org.slf4j.Logger +import de.osmogrid.util.quantities.PowerDensity +import edu.ie3.datamodel.models.input.container.SubGridContainer +import edu.ie3.osmogrid.exception.IllegalCalculationException +import edu.ie3.osmogrid.guardian.OsmoGridGuardian.RepLvGrids +import edu.ie3.osmogrid.lv.LvCoordinator +import edu.ie3.osmogrid.lv.LvCoordinator.LvCoordinatorEvent import edu.ie3.osmogrid.lv.LvGenerator.LvGeneratorEvent +import edu.ie3.osmogrid.model.LoadLocation +import edu.ie3.osmogrid.model.WayUtils.RichClosedWay +import edu.ie3.util.geo.GeoUtils +import edu.ie3.util.osm.OsmEntities.Way +import edu.ie3.util.osm.{OsmEntities, OsmModel} +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Power +import scala.util.{Failure, Success, Try} -object LvRegionCoordinator { +object LvRegionCoordinator extends LoadCalculation with LoadClustering { sealed trait LvRegionCoordinatorEvent + final case class ReqLvGrids( + osmContainer: OsmModel, + replyTo: ActorRef[LvCoordinatorEvent] + ) extends LvRegionCoordinatorEvent def apply( - lvGeneratorPool: ActorRef[LvGeneratorEvent] - ): Behaviors.Receive[LvRegionCoordinatorEvent] = idle(lvGeneratorPool) + lvGeneratorPool: ActorRef[LvGeneratorEvent], + loadDensity: ComparableQuantity[PowerDensity], + restrictSubgridsToLanduseAreas: Boolean + ): Behaviors.Receive[LvRegionCoordinatorEvent] = + idle(lvGeneratorPool, loadDensity, restrictSubgridsToLanduseAreas) private def idle( - lvGeneratorPool: ActorRef[LvGeneratorEvent] + lvGeneratorPool: ActorRef[LvGeneratorEvent], + loadDensity: ComparableQuantity[PowerDensity], + restrictSubgridsToLanduseAreas: Boolean ): Behaviors.Receive[LvRegionCoordinatorEvent] = Behaviors.receive { + case (ctx, ReqLvGrids(osmContainer, replyTo)) => + implicit val logger: Logger = ctx.log + logger.debug("Received osm data for a given region. Start execution.") + + /* Determine the location of loads */ + val loadLocations = determineLoadLocations(osmContainer, loadDensity) + + clusterLoads(loadLocations, osmContainer, restrictSubgridsToLanduseAreas) + + /* TODO + 1) Cluster the given loads + 2) Generate sub-graphs based on clusters + 3) Distribute work to worker pool and collect the result + */ + + replyTo ! LvCoordinator.RepLvGrids(Vector.empty[SubGridContainer]) + Behaviors.same case (ctx, unsupported) => ctx.log.warn(s"Received unsupported message '$unsupported'.") Behaviors.stopped diff --git a/src/main/scala/edu/ie3/osmogrid/model/Coordinate.scala b/src/main/scala/edu/ie3/osmogrid/model/Coordinate.scala new file mode 100644 index 00000000..b63a0d0b --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/model/Coordinate.scala @@ -0,0 +1,17 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.model + +import org.locationtech.jts.geom.Point + +case class Coordinate(lat: Double, lon: Double) + +object Coordinate { + implicit class RichPoint(point: Point) { + def toCoordinate: Coordinate = Coordinate(point.getY, point.getX) + } +} diff --git a/src/main/scala/edu/ie3/osmogrid/model/LoadLocation.scala b/src/main/scala/edu/ie3/osmogrid/model/LoadLocation.scala new file mode 100644 index 00000000..46936128 --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/model/LoadLocation.scala @@ -0,0 +1,29 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.model + +import edu.ie3.util.osm.OsmEntities.{OsmEntity, Way} +import tech.units.indriya.ComparableQuantity + +import javax.measure.quantity.Power + +/** Point-based location of an electrical node + * + * @param location + * The geographical location + * @param load + * Electrical load + * @param source + * Source osm entity, the load has been derived from + * @tparam T + * Type of source + */ +final case class LoadLocation[T <: OsmEntity]( + location: Coordinate, + load: ComparableQuantity[Power], + source: T +) diff --git a/src/main/scala/edu/ie3/osmogrid/model/WayUtils.scala b/src/main/scala/edu/ie3/osmogrid/model/WayUtils.scala new file mode 100644 index 00000000..0b012caf --- /dev/null +++ b/src/main/scala/edu/ie3/osmogrid/model/WayUtils.scala @@ -0,0 +1,51 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.model + +import breeze.stats.mean +import edu.ie3.osmogrid.exception.IllegalCalculationException +import edu.ie3.osmogrid.model.Coordinate.RichPoint +import edu.ie3.util.geo.GeoUtils +import edu.ie3.util.osm.OsmEntities.{ClosedWay, Node} +import edu.ie3.util.CollectionUtils.RichList +import org.locationtech.jts.geom.Coordinates +import tech.units.indriya.ComparableQuantity +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units + +import javax.measure.quantity.Area +import scala.util.{Failure, Success, Try} + +object WayUtils { + + @deprecated("Will be part of ClosedWay in soon future.") + implicit class RichClosedWay(way: ClosedWay) { + + /** Determine the area of the polygon. + * + * @return + * The area of the polygon + */ + def area: Try[ComparableQuantity[Area]] = Success( + Quantities.getQuantity( + 42d, + Units.SQUARE_METRE + ) + ) + + def center: Coordinate = way.nodes + .slice( + 0, + way.nodes.length - 1 + ) // Last node has to be the same as the first one to be a closed way + .map(_.coordinates.toCoordinate) + .map { case Coordinate(lat, lon) => (lat, lon) } + .unzip match { + case (lats, lons) => Coordinate(mean(lats), mean(lons)) + } + } +} diff --git a/src/main/scala/edu/ie3/util/CollectionUtils.scala b/src/main/scala/edu/ie3/util/CollectionUtils.scala new file mode 100644 index 00000000..521279c0 --- /dev/null +++ b/src/main/scala/edu/ie3/util/CollectionUtils.scala @@ -0,0 +1,27 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.util + +object CollectionUtils { + implicit class RichList[T](list: List[T]) { + + /** Rotate the elements in the list + * @param positions + * Rotate entries by this amount + * @return + * The rotated list + */ + def rotate(positions: Int): List[T] = { + val shift = positions % list.length + val (head, tail) = + if (shift >= 0) list.splitAt(shift) + else list.splitAt(list.length + shift) + + tail.appendedAll(head) + } + } +} diff --git a/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala index 2c26b19a..4711d6ca 100644 --- a/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala +++ b/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala @@ -21,7 +21,8 @@ class ConfigFailFastSpec extends UnitSpec { """input.asset.file.directory = "asset_input_dir" |input.asset.file.hierarchic = false |output.file.directory = "output_file_path" - |generation.lv.distinctHouseConnections = true""".stripMargin + |generation.lv.distinctHouseConnections = true + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -38,7 +39,8 @@ class ConfigFailFastSpec extends UnitSpec { |input.asset.file.directory = "asset_input_dir" |input.asset.file.hierarchic = false |output.file.directory = "output_file_path" - |generation.lv.distinctHouseConnections = true""".stripMargin + |generation.lv.distinctHouseConnections = true + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -53,7 +55,8 @@ class ConfigFailFastSpec extends UnitSpec { OsmoGridConfigFactory.parseWithoutFallback { """input.osm.pbf.file = "input_file_path" |output.file.directory = "output_file_path" - |generation.lv.distinctHouseConnections = true""".stripMargin + |generation.lv.distinctHouseConnections = true + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -70,7 +73,8 @@ class ConfigFailFastSpec extends UnitSpec { |input.asset.file.directory = "" |input.asset.file.hierarchic = false |output.file.directory = "output_file_path" - |generation.lv.distinctHouseConnections = true""".stripMargin + |generation.lv.distinctHouseConnections = true + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -109,7 +113,8 @@ class ConfigFailFastSpec extends UnitSpec { |output.file.directory = "output_file_path" |generation.lv.amountOfGridGenerators = 0 |generation.lv.amountOfRegionCoordinators = 5 - |generation.lv.distinctHouseConnections = false""".stripMargin + |generation.lv.distinctHouseConnections = false + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -128,7 +133,8 @@ class ConfigFailFastSpec extends UnitSpec { |output.file.directory = "output_file_path" |generation.lv.amountOfGridGenerators = -42 |generation.lv.amountOfRegionCoordinators = 5 - |generation.lv.distinctHouseConnections = false""".stripMargin + |generation.lv.distinctHouseConnections = false + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -147,7 +153,8 @@ class ConfigFailFastSpec extends UnitSpec { |output.file.directory = "output_file_path" |generation.lv.amountOfGridGenerators = 10 |generation.lv.amountOfRegionCoordinators = 0 - |generation.lv.distinctHouseConnections = false""".stripMargin + |generation.lv.distinctHouseConnections = false + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -166,7 +173,8 @@ class ConfigFailFastSpec extends UnitSpec { |output.file.directory = "output_file_path" |generation.lv.amountOfGridGenerators = 10 |generation.lv.amountOfRegionCoordinators = -42 - |generation.lv.distinctHouseConnections = false""".stripMargin + |generation.lv.distinctHouseConnections = false + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => val exc = @@ -176,6 +184,26 @@ class ConfigFailFastSpec extends UnitSpec { fail(s"Config generation failed with an exception: '$exception'") } } + + "fail on negative power density of a building in an lv grid coordinators" in { + OsmoGridConfigFactory.parseWithoutFallback { + """input.osm.pbf.file = "input_file_path" + |input.asset.file.directory = "asset_input_dir" + |input.asset.file.hierarchic = false + |output.file.directory = "output_file_path" + |generation.lv.amountOfGridGenerators = 10 + |generation.lv.amountOfRegionCoordinators = 5 + |generation.lv.distinctHouseConnections = false + |generation.lv.loadDensity = -4.3""".stripMargin + } match { + case Success(cfg) => + val exc = + intercept[IllegalConfigException](ConfigFailFast.check(cfg)) + exc.msg shouldBe "The load density of a building in an lv grid needs to be positive (provided: -4.3)." + case Failure(exception) => + fail(s"Config generation failed with an exception: '$exception'") + } + } } "having a valid config" should { @@ -185,7 +213,8 @@ class ConfigFailFastSpec extends UnitSpec { |input.asset.file.directory = "asset_input_dir" |input.asset.file.hierarchic = false |output.file.directory = "output_file_path" - |generation.lv.distinctHouseConnections = true""".stripMargin + |generation.lv.distinctHouseConnections = true + |generation.lv.loadDensity = 5.2""".stripMargin } match { case Success(cfg) => noException shouldBe thrownBy { diff --git a/src/test/scala/edu/ie3/osmogrid/model/WayUtilsSpec.scala b/src/test/scala/edu/ie3/osmogrid/model/WayUtilsSpec.scala new file mode 100644 index 00000000..164d70a2 --- /dev/null +++ b/src/test/scala/edu/ie3/osmogrid/model/WayUtilsSpec.scala @@ -0,0 +1,128 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.osmogrid.model + +import edu.ie3.osmogrid.exception.TestPreparationFailedException +import edu.ie3.osmogrid.model.WayUtils.RichClosedWay +import edu.ie3.test.common.{UnitSpec, WayTestData} +import edu.ie3.util.osm.OsmEntities +import edu.ie3.util.osm.OsmEntities.{ClosedWay, Node} +import org.locationtech.jts.geom.impl.CoordinateArraySequence +import org.locationtech.jts.geom.{GeometryFactory, Point, PrecisionModel} +import tech.units.indriya.unit.Units + +import java.time.ZonedDateTime +import java.util.UUID +import scala.util.{Failure, Success, Try} + +class WayUtilsSpec extends UnitSpec with WayTestData { + "Closed way with enhances functionality" when { + "calculating the center coordinate" should { + "provide correct values for a rectangle" in { + WayUtilsSpec.buildClosedWay( + List((0, 0), (0, 5), (3, 5), (3, 0)) + ) match { + case Success(rectangle) => + rectangle.center match { + case Coordinate(lat, lon) => + lat shouldBe 1.5 +- 1e-3 + lon shouldBe 2.5 +- 1e-3 + } + case Failure(exception) => fail("Test preparation failed.", exception) + } + } + + "provide correct values for a triangle" in { + WayUtilsSpec.buildClosedWay( + List((0, 0), (0, 5), (3, 2.5)) + ) match { + case Success(rectangle) => + rectangle.center match { + case Coordinate(lat, lon) => + lat shouldBe 1.0 +- 1e-3 + lon shouldBe 2.5 +- 1e-3 + } + case Failure(exception) => fail("Test preparation failed.", exception) + } + } + + "provide correct values for a ditched rectangle" in { + WayUtilsSpec.buildClosedWay( + List((0, 0), (0, 5), (2, 2.5), (3, 5), (3, 0)) + ) match { + case Success(rectangle) => + rectangle.center match { + case Coordinate(lat, lon) => + lat shouldBe 1.6 +- 1e-3 + lon shouldBe 2.5 +- 1e-3 + } + case Failure(exception) => fail("Test preparation failed.", exception) + } + } + } + + "determining the enclosed area" should { + "deliver correct result" in { + G2.building.area match { + case Success(area) => + area + .to(Units.SQUARE_METRE) + .getValue + .doubleValue() shouldBe 42d +- 1e-3 // G2.buildingArea.getValue +// .doubleValue() +- 1e-3 + case Failure(exception) => + fail("Determination of area failed.", exception) + } + } + } + } +} + +object WayUtilsSpec { + val geomFactory = + new GeometryFactory(new PrecisionModel(PrecisionModel.FIXED)) + def buildClosedWay(coordinates: List[(Double, Double)]): Try[ClosedWay] = + buildNodes(coordinates).map( + ClosedWay( + UUID.randomUUID(), + 0, + ZonedDateTime.now(), + Map.empty[String, String], + _ + ) + ) + + private def buildNodes( + coordinates: List[(Double, Double)] + ): Try[List[Node]] = { + val nodes = coordinates.map { case (y, x) => + Node( + UUID.randomUUID(), + 0, + ZonedDateTime.now(), + Map.empty[String, String], + new Point( + new CoordinateArraySequence( + Array(new org.locationtech.jts.geom.Coordinate(x, y)) + ), + geomFactory + ) + ) + } + /* Replicate first node (mandatory for a closed way) */ + nodes.headOption match { + case Some(head) => + Success( + nodes.appended(head) + ) + case None => + Failure( + TestPreparationFailedException("Unable to close nodes for this way.") + ) + } + } +} diff --git a/src/test/scala/edu/ie3/test/common/WayTestData.scala b/src/test/scala/edu/ie3/test/common/WayTestData.scala new file mode 100644 index 00000000..7b43ebdc --- /dev/null +++ b/src/test/scala/edu/ie3/test/common/WayTestData.scala @@ -0,0 +1,157 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.test.common + +import edu.ie3.osmogrid.model.Coordinate +import edu.ie3.osmogrid.exception.TestPreparationFailedException +import edu.ie3.util.osm.OsmEntities.{ClosedWay, Node} +import org.locationtech.jts.geom +import org.locationtech.jts.geom.impl.CoordinateArraySequence +import org.locationtech.jts.geom.{GeometryFactory, Point, PrecisionModel} +import tech.units.indriya.ComparableQuantity +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units + +import java.time.{ZoneId, ZonedDateTime} +import java.util.UUID +import javax.measure.quantity.Area + +trait WayTestData { + + /** This it TU Dortmund's BCI-G2 building as of 12/9/2021. The area has been + * calculated with JOSM utilizing the measurement plugin + * + * @see + * https://www.openstreetmap.org/way/24710099 + */ + object G2 { + private val coordinates = { + val tempCoordinates = Vector( + Coordinate( + 7.41203840000, + 51.49282570000 + ), + Coordinate( + 7.41206190000, + 51.49275640000 + ), + Coordinate( + 7.41207890000, + 51.49275870000 + ), + Coordinate( + 7.41208990000, + 51.49272690000 + ), + Coordinate( + 7.41203640000, + 51.49271980000 + ), + Coordinate( + 7.41203680000, + 51.49271890000 + ), + Coordinate( + 7.41199410000, + 51.49271330000 + ), + Coordinate( + 7.41199780000, + 51.49270280000 + ), + Coordinate( + 7.41200200000, + 51.49269070000 + ), + Coordinate( + 7.41204420000, + 51.49269620000 + ), + Coordinate( + 7.41204490000, + 51.49269540000 + ), + Coordinate( + 7.41209800000, + 51.49270240000 + ), + Coordinate( + 7.41211210000, + 51.49266100000 + ), + Coordinate( + 7.41216780000, + 51.49266840000 + ), + Coordinate( + 7.41218810000, + 51.49260880000 + ), + Coordinate( + 7.41239910000, + 51.49263660000 + ), + Coordinate( + 7.41236470000, + 51.49273880000 + ), + Coordinate( + 7.41235700000, + 51.49276140000 + ), + Coordinate( + 7.41232250000, + 51.49286340000 + ), + Coordinate( + 7.41225940000, + 51.49285500000 + ), + Coordinate( + 7.41222390000, + 51.49285030000 + ) + ) + tempCoordinates.headOption match { + case Some(headCoordinate) => tempCoordinates.appended(headCoordinate) + case None => + throw TestPreparationFailedException( + "Unable to repeat the first coordinate." + ) + } + } + + private val geometryFactory = new GeometryFactory( + new PrecisionModel(PrecisionModel.FIXED) + ) + private val lastEdited = + ZonedDateTime.of(2019, 9, 27, 11, 48, 0, 0, ZoneId.of("UTC")) + val building: ClosedWay = ClosedWay( + UUID.fromString("25b7b8d3-e2dd-471d-b2a0-f45cbcaca730"), + 24710099, + lastEdited, + Map.empty[String, String], + coordinates.map { case Coordinate(lat, lon) => + Node( + UUID.randomUUID(), + 0, + lastEdited, + Map.empty[String, String], + new Point( + new CoordinateArraySequence( + Array(new geom.Coordinate(lon, lat)) + ), + geometryFactory + ) + ) + }.toList + ) + + val buildingArea: ComparableQuantity[Area] = + Quantities.getQuantity(487.043, Units.SQUARE_METRE) + } +} diff --git a/src/test/scala/edu/ie3/util/CollectionUtilsSpec.scala b/src/test/scala/edu/ie3/util/CollectionUtilsSpec.scala new file mode 100644 index 00000000..5cba3972 --- /dev/null +++ b/src/test/scala/edu/ie3/util/CollectionUtilsSpec.scala @@ -0,0 +1,40 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.util + +import edu.ie3.util.CollectionUtils.RichList +import edu.ie3.test.common.UnitSpec + +class CollectionUtilsSpec extends UnitSpec { + "Collection utilities" when { + "dealing with lists" when { + "rotating lists" should { + val testList: List[Int] = List(1, 2, 3, 4, 5) + + "provide correct entries, if not rotating at all" in { + testList.rotate(0) shouldBe List(1, 2, 3, 4, 5) + } + + "provide correct entries, if rotating forward" in { + testList.rotate(2) shouldBe List(3, 4, 5, 1, 2) + } + + "provide correct entries, if rotating forward by more elements, than the list is long" in { + testList.rotate(7) shouldBe List(3, 4, 5, 1, 2) + } + + "provide correct entries, if rotating backwards" in { + testList.rotate(-2) shouldBe List(4, 5, 1, 2, 3) + } + + "provide correct entries, if rotating backwards by more elements, than the list is long" in { + testList.rotate(-7) shouldBe List(4, 5, 1, 2, 3) + } + } + } + } +} diff --git a/src/test/scala/edu/ie3/util/geo/GeoUtilScalaSpec.scala b/src/test/scala/edu/ie3/util/geo/GeoUtilScalaSpec.scala new file mode 100644 index 00000000..a2cd81bd --- /dev/null +++ b/src/test/scala/edu/ie3/util/geo/GeoUtilScalaSpec.scala @@ -0,0 +1,45 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.util.geo + +import edu.ie3.osmogrid.model.Coordinate +import edu.ie3.test.common.UnitSpec + +class GeoUtilScalaSpec extends UnitSpec { + "Providing useful means for geographic and geometric stuff" when { + "calculating the enclosed area" should { + "be happy in any case" in { + true shouldBe true + } + // Keep this spec, if the test logic is helpful in future as well. + /*"provide correct area for a rectangle" in { + val coordinates = List((0, 0), (0, 5), (3, 5), (3, 0)).map { + case (lon, lat) => Coordinate(lat, lon) + } + + GeoUtilScala.enclosedArea(coordinates) shouldBe 15.0 +- 1e-3 + } + + "provide correct area for a triangle" in { + val coordinates = List((0.0, 0.0), (0.0, 5.0), (3.0, 2.5)).map { + case (lon, lat) => Coordinate(lat, lon) + } + + GeoUtilScala.enclosedArea(coordinates) shouldBe 7.5 +- 1e-3 + } + + "provide correct area for a ditched rectangle" in { + val coordinates = + List((0.0, 0.0), (2.0, 2.5), (0.0, 5.0), (3.0, 5.0), (3.0, 0.0)).map { + case (lon, lat) => Coordinate(lat, lon) + } + + GeoUtilScala.enclosedArea(coordinates) shouldBe 10.0 +- 1e-3 + }*/ + } + } +}