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

com.waioeka.graph.Graph.scala Maven / Gradle / Ivy

There is a newer version: 0.0.6
Show newest version
/*
 * Copyright (c) 2016
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.waioeka.graph

import scala.annotation.tailrec


/**
  * Provides method for generating connected (random) DAG.
  */
object Graph {

  /** Define our vertex type as an Int. */
  type Vertex = Int

  /** Edge from a vertex to a vertex. */
  case class Edge(from: Vertex, to: Vertex)

  /** Graph is an adjacent list of edges. */
  case class Graph(numVertices: Int, adjacencySet: Set[Edge]) {
    /**
      * Return a 'DOT' representation of the graph.
      */
    def toDotString : String  = {
      val builder = StringBuilder.newBuilder
      builder.append("digraph {\n")
      for (edge <- adjacencySet) {
        builder.append(s"${edge.from} -> ${edge.to}\n")
      }
      builder.append("}\n")
      builder.toString()
    }
  }

  /**
    * Find the root parents of the graph, i.e. all vertices that have no incoming edges.
    *
    * @param g  the graph.
    * @return   a set containing all vertices that have no incoming edges.
    */
  def parents(g: Graph) = {
    val vertices = g.adjacencySet.map(_.to)
    g.adjacencySet.filterNot(e => vertices.contains(e.from)).map(_.from)
  }


  /**
    * Find the root children of the graph, i.e. all vertices that have no outgoing edges.
    * @param g
    * @return
    */
  def children(g: Graph) = {
    val vertices = g.adjacencySet.map(_.from)
    g.adjacencySet.filterNot(e => vertices.contains(e.to)).map(_.to)
  }

  /**
    * Creates a random DAG, It does this by randomly setting entries in a lower diagonal matrix.
    *
    * n.b.
    * * there may be more than one connected component.
    * * edges on the diagonal (i.e. edge from a vertex to itself) are filtered out.
    *
    * Filtering out vertices with edges to themselves should be optional.
    * Common to filter out.
    *
    * @param numVertices the number of vertices.
    * @param probOfEdge  the probability of an edge.
    * @param d           the distribution to use for random variables.
    * @return a DAG, may have more than one connected component.
    */
  private[this] def randomEdges(numVertices: Int, probOfEdge: Double)(implicit d: Distribution[Double]): Graph = {
    val edges = for {
      i <- 1 to numVertices
      j <- 1 to i
      if i != j && d.get <= probOfEdge
    } yield Edge(j, i)
    Graph(numVertices,edges.toSet)
  }

  /**
    * Generates a random DAG with one component. The algorithm is as follows:
    *
    *  1. Generate random initial set of edges. This is done by iterating over a
    * lower diagonal of a matrix of size equal to number of vertices. If the
    * random variable at the element is less than a probability, create an edge
    * from the column to the row index.
    *
    *  2. Find the number of components in the graph generated. If the number of
    *  components = 1, return the graph. Otherwise, connect the components.
    *
    * @param numVertices the number of vertices in the dag.
    * @param probOfEdge  the probability of an edge between two nodes in the dag.
    * @param d           the distribution to use for random number generation.
    * @return a connected DAG.
    */
  def randomDAG(numVertices: Int, probOfEdge: Double)(implicit d: Distribution[Double]): Graph = {
    require(numVertices > 1,"Number of vertices must be greater than one.")
    reduce(randomEdges(numVertices, probOfEdge))
  }

  /**
    * Topological sort, code adapted from this source:
    *
    *   https://gist.github.com/ThiporKong/4399695
    *
    * @param g  the graph to sort.
    * @return   topological sort of the graph or none.
    */
  def tsort(g: Graph): Option[Iterable[Vertex]] = {
    @tailrec
    def tsort0(toPreds: Map[Vertex, Set[Vertex]], done: Iterable[Vertex]): Option[Iterable[Vertex]] = {
      val (noPreds, hasPreds) = toPreds.partition { _._2.isEmpty }
      if (noPreds.isEmpty) {
        if (hasPreds.isEmpty) Some(done) else None
      } else {
        val found = noPreds.keys
        tsort0(hasPreds.mapValues { _ -- found }, done ++ found)
      }
    }
    val toPred = g.adjacencySet.foldLeft(Map[Vertex, Set[Vertex]]()) { (acc, e) =>
      acc + (e.from -> acc.getOrElse(e.from, Set())) + (e.to -> (acc.getOrElse(e.to, Set()) + e.from))
    }
    tsort0(toPred, Seq())
  }

  /**
    * Reduce a graph that may have more than one component to
    * a single component DAG.
    *
    * @param graph the graph to reduce.
    * @return a dag that has a single component.
    */
  private[this] def reduce(graph: Graph): Graph = {
    val disjoint = DisjointSet(graph.numVertices).union(graph.adjacencySet)

    def root(vertex: Vertex): Vertex =
      if (disjoint.parents(vertex) == vertex) vertex else root(disjoint.parents(vertex))

    val connected = disjoint.numComponents match {
      case 1 => graph
      case _ =>
        /*
         * Possible join points are the parents, but a vertex may have more
         * than one parent. Iterate over the parents and form an edge if and
         * only if the resulting graph is acyclic.
         */
        val points = (1 to graph.numVertices).map(root).distinct

        val start = if (graph.adjacencySet.nonEmpty) graph.adjacencySet.toList
                    else List(Edge(points.head,points.last))

        val edges = points.foldLeft(start)((acc, v) => {
          val edge = Edge(acc.head.from, v)
          val g0 = Graph(graph.numVertices, graph.adjacencySet + edge)
          val update = tsort(g0) match {
            case Some(_) => acc :+ Edge(acc.head.from, v)
            case None => acc
          }
          update
        })
        Graph(graph.numVertices,edges.toSet)
    }
    connected
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy