Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Longest path #79

Merged
merged 9 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Graphoscope/Graphoscope.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<Compile Include="Measures/Eccentricity.fs" />
<Compile Include="Measures\Radius.fs" />
<Compile Include="Measures\Diameter.fs" />
<Compile Include="Measures/LongestPath.fs" />
<Compile Include="RandomModels/Gilbert.fs" />
<Compile Include="RandomModels\BollobasRiordan.fs" />
<Compile Include="RandomModels\ErdosRenyi.fs" />
Expand Down
189 changes: 189 additions & 0 deletions src/Graphoscope/Measures/LongestPath.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
namespace Graphoscope.Measures

open Graphoscope
open System.Collections.Generic

type LongestPath() =

Check warning on line 6 in src/Graphoscope/Measures/LongestPath.fs

View check run for this annotation

Codecov / codecov/patch

src/Graphoscope/Measures/LongestPath.fs#L6

Added line #L6 was not covered by tests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LibraChris nitpick: you probably didn't need the constructor on this type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. But this is implemented because every type annotation in this library includes this constructor. It's simply designed to conform to the established schema.


/// <summary>
/// Determines whether a cycle can be formed in the given FGraph starting from the specified node,
/// using an algorithm based on priority queues and breadth-first traversal.
/// </summary>
/// <param name="starting">The node from which to check for the existence of a cycle.</param>
/// <param name="graph">The FGraph in which to search for the cycle.</param>
/// <returns>True if a cycle is found, otherwise false.</returns>
static member hasCycleFromNodeInFGraph (starting : 'NodeKey) (graph : FGraph<'NodeKey, 'NodeData, 'EdgeData>) =
let visited = HashSet<'NodeKey>()
//let stack = Stack<'NodeKey>()
let priorityQueue: Queue<('NodeKey)> = System.Collections.Generic.Queue()//Priority_Queue.SimplePriorityQueue<('NodeKey*float),float>()

//stack.Push(starting)
priorityQueue.Enqueue(starting)
visited.Add(starting) |> ignore


let rec outerLoop counter =
if priorityQueue.Count>0 then
//if stack.Count > 0 then
let nodeKey = priorityQueue.Dequeue() //et nodeKey = (stack.Pop())
let (_, nd, s) = graph.[nodeKey]

printfn $"{nodeKey}"
let rec innerLoops counter =
if counter=s.Count then
outerLoop (counter+1)
else
let node = s.Keys|>Seq.item counter
if node=starting then
true
elif not(visited.Contains(node)) then
//stack.Push(node)
priorityQueue.Enqueue(node)
visited.Add(node) |> ignore
innerLoops (counter+1)
else
innerLoops (counter+1)
innerLoops 0
else
false
outerLoop 0

