All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ch.ninecode.cim.CIMNetworkTopologyProcessor.scala Maven / Gradle / Ivy

There is a newer version: 2.12-3.0.1-5.1.1
Show newest version
package ch.ninecode.cim

import org.apache.spark.graphx.Edge
import org.apache.spark.graphx.EdgeDirection
import org.apache.spark.graphx.EdgeTriplet
import org.apache.spark.graphx.Graph
import org.apache.spark.graphx.Graph.graphToGraphOps
import org.apache.spark.graphx.VertexId
import org.apache.spark.rdd.RDD
import org.apache.spark.rdd.RDD.rddToPairRDDFunctions
import org.apache.spark.sql.SparkSession
import org.apache.spark.storage.StorageLevel
import org.slf4j.LoggerFactory
import org.slf4j.Logger

import ch.ninecode.model._

/**
 * Create a topology.
 *
 * Create TopologicalNode and optionally TopologicalIsland RDD that encode the
 * connections between electrical elements and updates the Terminal and ConnectivityNode
 * RDD to reference them.
 *
 * This creates a "bus-branch" model, but retains the "node-breaker" model.
 *
 * Based on ConnectivityNode elements and connecting edges, find the topology that has:
 *
 * - each substation has a single bus (TopologicalNode) at each nominal voltage level
 * for each set of BusbarSection that are connected by closed switches
 * - eliminates switches based on their open/closed state
 * - assigns each ConnectivityNode to exactly one TopologicalNode
 * - assigns each TopologicalNode to exactly one TopologicalIsland
 * (islands are un-connected groups of internally-connected TopologicalNode)
 * - assigns to each TopologicalNode only one ConnectivityNodeContainer
 * (a new unique generated container or one of the possibly many
 * different existing ConnectivityNodeContainer (Bay, Line, Substation, VoltageLevel)
 * of all the ConnectivityNode with the same TopologicalNode)
 *
 * There are flags that affect whether a Switch (force_retain_switches)
 * or Fuse (force_retain_fuses) is included in the topology:
 *
 * 
 * * * * * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
* force_retain_switches * * Switch.retain * * Switch.open/
* Switch.normalOpen *
* in Topology *
* ForceTrue * * don’t care * * don’t care * * yes *
* ForceFalse * * don’t care * * true * * yes *
* false * * no *
* Unforced * * true * * don’t care * * yes *
* false * * true * * yes *
* false * * no *
* * * To be done eventually: * * - create EquivalentEquipment (branch, injection, shunt) for an EquivalentNetwork * * @param spark the session with CIM RDD defined, for which the topology should be calculated * @param options options in effect for the process operation */ case class CIMNetworkTopologyProcessor (spark: SparkSession, options: CIMTopologyOptions) extends CIMRDD { private implicit val session: SparkSession = spark private implicit val log: Logger = LoggerFactory.getLogger (getClass) /** * Index of normalOpen field in Switch bitmask. */ val normalOpenMask: Int = Switch.fields.indexOf ("normalOpen") /** * Index of open field in Switch bitmask. */ val openMask: Int = Switch.fields.indexOf ("open") /** * Index of retained field in Switch bitmask. */ val retainedMask: Int = Switch.fields.indexOf ("retained") /** * Method to determine if a switch is closed (both terminals are the same topological node). * * If the switch has the open attribute set, use that. * Otherwise if it has the normalOpen attribute set, use that. * Otherwise assume it is the default state set by * CIMTopologyOptions.default_switch_open_state which means not closed unless explicitly set. * * @param switch The switch object to test. * @return true if the switch is closed, false otherwise. */ def switchClosed (switch: Switch): Boolean = { if (0 != (switch.bitfields (openMask / 32) & (1 << (openMask % 32)))) !switch.open // open valid else if (0 != (switch.bitfields (normalOpenMask / 32) & (1 << (normalOpenMask % 32)))) !switch.normalOpen else !options.default_switch_open_state } /** * Method to determine if a switch should be retained in the topology. * * @param switch The switch object to test. * @return true if the switch should be retained, false otherwise. */ def retainSwitch (switch: Switch): Boolean = { options.force_retain_switches match { case ForceTrue => true case ForceFalse => false case Unforced => if (0 != (switch.bitfields (retainedMask / 32) & (1 << (retainedMask % 32)))) switch.retained else false } } /** * Method to determine if a fuse should be retained in the topology. * * @param switch The switch object to test. * @return true if the switch should be retained, false otherwise. */ def retainFuse (switch: Switch): Boolean = { options.force_retain_fuses match { case ForceTrue => true case ForceFalse => false case Unforced => if (0 != (switch.bitfields (retainedMask / 32) & (1 << (retainedMask % 32)))) switch.retained else false } } /** * Method to determine if a switch can be represented as a single topological node. * * @param switch The switch object to test. * @return true if the switch is effectively one node, false otherwise. */ def isSwitchOneNode (switch: Switch): Boolean = { !retainSwitch (switch) && switchClosed (switch) } /** * Method to determine if a fuse can be represented as a single topological node. * * @param switch The fuse object to test. * @return true if the fuse is effectively one node, false otherwise. */ def isFuseOneNode (switch: Switch): Boolean = { !retainFuse (switch) && switchClosed (switch) } /** * Method to determine if the nodes for an element are the same topological node. * * @param element The element to test. * @return true if the element is effectively one node, false otherwise. */ //noinspection ScalaStyle def isSameNode (element: Element): Boolean = { element match { case switch: Switch => isSwitchOneNode (switch) case mktswitch: MktSwitch => isSwitchOneNode (mktswitch.Switch) case cut: Cut => isSwitchOneNode (cut.Switch) case disconnector: Disconnector => isSwitchOneNode (disconnector.Switch) case fuse: Fuse => isFuseOneNode (fuse.Switch) case grounddisconnector: GroundDisconnector => isSwitchOneNode (grounddisconnector.Switch) case jumper: Jumper => isSwitchOneNode (jumper.Switch) case protectedswitch: ProtectedSwitch => isFuseOneNode (protectedswitch.Switch) case sectionaliser: Sectionaliser => isSwitchOneNode (sectionaliser.Switch) case breaker: Breaker => isFuseOneNode (breaker.ProtectedSwitch.Switch) case loadbreakswitch: LoadBreakSwitch => isFuseOneNode (loadbreakswitch.ProtectedSwitch.Switch) case recloser: Recloser => isFuseOneNode (recloser.ProtectedSwitch.Switch) case _: PowerTransformer => false case line: ACLineSegment => !((line.Conductor.len > 0.0) && ((line.r > 0.0) || (line.x > 0.0))) case _: Conductor => true case _ => log.warn (s"topological node processor encountered edge with unhandled class '${element.getClass.getName}', assumed zero impedance") true } } /** * Method to determine if a switch can be represented as a single topological island. * * @param switch The switch object to test. * @return true if the switch is effectively one island, false otherwise. */ def isSwitchOneIsland (switch: Switch): Boolean = { options.force_switch_separate_islands match { case ForceTrue => false case ForceFalse => true case Unforced => switchClosed (switch) } } /** * Method to determine if a fuse can be represented as a single topological island. * * @param switch The fuse object to test. * @return true if the fuse is effectively one island, false otherwise. */ def isFuseOneIsland (switch: Switch): Boolean = { options.force_fuse_separate_islands match { case ForceTrue => false case ForceFalse => true case Unforced => switchClosed (switch) } } /** * Method to determine if the nodes for an element are in the same topological island. * * @param element The element to test. * @return true if the element is effectively one node, false otherwise. */ //noinspection ScalaStyle def isSameIsland (element: Element): Boolean = { element match { case switch: Switch => isSwitchOneIsland (switch) case mktswitch: MktSwitch => isSwitchOneIsland (mktswitch.Switch) case cut: Cut => isSwitchOneIsland (cut.Switch) case disconnector: Disconnector => isSwitchOneIsland (disconnector.Switch) case fuse: Fuse => isFuseOneIsland (fuse.Switch) case grounddisconnector: GroundDisconnector => isSwitchOneIsland (grounddisconnector.Switch) case jumper: Jumper => isSwitchOneIsland (jumper.Switch) case protectedswitch: ProtectedSwitch => isFuseOneIsland (protectedswitch.Switch) case sectionaliser: Sectionaliser => isSwitchOneIsland (sectionaliser.Switch) case breaker: Breaker => isFuseOneIsland (breaker.ProtectedSwitch.Switch) case loadbreakswitch: LoadBreakSwitch => isFuseOneIsland (loadbreakswitch.ProtectedSwitch.Switch) case recloser: Recloser => isFuseOneIsland (recloser.ProtectedSwitch.Switch) case _: PowerTransformer => false case _: ACLineSegment => true case _: Conductor => true case _ => log.warn (s"topological island processor encountered edge with unhandled class '${element.getClass.getName}', assumed zero impedance") true } } /** * Get the underlying ConductingEquipment superclass. * * @param element the element to traverse * @return Some(ConductingEquipment) or None if the Element is not a ConductingEquipment subclass */ def conductingEquipment (element: Element): Option[ConductingEquipment] = { element match { case conducting: ConductingEquipment => Some (conducting) case null => None case e: Element => conductingEquipment (e.sup) } } /** * Construct edges for terminal pairs of this equipment. * * @param args Element (ConductingEquipment) and the Terminal that reference it * @return edges for each terminal pair */ def toEdges (args: (Element, Iterable[Terminal])): List[CIMEdgeData] = { val (element, terms) = args // get the ConductingEquipment conductingEquipment (element) match { case Some (conducting) => // make an array of terminals sorted by sequence number val terminals = terms .toArray .sortBy (_.ACDCTerminal.sequenceNumber) // make an edge for each pair of terminals, // eliminate edges with only one terminal with a ConnectivityNode val edges = for (i <- 1 until terminals.length) yield CIMEdgeData ( terminals (0).ConnectivityNode, terminals (i).ConnectivityNode, conducting.id, // same as terminals(n).ConductingEquipment, isSameNode (element), // edge connects its nodes isSameIsland (element)) // edge has its nodes in the same island edges.toList case None => // shouldn't happen, terminals always reference ConductingEquipment, right? // throw new Exception (s"element ${e.id} is not derived from ConductingEquipment") // ProtectionEquipment and CurrentRelay are emitted with terminals even though they shouldn't be List () } } /** * Compute the hash code for the string to be used as a VertextId (Long). * * @param string the mRID to convert into a VertexId * @return the VertexId for the mRID */ def asVertexId (string: String): VertexId = { var h = 2166136261L for (c <- string) h = (h * 16777619) ^ c h } /** * Create a GraphX Edge. * * @param e the data to pack in the edge * @return an Edge[T] object */ def asEdge (e: CIMEdgeData): Edge[CIMEdgeData] = Edge (asVertexId (e.id_cn_1), asVertexId (e.id_cn_2), e) /** * Construct vertex data for the ConnectivityNode and voltage. * * @param a the ConnectivityNode and its voltage * @return the data for the vertex */ def toVertex (a: (ConnectivityNode, String)): CIMVD = { val (node, voltage) = a CIMVD (asVertexId (node.id), node.id, voltage, node.ConnectivityNodeContainer) } def makeGraph (): Graph[CIMVD, CIMEdgeData] = { // get Element that are ConductingEquipment val equipment = getOrElse [Element]("Elements").filter (conductingEquipment (_).isDefined) // get the terminals with ConnectivityNode keyed by equipment val terms = getOrElse [Terminal] .filter (x => (null != x.ConnectivityNode) && ("" != x.ConnectivityNode)) // map elements with their terminal 'pairs' to edges val edges = equipment.keyBy (_.id).join (terms.groupBy (_.ConductingEquipment)) .values.flatMap (toEdges).map (asEdge).persist (options.storage).setName ("CIMNetworkTopology_NodeEdges") // transformer list(end#, voltage) val end_voltages = getOrElse [PowerTransformerEnd].map (x => (x.PowerTransformer, (x.TransformerEnd.endNumber, x.TransformerEnd.BaseVoltage))).groupByKey val equipment_voltages = getOrElse [ConductingEquipment].flatMap ( x => if ((null == x.BaseVoltage) || ("" == x.BaseVoltage)) None else Some ((x.id, Iterable ((1, x.BaseVoltage), (2, x.BaseVoltage))))) // same voltage for both terminals .union (end_voltages) // get the voltage for each ConnectivityNode by joining through Terminal val e = terms .map (x => (x.ConductingEquipment, (x.ACDCTerminal.sequenceNumber, x.ConnectivityNode))) .groupByKey .join (equipment_voltages).values // ([(term#, ConnectivityNode)], [(end#, voltage)]) .flatMap ( x => x._1.flatMap ( y => x._2.find (_._1 == y._1) match { case Some (voltage) => Some ((y._2, voltage._2)) case None => None } ) ) .groupByKey.map ( x => { val voltages = x._2.filter (_ != null) val voltage = voltages.headOption.getOrElse ("Unknown") if (options.debug) if (!voltages.forall (_ == voltage)) log.error (s"conflicting voltages on node ${x._1} (${voltages.mkString (",")})") (x._1, voltage) } ) // get the vertices val vertices = getOrElse [ConnectivityNode].keyBy (_.id).join (e) .values.map (toVertex).keyBy (_.node).persist (options.storage).setName ("CIMNetworkTopology_NodeVertices") if (options.debug) { // check for uniqueness of VertexId val duplicates = vertices.groupByKey.filter (_._2.size > 1) duplicates.collect.foreach ( x => { val (vertex, data) = x data.toList match { case head :: tail :: Nil => log.error (s"VertexId clash ($vertex) for ${head.node_label} and ${tail.node_label}") case _ => } } ) // check for missing vertices val cn = edges.flatMap (x => List ((x.attr.id_cn_1, x.attr.id_equ), (x.attr.id_cn_2, x.attr.id_equ))) val missing = cn.leftOuterJoin (vertices.keyBy (_._2.node_label)) .filter (_._2._2 match { case None => true case _ => false }).map (x => (x._2._1, x._1)) missing.collect.foreach ( x => { val (id, node) = x log.error (s"$id missing ConnectivityNode $node") } ) } // construct the initial graph from the edges Graph.apply (vertices, edges, CIMVD (), options.storage, options.storage) .persist (options.storage) } def nodeVertex (id: VertexId, data: CIMVD, message: CIMVD): CIMVD = { if (null != message) // not initialization call? if (data.node > message.node) message else data else { val _ = id // reference unused parameter id data } } def nodeSendMessage (triplet: EdgeTriplet[CIMVD, CIMEdgeData]): Iterator[(VertexId, CIMVD)] = { if (!triplet.attr.isZero) Iterator.empty // send no message across a topological boundary else { if (options.debug) if ((null != triplet.srcAttr.voltage) && (null != triplet.dstAttr.voltage)) if (triplet.srcAttr.voltage != triplet.dstAttr.voltage) log.error (s"conflicting node voltages across edge ${triplet.attr.id_equ}, ${triplet.srcAttr.node_label}:${triplet.srcAttr.voltage}, ${triplet.dstAttr.node_label}:${triplet.dstAttr.voltage}") if (triplet.srcAttr.node < triplet.dstAttr.node) { if (options.debug && log.isDebugEnabled) log.debug (s"${triplet.attr.id_equ}: from src:${triplet.srcId} to dst:${triplet.dstId} ${triplet.srcAttr.toString} ---> ${triplet.dstAttr.toString}") Iterator ((triplet.dstId, triplet.srcAttr)) } else if (triplet.srcAttr.node > triplet.dstAttr.node) { if (options.debug && log.isDebugEnabled) log.debug (s"${triplet.attr.id_equ}: from dst:${triplet.dstId} to src:${triplet.srcId} ${triplet.dstAttr.toString} ---> ${triplet.srcAttr.toString}") Iterator ((triplet.srcId, triplet.dstAttr)) } else Iterator.empty } } def nodeMergeMessage (a: CIMVD, b: CIMVD): CIMVD = { if (options.debug) if ((null != a.voltage) && (null != b.voltage)) if (a.voltage != b.voltage) { val (from, to) = if (a.node <= b.node) (b, a) else (a, b) log.error (s"conflicting node voltages, merging: ${from.node_label}:${from.voltage} into ${to.node_label}:${to.voltage}") } if (a.node <= b.node) a else b } def identifyNodes (graph: Graph[CIMVD, CIMEdgeData]): Graph[CIMVertexData, CIMEdgeData] = { // traverse the graph with the Pregel algorithm, // assigns the minimum VertexId of all electrically identical nodes to each of them // Note: on the first pass through the Pregel algorithm all nodes get a null message graph.pregel[CIMVD](null, 10000, EdgeDirection.Either)(nodeVertex, nodeSendMessage, nodeMergeMessage) // change to VertexData .mapVertices ((_, v) => CIMVertexData (0L, "", v.node, v.node_label, v.voltage, v.container)) .persist (options.storage) } def to_islands (nodes: Iterable[((CIMVertexData, ConnectivityNode), Option[(Terminal, Element)])]): (VertexId, TopologicalIsland) = { def op (current: (List[Terminal], ConnectivityNode), data: ((CIMVertexData, ConnectivityNode), Option[(Terminal, Element)])): (List[Terminal], ConnectivityNode) = { data._2 match { case Some ((terminal, element)) => // get the alphabetically least connectivity node name as a fall-back val cn = data._1._2 val best = if (null == current._2) cn else if (cn.id < current._2.id) cn else current._2 // get the transformer secondary terminal element match { case _: PowerTransformer => if (terminal.ACDCTerminal.sequenceNumber > 1) (current._1 :+ terminal, best) else (current._1, best) case _ => (current._1, best) } case None => current } } val (lv_terminals, cn) = nodes.foldLeft (Tuple2 [List[Terminal], ConnectivityNode](List [Terminal](), null))(op) // based on how many transformer terminals there are, construct a suitable name def trafos (l: List[Terminal]): String = l.map (_.ConductingEquipment).mkString ("_") val terminals = lv_terminals.sortBy (_.id) val base = terminals match { case t1 :: Nil => t1.id case t1 :: _ => s"${trafos (terminals)}_terminal_${t1.ACDCTerminal.sequenceNumber}" case _ => cn.id } val name = s"${base}_island" val basic = BasicElement ( null, mRID = name ) basic.bitfields = BasicElement.fieldsToBitfields ("mRID") val obj = IdentifiedObject ( basic, aliasName = cn.IdentifiedObject.aliasName, description = cn.IdentifiedObject.description, mRID = name, name = cn.id ) obj.bitfields = IdentifiedObject.fieldsToBitfields ("aliasName", "description", "mRID", "name") val island = TopologicalIsland (obj) island.bitfields = TopologicalIsland.fieldsToBitfields () ( nodes.headOption.fold (0L)(x => x._1._1.island), island ) } def to_nodes (arg: (VertexId, CIMVertexData, Option[TopologicalIsland])): TopologicalNode = { val name = arg._2.name val island = arg._3 match { case Some (i) => i.id case _ => "" } val element = BasicElement ( null, mRID = name ) element.bitfields = BasicElement.fieldsToBitfields ("mRID") val obj = IdentifiedObject ( element, aliasName = arg._1.toString, mRID = name ) obj.bitfields = IdentifiedObject.fieldsToBitfields ("aliasName", "mRID") val node = TopologicalNode ( obj, BaseVoltage = arg._2.voltage, ConnectivityNodeContainer = arg._2.container, TopologicalIsland = island ) node.bitfields = TopologicalNode.fieldsToBitfields ("BaseVoltage", "ConnectivityNodeContainer", "TopologicalIsland") node } def update_cn (arg: (ConnectivityNode, Option[CIMVertexData])): ConnectivityNode = { val (c, data) = arg data match { case Some (node) => // ToDo: should be: c.copy (TopologicalNode = node.name) val cn = ConnectivityNode ( c.IdentifiedObject, ConnectivityNodeContainer = c.ConnectivityNodeContainer, TopologicalNode = node.name ) cn.bitfields = ConnectivityNode.fieldsToBitfields ("ConnectivityNodeContainer", "TopologicalNode") cn case None => c } } def update_terminals (arg: (Terminal, Option[CIMVertexData])): Terminal = { val t = arg._1 arg._2 match { case Some (node) => // ToDo: should be: t.copy (TopologicalNode = node.name) val terminal = Terminal ( t.ACDCTerminal, phases = t.phases, AuxiliaryEquipment = t.AuxiliaryEquipment, BranchGroupTerminal = t.BranchGroupTerminal, Bushing = t.Bushing, Circuit = t.Circuit, ConductingEquipment = t.ConductingEquipment, ConnectivityNode = t.ConnectivityNode, ConverterDCSides = t.ConverterDCSides, EquipmentFaults = t.EquipmentFaults, HasFirstMutualCoupling = t.HasFirstMutualCoupling, HasSecondMutualCoupling = t.HasSecondMutualCoupling, NormalHeadFeeder = t.NormalHeadFeeder, PinTerminal = t.PinTerminal, RegulatingControl = t.RegulatingControl, RemoteInputSignal = t.RemoteInputSignal, SvPowerFlow = t.SvPowerFlow, TieFlow = t.TieFlow, TopologicalNode = node.name, // the one changed property TransformerEnd = t.TransformerEnd ) val bitfields = t.bitfields.clone () val position = Terminal.fields.indexOf ("TopologicalNode") bitfields (position / 32) |= (1 << (position % 32)) terminal.bitfields = bitfields terminal case None => t } } def islandVertex (id: VertexId, attr: CIMIslandData, msg: CIMIslandData): CIMIslandData = { if (null == msg) { val _ = id // reference unused parameter id // initially assign each node to it's own island attr.copy (island = attr.node) } else if (attr.island > msg.island) attr.copy (island = msg.island, island_label = msg.island_label) else attr } def islandSendMessage (triplet: EdgeTriplet[CIMIslandData, CIMEdgeData]): Iterator[(VertexId, CIMIslandData)] = { if (!triplet.attr.isConnected) Iterator.empty // send no message across a topological boundary else if (triplet.srcAttr.island < triplet.dstAttr.island) Iterator ((triplet.dstId, triplet.srcAttr)) else if (triplet.srcAttr.island > triplet.dstAttr.island) Iterator ((triplet.srcId, triplet.dstAttr)) else Iterator.empty } def islandMergeMessage (a: CIMIslandData, b: CIMIslandData): CIMIslandData = if (a.island < b.island) a else b /** * Use GraphX to identify nodes that are connected electrically * * @see isSameIsland(Element) * @param graph The results from the topological node processing step. * @return A new graph with vertices having island information. */ def identifyIslands (graph: Graph[CIMVertexData, CIMEdgeData]): Graph[CIMVertexData, CIMEdgeData] = { def to_topo_edge (triplet: EdgeTriplet[CIMVertexData, CIMEdgeData]): CIMEdgeData = { val edge = triplet.attr CIMEdgeData (triplet.srcAttr.node_label, triplet.dstAttr.node_label, edge.id_equ, edge.isZero, edge.isConnected) } def to_island_vertices (edge: CIMEdgeData): Iterable[CIMIslandData] = { if (edge.id_cn_2 == null) List (CIMIslandData (asVertexId (edge.id_cn_1), edge.id_cn_1)) else List (CIMIslandData (asVertexId (edge.id_cn_1), edge.id_cn_1), CIMIslandData (asVertexId (edge.id_cn_2), edge.id_cn_2)) } def mapper (d: ((VertexId, CIMVertexData), Option[CIMIslandData])): (VertexId, CIMVertexData) = { val ((id, vertex), island) = d island match { case Some (data) => (id, vertex.copy (island = data.island, island_label = data.island_label)) case _ => (id, vertex.copy (island = asVertexId (vertex.node_label), island_label = vertex.node_label)) } } // make new edges with TopologicalNode VertexId for edges with feet in different nodes val e: RDD[CIMEdgeData] = graph.triplets .filter (edge => edge.srcAttr.node != edge.dstAttr.node) .map (to_topo_edge) val nodes: RDD[(VertexId, CIMIslandData)] = e.flatMap (to_island_vertices).keyBy (_.node) val edges: RDD[Edge[CIMEdgeData]] = e.map (x => Edge (asVertexId (x.id_cn_1), asVertexId (x.id_cn_2), x)) val topo_graph: Graph[CIMIslandData, CIMEdgeData] = Graph.apply (nodes, edges, CIMIslandData (0, ""), options.storage, options.storage) // run the Pregel algorithm over the reduced topological graph val g = topo_graph.pregel[CIMIslandData](null, 10000, EdgeDirection.Either)(islandVertex, islandSendMessage, islandMergeMessage) // update the original graph with the islands val v = graph.vertices.keyBy (_._2.node).leftOuterJoin (g.vertices.values.keyBy (_.node)).values.map (mapper) // make a new graph with the updated vertices val ret: Graph[CIMVertexData, CIMEdgeData] = Graph.apply (v, graph.edges, CIMVertexData (), options.storage, options.storage) // persist the RDD to avoid recomputation { val _ = ret.vertices.persist (options.storage).setName ("CIMNetworkTopology_IslandVertices") } ret } /** * Create new TopologicalNode and optionally TopologicalIsland RDD based on connectivity. * * Any existing RDDs with these names will be unpersisted. * Hierarchical nested case class RDDs will also be updated. * * @return The new Elements RDD - with TopologicalNode and TopologicalIsland objects included. */ def process: RDD[Element] = { log.info ("performing Network Topology Processing") implicit val storage: StorageLevel = options.storage // for put() // get the initial graph based on edges if (options.debug && log.isDebugEnabled) log.debug (s"makeGraph") val initial: Graph[CIMVD, CIMEdgeData] = makeGraph () // get the topological nodes log.info ("identifyNodes") var graph: Graph[CIMVertexData, CIMEdgeData] = identifyNodes (initial) // get the original island and node RDD val old_ti = getOrElse [TopologicalIsland] val old_tn = getOrElse [TopologicalNode] // create a new TopologicalNode RDD val (new_tn, new_ti) = if (options.identify_islands) { // get the topological islands log.info ("identifyIslands") graph = identifyIslands (graph) // get the terminals keyed by equipment with equipment val elements = getOrElse [Element]("Elements").keyBy (_.id) val terms = getOrElse [Terminal].keyBy (_.ConductingEquipment).join (elements).values // map each graph vertex to the terminals val vertices = getOrElse [ConnectivityNode].map (x => (asVertexId (x.id), x)) val td_plus = graph.vertices.join (vertices).values.filter (_._1.island != 0L).keyBy (_._2.id).leftOuterJoin (terms.keyBy (_._1.ConnectivityNode)).values val islands = td_plus.groupBy (_._1._1.island).values.filter (_.exists (_._2.isDefined)).map (to_islands) // create a new TopologicalIsland RDD val new_ti = islands.values if (options.debug && log.isDebugEnabled) log.debug (s"${new_ti.count} islands identified") // swap the old TopologicalIsland RDD for the new one if (options.debug && log.isDebugEnabled) log.debug (s"RDD[TopologicalIsland]") put (new_ti) // we only need the source vertex from each group to make nodes val nodes = graph.vertices .filter (x => x._1 == x._2.node) // vertex id in the graph is the same vertex id of the ConnectivityNode .keyBy (_._2.island).join (islands).values .map (x => (x._1._1, x._1._2, Some (x._2))) .map (to_nodes) if (options.debug && log.isDebugEnabled) log.debug (s"${nodes.count} nodes") // unpersist the graph nodes and edges { val _ = graph.vertices.unpersist (false) } (nodes, new_ti) } else { // we only need the source vertex from each group to make nodes val nodes = graph.vertices .filter (x => x._1 == x._2.node) // vertex id in the graph is the same vertex id of the ConnectivityNode .map (x => (x._1, x._2, None)) .map (to_nodes) if (options.debug && log.isDebugEnabled) log.debug (s"${nodes.count} nodes") (nodes, spark.sparkContext.emptyRDD [TopologicalIsland]) } // swap the old TopologicalNode RDD for the new one if (options.debug && log.isDebugEnabled) log.debug (s"RDD[TopologicalNode]") put (new_tn) // but the other RDD (ConnectivityNode and Terminal also ACDCTerminal) need to be updated in IdentifiedObject and Element // assign every ConnectivityNode to a TopologicalNode val old_cn = getOrElse [ConnectivityNode] val new_cn = old_cn.keyBy (a => asVertexId (a.id)).leftOuterJoin (graph.vertices).values.map (update_cn) // swap the old ConnectivityNode RDD for the new one if (options.debug && log.isDebugEnabled) log.debug (s"RDD[ConnectivityNode]") put (new_cn, true) // assign every Terminal with a connectivity node to a TopologicalNode // note: keep the original enclosed ACDCTerminal objects val old_terminals = getOrElse [Terminal] val t_with = old_terminals.filter (null != _.ConnectivityNode) val t_without = old_terminals.filter (null == _.ConnectivityNode) val new_terminals = t_with.keyBy (a => asVertexId (a.ConnectivityNode)).leftOuterJoin (graph.vertices).values.map (update_terminals) .union (t_without) // swap the old Terminal RDD for the new one if (options.debug && log.isDebugEnabled) log.debug (s"RDD[Terminal]") put (new_terminals, true) // make a union of all old RDD as IdentifiedObject val oldobj = old_ti.map (_.IdentifiedObject). union (old_tn.map (_.IdentifiedObject)). union (old_cn.map (_.IdentifiedObject)). union (old_terminals.map (_.ACDCTerminal.IdentifiedObject)) // make a union of all new RDD as IdentifiedObject val newobj = new_ti.map (_.IdentifiedObject). union (new_tn.map (_.IdentifiedObject)). union (new_cn.map (_.IdentifiedObject)). union (new_terminals.map (_.ACDCTerminal.IdentifiedObject)) // replace identified objects in IdentifiedObject val old_idobj = getOrElse [IdentifiedObject] val new_idobj = old_idobj.keyBy (_.id).subtract (oldobj.keyBy (_.id)).values.union (newobj) // swap the old IdentifiedObject RDD for the new one if (options.debug && log.isDebugEnabled) log.debug (s"RDD[IdentifiedObject]") put (new_idobj, true) // make a union of all old RDD as Element val oldelem = old_ti.asInstanceOf[RDD[Element]]. union (old_tn.asInstanceOf [RDD[Element]]). union (old_cn.asInstanceOf [RDD[Element]]). union (old_terminals.asInstanceOf [RDD[Element]]) // make a union of all new RDD as Element val newelem = new_ti.asInstanceOf[RDD[Element]]. union (new_tn.asInstanceOf [RDD[Element]]). union (new_cn.asInstanceOf [RDD[Element]]). union (new_terminals.asInstanceOf [RDD[Element]]) // replace elements in Elements val old_elements = getOrElse [Element]("Elements") val new_elements = old_elements.keyBy (_.id).subtract (oldelem.keyBy (_.id)).values.union (newelem) // swap the old Elements RDD for the new one if (options.debug && log.isDebugEnabled) log.debug (s"RDD[Element]") put (new_elements, "Elements", true) log.info ("finished Network Topology Processing") new_elements } /** * Conditionally create new TopologicalNode and optionally TopologicalIsland RDD based on connectivity. * * Note, if these RDD exist and are already populated, this method does nothing. * Otherwise it calls the [[process ]] method. * * @return Either the old Elements RDD or a new Elements RDD - with TopologicalNode and TopologicalIsland objects included. */ def processIfNeeded: RDD[Element] = { val nodes = getOrElse [TopologicalNode] if (nodes.isEmpty) process else { val islands = getOrElse [TopologicalIsland] if (options.identify_islands && islands.isEmpty) process else get [Element]("Elements") } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy