diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ce6a267..4094210 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -4,27 +4,23 @@ "tools": { "dotnet-format": { "version": "5.1.250801", - "commands": [ - "dotnet-format" - ] + "commands": ["dotnet-format"], + "rollForward": false }, "dotnet-script": { "version": "1.4.0", - "commands": [ - "dotnet-script" - ] + "commands": ["dotnet-script"], + "rollForward": false }, "csharpier": { - "version": "0.25.0", - "commands": [ - "dotnet-csharpier" - ] + "version": "0.29.2", + "commands": ["dotnet-csharpier"], + "rollForward": false }, "husky": { "version": "0.6.1", - "commands": [ - "husky" - ] + "commands": ["husky"], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 0d086ec..ce1607f 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -1,4 +1,3 @@ - # ref https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net name: dotnet package @@ -9,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ["6.0.x", "7.0.x"] + dotnet-version: ["6.0.x", "7.0.x", "8.0.x"] steps: - uses: actions/checkout@v3 @@ -20,7 +19,7 @@ jobs: - name: Install dependencies run: dotnet tool restore - name: Unit test - run: dotnet test --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}" + run: dotnet test SharpGraph.sln --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}" - name: Upload dotnet test results uses: actions/upload-artifact@v3 with: diff --git a/.gitignore b/.gitignore index 57a1574..b2a888b 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,7 @@ FakesAssemblies/ # Visual Studio 6 workspace options file *.opt + +.fake + +scratch.txt \ No newline at end of file diff --git a/README.md b/README.md index 20a0d11..7befe7d 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,40 @@ - # SharpGraph - - A C# Library Package for `.Net 6` and above, for the purpose of working with graphs and graph algorithms. +A C# Library Package for `.Net 6` and above, for the purpose of working with graphs and graph algorithms. ![unit test workflow](https://github.com/jonghough/SharpGraph/actions/workflows/unit-test.yaml/badge.svg?branch=master) - + ## Project + This is a .NET library containing an assortment of graph algorithms. ## Nuget + ``` dotnet add package SharpGraphLib --version 1.0.1 ``` -#### Some currently implemented algorithms: - - -| Algorithm | Usage | -|---------------------------------|----------------------------------------------------| -|Dijkstra | for finding shortest path between two nodes on a connected weighted graph, with non-negative weights| -|A-Star | for finding shortest path between two nodes on a connected weighted graph, with non-negative weights - usually faster than Dijkstra| -|Cycle finding | find all cycles on a given graph. Find Hamiltonian cycles.| -|Bellman-Ford | for finding shortest path between two nodes on a connected, weighted graph with positive or negative weights, and no negative weight cycles| -|Spanning Tree | finds the minimum spanning tree for a connected, weighted graph| -|Connected subgraphs | find all maximally connected subgraphs of a graph| -|bionnected subgraphs | find all biconnected subgraphs of a graph| -|General DFS and BFS search algorithms | For graph searching | -|Find minimum cut of a graph | Necessary edges to keep the graph connected| -|Maximum Flow | Find max flow in a flow network | -|Planarity testing | Checks if a graph is planar | +#### Some currently implemented algorithms + +Some of the implemented algorithms and functions are shown below. + +| Algorithm | Usage | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Dijkstra | for finding shortest path between two nodes on a connected weighted graph, with non-negative weights | +| A-Star | for finding shortest path between two nodes on a connected weighted graph, with non-negative weights - usually faster than Dijkstra | +| Cycle finding | find all cycles on a given graph. Find Hamiltonian cycles. | +| Bellman-Ford | for finding shortest path between two nodes on a connected, weighted graph with positive or negative weights, and no negative weight cycles | +| Spanning Tree | finds the minimum spanning tree for a connected, weighted graph | +| Steiner Tree | find minimal steiner tree | +| Line Graph | find line graph transform of a graph | +| Matrix conversion | convert graph to adjacency matrix | +| Connected subgraphs | find all maximally connected subgraphs of a graph | +| Biconnected subgraphs | find all biconnected subgraphs of a graph | +| General DFS and BFS search algorithms | For graph searching | +| Find minimum cut of a graph | Necessary edges to keep the graph connected | +| Maximum Flow | Find max flow in a flow network | +| Planarity testing | Checks if a graph is planar | - ## Example usage We can create a graph by specifying the edges and labeling nodes as below. @@ -40,10 +43,10 @@ We can create a graph by specifying the edges and labeling nodes as below. Edge eAB = new Edge ("A","B"); Edge eAC = new Edge ("A","C"); Edge eCD = new Edge ("C","D"); -Edge eDE = new Edge ("D","E"); -Edge eAF = new Edge ("A","F"); -Edge eBF = new Edge ("B","F"); -Edge eDF = new Edge ("D","F"); +Edge eDE = new Edge ("D","E"); +Edge eAF = new Edge ("A","F"); +Edge eBF = new Edge ("B","F"); +Edge eDF = new Edge ("D","F"); List list = new List (); list.Add (eAB); @@ -54,7 +57,7 @@ list.Add (eAF); list.Add (eBF); list.Add (eDF); -Graph g = new Graph (list); +Graph g = new Graph (list); ``` ### create a complete graph (graph where all nodes are connected to each other) @@ -77,8 +80,10 @@ for(int i = 0; i < 10;i++){ } g.IsConnected(); //true ``` + As another exampler, we can generate a complete graph on 10 nodes and check if it is connected. We can also find biconnected components of the graph. + ```csharp var g = GraphGenerator.CreateComplete(NodeGenerator.GenerateNodes(10)); // generate complete graph g.IsConnected(); //true @@ -89,9 +94,9 @@ g.FindBiconnectedComponents (); // complete graph so only 1 biconnected componen ### find minimum spanning tree of a connected weighted graph ```csharp -Node b1 = new Node ("1"); -Node b2 = new Node ("2"); -Node b3 = new Node ("3"); +Node b1 = new Node ("1"); +Node b2 = new Node ("2"); +Node b3 = new Node ("3"); Edge wedge1 = new Edge (b1, b2); Edge wedge2 = new Edge (b1, b3); @@ -113,6 +118,7 @@ var span = g.GenerateMinimumSpanningTree (); ### find ALL cycles in a graph (this algorithm is very slow, don't use for big graphs) #### example + ![cycles](/graph_cycles.png) In the above graph there are 3 basic cycles. This is easily calculated. For more complicated graphs the @@ -120,16 +126,16 @@ calculation can be quite complex. For example, for the complete graph on 8 nodes ```csharp HashSet nodes7 = NodeGenerator.GenerateNodes(7); -var graph7 = GraphGenerator.CreateComplete(nodes7); +var graph7 = GraphGenerator.CreateComplete(nodes7); var cycles = graph7.FindSimpleCycles(); // number of cycles should be 1172 ``` -### find shortest path between two nodes on a *weighted graph* +### find shortest path between two nodes on a _weighted graph_ -We can find the minimum distance path between any two nodes on a connected graph using `Dijkstra's Algorithm`. +We can find the minimum distance path between any two nodes on a connected graph using `Dijkstra's Algorithm`. -```csharp +```csharp Edge eAB = new Edge ("A", "B"); Edge eAG = new Edge ("A", "G"); Edge eCD = new Edge ("C", "D"); @@ -170,45 +176,54 @@ List path = g.FindMinPath (b1, b8); ``` -### find shortest path between two nodes on a *weighted graph* with A-Star +### find shortest path between two nodes on a _weighted graph_ with A-Star For a faster algorithm, we can use the `A-star algorithm` to find the minimum path on the same graph as above. -```csharp +```csharp List path = g.FindMinPath (b1, b6, new TestHeuristic (5)); ``` - + ## Design There are four main objects: -### Graph -an object which contains collections of *edges* and *nodes*. Most algorithms and functionality are Graph methods in this design. Graphs can be copied and merged. +### Graph + +an object which contains collections of _edges_ and _nodes_. Most algorithms and functionality are Graph methods in this design. Graphs can be copied and merged. + +### Edge + +contains pairs of _Nodes_. Can be associated with any number of *EdgeComponent*s. Comparison of Edges is based on whether their nodes are identical. -### Edge -contains pairs of *Nodes*. Can be associated with any number of *EdgeComponent*s. Comparison of Edges is based on whether their nodes are identical. ### Node -In a given graph, a node (or *vertex*) must have a unique name, a label, that defines it. + +In a given graph, a node (or _vertex_) must have a unique name, a label, that defines it. ### Components -Edges and Nodes share a similar *Component* architecture, where a given edge may have attached to it multiple `EdgeComponent` subclass instances. Similarly, Nodes can have multiple + +Edges and Nodes share a similar _Component_ architecture, where a given edge may have attached to it multiple `EdgeComponent` subclass instances. Similarly, Nodes can have multiple `NodeComponent` subclass instances attached to them. -In this way, we can define weight edges as a new component `MyEdgeWeight`, which can contain a `float` value (the weight), and attach this component to each edge of our graph. This is simpler than having a subclass of `Edge`, `WeightedEdge` and having web of generics in the library. It is also very flexible and allows pretty much any type of `Node` or `Edge` type without creating subclasses. Just define our component and attach it to each edge / node. +In this way, we can define weight edges as a new component `MyEdgeWeight`, which can contain a `float` value (the weight), and attach this component to each edge of our graph. This is simpler than having a subclass of `Edge`, `WeightedEdge` and having web of generics in the library. It is also very flexible and allows pretty much any type of `Node` or `Edge` type without creating subclasses. Just define our component and attach it to each edge / node. -For example, a *Directed Graph* is just a graph with a *DirectionComponent* attached to each edge in the graph. +For example, a _Directed Graph_ is just a graph with a _DirectionComponent_ attached to each edge in the graph. + + ## testing In the base directory + ``` dotnet test ./ ``` - ## building + see [dotnet link](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build) + ``` dotnet build --configuration Release -``` \ No newline at end of file +``` diff --git a/SharpGraph.Builder.Test/SharpGraph.Builder.Test.fsproj b/SharpGraph.Builder.Test/SharpGraph.Builder.Test.fsproj new file mode 100644 index 0000000..2626928 --- /dev/null +++ b/SharpGraph.Builder.Test/SharpGraph.Builder.Test.fsproj @@ -0,0 +1,26 @@ + + + + net8.0 + + false + false + true + + + + + + + + + + + + + + + + + + diff --git a/SharpGraph.Builder.Test/Tests.fs b/SharpGraph.Builder.Test/Tests.fs new file mode 100644 index 0000000..e8dd928 --- /dev/null +++ b/SharpGraph.Builder.Test/Tests.fs @@ -0,0 +1,124 @@ +module Tests + +open Xunit +open FParsec +open SharpGraph.Builder.Parser +open SharpGraph.Builder.Compiler + +[] +[] +['B'->'V'->(0..10)", true)>] +['B','V'->(0..10)", true)>] +['B',('V'->(0..10))", true)>] +[] +[] +[(2->5)", true)>] +['B node')!", true)>] +['B node')!)->((1..4)!)", true)>] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[", false)>] +[", false)>] +[1", false)>] +['node 2", false)>] +[] +['b'", false)>] +[(\r\n5\r\n,6)", true)>] +[] +[] +[] +[] +[] +[] +[] +let ``Test Parsing Graphs`` (expression: string, expectedResult) = + let testParser parser str = + match run parser str with + | Success(result, _, _) -> + printfn "Success: %A" result + true + + | Failure(errorMsg, _, _) -> + printfn "Failure: %s" errorMsg + false + + // Test the Range parser + let parsedExpression = testParser (pExpression .>> eof) expression + Assert.Equal(expectedResult, parsedExpression) + + +[] +[] +[] +[] +[] +[] +['a'", 1, 0, 1)>] +[] +[] +['b'", 2, 1, 1)>] +[] +[] +[4..10", 9, 18, 1)>] // bipartite k3,6 +[ 2), (2 -> 'other'), (2->'NodethreeX')", 4, 3, 1)>] +['B node')!)->((1..4)!)", 5, 10, 1)>] +[] +[] +[] +[] +[] +[] +[] +[] +[] +['&%$$'", 2, 1, 1)>] +[2->3->4->5->6),7", 7, 15, 2)>] +[3->4->'node1'", 3, 3, 1)>] +['node2'->'node3'->'node4'->'node5'->'node6'->'node7'->'node1'", 7, 21, 1)>] +[] +[] +[] +[] +[] +[1->2->3->4), +(5..7!), +(4->5),(0->7)", + 8, + 13, + 1)>] +[200)", 201, 1, 200)>] +[] +[] +[] + +[] +let ``Test Compiling Graphs`` + (input: string, expectedNodeCount: int, expectedEdgeCount: int, expectedConnectedComponents) + = + let a = Compile input + Assert.Equal(expectedNodeCount, a.GetNodes().Count) + Assert.Equal(expectedEdgeCount, a.GetEdges().Count) + Assert.Equal(expectedConnectedComponents, a.GetConnectedComponents().Count) diff --git a/SharpGraph.Builder/.config/dotnet-tools.json b/SharpGraph.Builder/.config/dotnet-tools.json new file mode 100644 index 0000000..517f9b3 --- /dev/null +++ b/SharpGraph.Builder/.config/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} \ No newline at end of file diff --git a/SharpGraph.Builder/Compiler.fs b/SharpGraph.Builder/Compiler.fs new file mode 100644 index 0000000..81030ae --- /dev/null +++ b/SharpGraph.Builder/Compiler.fs @@ -0,0 +1,148 @@ +namespace SharpGraph.Builder + +/// +/// +/// +module Compiler = + open SharpGraph + open Microsoft.FSharp.Collections + open Parser + open FParsec + + /// + /// + /// + let rec processPairs (list: List) (graph: Graph) = + list + |> List.mapi (fun i item -> + list + |> List.iteri (fun j otherItem -> + if j > i then + graph.AddEdge(list.[i], list.[j]))) + + + /// + /// + /// + and GeneratePath (g: Graph) = + let n1 = new System.Collections.Generic.List(g.GetNodes()) + n1 |> Seq.pairwise |> Seq.iter (fun (a, b) -> g.AddEdge(a, b)) |> ignore + g + + /// + /// + /// + and GenerateCycle (g: Graph) = + let n1 = new System.Collections.Generic.List(g.GetNodes()) + + n1 + |> Seq.pairwise + |> Seq.iter (fun (a, b) -> + if a <> b then + g.AddEdge(a, b)) + + g.AddEdge(n1[0], n1[n1.Count - 1]) + g + + /// + /// + /// + and GenerateComplete (g: Graph) = + let n1 = g.GetNodes() + let nodeList = new System.Collections.Generic.List(n1) + nodeList |> List.ofSeq |> (fun l -> processPairs l g |> ignore) + g + + /// + /// + /// + and CreateEdges (g1: Graph) (g2: Graph) = + let n1 = g1.GetNodes() + let n2 = g2.GetNodes() + let g = g1.MergeWith(g2) + + n1 + |> Seq.iter (fun n -> + n2 + |> Seq.iter (fun m -> + if n <> m then + g.AddEdge(n, m))) + + g + + /// + /// + /// + and BinaryOperatorExp (op: BinaryOperator) (e1: Expression) (e2: Expression) = + match op with + | Aggregate -> + let g1: Graph = HandleExpression e1 + let g2: Graph = HandleExpression e2 + g1.MergeWith(g2) + | Edge -> CreateEdges (HandleExpression(e1)) (HandleExpression(e2)) + | BipartiteComplete -> CreateEdges (HandleExpression(e1)) (HandleExpression(e2)) + + /// + /// + /// + and HandleUnaryExp exp op = + match op with + | Complete -> GenerateComplete(HandleExpression(exp)) + | Cycle -> GenerateCycle(HandleExpression(exp)) + | Path -> GeneratePath(HandleExpression(exp)) + + /// + /// + /// + and HandleNodeIdentifier nid = + match nid with + | NamedNode(namedId) -> + match namedId with + | IdentifierName(name) -> new Graph([| name |]) + | NumberedNode n -> new Graph([| string (n) |]) + + /// + /// + /// + and HandleRangeExpression (r: Range) : Graph = + match r with + | Range(first, last) -> + let ints = seq {for i in first .. (last-1) -> i} + let nodes = ints |> Seq.map ( fun i -> new Node(string(i)) ) + new Graph(Array.ofSeq nodes) + + /// + /// + /// + and HandleExpression exp = + match exp with + | NodeIdentifierExp(nodeIdentifier) -> HandleNodeIdentifier nodeIdentifier + | RangeExp(range) -> HandleRangeExpression range + | UnaryExp(e, op) -> HandleUnaryExp e op + | BinaryOperatorExp(op, e1, e2) -> BinaryOperatorExp op e1 e2 + + /// + /// + /// + let Parse code = + System.Diagnostics.Debug.WriteLine("BEGIN PARSING") + + match runParserOnString (pExpression .>> eof) () "Tokamak reaction stream" code with + | Success(result, _, _) -> + System.Diagnostics.Debug.WriteLine("FINISH PARSING") + result + | Failure(msg, _, _) -> + System.Diagnostics.Debug.WriteLine("Error " + msg + "Reactor core containment failed!") + failwith msg + + let Compile (text: string) = + let result = HandleExpression(Parse text) + + System.Console.WriteLine( + "graph number of nodes " + + string (result.GetNodes().Count) + + " and edges: " + + string (result.GetEdges().Count) + ) + + result diff --git a/SharpGraph.Builder/Parser.fs b/SharpGraph.Builder/Parser.fs new file mode 100644 index 0000000..c2df37e --- /dev/null +++ b/SharpGraph.Builder/Parser.fs @@ -0,0 +1,89 @@ +namespace SharpGraph.Builder + +module Parser = + open FParsec + + + type NodeIdentifier = + | NamedNode of IdentifierName + | NumberedNode of int + + and IdentifierName = IdentifierName of string + and Range = Range of int * int + + and UnaryOperator = + | Complete + | Cycle + | Path + + and BinaryOperator = + | BipartiteComplete + | Aggregate + | Edge + + and Expression = + | NodeIdentifierExp of NodeIdentifier + | RangeExp of Range + | UnaryExp of Expression * UnaryOperator + | BinaryOperatorExp of BinaryOperator * Expression * Expression + + + let ws = spaces + + let stringLiteral: Parser = + between (pchar ''') (pchar ''') (manyChars (noneOf "'")) |>> IdentifierName + + let pNamedNode: Parser = stringLiteral |>> NamedNode + + let pNumberedNode: Parser = + pint32 .>> spaces .>> notFollowedByString ".." |>> NumberedNode + + let pNodeIdentifier: Parser = + attempt pNamedNode <|> pNumberedNode + + let pRange: Parser = + let rangeContent = + pipe2 (pint32 .>> pstring "..") pint32 (fun start end' -> Range(start, end')) + + attempt (between (pchar '(') (pchar ')') rangeContent) <|> rangeContent + + + let pExpression, pExpressionRef = createParserForwardedToRef () + + let pSimpleExpression: Parser = + choice + [ attempt ( + pRange <|> (between (pchar '(' >>. spaces) (spaces >>. pchar ')') pRange) + |>> RangeExp + ) + attempt ( + pNodeIdentifier + <|> (between (pchar '(' >>. spaces) (spaces >>. pchar ')') pNodeIdentifier) + |>> NodeIdentifierExp + ) ] + + + let opp = new OperatorPrecedenceParser() + + opp.TermParser <- + pSimpleExpression + <|> (between (pchar '(' .>> spaces) (pchar ')' .>> spaces) pExpression) + + opp.AddOperator(InfixOperator("->", ws, 1, Associativity.Left, fun x y -> BinaryOperatorExp(Edge, x, y))) + opp.AddOperator(InfixOperator(",", ws, 2, Associativity.Left, fun x y -> BinaryOperatorExp(Aggregate, x, y))) + + opp.AddOperator( + InfixOperator("#", ws, 3, Associativity.Left, fun x y -> BinaryOperatorExp(BipartiteComplete, x, y)) + ) + + opp.AddOperator(PostfixOperator("!", ws, 4, true, fun x -> UnaryExp(x, Complete))) + opp.AddOperator(PostfixOperator("*", ws, 4, true, fun x -> UnaryExp(x, Cycle))) + opp.AddOperator(PostfixOperator("|", ws, 4, true, fun x -> UnaryExp(x, Path))) + + let pOperatorExpression = ws >>. opp.ExpressionParser .>> ws + + let expressionParser = + let c = choice [ (attempt pOperatorExpression .>> notFollowedBy pExpression ); attempt pSimpleExpression ] + attempt c + + pExpressionRef.Value <- expressionParser diff --git a/SharpGraph.Builder/README.md b/SharpGraph.Builder/README.md new file mode 100644 index 0000000..dc6c5e5 --- /dev/null +++ b/SharpGraph.Builder/README.md @@ -0,0 +1,129 @@ +# SharpGraph.Builder + +Builder library for `SharpGraphLib` SharpGraph library. This allwos the use of a DSL to quickly and easily construct graphs. + +## Building Graphs + +### GraphGenerator + +The `GraphGenerator` static class contians multiple methods for building common graphs. +Example + +```csharp +// generate a barbell graph +var g1 = GraphGenerator.GenerateBarbell(10,10); +// generate complete graph on 5 vertices +var g2 = GraphGenerator.CreateComplete(5); +``` + +### DSL Syntax + +There is a second method to generate graphs, which is to use the inbuilt simple DSL. The syntax is terse, to allow easy building of graphs. +An example which builds the complete graph on 5 vertices, with nodes labelled 0 to 4. The `!` character builds a complete graph of everything to the left of it. + +``` +var g = GraphCompiler.Build("5!") +``` + +Syntax + +| term | meaning | example | +| ---- | ------------------------------------------------------------------------------------------ | -------- | +| 'a' | named node, will produce a grpah of single node, labeled "a" | 'a' | +| 1..4 | builds a graph of nodes labeled 1,2,3, with no edges | 3..5 +| ! | build complete graph | 4! | +| \* | build cyclic graph | (1..6)\* | +| -> | builds edges by connecting all nodes on the left graph, with all nodes on the right graph. | +| , | combines left graph and right graph with no edge connections | + +Below we have some examples of using the DSL to easily create graphs. + +#### Complete graph + +There are multiple ways to generate a complete graph. +Below is the simplest, with nodes labeled 0 to 4 in this case. + +``` +GraphCompiler.Build("5!") +``` + +We could also use `->` syntax, as below, where we generate the complete graph on 5 vertices with labels "A" to "E". + +``` +GraphCompiler.Build("(((A->B)->C)->D)->E") +``` + +Note the excessive parentheses. We don't actually need them and could use + +``` +GraphCompiler.Build("A->B->C->D->E") +``` + +To generate a complete graph, we could also use the range syntax, as in the example below. +`10..20` generates a graph of 10 nodes labeled from 10 to 19, with no edges. Then `!` builds a complete graph +of this to generate the complete graph on 10 vertices, with labels from 10 to 19. + +``` +GraphCompiler.Build("10..20!") +``` + +### Complete Bipartite Graph + +Similar to the complete graph, we could build complete bipartite graphs. +The right arrow `->` connects all nodes on the left side of it to all nodes on the right. +So, for example + +``` +GraphCompiler.Build("0..3->3..6") +``` + +will create an edgeless graph with nodes 0,1,2, with `0..3`, and similarly with `3..6`, we get an edgeless graph with nodes 3,4,5. Then the `->` right arrow connects all nodes on the left, with all nodes on the right to get a bipartite complete grpah "k(3,3)". + +If we wanted non-integer labels on our nodes we could do + +``` +GraphCompiler.Build("(Node1, Node2, Node3)->(Node4,Node5,Node6)") +``` + +### Cyclic graphs + +We have a primitive operation to generate cyclic graphs, `*`. This works muc the same as `!` for the generation of complete graphs. +Note that in terms of operator precedence `!` is higher than `*`. + +Example. The below example will create the cyclic graph "C(5)". + +``` +GraphCompiler.Build("5*") +``` + +We can also do the same with + +``` +GraphCompiler.Build("0..4*") +``` + +### Paths + +To create paths we can combine `,` and `->` as below. THe below example creates a path `A0-A1-A2-A3-A4`. + +``` +GraphCompiler.Build("A0->A1,A1->A2,A2->A3,A3->A4"); +``` + +### More complex constructions + +As an example, we can construct a Petersen Graph using the DSL syntax. + +``` +"5*,0->5,1->6,2->7,3->8,4->9,5->7,7->9,9->6,6->8,8->5" +``` + +## Node naming + +Creating node names (labels) with the DSL requires following a few rules. Node labels can be stringified integers, have we have seen. Or can be given names, e.g. "Node1". Naming rules are that the first character must be an alphabet character (`[a-zA-Z]`), subsequent characters can be any alphanumeric character, or the characters `_` and `-`. The name must end with an alphanumeric character. + +The following are allowed +"a", "A", "This_is_a_NoDe1", "x--2--4". + +The following are forbidden +"\_a", "-A", "M--", "ThiS-Is-a-nOdE10-", "A()!+B" \ No newline at end of file diff --git a/SharpGraph.Builder/SharpGraph.Builder.fsproj b/SharpGraph.Builder/SharpGraph.Builder.fsproj new file mode 100644 index 0000000..7865310 --- /dev/null +++ b/SharpGraph.Builder/SharpGraph.Builder.fsproj @@ -0,0 +1,24 @@ + + + SharpGraphLib.Builder + net8.0 + README.md + 1.0.3 + Jonathan Hough + Jonathan Hough + true + + + + + + + + + + + + + + + diff --git a/SharpGraph.Tests/SharpGraph.Tests.csproj b/SharpGraph.Tests/SharpGraph.Tests.csproj index d9ecf8e..e38a667 100644 --- a/SharpGraph.Tests/SharpGraph.Tests.csproj +++ b/SharpGraph.Tests/SharpGraph.Tests.csproj @@ -1,19 +1,22 @@ - net6.0 + net8.0 false - - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/SharpGraph.Tests/test/CycleTest.cs b/SharpGraph.Tests/test/CycleTest.cs index 9247bc5..d9d57fc 100644 --- a/SharpGraph.Tests/test/CycleTest.cs +++ b/SharpGraph.Tests/test/CycleTest.cs @@ -4,6 +4,12 @@ // See LICENSE file in the samples root for full license information. // +using System; +// +// Copyright (C) 2023 Jonathan Hough. +// Copyright Licensed under the MIT license. +// See LICENSE file in the samples root for full license information. +// using Xunit; namespace SharpGraph @@ -25,14 +31,10 @@ public class OtherGraphTest [Fact] public void CompleteGenerationTest() { - const int nodeNum = 15; - const int width = 3; - var nodes = NodeGenerator.GenerateNodes(nodeNum); - var g = GraphGenerator.GenerateGrid(nodes, width); - Assert.Equal( - ((width - 1) * (nodeNum / width)) + (width * ((nodeNum / width) - 1)), - g.GetEdges().Count - ); + int width = 3; + int height = 5; + var g = GraphGenerator.GenerateGrid(width, height); + Assert.Equal(((width - 1) * height) + (width * (height - 1)), g.GetEdges().Count); } } @@ -84,6 +86,40 @@ public void CycleTestCyclic() Assert.Single(cycleGraph.FindSimpleCycles()); } + [Fact] + public void CycleTestWithSingleCycle() + { + Graph g = new(); + g.AddEdge("a", "b"); + g.AddEdge("1", "2"); + g.AddEdge("2", "3"); + g.AddEdge("3", "4"); + g.AddEdge("4", "1"); + var cycles = g.FindSimpleCycles(); + var c = cycles.Count; + Assert.True(c == 1); + Assert.Contains(new Node("1"), cycles[0]); + Assert.Contains(new Node("2"), cycles[0]); + Assert.Contains(new Node("3"), cycles[0]); + Assert.Contains(new Node("4"), cycles[0]); + } + + [Fact] + public void CycleTestWithTwoCycles() + { + Graph g = new(); + g.AddEdge("a", "b"); + g.AddEdge("b", "c"); + g.AddEdge("c", "a"); + g.AddEdge("1", "2"); + g.AddEdge("2", "3"); + g.AddEdge("3", "4"); + g.AddEdge("4", "1"); + var cycles = g.FindSimpleCycles(); + var c = cycles.Count; + Assert.True(c == 2); + } + [Fact] public void CycleTestCyclicWithPath() { diff --git a/SharpGraph.Tests/test/GraphGenerationTest.cs b/SharpGraph.Tests/test/GraphGenerationTest.cs new file mode 100644 index 0000000..7a3acdf --- /dev/null +++ b/SharpGraph.Tests/test/GraphGenerationTest.cs @@ -0,0 +1,181 @@ +// +// Copyright (C) 2023 Jonathan Hough. +// Copyright Licensed under the MIT license. +// See LICENSE file in the samples root for full license information. +// + +using System; +using Microsoft.CSharp.RuntimeBinder; +using Xunit; + +namespace SharpGraph +{ + public class GraphGeneratorTest + { + [Fact] + public void TestGeneratingTuranGraph1() + { + var g = GraphGenerator.GenerateTuranGraph(5, 2); + Assert.True(g.GetNodes().Count == 5); + Assert.True(g.GetEdges().Count == 8); + Assert.True(g.GetEdge("1", "2") != null); + Assert.True(g.GetEdge("1", "0") == null); + } + + [Fact] + public void TestGeneratingTuranGraph2() + { + var g = GraphGenerator.GenerateTuranGraph(10, 3); + Assert.True(g.GetNodes().Count == 10); + Assert.True(g.GetEdges().Count == 36); + Assert.True(g.GetEdge("1", "2") == null); + Assert.True(g.GetEdge("3", "4") == null); + Assert.True(g.GetEdge("1", "8") != null); + } + + [Fact] + public void TestGeneratePath() + { + var g = GraphGenerator.GeneratePath(5, "N"); + Assert.True(g.GetNodes().Count == 5); + foreach (var n in g.GetNodes()) + { + if (n.GetLabel() == "N0" || n.GetLabel() == "N4") + { + Assert.True(g.GetAdjacent(n, false).Count == 1); + } + else + { + Assert.True(g.GetAdjacent(n, false).Count == 2); + } + } + } + + [Theory] + [InlineData(4, 8, 10)] + [InlineData(5, 10, 13)] + [InlineData(20, 40, 58)] + public void TestGenerateLadder( + uint nodeCountPerSide, + int expectedNodeCount, + int expectedEdgeCount + ) + { + var g = GraphGenerator.GenerateLadderGraph(nodeCountPerSide); + Assert.True(g.GetNodes().Count == expectedNodeCount); + Assert.True(g.GetEdges().Count == expectedEdgeCount); + Assert.True(g.IsConnected()); + } + + [Fact] + public void TestGeneratePetersenGraph() + { + var g = GraphGenerator.GeneratePetersenGraph(); + Assert.True(g.GetNodes().Count == 10); + Assert.True(g.GetEdges().Count == 15); + Assert.True(g.IsConnected()); + } + + [Theory] + [InlineData(4, false, 5, 8)] + [InlineData(5, false, 6, 10)] + [InlineData(10, false, 11, 20)] + [InlineData(2, true, 0, 0)] + public void TestGenerateWheelGraph( + int spokesCount, + bool throwsException, + int expectedNodeCount, + int expectedEdgeCount + ) + { + if (throwsException) + { + Assert.Throws(() => GraphGenerator.GenerateWheelGraph(spokesCount)); + } + else + { + var g = GraphGenerator.GenerateWheelGraph(spokesCount); + Assert.Equal(g.GetNodes().Count, expectedNodeCount); + Assert.Equal(g.GetEdges().Count, expectedEdgeCount); + Assert.True(g.IsConnected()); + } + } + + [Theory] + [InlineData(4, 4, 16, 24)] + [InlineData(1, 1, 1, 0)] + [InlineData(12, 14, 12 * 14, (12 * 13) + (14 * 11))] + [InlineData(2, 16, 32, (2 * 15) + (16 * 1))] + public void TestGenerateGrid( + int width, + int height, + int expectedNodeCount, + int expectedEdgeCount + ) + { + var g = GraphGenerator.GenerateGrid(width, height); + Assert.Equal(g.GetNodes().Count, expectedNodeCount); + Assert.Equal(g.GetEdges().Count, expectedEdgeCount); + Assert.True(g.IsConnected()); + } + + [Theory] + [InlineData(4, 4, 4, 64, (24 * 4) + (3 * 16))] + [InlineData(3, 1, 6, 18, (2 * 6) + (5 * 3))] + [InlineData(1, 1, 1, 1, 0)] + public void TestGenerate3DGrid( + int width, + int height, + int depth, + int expectedNodeCount, + int expectedEdgeCount + ) + { + var g = GraphGenerator.Generate3DGrid(width, height, depth); + Assert.Equal(g.GetNodes().Count, expectedNodeCount); + Assert.Equal(g.GetEdges().Count, expectedEdgeCount); + Assert.True(g.IsConnected()); + } + + [Theory] + [InlineData(0, 1, 1)] + [InlineData(1, 0, 1)] + [InlineData(1, 1, 0)] + [InlineData(-1, -6, 1)] + public void TestGenerate3DGridFailure(int width, int height, int depth) + { + Assert.Throws( + () => GraphGenerator.Generate3DGrid(width, height, depth) + ); + } + + [Theory] + [InlineData(1, 1, 2, 1)] + [InlineData(4, 3, 7, 10)] + public void TestGenerateBarbell( + int leftSide, + int rightSide, + int expectedNodeCount, + int expectedEdgeCount + ) + { + var g = GraphGenerator.GenerateBarbell(leftSide, rightSide); + Assert.Equal(g.GetNodes().Count, expectedNodeCount); + Assert.Equal(g.GetEdges().Count, expectedEdgeCount); + Assert.True(g.IsConnected()); + } + + [Theory] + [InlineData(1, 1, 0)] + [InlineData(6, 6, 15)] + [InlineData(3, 3, 3)] + [InlineData(9, 9, 36)] + public void TestGenerateComplete(int size, int expectedNodeCount, int expectedEdgeCount) + { + var g = GraphGenerator.CreateComplete((uint)size); + Assert.Equal(g.GetNodes().Count, expectedNodeCount); + Assert.Equal(g.GetEdges().Count, expectedEdgeCount); + Assert.True(g.IsConnected()); + } + } +} diff --git a/SharpGraph.Tests/test/GraphTest.cs b/SharpGraph.Tests/test/GraphTest.cs index 3890603..7f3a761 100644 --- a/SharpGraph.Tests/test/GraphTest.cs +++ b/SharpGraph.Tests/test/GraphTest.cs @@ -30,7 +30,7 @@ public void TestNodeAndEdgeEquality() Assert.Equal(e1, e2); Assert.True(e1 == e2); - Assert.True(e3 != e4); + Assert.True(e3 == e4); Assert.True(e3.From() == e4.To()); } @@ -132,13 +132,11 @@ public void TestRemoveEdge3() Assert.Equal(15, g1.GetEdges().Count); var success = g1.RemoveEdge("3", "1"); - Assert.False(success, "the edge with from=3, to=1 does not exist"); + Assert.True(success, "the edge with from=3, to=1 does not exist"); - Assert.Equal(15, g1.GetEdges().Count); + Assert.Equal(14, g1.GetEdges().Count); - var nodes = new HashSet(); - nodes.Add(new Node("3")); - nodes.Add(new Node("1")); + var nodes = new HashSet { new Node("3"), new Node("1") }; g1.RemoveEdge(nodes); Assert.Equal(14, g1.GetEdges().Count); } @@ -177,27 +175,6 @@ public void TestCopy() } } - [Fact] - public void TestGeneratingTuranGraph1() - { - var g = GraphGenerator.GenerateTuranGraph(5, 2); - Assert.True(g.GetNodes().Count == 5); - Assert.True(g.GetEdges().Count == 8); - Assert.True(g.GetEdge("1", "2") != null); - Assert.True(g.GetEdge("1", "0") == null); - } - - [Fact] - public void TestGeneratingTuranGraph2() - { - var g = GraphGenerator.GenerateTuranGraph(10, 3); - Assert.True(g.GetNodes().Count == 10); - Assert.True(g.GetEdges().Count == 36); - Assert.True(g.GetEdge("1", "2") == null); - Assert.True(g.GetEdge("3", "4") == null); - Assert.True(g.GetEdge("1", "8") != null); - } - [Fact] public void TestGraphConstructorRegex1() { diff --git a/SharpGraph.Tests/test/SteinerTreeTest.cs b/SharpGraph.Tests/test/SteinerTreeTest.cs index bdb3098..3a41a2b 100644 --- a/SharpGraph.Tests/test/SteinerTreeTest.cs +++ b/SharpGraph.Tests/test/SteinerTreeTest.cs @@ -214,13 +214,12 @@ public void TestSteinerTreeForK6_2() [Fact] public void TestSteinerTreeForGrid1() { - var ns = NodeGenerator.GenerateNodes(5 * 5 * 2); - var g = GraphGenerator.Generate3DGrid(ns, 5, 2); + var g = GraphGenerator.Generate3DGrid(5, 5, 2); var rand = new Random(); g.GetEdges().ForEach(e => g.AddComponent(e).Weight = rand.Next(1000)); var treeSet = new HashSet(); - var nsList = new List(ns); + var nsList = new List(g.GetNodes()); treeSet.Add(nsList[0]); treeSet.Add(nsList[10]); treeSet.Add(nsList[20]); @@ -234,17 +233,13 @@ public void TestSteinerTreeForGrid1() [Fact] public void TestSteinerTreeForGrid2() { - var ns = NodeGenerator.GenerateNodes(7 * 3 * 3); - var g = GraphGenerator.Generate3DGrid(ns, 3, 3); + var g = GraphGenerator.Generate3DGrid(7, 3, 3); var rand = new Random(); g.GetEdges().ForEach(e => g.AddComponent(e).Weight = 1); - var treeSet = new HashSet(); - var nsList = new List(ns); - treeSet.Add(nsList[0]); - treeSet.Add(nsList[(7 * 3 * 3) - 1]); + var treeSet = new HashSet { new Node("0_0_0"), new Node("2_6_2") }; var tree = g.GenerateMinimalSpanningSteinerTree(treeSet); - Assert.True(tree.GetNodes().Count == 11); // 7 down + 2 across + 2 deep + Assert.Equal(11, tree.GetNodes().Count); // 7 across + 2 down + 2 deep Assert.True(tree.FindSimpleCycles().Count == 0); } } diff --git a/SharpGraph.Tests/test/TrianglesTest.cs b/SharpGraph.Tests/test/TrianglesTest.cs index 3974662..30781c3 100644 --- a/SharpGraph.Tests/test/TrianglesTest.cs +++ b/SharpGraph.Tests/test/TrianglesTest.cs @@ -4,6 +4,7 @@ // See LICENSE file in the samples root for full license information. // +using System; using Xunit; namespace SharpGraph @@ -57,6 +58,7 @@ public void TestTriangles4() g.AddEdge("1", "4"); triangles = g.FindTriangles(); + Assert.Equal(3, triangles.Count); } diff --git a/SharpGraph.sln b/SharpGraph.sln index 5469217..53e6b2f 100644 --- a/SharpGraph.sln +++ b/SharpGraph.sln @@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGraph", "SharpGraph\Sh EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGraph.Tests", "SharpGraph.Tests\SharpGraph.Tests.csproj", "{DE8B693E-9364-4708-B2EF-D9DA2172A674}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "SharpGraph.Builder", "SharpGraph.Builder\SharpGraph.Builder.fsproj", "{549207EE-F1F7-4E76-8996-CBBDE8FD76E0}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "SharpGraph.Builder.Test", "SharpGraph.Builder.Test\SharpGraph.Builder.Test.fsproj", "{E719B05B-A97A-4D7D-9BBC-3A190FF01893}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,5 +48,53 @@ Global {DE8B693E-9364-4708-B2EF-D9DA2172A674}.Release|x64.Build.0 = Release|Any CPU {DE8B693E-9364-4708-B2EF-D9DA2172A674}.Release|x86.ActiveCfg = Release|Any CPU {DE8B693E-9364-4708-B2EF-D9DA2172A674}.Release|x86.Build.0 = Release|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Debug|x64.ActiveCfg = Debug|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Debug|x64.Build.0 = Debug|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Debug|x86.ActiveCfg = Debug|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Debug|x86.Build.0 = Debug|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Release|Any CPU.Build.0 = Release|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Release|x64.ActiveCfg = Release|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Release|x64.Build.0 = Release|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Release|x86.ActiveCfg = Release|Any CPU + {34B5A4DA-C27E-4B50-9B5B-5874A58C8226}.Release|x86.Build.0 = Release|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Debug|Any CPU.Build.0 = Debug|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Debug|x64.ActiveCfg = Debug|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Debug|x64.Build.0 = Debug|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Debug|x86.ActiveCfg = Debug|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Debug|x86.Build.0 = Debug|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Release|Any CPU.ActiveCfg = Release|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Release|Any CPU.Build.0 = Release|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Release|x64.ActiveCfg = Release|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Release|x64.Build.0 = Release|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Release|x86.ActiveCfg = Release|Any CPU + {378E99D9-F9F1-4E33-B645-30BDD4CAF272}.Release|x86.Build.0 = Release|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Debug|x64.Build.0 = Debug|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Debug|x86.Build.0 = Debug|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Release|Any CPU.Build.0 = Release|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Release|x64.ActiveCfg = Release|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Release|x64.Build.0 = Release|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Release|x86.ActiveCfg = Release|Any CPU + {549207EE-F1F7-4E76-8996-CBBDE8FD76E0}.Release|x86.Build.0 = Release|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Debug|x64.ActiveCfg = Debug|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Debug|x64.Build.0 = Debug|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Debug|x86.ActiveCfg = Debug|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Debug|x86.Build.0 = Debug|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Release|Any CPU.Build.0 = Release|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Release|x64.ActiveCfg = Release|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Release|x64.Build.0 = Release|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Release|x86.ActiveCfg = Release|Any CPU + {E719B05B-A97A-4D7D-9BBC-3A190FF01893}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SharpGraph/SharpGraph.csproj b/SharpGraph/SharpGraph.csproj index ff76076..101a937 100644 --- a/SharpGraph/SharpGraph.csproj +++ b/SharpGraph/SharpGraph.csproj @@ -1,10 +1,10 @@  Library - net6.0 + net8.0 SharpGraphLib README.md - 1.0.1 + 1.0.3 Jonathan Hough Jonathan Hough @@ -19,6 +19,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/SharpGraph/src/algorithms/GraphGenerator.cs b/SharpGraph/src/algorithms/GraphGenerator.cs index 7278762..824831e 100644 --- a/SharpGraph/src/algorithms/GraphGenerator.cs +++ b/SharpGraph/src/algorithms/GraphGenerator.cs @@ -5,7 +5,10 @@ // using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Xml.Schema; namespace SharpGraph { @@ -53,6 +56,18 @@ public static Graph CreateComplete(params string[] args) /// A complete graph. public static Graph CreateComplete(HashSet nodeSet) { + if (nodeSet.Count == 0) + { + throw new ArgumentException( + $"Size of node set is {nodeSet.Count}, but must be at least 1." + ); + } + + if (nodeSet.Count == 1) + { + return new Graph(nodeSet); + } + var nodeList = new List(nodeSet); var edgeList = new List(); for (var i = 0; i < nodeList.Count - 1; i++) @@ -70,6 +85,18 @@ public static Graph CreateComplete(HashSet nodeSet) public static Graph CreateComplete(int size, string nodePrefix) { + if (size == 0) + { + throw new ArgumentException($"Size of node set is {size}, but must be at least 1."); + } + + if (size == 1) + { + var nodeSet = new HashSet { new Node(nodePrefix + "0") }; + + return new Graph(nodeSet); + } + var nodes = new HashSet(); for (var i = 0; i < size; i++) { @@ -96,11 +123,9 @@ public static Graph GenerateRandomGraph(HashSet nodes, float edgeProb) { var ep = edgeProb; ep = - ep < 0 - ? 0 - : ep > 1.0f - ? 1.0f - : ep; + ep < 0 ? 0 + : ep > 1.0f ? 1.0f + : ep; var compG = CreateComplete(nodes); var rand = new Random(Guid.NewGuid().GetHashCode()); @@ -170,6 +195,28 @@ HashSet rightNodes return new Graph(edges); } + /// + /// Generates the complete bipartite graph with left sdie having. leftNodeCount nodes and + /// the right side having. rightNodeCount nodes. Each node on left side is adjacent to each and only the + /// nodes on the right side, and vice versa. + /// + /// nodes for the left side. + /// nodes for the right side. + /// Complete bipartite graph. + public static Graph GenerateBipartiteComplete(int leftNodeCount, int rightNodeCount) + { + var leftNodes = Enumerable + .Range(0, leftNodeCount) + .Select(i => new Node("L" + i.ToString())) + .ToHashSet(); + var rightNodes = Enumerable + .Range(0, rightNodeCount) + .Select(i => new Node("R" + i.ToString())) + .ToHashSet(); + + return GenerateBipartiteComplete(leftNodes, rightNodes); + } + /// /// Creates a cyclic graph on n vertices, where n is the given arguemtn positive integer. /// @@ -223,8 +270,8 @@ public static Graph GenerateBarbell(HashSet leftSet, HashSet rightSe var gLeft = CreateComplete(leftSet); var gRight = CreateComplete(rightSet); - var left = gLeft.GetEdges()[0].From(); - var right = gRight.GetEdges()[0].From(); + var left = new List(gLeft.GetNodes())[0]; + var right = new List(gRight.GetNodes())[0]; // the bar edge. var e = new Edge(left, right); @@ -236,8 +283,7 @@ public static Graph GenerateBarbell(HashSet leftSet, HashSet rightSe return new Graph(allEdges); } - /// - /// + /// /// Creates a barbell graph, which is defined as two complete graphs connected by a single edge. /// The leftSize and. rightSize>\i? arguments give the number of nodes of the two generated complete graphs. /// @@ -275,21 +321,24 @@ public static Graph GenerateBarbell(int leftSize, int rightSize) return GenerateBarbell(left, right); } - public static Graph GenerateGrid(HashSet nodeSet, int width) + public static Graph GenerateGrid(int width, int height, string nodePrefix = "") { - if (nodeSet.Count % width != 0) + if (width < 1 || height < 1) { - throw new Exception("Number of nodes must be a multiple of width."); + throw new NonPositiveArgumentException( + $"Non positive argument in width and height: {width}, {height}." + ); } - var height = nodeSet.Count / width; var edgeList = new List(); - var nodeList = new List(nodeSet); for (var i = 0; i < width; i++) { for (var j = 0; j < height - 1; j++) { - var e = new Edge(nodeList[i + (j * width)], nodeList[i + ((j + 1) * width)]); + var e = new Edge( + new Node($"{nodePrefix}{i}_{j}"), + new Node($"{nodePrefix}{i}_{j + 1}") + ); edgeList.Add(e); } } @@ -298,18 +347,25 @@ public static Graph GenerateGrid(HashSet nodeSet, int width) { for (var j = 0; j < height; j++) { - var e = new Edge(nodeList[i + (j * width)], nodeList[i + (j * width) + 1]); + var e = new Edge( + new Node($"{nodePrefix}{i}_{j}"), + new Node($"{nodePrefix}{i + 1}_{j}") + ); edgeList.Add(e); } } - return new Graph(edgeList); + var g = new Graph(edgeList); + + // in case no edges, we still have a solitary node. + g.AddNode($"{nodePrefix}0_0"); + return g; } /// /// Generates a 3D grid (lattice) of nodes where each node is adjacent to the nearest nodes in /// the cardinal directions (left,right, up,down, front,back). The dimensions (in terms of nodes) - /// of the resulting graph will be width x depth x height, where height is calcualted as + /// of the resulting graph will be width x depth x height, where height is calculated as /// the number of nodes in the nodeSet divided by (width x depth). /// This means the number of nodes in nodeSet must be a multiple of widthxdepth. /// @@ -317,57 +373,42 @@ public static Graph GenerateGrid(HashSet nodeSet, int width) /// width of the graph in terms of nodes. /// depth of the graph in terms of nodes. /// 3D grid. - public static Graph Generate3DGrid(HashSet nodeSet, int width, int depth) + public static Graph Generate3DGrid(int width, int height, int depth) { - if (nodeSet.Count % (width * depth) != 0) - { - throw new Exception("Number of nodes must be a multiple of width * depth."); - } - - var height = nodeSet.Count / (width * depth); - - var edgeLists = new List>(); - var masterList = new List(nodeSet); - var nodeLists = new List>(); - var ctr = 0; - for (var i = 0; i < depth; i++) + if (width < 1 || height < 1 || depth < 1) { - var set2 = new HashSet(); - var nl = new List(); - for (var j = 0; j < width * height; j++) - { - set2.Add(masterList[(width * height * i) + j]); - - nl.Add(masterList[(width * height * i) + j]); - ctr++; - } - - var g = GenerateGrid(set2, width); - edgeLists.Add(g.GetEdges()); - nodeLists.Add(nl); + throw new NonPositiveArgumentException( + $"Non positive argument in width and height and depth: {width}, {height}, {depth}." + ); } - var edges = new List(); - - for (var i = 0; i < nodeLists.Count - 1; i++) + var grid0 = GenerateGrid(width, height, "0_"); + var g = new Graph(); + g = g.MergeWith(grid0); + for (int i = 1; i < depth; i++) { - edges.AddRange(edgeLists[i]); - for (var j = 0; j < nodeLists[i].Count; j++) + var grid = GenerateGrid(width, height, $"{i}_"); + g = g.MergeWith(grid); + for (int j = 0; j < width; j++) { - edges.Add(new Edge(nodeLists[i][j], nodeLists[i + 1][j])); + for (int k = 0; k < height; k++) + { + var e = new Edge(new Node($"{i - 1}_{j}_{k}"), new Node($"{i}_{j}_{k}")); + g.AddEdge(e); + } } } - edges.AddRange(edgeLists[edgeLists.Count - 1]); - return new Graph(edges); + return g; } /// /// Creates a wheel graph with the given number of spokes. - /// + /// Note that the number of nodes of the generated graph will be equal to then number + /// of spokes +1. /// Wheel Graph definition.. /// - /// Number of spokes of the graph. Mus tbe at least 3. + /// Number of spokes of the graph. Must be at least 3. /// Wheel graph. public static Graph GenerateWheelGraph(int spokesCount) { @@ -384,7 +425,7 @@ public static Graph GenerateWheelGraph(int spokesCount) var c = 0; var edges = new List(); var center = new Node("center"); - while (c++ < spokesCount) + while (c < spokesCount) { var edge = new Edge( string.Format("{0}", c), @@ -392,49 +433,26 @@ public static Graph GenerateWheelGraph(int spokesCount) ); edges.Add(edge); edges.Add(new Edge(center.GetLabel(), string.Format("{0}", c))); + c++; } return new Graph(edges); } /// - /// Creates the. Peterson Graph + /// Creates the. Petersen Graph /// - /// A copy of the Peterson Graph. - public static Graph GeneratePetersonGraph() + /// A copy of the Petersen Graph. + public static Graph GeneratePetersenGraph(string nodePrefix = "") { - var nodes = new HashSet(); - for (var i = 0; i < 5; i++) - { - var n = new Node(string.Empty + i); - nodes.Add(n); - } - - var nodeList = new List(nodes); - var edgeList = new List(); - for (var i = 0; i < nodeList.Count - 1; i++) - { - for (var j = i + 2; j < nodeList.Count; j++) - { - var e = new Edge(nodeList[i], nodeList[j]); - edgeList.Add(e); - } - } - - for (var i = 0; i < nodeList.Count; i++) - { - var outer = new Edge(nodeList[i], new Node(string.Empty + (i + 5))); - edgeList.Add(outer); - edgeList.Add( - new Edge( - new Node(string.Empty + (i + 5)), - new Node(string.Empty + ((i + 6) % 10)) - ) - ); - } + var g = new Graph( + $@" + {nodePrefix}0->{nodePrefix}2,{nodePrefix}0->{nodePrefix}3,{nodePrefix}1->{nodePrefix}3,{nodePrefix}1->{nodePrefix}4,{nodePrefix}2->{nodePrefix}4, + {nodePrefix}0->{nodePrefix}5,{nodePrefix}1->{nodePrefix}6,{nodePrefix}2->{nodePrefix}7,{nodePrefix}3->{nodePrefix}8,{nodePrefix}4->{nodePrefix}9, + {nodePrefix}5->{nodePrefix}6,{nodePrefix}6->{nodePrefix}7,{nodePrefix}7->{nodePrefix}8,{nodePrefix}8->{nodePrefix}9,{nodePrefix}9->{nodePrefix}0" + ); - var graph = new Graph(edgeList); - return graph; + return g; } /// @@ -464,7 +482,7 @@ public static Graph GenerateTuranGraph(uint n, uint r) { if (i + j > n) { - goto COMPLETE; + return g; } part.Add(i + j); @@ -485,8 +503,6 @@ public static Graph GenerateTuranGraph(uint n, uint r) i += r; } - COMPLETE: { } - return g; } @@ -510,5 +526,29 @@ public static void GenerateRandomWeights(Graph graph, float minWeight, float max ew.Weight = weight; } } + + public static Graph GeneratePath(uint nodeCount, string nodePrefix = "Node") + { + var nodeNames = Enumerable + .Range(0, ((int)nodeCount) - 1) + .Select(i => $"{nodePrefix}{i}->{nodePrefix}{i + 1}"); + var constructorString = string.Join(",", nodeNames); + return new Graph(constructorString); + } + + public static Graph GenerateLadderGraph(uint pathNodeCount) + { + var path1 = GeneratePath(pathNodeCount, "A"); + var path2 = GeneratePath(pathNodeCount, "B"); + + var nodes1 = path1.GetNodes().Select(n => n.GetLabel()).OrderBy(s => s).ToList(); + var nodes2 = path2.GetNodes().Select(n => n.GetLabel()).OrderBy(s => s).ToList(); + var zip = nodes1.Zip(nodes2, (a, b) => new { First = a, Second = b }); + var edges = zip.Select(z => z.First + "->" + z.Second).ToList(); + var edgeString = string.Join(",", edges); + var g = path1.MergeWith(path2); + g.AddEdges(edgeString); + return g; + } } } diff --git a/SharpGraph/src/algorithms/LineGraph.cs b/SharpGraph/src/algorithms/LineGraph.cs index 4554000..fbf66b1 100644 --- a/SharpGraph/src/algorithms/LineGraph.cs +++ b/SharpGraph/src/algorithms/LineGraph.cs @@ -61,6 +61,10 @@ public static Graph GenerateLineGraph(Graph graph, ILineGraphBuilder builder) ); } + // public static Graph GenerateInverseLineGraph(Graph g) + // { + + // } internal class EdgeComparer : IEqualityComparer { public bool Equals(Edge x, Edge y) diff --git a/SharpGraph/src/algorithms/NodeGenerator.cs b/SharpGraph/src/algorithms/NodeGenerator.cs index f5b7bc6..471d304 100644 --- a/SharpGraph/src/algorithms/NodeGenerator.cs +++ b/SharpGraph/src/algorithms/NodeGenerator.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; namespace SharpGraph { @@ -45,5 +47,21 @@ public static HashSet GenerateNodes(int number) return nodes; } + + public static HashSet GenerateNodes(Collection nodeCollection) + { + if (nodeCollection.Count < 1) + { + throw new Exception( + string.Format( + "argument 'number' must be a positive integer. Value given {0}", + nodeCollection.Count + ) + ); + } + + var nodes = new HashSet(); + return nodeCollection.Select(s => new Node(s)).ToHashSet(); + } } } diff --git a/SharpGraph/src/algorithms/NonPositiveArgumentException.cs b/SharpGraph/src/algorithms/NonPositiveArgumentException.cs new file mode 100644 index 0000000..22a2818 --- /dev/null +++ b/SharpGraph/src/algorithms/NonPositiveArgumentException.cs @@ -0,0 +1,19 @@ +// +// Copyright (C) 2023 Jonathan Hough. +// Copyright Licensed under the MIT license. +// See LICENSE file in the samples root for full license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace SharpGraph +{ + public class NonPositiveArgumentException : Exception + { + public NonPositiveArgumentException(string message) + : base(message) { } + } +} diff --git a/SharpGraph/src/algorithms/mincut/MinCut.cs b/SharpGraph/src/algorithms/mincut/MinCut.cs index cafd702..82ebf69 100644 --- a/SharpGraph/src/algorithms/mincut/MinCut.cs +++ b/SharpGraph/src/algorithms/mincut/MinCut.cs @@ -48,8 +48,8 @@ public int FindMinCut() nCount = modGraph.GetNodes().Count; } - var sum = modGraph.edges - .Select(e => modGraph.GetComponent(e).Multiplicity) + var sum = modGraph + .edges.Select(e => modGraph.GetComponent(e).Multiplicity) .Sum(); return sum; } @@ -100,19 +100,19 @@ private Graph ContractEdge(Edge edge) dict[rex] += mult; } - var rexi = new Edge(e.To(), newNode); - - if (this.edges.Contains(rexi)) - { - if (!toRemove.Contains(rexi)) - { - toRemove.Add(rexi); - if (dict.ContainsKey(rexi)) - { - dict[rex] += dict[rexi]; - } - } - } + // var rexi = new Edge(e.To(), newNode); + + // if (this.edges.Contains(rexi)) + // { + // if (!toRemove.Contains(rexi)) + // { + // toRemove.Add(rexi); + // if (dict.ContainsKey(rexi)) + // { + // dict[rex] += dict[rexi]; + // } + // } + // } } else if (e.To() == edge.To()) { @@ -129,19 +129,19 @@ private Graph ContractEdge(Edge edge) dict[rex] += mult; } - var rexi = new Edge(newNode, e.From()); - - if (this.edges.Contains(rexi)) - { - if (!toRemove.Contains(rexi)) - { - toRemove.Add(rexi); - if (dict.ContainsKey(rexi)) - { - dict[rex] += dict[rexi]; - } - } - } + // var rexi = new Edge(newNode, e.From()); + + // if (this.edges.Contains(rexi)) + // { + // if (!toRemove.Contains(rexi)) + // { + // toRemove.Add(rexi); + // if (dict.ContainsKey(rexi)) + // { + // dict[rex] += dict[rexi]; + // } + // } + // } } } diff --git a/SharpGraph/src/core/Edge.cs b/SharpGraph/src/core/Edge.cs index 76d7e59..0d486ed 100644 --- a/SharpGraph/src/core/Edge.cs +++ b/SharpGraph/src/core/Edge.cs @@ -113,7 +113,8 @@ public override string ToString() public bool Equals(Edge other) { - return this.from == other.from && this.to == other.to; + return (this.from == other.from && this.to == other.to) + || (this.from == other.to && this.to == other.from); } public override bool Equals(object obj) @@ -128,7 +129,10 @@ public override bool Equals(object obj) public override int GetHashCode() { - var h = (this.from.GetHashCode() ^ 38334421) + (this.to.GetHashCode() * 11); + var x = this.from.GetHashCode() * this.to.GetHashCode(); + var h = (this.from.GetHashCode() ^ 38334421) + (this.to.GetHashCode() ^ 43220011); + h = h * ((this.from.GetHashCode() ^ 43220011) + (this.to.GetHashCode() ^ 38334421)); + h = h ^ (x * 43487273); return h; } diff --git a/SharpGraph/src/core/Graph.Triangles.cs b/SharpGraph/src/core/Graph.Triangles.cs index 1d710e1..54e8764 100644 --- a/SharpGraph/src/core/Graph.Triangles.cs +++ b/SharpGraph/src/core/Graph.Triangles.cs @@ -122,56 +122,40 @@ public List> FindTriangles() for (var j = i + 1; j < this.edges.Count - 1; j++) { var e2 = this.edges[j]; - if (e1.From() == e2.From()) + if (e1.From() == e2.From() && e1 != e2) { var ep1 = new Edge(e1.To(), e2.To()); - var ep2 = new Edge(e2.To(), e1.To()); - if ( - (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) - || (this.edges.Contains(ep2) && !dic.ContainsKey(ep2)) - ) + if (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) { var l = new Tuple(e1.From(), e1.To(), e2.To()); dic[e2] = j; result.Add(l); } } - else if (e1.From() == e2.To()) + else if (e1.From() == e2.To() && e1 != e2) { var ep1 = new Edge(e1.To(), e2.From()); - var ep2 = new Edge(e2.From(), e1.To()); - if ( - (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) - || (this.edges.Contains(ep2) && !dic.ContainsKey(ep2)) - ) + if (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) { var l = new Tuple(e1.From(), e1.To(), e2.From()); dic[e2] = j; result.Add(l); } } - else if (e1.To() == e2.To()) + else if (e1.To() == e2.To() && e1 != e2) { var ep1 = new Edge(e1.From(), e2.From()); - var ep2 = new Edge(e2.From(), e1.From()); - if ( - (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) - || (this.edges.Contains(ep2) && !dic.ContainsKey(ep2)) - ) + if (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) { var l = new Tuple(e1.To(), e1.From(), e2.From()); dic[e2] = j; result.Add(l); } } - else if (e1.To() == e2.From()) + else if (e1.To() == e2.From() && e1 != e2) { var ep1 = new Edge(e1.From(), e2.To()); - var ep2 = new Edge(e2.To(), e1.From()); - if ( - (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) - || (this.edges.Contains(ep2) && !dic.ContainsKey(ep2)) - ) + if (this.edges.Contains(ep1) && !dic.ContainsKey(ep1)) { var l = new Tuple(e1.To(), e1.From(), e2.To()); dic[e2] = j; diff --git a/SharpGraph/src/core/Graph.cs b/SharpGraph/src/core/Graph.cs index 96a8921..1d1e996 100644 --- a/SharpGraph/src/core/Graph.cs +++ b/SharpGraph/src/core/Graph.cs @@ -59,6 +59,15 @@ public Graph(List edges, HashSet nodes) } } + public Graph(HashSet nodes) + : this() + { + foreach (var node in nodes) + { + this.AddNode(node); + } + } + public Graph(params string[] nodes) : this() { @@ -159,8 +168,8 @@ public Graph(string fromToString) public Graph(Graph g, HashSet nodes) { this.Init(); - var filteredEdges = g.edges - .Where(e => nodes.Contains(e.From()) && nodes.Contains(e.To())) + var filteredEdges = g + .edges.Where(e => nodes.Contains(e.From()) && nodes.Contains(e.To())) .ToList(); foreach (var e in filteredEdges) { @@ -286,6 +295,15 @@ public void AddEdge(string edgeString) } } + public void AddEdges(string edgesString) + { + var edges = edgesString.Split(','); + foreach (var e in edges) + { + this.AddEdge(e); + } + } + public Edge? GetEdge(HashSet nodes) { if (nodes.Count != 2) @@ -368,7 +386,13 @@ public bool RemoveEdge(string fromNode, string toNode) public bool RemoveEdge((Node, Node) fromToNodes) { var edge = new Edge(fromToNodes.Item1, fromToNodes.Item2); - return this.RemoveEdge(edge); + var res = this.RemoveEdge(edge); + if (!res) + { + return this.RemoveEdge(new Edge(fromToNodes.Item2, fromToNodes.Item1)); + } + + return res; } public bool RemoveEdge(Edge edge) @@ -411,9 +435,12 @@ public Graph MergeWith(Graph graph2) internal bool AddEdge(Edge edge) { - if (this.GetEdges().Contains(edge)) + foreach (var e in this.edges) { - return false; + if (e == edge) + { + return false; + } } if (this.edgeComponents.ContainsKey(edge))