/// <summary>
/// Determines whether a cycle exists in the given FGraph.
/// </summary>
/// <param name="graph">The FGraph to check for the presence of a cycle</param>
/// <returns>True if a cycle is found, otherwise false.</returns>
static member hasCycleInFGraph (graph : FGraph<'NodeKey, 'NodeData, 'EdgeData>) =

let nodes = graph|>Seq.map(fun kvp -> kvp.Key)

let rec isCyclic (counter:int) =
if counter = (nodes|>Seq.length) then
false
else
let node = nodes |>Seq.item counter
if LongestPath.hasCycleFromNodeInFGraph node graph then
true
else
isCyclic (counter+1)
isCyclic 0

/// <summary>
/// Computes longest paths from <paramref name="starting"/> for <paramref name="graph"/> using an inverse Dijkstra's algorithm.
/// </summary>
/// <param name="starting"> Calculate the longest paths from this node.</param>
/// <param name="getEdgeWeight"> Function to convert the EdgeData to a float.</param>
/// <param name="graph"> The FGraph for which to compute the longest path. Has to be an Directed Acyclic Graph.</param>
/// <remarks>If there isn't a path between two edges, the distance is set to `infinity`.</remarks>
/// <returns>Tuples of an ordered list containing the path and the distance of the longest path.</returns>
static member getLongestPathOfFGraphUnchecked (starting : 'NodeKey) (getEdgeWeight : 'EdgeData -> float) (graph : FGraph<'NodeKey, 'NodeData, 'EdgeData> ) =

//Inverse Version of Djikstra, transforming the edgeWeights into negatives
let getLongestPathDictOfFGraph (starting : 'NodeKey) (getEdgeWeight : 'EdgeData -> float) (graph : FGraph<'NodeKey, 'NodeData, 'EdgeData> ) =
let distance = Dictionary<'NodeKey, ('NodeKey*float)>()
//let priorityQueue = SortedSet<'NodeKey * float>(Comparer<'NodeKey * float>.Create(fun (_, d1) (_, d2) -> compare d1 d2))
let priorityQueue: Queue<('NodeKey * float)> = System.Collections.Generic.Queue()//Priority_Queue.SimplePriorityQueue<('NodeKey*float),float>()
let infinity = System.Double.MaxValue

// Initialize distances to infinity for all nodes except the starting node
// TODO: this can be improved by getOrDefault
for nodeKey in graph.Keys do
if nodeKey = starting then
distance.[nodeKey] <- (starting,0.)
else
distance.[nodeKey] <- (starting,infinity)

priorityQueue.Enqueue((starting, 0.)) |> ignore

while priorityQueue.Count > 0 do
let (currentNode, currentDistance) = priorityQueue.Dequeue()

let (_, _, predecessors) = graph.[currentNode]

for kv in predecessors do
if kv.Key <> currentNode then
let kvValue = kv.Value |> getEdgeWeight |> fun x -> -x
//if kvValue < 0. then failwithf "Dijkstra does not handle neg. edge weight"
let totalDistance = (currentDistance + kvValue) // Assuming edgeWeight is always 1 in this example
let prevNode,prevDistance = distance.[kv.Key]

// Improve getValue
if totalDistance < prevDistance then
distance.[kv.Key] <- (currentNode,totalDistance)
priorityQueue.Enqueue(kv.Key,totalDistance) |> ignore
Seq.sortBy snd priorityQueue |>ignore

distance

//Contains the dictionary create by getLongestPathDictOfFGraph of nodeKey,(pred,pathLenght)
let longestPathDict = getLongestPathDictOfFGraph (starting : 'NodeKey) (getEdgeWeight : 'EdgeData -> float) (graph : FGraph<'NodeKey, 'NodeData, 'EdgeData> )

//Contains the most distant nodeKey and the negative path lenght to it
let mostDistantNode,longestPath =
longestPathDict
|>Seq.minBy(fun kvp ->
kvp.Value
|>snd
)
|>fun kvp ->
kvp.Key,snd kvp.Value

//Function to recreate the path to the node with the longest path based on the inverse Djikstra. Returns an ordered List of the path from the starting node to the most distant node
let rec reconstructPath (path: 'NodeKey list) =
if List.head path = starting then
path
else
let currentHead = List.head path
let pred: 'NodeKey = longestPathDict.[currentHead] |>fst
reconstructPath (pred::path)

reconstructPath [mostDistantNode] , (longestPath|>fun x -> x*(-1.))

/// <summary>
/// Computes longest paths from <paramref name="starting"/> for <paramref name="graph"/> using an inverse Dijkstra's algorithm.
/// </summary>
/// <param name="starting"> Calculate the longest paths from this node.</param>
/// <param name="getEdgeWeight"> Function to convert the EdgeData to a float.</param>
/// <param name="graph"> The FGraph for which to compute the longest path. Has to be an Directed Acyclic Graph.</param>
/// <remarks>Uses hasCycleInFGraph to check for cyclic character. If the given FGraph is not a Directed Acyclic Graph this will fail.</remarks>
/// <returns>Tuples of an ordered list containing the path and the distance of the longest path.</returns>
static member getLongestPathOfFGraph (starting : 'NodeKey) (getEdgeWeight : 'EdgeData -> float) (graph : FGraph<'NodeKey, 'NodeData, 'EdgeData> ) =
if LongestPath.hasCycleInFGraph graph then
failwith "The given FGraph is not a Directed Acyclic Graph!"
else
LongestPath.getLongestPathOfFGraphUnchecked starting getEdgeWeight graph


/// <summary>
/// Computes longest paths from <paramref name="starting"/> for <paramref name="graph"/> using an inverse Dijkstra's algorithm.
/// </summary>
/// <param name="starting"> Calculate the longest paths from this node.</param>
/// <param name="graph"> The FGraph for which to compute the longest path. Has to be an Directed Acyclic Graph.</param>
/// <remarks>If there isn't a path between two edges, the distance is set to `infinity`.</remarks>
/// <remarks>Compute sets all EdgeWeights to 1. If you do not want this, consider computeByEdgeData of computeByEdgeDataWith.</remarks>
/// <returns>Tuples of an ordered list containing the path and the distance of the longest path.</returns>
static member compute((starting : 'NodeKey),(graph : FGraph<'NodeKey, 'NodeData, 'EdgeData> )) =
LongestPath.getLongestPathOfFGraph starting (fun x -> 1.) graph

/// <summary>
/// Computes longest paths from <paramref name="starting"/> for <paramref name="graph"/> using an inverse Dijkstra's algorithm.
/// </summary>
/// <param name="starting"> Calculate the longest paths from this node.</param>
/// <param name="graph"> The FGraph for which to compute the longest path. Has to be an Directed Acyclic Graph.</param>
/// <remarks>If there isn't a path between two edges, the distance is set to `infinity`.</remarks>
/// <remarks>computeByEdgeData uses the value in EdgeData as the distance. Only usable with float weights as EdgeData.</remarks>
/// <returns>Tuples of an ordered list containing the path and the distance of the longest path.</returns>
static member computeByEdgeData((starting : 'NodeKey),(graph : FGraph<'NodeKey, 'NodeData, float> )) =
LongestPath.getLongestPathOfFGraph starting id graph

/// <summary>
/// Computes longest paths from <paramref name="starting"/> for <paramref name="graph"/> using an inverse Dijkstra's algorithm.
/// </summary>
/// <param name="starting"> Calculate the longest paths from this node.</param>
/// <param name="getEdgeWeight"> Function to convert the EdgeData to a float.</param>
/// <param name="graph"> The FGraph for which to compute the longest path. Has to be an Directed Acyclic Graph.</param>
/// <remarks>If there isn't a path between two edges, the distance is set to `infinity`.</remarks>
/// <remarks>computeByEdgeDataWith uses <paramref name="getEdgeWeight"/> to convert the EdgeData into the needed float weights.</remarks>
/// <returns>Tuples of an ordered list containing the path and the distance of the longest path.</returns>
static member computeByEdgeDataWith((starting : 'NodeKey),(getEdgeWeight : 'EdgeData -> float),(graph : FGraph<'NodeKey, 'NodeData, 'EdgeData> )) =
LongestPath.getLongestPathOfFGraph starting getEdgeWeight graph

Check warning on line 189 in src/Graphoscope/Measures/LongestPath.fs

View check run for this annotation

Codecov / codecov/patch

src/Graphoscope/Measures/LongestPath.fs#L189

Added line #L189 was not covered by tests
1 change: 1 addition & 0 deletions tests/Graphoscope.Tests/Graphoscope.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Compile Include="GraphGenerators.fs" />
<Compile Include="WedgeCount.fs" />
<Compile Include="Components.fs" />
<Compile Include="LongestPath.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
Expand Down
49 changes: 49 additions & 0 deletions tests/Graphoscope.Tests/LongestPath.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module LongestPath

open System
open Xunit
open Graphoscope
open System.IO

open Xunit

[<Fact>]
let ``smallCyclicGraphReturnsExceptedResults``() =
let cyclicGraphExample =
seq{
"a","b",1.
"a","c",1.
"a","d",15.
"a","e",1.
"c","e",1.
"b","d",13.
"d","e",1.
"e","b",1.
"d","d",2.
}
|> Seq.map(fun (s, t, w) -> (s, s, t, t, w))
|> FGraph.ofSeq

let ex = new System.Exception("The given FGraph is not a Directed Acyclic Graph!")

// Assert
Assert.Throws<Exception>(fun () -> Measures.LongestPath.compute("a", cyclicGraphExample) |> ignore)
|> fun thrown -> Assert.Equal(ex.Message, thrown.Message)

[<Fact>]
let smallAcyclicGraphReturnsExceptedResults () =

let acyclicGraphExample =
seq{
"A","B",2.
"A","D",1.
"B","C",2.
"D","C",17.2
"A","C",18.

}
|>Seq.map(fun (s,t,w) -> (s,s,t,t,w))
|>FGraph.ofSeq

Assert.Equal((["A"; "D"; "C"], 18.2),Measures.LongestPath.computeByEdgeData("A",acyclicGraphExample))
Assert.Equal((["A"; "B"; "C"], 2.),Measures.LongestPath.compute("A",acyclicGraphExample))
Loading