diff --git a/.gitignore b/.gitignore index 7cf28aed43..4e7c9b7103 100644 --- a/.gitignore +++ b/.gitignore @@ -201,7 +201,13 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.gitignore.io/api/java,macos,windows,eclipse +# Metals +.metals/ +.bloop/ +project/**/metals.sbt + + +# End of https://www.gitignore.io/api/java,macos,windows,eclipse, metals /target/ diff --git a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala index f2eab5ff1d..af90cfc344 100644 --- a/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala +++ b/src/main/scala/edu/ie3/simona/model/grid/GridModel.scala @@ -29,7 +29,6 @@ import org.jgrapht.graph.{DefaultEdge, SimpleGraph} import java.time.ZonedDateTime import java.util.UUID -import scala.collection.immutable.ListSet import scala.jdk.CollectionConverters._ /** Representation of one physical electrical grid. It holds the references to @@ -440,19 +439,6 @@ object GridModel { throw new InvalidGridException( s"The grid model for subnet ${gridModel.subnetNo} has multiple nodes with the same name!" ) - - // multiple switches @ one node -> not supported yet! - val switchVector = gridModel.gridComponents.switches.foldLeft( - Vector.empty[UUID] - )((vector, switch) => (vector :+ switch.nodeAUuid) :+ switch.nodeBUuid) - val uniqueSwitchNodeIds = switchVector.toSet.toList - if (switchVector.diff(uniqueSwitchNodeIds).nonEmpty) { - throw new InvalidGridException( - s"The grid model for subnet ${gridModel.subnetNo} has nodes with multiple switches. This is not supported yet! Duplicates are located @ nodes: ${switchVector - .diff(uniqueSwitchNodeIds)}" - ) - } - } /** Checks all ControlGroups if a) Transformer of ControlGroup and Measurement @@ -646,40 +632,82 @@ object GridModel { def updateUuidToIndexMap(gridModel: GridModel): Unit = { val switches = gridModel.gridComponents.switches - val nodes = gridModel.gridComponents.nodes + val nodes = gridModel.gridComponents.nodes.distinct + + // map for each node that is directly connected to a switch, + // to all nodes that are directly connected via switch + val nodeConnections: Map[UUID, Set[UUID]] = + switches.filter(_.isClosed).foldLeft(Map.empty[UUID, Set[UUID]]) { + (acc, switch) => + acc + .updated( + switch.nodeAUuid, + acc.getOrElse(switch.nodeAUuid, Set.empty) + switch.nodeBUuid, + ) + .updated( + switch.nodeBUuid, + acc.getOrElse(switch.nodeBUuid, Set.empty) + switch.nodeAUuid, + ) + } - val nodesAndSwitches: ListSet[SystemComponent] = ListSet - .empty[SystemComponent] ++ switches ++ nodes + // create sets of nodes to be fused together and assign common indices + val switchConnectedNodes = + findConnectedNodes(nodeConnections).zipWithIndex.flatMap { + case (nodes, idx) => nodes.map(_ -> idx) + }.toMap - val updatedNodeToUuidMap = nodesAndSwitches + // also account for all missing nodes (not connected to a switch) + val offset = switchConnectedNodes.values.maxOption.map(_ + 1).getOrElse(0) + val (updatedNodeToUuidMap, _) = nodes .filter(_.isInOperation) - .filter { - case switch: SwitchModel => switch.isClosed - case _: NodeModel => true - } - .zipWithIndex - .foldLeft(Map.empty[UUID, Int]) { - case (map, (gridComponent, componentId)) => - gridComponent match { - case switchModel: SwitchModel => - map ++ Map( - switchModel.nodeAUuid -> componentId, - switchModel.nodeBUuid -> componentId, - ) - - case nodeModel: NodeModel => - if (!map.contains(nodeModel.uuid)) { - val idx = map.values.toList.sorted.lastOption - .getOrElse( - -1 - ) + 1 // if we didn't found anything in the list, we don't have switches and want to start @ 0 - map + (nodeModel.uuid -> idx) - } else { - map - } + .foldLeft(Map.empty[UUID, Int], offset) { + case ((map, nextIdx), nodeModel) => + switchConnectedNodes.get(nodeModel.uuid) match { + case Some(idx) => (map + (nodeModel.uuid -> idx), nextIdx) + case None => + (map + (nodeModel.uuid -> nextIdx), nextIdx + 1) } } gridModel._nodeUuidToIndexMap = updatedNodeToUuidMap } + + /** Build sets of connected nodes via depth-first search + * + * @param nodeConnections + * The connected nodes for each node + * @return + * The sets of nodes + */ + private def findConnectedNodes( + nodeConnections: Map[UUID, Set[UUID]] + ): Seq[Seq[UUID]] = { + + def dfs(node: UUID, visited: Set[UUID] = Set.empty): Set[UUID] = { + nodeConnections + .getOrElse(node, Set.empty) + .foldLeft(visited + node) { case (accVisited, neighbor) => + if (accVisited.contains(neighbor)) + accVisited + else + dfs(neighbor, accVisited) + } + } + + val (_, components) = + nodeConnections.keys.foldLeft((Set.empty[UUID], Seq.empty[Seq[UUID]])) { + case ((visited, components), node) => + if (visited.contains(node)) { + (visited, components) + } else { + val component = dfs(node) + val updatedVisited = visited ++ component + val updatedComponents = components :+ component.toSeq + + (updatedVisited, updatedComponents) + } + } + + components + } } diff --git a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala index dc4fab6ef2..2f29248054 100644 --- a/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/grid/GridSpec.scala @@ -9,12 +9,14 @@ package edu.ie3.simona.model.grid import breeze.linalg.DenseMatrix import breeze.math.Complex import breeze.numerics.abs -import edu.ie3.datamodel.exceptions.InvalidGridException import edu.ie3.datamodel.models.input.MeasurementUnitInput import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.simona.exceptions.GridInconsistencyException import edu.ie3.simona.model.control.{GridControls, TransformerControlGroupModel} -import edu.ie3.simona.model.grid.GridModel.GridComponents +import edu.ie3.simona.model.grid.GridModel.{ + GridComponents, + updateUuidToIndexMap, +} import edu.ie3.simona.test.common.input.{GridInputTestData, LineInputTestData} import edu.ie3.simona.test.common.model.grid.{ BasicGrid, @@ -265,53 +267,6 @@ class GridSpec } - "throw an InvalidGridException if two switches are connected @ the same node" in new BasicGridWithSwitches { - // enable nodes - override val nodes: Seq[NodeModel] = super.nodes - nodes.foreach(_.enable()) - - // add a second switch @ node13 (between node1 and node13) - val secondSwitch = new SwitchModel( - UUID.fromString("ebeaad04-0ee3-4b2e-ae85-8c76a583295b"), - "SecondSwitch1", - defaultOperationInterval, - node1.uuid, - node13.uuid, - ) - // add the second switch + enable switches - override val switches: Set[SwitchModel] = super.switches + secondSwitch - switches.foreach(_.enable()) - // open the switches - switches.foreach(_.open()) - - // get the grid from the raw data - val gridModel = new GridModel( - 1, - default400Kva10KvRefSystem, - GridComponents( - nodes, - lines, - Set(transformer2wModel), - Set.empty[Transformer3wModel], - switches, - ), - GridControls.empty, - ) - - // get the private method for validation - val validateConsistency: PrivateMethod[Unit] = - PrivateMethod[Unit](Symbol("validateConsistency")) - - // call the validation method - val exception: InvalidGridException = intercept[InvalidGridException] { - GridModel invokePrivate validateConsistency(gridModel) - } - - // expect an exception for node 13 - exception.getMessage shouldBe s"The grid model for subnet 1 has nodes with multiple switches. This is not supported yet! Duplicates are located @ nodes: Vector(${node13.uuid})" - - } - "update the nodeUuidToIndexMap correctly, when the given grid" must { "contain 3 open switches" in new BasicGridWithSwitches { @@ -474,6 +429,131 @@ class GridSpec nodes.map(node => node.uuid).toVector.sorted ) } + + "contains two closed switches with a common node" in new BasicGridWithSwitches { + // enable nodes + override val nodes: Seq[NodeModel] = super.nodes + nodes.foreach(_.enable()) + + // add a second switch @ node13 (between node1 and node13) + val secondSwitch = new SwitchModel( + UUID.fromString("ebeaad04-0ee3-4b2e-ae85-8c76a583295b"), + "SecondSwitch1", + defaultOperationInterval, + node1.uuid, + node13.uuid, + ) + // add the second switch + enable switches + override val switches: Set[SwitchModel] = super.switches + secondSwitch + switches.foreach(_.enable()) + // close the switches + switches.foreach(_.close()) + + // get the grid from the raw data + val gridModel = new GridModel( + 1, + default400Kva10KvRefSystem, + GridComponents( + nodes, + lines, + Set(transformer2wModel), + Set.empty[Transformer3wModel], + switches, + ), + GridControls.empty, + ) + + updateUuidToIndexMap(gridModel) + + // nodes 1, 13 and 14 should map to the same node + val node1Index = gridModel.nodeUuidToIndexMap + .get(node1.uuid) + .value + gridModel.nodeUuidToIndexMap.get(node13.uuid).value shouldBe node1Index + gridModel.nodeUuidToIndexMap.get(node14.uuid).value shouldBe node1Index + } + + "contains closed switches in a complex pattern" in new BasicGridWithSwitches { + // just six nodes from the basic grid + override val nodes: Seq[NodeModel] = + Seq(node1, node2, node3, node4, node5, node6) + nodes.foreach(_.enable()) + + // nodes 1-4 connected by switches in a rectangle plus diagonal, + // nodes 5-6 connected separately + override val switches: Set[SwitchModel] = Set( + SwitchModel( + UUID.fromString("0-0-0-0-1"), + "Switch1", + defaultOperationInterval, + node1.uuid, + node2.uuid, + ), + SwitchModel( + UUID.fromString("0-0-0-0-2"), + "Switch2", + defaultOperationInterval, + node2.uuid, + node3.uuid, + ), + SwitchModel( + UUID.fromString("0-0-0-0-3"), + "Switch3", + defaultOperationInterval, + node3.uuid, + node4.uuid, + ), + SwitchModel( + UUID.fromString("0-0-0-0-4"), + "Switch4", + defaultOperationInterval, + node1.uuid, + node4.uuid, + ), + SwitchModel( + UUID.fromString("0-0-0-0-5"), + "Switch5", + defaultOperationInterval, + node2.uuid, + node4.uuid, + ), + SwitchModel( + UUID.fromString("0-0-0-0-6"), + "Switch6", + defaultOperationInterval, + node5.uuid, + node6.uuid, + ), + ) + switches.foreach(_.enable()) + // close the switches + switches.foreach(_.close()) + + // get the grid from the raw data + val gridModel = new GridModel( + 1, + default400Kva10KvRefSystem, + GridComponents( + nodes, + Set.empty, + Set.empty, + Set.empty, + switches, + ), + GridControls.empty, + ) + + // testing the assignment of nodes to indices + updateUuidToIndexMap(gridModel) + + gridModel.nodeUuidToIndexMap.get(node1.uuid).value shouldBe 0 + gridModel.nodeUuidToIndexMap.get(node2.uuid).value shouldBe 0 + gridModel.nodeUuidToIndexMap.get(node3.uuid).value shouldBe 0 + gridModel.nodeUuidToIndexMap.get(node4.uuid).value shouldBe 0 + gridModel.nodeUuidToIndexMap.get(node5.uuid).value shouldBe 1 + gridModel.nodeUuidToIndexMap.get(node6.uuid).value shouldBe 1 + } + } "build correct transformer control models" should {