From 00174ae81df2a2838bc12280fae069526321be74 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Fri, 5 Jun 2026 21:30:28 -0500 Subject: [PATCH] WIP on a refactor and massive set of improvements --- .../main/scala/gs/graph/v0/Adjacency.scala | 376 +++++++++++++++--- .../src/main/scala/gs/graph/v0/Edge.scala | 13 + .../src/main/scala/gs/graph/v0/Graph.scala | 5 - .../scala/gs/graph/v0/GraphException.scala | 18 + .../scala/gs/graph/v0/GraphTraversal.scala | 95 +++-- .../scala/gs/graph/v0/UndirectedGraph.scala | 19 - .../main/scala/gs/graph/v0/data/DataDag.scala | 12 +- .../scala/gs/graph/v0/data/DataDigraph.scala | 12 +- .../main/scala/gs/graph/v0/directed/Dag.scala | 12 +- .../scala/gs/graph/v0/directed/Digraph.scala | 46 +-- .../graph/v0/directed/SingleRootDigraph.scala | 144 ------- .../directed/StronglyConnectedComponent.scala | 77 ++-- .../gs/graph/v0/UndirectedGraphTests.scala | 3 - 13 files changed, 476 insertions(+), 356 deletions(-) delete mode 100644 modules/core/src/main/scala/gs/graph/v0/directed/SingleRootDigraph.scala diff --git a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala index fcce9de..471322c 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala @@ -1,14 +1,32 @@ package gs.graph.v0 +import scala.collection.IndexedSeqView import scala.collection.mutable.ListBuffer -/** An immutable adjacency list representation. - * - * The index of the collection is the "from" [[Vertex]], and each element of - * the corresponding vector is a "to" [[Vertex]] -- there are edges _from_ some - * vertex _to_ another vertex. +/** An adjacency list representation. */ -final class Adjacency private (val neighbors: Vector[Vector[Vertex]]): +sealed trait Adjacency: + /** @return + * The number of vertices in this adjacency list. + */ + def numberOfVertices: Size + + /** @return + * The number of edges in this adjacency list. + */ + def numberOfEdges: Size + + /** @return + * View over the vertices in this adjacency. + */ + def vertices: IndexedSeqView[Vertex] + + /** @return + * The list of entries in this adjacency. Each entry is a vertex paired + * with the neighbors of that vertex. + */ + def entries: Map[Vertex, Vector[Vertex]] + /** Get the vector of [[Vertex]] that receive a connection _from_ the input * [[Vertex]]. * @@ -18,7 +36,16 @@ final class Adjacency private (val neighbors: Vector[Vector[Vertex]]): * The vector of [[Vertex]] that receive a connection _from_ the input * [[Vertex]]. */ - def at(vertex: Vertex): Vector[Vertex] = neighbors(vertex.ordinal) + def at(vertex: Vertex): Vector[Vertex] + + /** Determine if the given [[Vertex]] is represented within this adjacency. + * + * @param vertex + * The [[Vertex]] to investigate. + * @return + * `True` if the [[Vertex]] is contained, `false` otherwise. + */ + def contains(vertex: Vertex): Boolean /** Get the vector of _incoming_ [[Vertex]] that point _to_ some input * [[Vertex]]. @@ -28,57 +55,47 @@ final class Adjacency private (val neighbors: Vector[Vector[Vertex]]): * @return * The list of [[Vertex]] that have an edge _to_ the input [[Vertex]]. */ - def incoming(vertex: Vertex): Vector[Vertex] = - if vertex.ordinal >= neighbors.length then Vector.empty - else - neighbors.zipWithIndex - .filter { - // ignore the neighbors of the input vertex - case (_, index) => index != vertex.ordinal - } - .filter(_._1.contains(vertex)) - .map(_._2) - .map(Vertex(_)) + def incoming(vertex: Vertex): Vector[Vertex] /** Express this [[Adjacency]] as a vector of [[Edge]]. * * @return * The vector of [[Edge]] derived from this [[Adjacency]]. */ - def toEdges(): Vector[Edge] = - neighbors.zipWithIndex.flatMap { case (tos, index) => - val from = Vertex(index) - tos.map(to => Edge(from, to)).distinct - } + def toEdges(): Vector[Edge] - /** The number of vertices represented by this adjacency list. - */ - lazy val numberOfVertices: Size = Size.fromVector(neighbors) - - /** Perform a linear traversal for each [[gs.graph.v0.Vertex]] to calculate - * the total number of edges in this adjacency list. + /** Extract a subset of vertices and their neighbors, such that the neighbors + * are defined within the subset. * + * Throws an exception if any input [[Vertex]] is not a member of this + * adjacency. + * + * @param vertices + * The subset of [[Vertex]]. * @return - * The number of edges in this adjacency list. + * The subset adjacency. */ - lazy val numberOfEdges: Size = Size(neighbors.map(_.length).reduce(_ + _)) + def subset(vertices: Set[Vertex]): Adjacency = + val notContained = vertices.filterNot(contains) + if notContained.nonEmpty then + throw GraphException.AdjacencyDoesNotContainVertices( + vertices, + numberOfVertices + ) + else + MappedAdjacency.validate(vertices.map { vertex => + // Only take the neighbors that are represented within the subset. + vertex -> at(vertex).filter(vertices.contains) + }.toMap) - /** All vertices that do not have inbound connections. + /** @inheritDocs */ - lazy val roots: Vector[Vertex] = - val counts = Array.fill(neighbors.length)(0) - neighbors.foreach { ns => - // Each vertex listed here is receiving an inbound connection, if we - // assume the graph is directed. - ns.foreach(v => counts(v.ordinal) += 1) - } - counts.filter(_ == 0).map(Vertex(_)).toVector - override def equals(that: Any): Boolean = - that match - case other: Adjacency => - neighbors.length == other.neighbors.length - && neighbors + val tuple = (this, that) + tuple match + case (me: DefaultAdjacency, other: DefaultAdjacency) => + me.neighbors.length == other.neighbors.length + && me.neighbors .zip(other.neighbors) .map { case (x, y) => x.sameElements(y) } .forall(result => result) @@ -86,6 +103,8 @@ final class Adjacency private (val neighbors: Vector[Vector[Vertex]]): object Adjacency: + given CanEqual[Adjacency, Adjacency] = CanEqual.derived + /** Instantiate a new [[Adjacency]] list from a raw representation. * * @param adj @@ -93,7 +112,8 @@ object Adjacency: * @return * The new [[Adjacency]]. */ - def apply(adj: Vector[Vector[Vertex]]): Adjacency = new Adjacency(adj) + def apply(adj: Vector[Vector[Vertex]]): Adjacency = + DefaultAdjacency.apply(adj) /** Create an empty adjacency list for some number of vertices. No vertex has * any connections to another vertex. @@ -104,19 +124,156 @@ object Adjacency: * Some new empty adjacency. */ def noEdges(numberOfVertices: Size): Adjacency = - new Adjacency(Vector.fill(numberOfVertices.value)(Vector.empty)) - - given CanEqual[Adjacency, Adjacency] = CanEqual.derived + DefaultAdjacency.noEdges(numberOfVertices) /** @return * The empty [[Adjacency]] list. */ - final val Empty: Adjacency = new Adjacency(Vector.empty) + final val Empty: Adjacency = DefaultAdjacency.Empty /** @return * The single-vertex [[Adjacency]] list. */ - final val Single: Adjacency = new Adjacency(Vector(Vector.empty)) + final val Single: Adjacency = DefaultAdjacency.Single + + /** Calculate an [[Adjacency]] from some collection of [[Edge]]. + * + * @param numberOfVertices + * The number of [[Vertex]] (`N`) in this graph. + * @param edges + * The collection of [[Edge]] present in this graph. + * @return + * The calculated [[Adjacency]]. + */ + def fromEdges( + numberOfVertices: Size, + edges: Iterable[Edge] + ): Adjacency = + DefaultAdjacency.fromEdges(numberOfVertices, edges) + + /** Calculate an [[Adjacency]] from some collection of [[Edge]]. + * + * @param edges + * The collection of [[Edge]] present in this graph. + * @return + * The calculated [[Adjacency]]. + */ + def fromEdges( + edges: Iterable[Edge] + ): Adjacency = + DefaultAdjacency.fromEdges(edges) + +end Adjacency + +/** An immutable adjacency list representation. + * + * This implementation uses a 0-based indexing system, where each vertex has an + * index from 0 to N-1. See [[MappedAdjacency]] for an implementation that does + * not require a complete ordered set. + * + * The index of the collection is the "from" [[Vertex]], and each element of + * the corresponding vector is a "to" [[Vertex]] -- there are edges _from_ some + * vertex _to_ another vertex. + * + * @param numberOfVertices + * The number of vertices represented by this [[Adjacency]]. + * @param neighbors + * The complete set of relationships. + */ +final class DefaultAdjacency private ( + val numberOfVertices: Size, + val neighbors: Vector[Vector[Vertex]] +) extends Adjacency: + + /** @inheritDocs + */ + override def vertices: IndexedSeqView[Vertex] = + (0 until numberOfVertices.value).view.map(Vertex(_)) + + /** @inheritDocs + */ + override def entries: Map[Vertex, Vector[Vertex]] = + neighbors.zipWithIndex.map { case (ns, index) => Vertex(index) -> ns }.toMap + + /** @inheritDocs + */ + override def at(vertex: Vertex): Vector[Vertex] = + if vertex < numberOfVertices then neighbors(vertex.ordinal) + else Vector.empty + + /** @inheritDocs + */ + override def contains(vertex: Vertex): Boolean = + vertex < numberOfVertices + + /** @inheritDocs + */ + override def incoming(vertex: Vertex): Vector[Vertex] = + if vertex >= numberOfVertices then Vector.empty + else + neighbors.zipWithIndex + .filter { + // ignore the neighbors of the input vertex + // select those that reference the input + case (values, index) => + (index != vertex.ordinal) && values.contains(vertex) + } + .map(_._2) + .map(Vertex(_)) + + /** @inheritDocs + */ + override def toEdges(): Vector[Edge] = + neighbors.zipWithIndex.flatMap { case (tos, index) => + val from = Vertex(index) + tos.map(to => Edge(from, to)).distinct + } + + /** @inheritDocs + */ + override lazy val numberOfEdges: Size = Size( + neighbors.map(_.length).reduce(_ + _) + ) + +object DefaultAdjacency: + + /** Instantiate a new [[Adjacency]] list from a raw representation. + * + * @param adj + * The underlying vector of values. + * @return + * The new [[Adjacency]]. + */ + def apply(adj: Vector[Vector[Vertex]]): Adjacency = + new DefaultAdjacency( + numberOfVertices = Size.fromVector(adj), + neighbors = adj + ) + + /** Create an empty adjacency list for some number of vertices. No vertex has + * any connections to another vertex. + * + * @param numberOfVertices + * The number of vertices in this disconnected graph. + * @return + * Some new empty adjacency. + */ + def noEdges(numberOfVertices: Size): Adjacency = + new DefaultAdjacency( + numberOfVertices = numberOfVertices, + neighbors = Vector.fill(numberOfVertices.value)(Vector.empty) + ) + + /** @return + * The empty [[Adjacency]] list. + */ + final val Empty: Adjacency = new DefaultAdjacency(Size.Zero, Vector.empty) + + /** @return + * The single-vertex [[Adjacency]] list. + */ + final val Single: Adjacency = + new DefaultAdjacency(Size.One, Vector(Vector.empty)) /** Calculate an [[Adjacency]] from some collection of [[Edge]]. * @@ -140,7 +297,7 @@ object Adjacency: else val _ = buffs(edge.from.ordinal).addOne(edge.to) } - new Adjacency(buffs.map(_.distinct.toVector)) + new DefaultAdjacency(numberOfVertices, buffs.map(_.distinct.toVector)) /** Calculate an [[Adjacency]] from some collection of [[Edge]]. * @@ -157,7 +314,7 @@ object Adjacency: val _ = edges.foreach { edge => val _ = buffs(edge.from.ordinal).addOne(edge.to) } - new Adjacency(buffs.map(_.distinct.toVector)) + new DefaultAdjacency(Size(numberOfVertices), buffs.map(_.distinct.toVector)) private def findMaximumVertex(edges: Iterable[Edge]): Vertex = var maximum = Vertex.Zero @@ -167,4 +324,115 @@ object Adjacency: } maximum -end Adjacency +end DefaultAdjacency + +/** An immutable adjacency list implementation. + * + * This implementation maps [[Vertex]] values to lists of their neighbors. + * + * @param numberOfVertices + * The number of vertices contained in the adjacency. + * @param neighbors + * The list of neighbors for each vertex in the adjacency. + */ +final class MappedAdjacency private ( + val numberOfVertices: Size, + val neighbors: Map[Vertex, Vector[Vertex]] +) extends Adjacency: + + /** @inheritDocs + */ + override def vertices: IndexedSeqView[Vertex] = + neighbors.keySet.toIndexedSeq.view + + /** @inheritDocs + */ + override def entries: Map[Vertex, Vector[Vertex]] = + neighbors + + /** @inheritDocs + */ + override lazy val numberOfEdges: Size = + Size(neighbors.map(_._2.length).reduce(_ + _)) + + /** @inheritDocs + */ + override def at(vertex: Vertex): Vector[Vertex] = + neighbors.get(vertex).getOrElse(Vector.empty) + + /** @inheritDocs + */ + override def contains(vertex: Vertex): Boolean = + neighbors.contains(vertex) + + /** @inheritDocs + */ + override def incoming(vertex: Vertex): Vector[Vertex] = + neighbors.view + .filter { + // ignore the neighbors of the input vertex + // select those that reference the input + case (key, values) => (key != vertex) && values.contains(vertex) + } + .map(_._1) + .toVector + + /** @inheritDocs + */ + override def toEdges(): Vector[Edge] = + neighbors.view.flatMap { case (from, tos) => + tos.map(to => Edge(from, to)).distinct + }.toVector + +object MappedAdjacency: + + final val Empty: Adjacency = new MappedAdjacency(Size.Zero, Map.empty) + + def single(vertex: Vertex): Adjacency = + new MappedAdjacency(Size.One, Map(vertex -> Vector.empty)) + + /** Validate the given mapped neighbors. Enforces that all neighbors of some + * vertex _must_ be keys of the map -- neighbors can only be vertices that + * are part of the same graph. + * + * Throws an exception if the input is invalid. + * + * @param neighbors + * The mapped neighbors. + * @return + * The new [[Adjacency]]. + */ + def validate( + neighbors: Map[Vertex, Vector[Vertex]] + ): Adjacency = + val vertices = neighbors.keySet + val _ = neighbors.foreach { case (v, ns) => + ns.foreach { candidate => + if !vertices.contains(candidate) then + throw GraphException.NeighborNotContainedByAdjacency( + candidate = candidate, + vertex = v + ) + else () + } + } + new MappedAdjacency(Size.of(neighbors), neighbors) + + /** Instantiate a new [[MappedAdjacency]] based on the given edges. + * + * @param edges + * The edges that represent the adjacency. + * @return + * the new adjacency instance. + */ + def fromEdges( + edges: Iterable[Edge] + ): Adjacency = + val acc = scala.collection.mutable.Map.empty[Vertex, ListBuffer[Vertex]] + edges.foreach { edge => + acc.getOrElseUpdate(edge.from, ListBuffer.empty).addOne(edge.to) + } + val neighbors = acc.map { case (v, ns) => v -> ns.toVector }.toMap + new MappedAdjacency(Size.of(neighbors), neighbors) + +end MappedAdjacency diff --git a/modules/core/src/main/scala/gs/graph/v0/Edge.scala b/modules/core/src/main/scala/gs/graph/v0/Edge.scala index bd3ab5b..8ec31c0 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Edge.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Edge.scala @@ -105,6 +105,19 @@ object Edge: v2: Int ): Edge = Edge(Vertex(v1), Vertex(v2)) + /** Instantiate a new Edge. + * + * Throws an exception if the given vertices are the same or are not valid + * [[Vertex]] values. + * + * @param pair + * The pair to convert to an edge. + * @return + * The new edge. + */ + def fromPair(pair: (Int, Int)): Edge = + Edge(Vertex(pair._1), Vertex(pair._2)) + /** Instantiate a list of Edge. * * @param edges diff --git a/modules/core/src/main/scala/gs/graph/v0/Graph.scala b/modules/core/src/main/scala/gs/graph/v0/Graph.scala index acd3e3c..f5a7258 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Graph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Graph.scala @@ -46,11 +46,6 @@ trait Graph: */ def isSingle: Boolean = numberOfVertices == Size.One - /** @return - * The roots that should be used for traversal operations. - */ - def selectRoots(): Vector[Vertex] - /** Get the neighbors for any given vertex. * * @param vertex diff --git a/modules/core/src/main/scala/gs/graph/v0/GraphException.scala b/modules/core/src/main/scala/gs/graph/v0/GraphException.scala index 0f43543..ff4a8fb 100644 --- a/modules/core/src/main/scala/gs/graph/v0/GraphException.scala +++ b/modules/core/src/main/scala/gs/graph/v0/GraphException.scala @@ -31,4 +31,22 @@ object GraphException: but that root exceeds the graph bound of '$numberOfVertices' vertices.""".stripMargin + case class AdjacencyDoesNotContainVertices( + vertices: Iterable[Vertex], + numberOfVertices: Size + ) extends GraphException: + + override def getMessage(): String = + s"""The following vertices were not found within an Adjacency of size + '$numberOfVertices': [${vertices.mkString(", ")}]""".stripMargin + + case class NeighborNotContainedByAdjacency( + candidate: Vertex, + vertex: Vertex + ) extends GraphException: + + override def getMessage(): String = + s"""Attempted to reference neighbor '$candidate' of vertex '$vertex', but + '$candidate' is not a valid vertex in the adjacency.""".stripMargin + end GraphException diff --git a/modules/core/src/main/scala/gs/graph/v0/GraphTraversal.scala b/modules/core/src/main/scala/gs/graph/v0/GraphTraversal.scala index 56f707c..0b1d407 100644 --- a/modules/core/src/main/scala/gs/graph/v0/GraphTraversal.scala +++ b/modules/core/src/main/scala/gs/graph/v0/GraphTraversal.scala @@ -159,16 +159,18 @@ object GraphTraversal: val s = Stack.empty[Vertex] val discovered = Array.fill(graph.numberOfVertices.value)(false) - graph.selectRoots().foreach { root => - val _ = s.push(root) - while !s.isEmpty - do - val v = s.pop() - if !discovered(v.ordinal) then - val _ = output.addOne(visit(v, graph.data(v.ordinal))) - discovered(v.ordinal) = true - graph.neighbors(v).foreach(w => s.push(w)) - else () + graph.view.foreach { root => + if !discovered(root.ordinal) then + val _ = s.push(root) + while !s.isEmpty + do + val v = s.pop() + if !discovered(v.ordinal) then + val _ = output.addOne(visit(v, graph.data(v.ordinal))) + discovered(v.ordinal) = true + val _ = graph.neighbors(v).reverseIterator.foreach(w => s.push(w)) + else () + else () } output.toList @@ -283,20 +285,24 @@ object GraphTraversal: ): Unit = val q = Queue.empty[Vertex] val visited = Array.fill(graph.numberOfVertices.value)(false) - val _ = graph.selectRoots().foreach(q.enqueue) - while !q.isEmpty - do - val v = q.dequeue() - if !visited(v.ordinal) then - val _ = visit(v, graph.data(v.ordinal)) - visited(v.ordinal) = true - graph.neighbors(v).foreach { neighbor => - if !visited(neighbor.ordinal) then q.enqueue(neighbor) + graph.view.foreach { root => + if !visited(root.ordinal) then + val _ = q.enqueue(root) + while !q.isEmpty + do + val v = q.dequeue() + if !visited(v.ordinal) then + val _ = visit(v, graph.data(v.ordinal)) + visited(v.ordinal) = true + graph.neighbors(v).foreach { neighbor => + if !visited(neighbor.ordinal) then q.enqueue(neighbor) + else () + } else () - } + () else () - () + } def bfs[A, Out]( graph: AnyGraphWithData[A], @@ -305,19 +311,23 @@ object GraphTraversal: val output = ListBuffer.empty[Out] val q = Queue.empty[Vertex] val visited = Array.fill(graph.numberOfVertices.value)(false) - val _ = graph.selectRoots().foreach(q.enqueue) - while !q.isEmpty - do - val v = q.dequeue() - if !visited(v.ordinal) then - val _ = output.addOne(visit(v, graph.data(v.ordinal))) - visited(v.ordinal) = true - graph.neighbors(v).foreach { neighbor => - if !visited(neighbor.ordinal) then q.enqueue(neighbor) + graph.view.foreach { root => + if !visited(root.ordinal) then + val _ = q.enqueue(root) + while !q.isEmpty + do + val v = q.dequeue() + if !visited(v.ordinal) then + val _ = output.addOne(visit(v, graph.data(v.ordinal))) + visited(v.ordinal) = true + graph.neighbors(v).foreach { neighbor => + if !visited(neighbor.ordinal) then q.enqueue(neighbor) + else () + } else () - } else () + } output.toList @@ -329,19 +339,22 @@ object GraphTraversal: var acc = initial val q = Queue.empty[Vertex] val visited = Array.fill(graph.numberOfVertices.value)(false) - val _ = graph.selectRoots().foreach(q.enqueue) - while !q.isEmpty - do - val v = q.dequeue() - if !visited(v.ordinal) then - acc = f(acc, graph.data(v.ordinal)) - visited(v.ordinal) = true - graph.neighbors(v).foreach { neighbor => - if !visited(neighbor.ordinal) then q.enqueue(neighbor) + graph.view.foreach { root => + if !visited(root.ordinal) then + while !q.isEmpty + do + val v = q.dequeue() + if !visited(v.ordinal) then + acc = f(acc, graph.data(v.ordinal)) + visited(v.ordinal) = true + graph.neighbors(v).foreach { neighbor => + if !visited(neighbor.ordinal) then q.enqueue(neighbor) + else () + } else () - } else () + } acc diff --git a/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala b/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala index 4e52df4..dd3d87c 100644 --- a/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala @@ -15,25 +15,6 @@ class UndirectedGraph( */ final override val disposition: GraphDisposition = GraphDisposition.Undirected - /** @inheritDocs - */ - override def selectRoots(): Vector[Vertex] = roots - - /** The lazily calculated roots of this graph. - * - * In an undirected graph, the roots are calculated by selecting an arbitrary - * vertex from each connected component in the graph. - */ - lazy val roots: Vector[Vertex] = - calculateRoots() - - private def calculateRoots(): Vector[Vertex] = - if numberOfVertices == Size.Zero then Vector.empty - else oneFromEachConnectedComponent().toVector - - private def oneFromEachConnectedComponent(): List[Vertex] = - ConnectedComponent.findAll(this).map(_.root) - end UndirectedGraph object UndirectedGraph: diff --git a/modules/core/src/main/scala/gs/graph/v0/data/DataDag.scala b/modules/core/src/main/scala/gs/graph/v0/data/DataDag.scala index 2899a5c..400fc19 100644 --- a/modules/core/src/main/scala/gs/graph/v0/data/DataDag.scala +++ b/modules/core/src/main/scala/gs/graph/v0/data/DataDag.scala @@ -3,7 +3,6 @@ package gs.graph.v0.data import gs.graph.v0.Adjacency import gs.graph.v0.GraphException import gs.graph.v0.Size -import gs.graph.v0.Vertex import gs.graph.v0.directed.Dag /** Specialization of [[Dag]] that associates _data_ with _each [[Vertex]]_. @@ -11,9 +10,8 @@ import gs.graph.v0.directed.Dag final class DataDag[A] private ( val data: Vector[A], n: Size, - a: Adjacency, - r: Vector[Vertex] -) extends Dag(n, a, r) + a: Adjacency +) extends Dag(n, a) with AnyGraphWithData[A] object DataDag: @@ -25,8 +23,7 @@ object DataDag: new DataDag[A]( Vector.empty, Size.Zero, - Adjacency.Empty, - Vector.empty + Adjacency.Empty ) /** Instantiate a new [[DataDag]] given an input [[Dag]] and some data. @@ -54,6 +51,5 @@ object DataDag: new DataDag( data, dag.numberOfVertices, - dag.adjacency, - dag.roots + dag.adjacency ) diff --git a/modules/core/src/main/scala/gs/graph/v0/data/DataDigraph.scala b/modules/core/src/main/scala/gs/graph/v0/data/DataDigraph.scala index 42e3e41..f026631 100644 --- a/modules/core/src/main/scala/gs/graph/v0/data/DataDigraph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/data/DataDigraph.scala @@ -3,7 +3,6 @@ package gs.graph.v0.data import gs.graph.v0.Adjacency import gs.graph.v0.GraphException import gs.graph.v0.Size -import gs.graph.v0.Vertex import gs.graph.v0.directed.Digraph /** Specialization of [[Digraph]] that associates _data_ with _each [[Vertex]]_. @@ -11,9 +10,8 @@ import gs.graph.v0.directed.Digraph final class DataDigraph[A] private ( val data: Vector[A], n: Size, - a: Adjacency, - r: Vector[Vertex] -) extends Digraph(n, a, r) + a: Adjacency +) extends Digraph(n, a) with AnyGraphWithData[A] object DataDigraph: @@ -25,8 +23,7 @@ object DataDigraph: new DataDigraph[A]( Vector.empty, Size.Zero, - Adjacency.Empty, - Vector.empty + Adjacency.Empty ) /** Instantiate a new [[DataDigraph]] given an input [[Digraph]] and some @@ -55,6 +52,5 @@ object DataDigraph: new DataDigraph( data, digraph.numberOfVertices, - digraph.adjacency, - digraph.roots + digraph.adjacency ) diff --git a/modules/core/src/main/scala/gs/graph/v0/directed/Dag.scala b/modules/core/src/main/scala/gs/graph/v0/directed/Dag.scala index a2892f6..33a7df1 100644 --- a/modules/core/src/main/scala/gs/graph/v0/directed/Dag.scala +++ b/modules/core/src/main/scala/gs/graph/v0/directed/Dag.scala @@ -3,7 +3,6 @@ package gs.graph.v0.directed import gs.graph.v0.Adjacency import gs.graph.v0.GraphError import gs.graph.v0.Size -import gs.graph.v0.Vertex /** Directed Acyclic Graph (DAG): Represents a [[Digraph]] with no cycles. * @@ -12,21 +11,20 @@ import gs.graph.v0.Vertex */ class Dag protected ( n: Size, - a: Adjacency, - r: Vector[Vertex] -) extends Digraph(n, a, r) + a: Adjacency +) extends Digraph(n, a) object Dag: /** The empty DAG. */ final val Empty: Dag = - new Dag(Size.Zero, Adjacency.Empty, Vector.empty) + new Dag(Size.Zero, Adjacency.Empty) /** DAG with a single vertex. */ final val Single: Dag = - new Dag(Size.One, Adjacency.Single, Vector(Vertex.Zero)) + new Dag(Size.One, Adjacency.Single) given CanEqual[Dag, Dag] = CanEqual.derived given CanEqual[Dag, Digraph] = CanEqual.derived @@ -42,7 +40,7 @@ object Dag: def validate(digraph: Digraph): Either[GraphError, Dag] = if !Digraph.hasCycle(digraph) then Right( - new Dag(digraph.numberOfVertices, digraph.adjacency, digraph.roots) + new Dag(digraph.numberOfVertices, digraph.adjacency) ) else Left(GraphError.CycleFound) diff --git a/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala b/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala index 9e3bd40..7fa4216 100644 --- a/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/directed/Digraph.scala @@ -13,22 +13,15 @@ import gs.graph.v0.Vertex * The number of [[Vertex]] present in this graph. * @param adjacency * The [[Adjacency]] that describes this graph. - * @param roots - * Complete collection of root vertices. */ class Digraph( val numberOfVertices: Size, - val adjacency: Adjacency, - val roots: Vector[Vertex] + val adjacency: Adjacency ) extends Graph: /** @inheritDocs */ override val disposition: GraphDisposition = GraphDisposition.Directed - /** @inheritDocs - */ - override def selectRoots(): Vector[Vertex] = roots - /** @inheritDocs */ override def equals(that: Any): Boolean = @@ -52,13 +45,13 @@ object Digraph: * An empty [[Digraph]]. */ final val Empty: Digraph = - new Digraph(Size.Zero, Adjacency.Empty, Vector.empty) + new Digraph(Size.Zero, Adjacency.Empty) /** @return * A [[Digraph]] with one vertex. */ final val Single: Digraph = - new Digraph(Size.One, Adjacency.Single, Vector(Vertex.Zero)) + new Digraph(Size.One, Adjacency.Single) def fromEdges( numberOfVertices: Size, @@ -66,8 +59,7 @@ object Digraph: ): Digraph = new Digraph( numberOfVertices = numberOfVertices, - adjacency = Adjacency.fromEdges(numberOfVertices, edges), - roots = findRootsForDirectedEdges(numberOfVertices, edges) + adjacency = Adjacency.fromEdges(numberOfVertices, edges) ) /** Instantiate a new [[Graph]] from the given [[Adjacency]]. @@ -82,37 +74,9 @@ object Digraph: ): Digraph = new Digraph( numberOfVertices = adjacency.numberOfVertices, - adjacency = adjacency, - roots = adjacency.roots + adjacency = adjacency ) - /** Find all roots for the given collection of [[Edge]]. - * - * This function assumes directed edges. - * - * @param numberOfVertices - * The number of vertices in the graph represented by the given edges. - * @param edges - * The collection of edges to analyze. - * @return - * The vector of roots for the given edges. - */ - def findRootsForDirectedEdges( - numberOfVertices: Size, - edges: Iterable[Edge] - ): Vector[Vertex] = - // Count of incoming relationships. - val counts = Array.fill(numberOfVertices.value)(0) - edges.foreach(edge => counts(edge.to.ordinal) += 1) - - // If there are any vertices with no incoming connections, those are roots. - // Note that these may be completely disconnected. - counts.zipWithIndex - .filter(_._1 == 0) - .map { case (_, index) => Vertex(index) } - .toVector - .distinct - /** Determine whether the given [[Digraph]] has any cycles. * * See: [[Dag]] diff --git a/modules/core/src/main/scala/gs/graph/v0/directed/SingleRootDigraph.scala b/modules/core/src/main/scala/gs/graph/v0/directed/SingleRootDigraph.scala deleted file mode 100644 index 111a3f6..0000000 --- a/modules/core/src/main/scala/gs/graph/v0/directed/SingleRootDigraph.scala +++ /dev/null @@ -1,144 +0,0 @@ -package gs.graph.v0.directed - -import gs.graph.v0.Adjacency -import gs.graph.v0.Edge -import gs.graph.v0.GraphException -import gs.graph.v0.Size -import gs.graph.v0.Vertex - -/** Specialization of [[Digraph]] that always has a single root vertex. - * - * @param n - * The number of [[Vertex]] present in this graph. - * @param a - * The [[Adjacency]] that describes this graph. - * @param r - * The singular root [[Vertex]]. - */ -class SingleRootDigraph( - n: Size, - a: Adjacency, - root: Vertex -) extends Digraph(n, a, Vector(root)) - -object SingleRootDigraph: - - /** Attempt to show that the given [[Digraph]] has a single root. - * - * @param dg - * The input [[Digraph]]. - * @return - * [[SingleRootDigraph]] or `None` if the number of roots is not 1. - */ - def fromDirectedGraph(dg: Digraph): Option[SingleRootDigraph] = - if dg.roots.size == 1 then - Some( - new SingleRootDigraph( - dg.numberOfVertices, - dg.adjacency, - dg.roots(0) - ) - ) - else None - - /** Given some edges, build a [[SingleRootDigraph]]. Throw an exception if - * this operation fails. - * - * @param numberOfVertices - * The number of [[Vertex]] in the graph. - * @param edges - * The collection of [[Edge]] that describe the graph. - * @param root - * The root [[Vertex]]. - * @return - * The new [[SingleRootDigraph]]. - */ - def fromEdgesUnsafe( - numberOfVertices: Size, - edges: Iterable[Edge], - root: Vertex - ): SingleRootDigraph = - if root < numberOfVertices then - new SingleRootDigraph( - numberOfVertices, - Adjacency.fromEdges(numberOfVertices, edges), - root - ) - else throw GraphException.RootOutOfBounds(root, numberOfVertices) - - /** Given some edges, build a [[SingleRootDigraph]] if that collection - * represents a graph with a single root. - * - * @param numberOfVertices - * The number of [[Vertex]] in the graph. - * @param edges - * The collection of [[Edge]] that describe the graph. - * @return - * The new [[SingleRootDigraph]], or `None` if the edges do not describe a - * digraph with a single root. - */ - def fromEdges( - numberOfVertices: Size, - edges: Iterable[Edge] - ): Option[SingleRootDigraph] = - val roots = Digraph.findRootsForDirectedEdges(numberOfVertices, edges) - if roots.size == 1 then - Some( - new SingleRootDigraph( - numberOfVertices, - Adjacency.fromEdges(numberOfVertices, edges), - roots(0) - ) - ) - else None - - /** Given some [[Adjacency]] and a given root [[Vertex]], instantiate a new - * [[SingleRootDigraph]]. - * - * Throws an exception if the given root is not contained within the - * [[Adjacency]]. - * - * @param adjacency - * The [[Adjacency]] which describes the graph. - * @param root - * The root [[Vertex]]. - * @return - * New [[SingleRootDigraph]] - */ - def fromAdjacencyUnsafe( - adjacency: Adjacency, - root: Vertex - ): SingleRootDigraph = - if root < adjacency.numberOfVertices then - new SingleRootDigraph( - adjacency.numberOfVertices, - adjacency, - root - ) - else throw GraphException.RootOutOfBounds(root, adjacency.numberOfVertices) - - /** Given some [[Adjacency]] and a given root [[Vertex]], instantiate a new - * [[SingleRootDigraph]] if the [[Vertex]] is within the graph.. - * - * @param adjacency - * The [[Adjacency]] which describes the graph. - * @param root - * The root [[Vertex]]. - * @return - * New [[SingleRootDigraph]], or `None` if the given root is not valid. - */ - def fromAdjacency( - adjacency: Adjacency - ): Option[SingleRootDigraph] = - val roots = adjacency.roots - if roots.size == 1 then - Some( - new SingleRootDigraph( - adjacency.numberOfVertices, - adjacency, - roots(0) - ) - ) - else None - -end SingleRootDigraph diff --git a/modules/core/src/main/scala/gs/graph/v0/directed/StronglyConnectedComponent.scala b/modules/core/src/main/scala/gs/graph/v0/directed/StronglyConnectedComponent.scala index 37b28f8..2e9dc28 100644 --- a/modules/core/src/main/scala/gs/graph/v0/directed/StronglyConnectedComponent.scala +++ b/modules/core/src/main/scala/gs/graph/v0/directed/StronglyConnectedComponent.scala @@ -1,5 +1,8 @@ package gs.graph.v0.directed +import gs.graph.v0.Adjacency +import gs.graph.v0.Edge +import gs.graph.v0.MappedAdjacency import gs.graph.v0.Vertex import scala.collection.mutable.ListBuffer import scala.collection.mutable.Stack @@ -10,49 +13,50 @@ import scala.collection.mutable.Stack * * This class is used with [[Digraph]]. See [[gs.graph.v0.ConnectedComponent]] * for general connected components. - * - * TODO: ADD EDGES */ -final class StronglyConnectedComponent private (val value: Set[Vertex]): +final class StronglyConnectedComponent private ( + val vertices: Set[Vertex], + val adjacency: Adjacency +): /** @inheritDocs */ override def equals(obj: Any): Boolean = obj match - case other: StronglyConnectedComponent => value.equals(other.value) - case _ => false + case other: StronglyConnectedComponent => + vertices.equals(other.vertices) && adjacency.equals(other.adjacency) + case _ => false /** @inheritDocs */ - override def hashCode(): Int = value.hashCode() + override def hashCode(): Int = adjacency.hashCode() /** @inheritDocs */ override def toString(): String = val sb = StringBuilder() - sb.append("[") - sb.append(value.mkString(",")) - sb.append("]") + sb.append("entries:[\n") + sb.append( + adjacency.entries + .map { case (v, ns) => s"$v -> ${ns.mkString(",")}" } + .mkString("\n") + ) + sb.append("\n]") sb.toString() /** @return * The number of vertices contained by this SCC. */ - def size: Int = value.size + lazy val size: Int = vertices.size /** @return * True if this SCC contains a single vertex. */ - def isSingle: Boolean = value.size == 1 - - /** @return - * The set of vertices in the SCC. - */ - def vertices: Set[Vertex] = value + def isSingle: Boolean = size == 1 /** Retrieve an arbitrary SCC member as the root vertex. */ - lazy val root: Vertex = value.toList.head + lazy val root: Vertex = vertices.toList.head end StronglyConnectedComponent @@ -67,19 +71,29 @@ object StronglyConnectedComponent: * @return * The new SCC. */ - def apply(value: Vertex*): StronglyConnectedComponent = - if value.isEmpty then + def apply(connections: Edge*): StronglyConnectedComponent = + if connections.isEmpty then throw new IllegalArgumentException( "Empty strongly connected components (SCC) are not allowed." ) - else new StronglyConnectedComponent(value.toSet) + else + val adj = MappedAdjacency.fromEdges(connections) + new StronglyConnectedComponent( + vertices = adj.vertices.toSet, + adjacency = adj + ) - def of(value: Int*): StronglyConnectedComponent = + def of(value: (Int, Int)*): StronglyConnectedComponent = if value.isEmpty then throw new IllegalArgumentException( "Empty strongly connected components (SCC) are not allowed." ) - else new StronglyConnectedComponent(value.map(Vertex.apply).toSet) + else + val adj = MappedAdjacency.fromEdges(value.map(Edge.fromPair)) + new StronglyConnectedComponent( + vertices = adj.vertices.toSet, + adjacency = adj + ) /** Instantiate a new SCC that contains a single [[Vertex]]. * @@ -89,7 +103,10 @@ object StronglyConnectedComponent: * The new SCC. */ def single(value: Vertex): StronglyConnectedComponent = - new StronglyConnectedComponent(Set(value)) + new StronglyConnectedComponent( + vertices = Set(value), + adjacency = MappedAdjacency.single(value) + ) given CanEqual[StronglyConnectedComponent, StronglyConnectedComponent] = CanEqual.derived @@ -201,9 +218,17 @@ object StronglyConnectedComponent: sccVertex = s.pop() scc.addOne(sccVertex) - if !scc.isEmpty then - output.addOne(new StronglyConnectedComponent(scc.toSet)) - else println("ERROR") + + // These accumulated vertices make up the SCC. + val vertices = scc.toSet + + output.addOne( + new StronglyConnectedComponent( + vertices = scc.toSet, + // Select the subset of the adjacency which is relevant to the SCC. + adjacency = g.adjacency.subset(vertices) + ) + ) else () /** Mutable state wrapper around an integer. diff --git a/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala b/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala index def71cc..635a51d 100644 --- a/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala @@ -9,7 +9,6 @@ class UndirectedGraphTests extends FunSuite: assertEquals(g.numberOfVertices, Size.Zero) assertEquals(g.adjacency, Adjacency.Empty) assertEquals(g.disposition, GraphDisposition.Undirected) - assertEquals(g.selectRoots(), Vector.empty) } test("should construct a graph based on # of edges and edge list") { @@ -24,7 +23,6 @@ class UndirectedGraphTests extends FunSuite: assertEquals(g.numberOfVertices, N) assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2))) assertEquals(g.neighbors(Vertex(4)), Vector.empty) - assertEquals(g.selectRoots(), Vector(Vertex.Zero)) } test("should construct a graph based on edge list") { @@ -38,7 +36,6 @@ class UndirectedGraphTests extends FunSuite: assertEquals(g.numberOfVertices, N) assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2))) assertEquals(g.neighbors(Vertex(4)), Vector.empty) - assertEquals(g.selectRoots(), Vector(Vertex.Zero)) } test("should calculate roots for each connected component") {