Last active
April 12, 2018 06:43
-
-
Save IanMercer/834335afc8d76f4ce64293bde9595744 to your computer and use it in GitHub Desktop.
A simple in-memory graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics; | |
namespace AboditUnits.Graph | |
{ | |
public partial class Graph<TNode, TRelation> | |
{ | |
/// <summary> | |
/// A relationship between two objects | |
/// </summary> | |
[DebuggerDisplay("{Start} -- {Predicate} --> {End}")] | |
public struct Edge | |
{ | |
public readonly TNode Start; | |
public readonly TRelation Predicate; | |
public readonly TNode End; | |
public Edge(TNode start, TRelation predicate, TNode end) | |
{ | |
this.Start = start; | |
this.Predicate = predicate; | |
this.End = end; | |
} | |
public override bool Equals(object obj) | |
{ | |
if (!(obj is Edge)) return false; | |
Edge other = (Edge) obj; | |
return this.Start.Equals(other.Start) | |
&& this.Predicate.Equals(other.Predicate) | |
&& this.End.Equals(other.End); | |
} | |
public override int GetHashCode() | |
{ | |
return this.Start.GetHashCode() ^ this.Predicate.GetHashCode() ^ this.End.GetHashCode(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.Linq; | |
namespace AboditUnits.Graph | |
{ | |
/// <summary> | |
/// An in-memory graph of statements (subject, predicate, object) | |
/// </summary> | |
public partial class Graph<TNode, TRelation> : IEnumerable<TNode> | |
where TNode : class, IEquatable<TNode> | |
where TRelation : IEquatable<TRelation> | |
{ | |
/// <summary> | |
/// Limit the number of edges that can be attached to any node | |
/// </summary> | |
public int Limit { get; set; } = 10000; | |
protected readonly ConcurrentDictionary<TNode, PredicateNext> StartIndexedEdges = new ConcurrentDictionary<TNode, PredicateNext>(); | |
protected readonly ConcurrentDictionary<TNode, PredicatePrevious> EndIndexedEdges = new ConcurrentDictionary<TNode, PredicatePrevious>(); | |
/// <summary> | |
/// Add a statement, returns true if it was added, false if already there | |
/// </summary> | |
/// <remarks> | |
/// Direct loops back to self are not allowed | |
/// </remarks> | |
public bool AddStatement(TNode start, TRelation predicate, TNode end) | |
{ | |
if (start == null || end == null) throw new Exception("Trying to relate a null"); | |
// If something is a synonym of itself, really don't care | |
if (start == end) return false; | |
return this.AddStatement(new Edge(start, predicate, end)); | |
} | |
/// <summary> | |
/// Add a statement, returns true if it was added, false if already there | |
/// </summary> | |
private bool AddStatement(Edge statement) | |
{ | |
var forward = new PredicateNext(statement.Predicate, statement.End); | |
if (!StartIndexedEdges.TryAdd(statement.Start, forward)) | |
{ | |
var current = StartIndexedEdges[statement.Start]; | |
// Skip if the statement is already there | |
if (current.Predicate.Equals(statement.Predicate) && current.End == statement.End) | |
return false; | |
int i = Limit; | |
while (current.Next != null) | |
{ | |
current = current.Next; | |
// Skip if the statement is already there | |
if (current.Predicate.Equals(statement.Predicate) && current.End == statement.End) | |
return false; | |
if (i-- < 0) throw new Exception("Infinite loop possible"); | |
} | |
current.Next = forward; | |
} | |
var reverse = new PredicatePrevious(statement.Predicate, statement.Start); | |
if (!EndIndexedEdges.TryAdd(statement.End, reverse)) | |
{ | |
var current = EndIndexedEdges[statement.End]; | |
// Skip if the statement is already there | |
if (current.Predicate.Equals(statement.Predicate) && current.Start == statement.Start) | |
return false; | |
int i = Limit; | |
while (current.Next != null) | |
{ | |
current = current.Next; | |
// Skip if the statement is already there | |
if (current.Predicate.Equals(statement.Predicate) && current.Start == statement.Start) | |
return false; | |
if (i-- < 0) throw new Exception("Infinite loop possible"); | |
} | |
current.Next = reverse; | |
} | |
return true; | |
} | |
public IEnumerable<TNode> Nodes | |
{ | |
get | |
{ | |
// Every node in the graph must be either a start node or an end node or it wouldn't be here | |
return this.StartIndexedEdges.Select(x => x.Key).Concat(this.EndIndexedEdges.Select(x => x.Key)).Distinct(); | |
} | |
} | |
/// <summary> | |
/// Examine every node in the graph for ones of type T | |
/// </summary> | |
public IEnumerable<T> GetNodes<T>() where T:TNode => this.Nodes.OfType<T>(); | |
/// <summary> | |
/// Get all the edges of the graph | |
/// </summary> | |
public IEnumerable<Edge> Edges | |
{ | |
get | |
{ | |
return this.StartIndexedEdges | |
.SelectMany(e => e.Value.Select(pn => new Edge(e.Key, pn.Predicate, pn.End))); | |
} | |
} | |
/// <summary> | |
/// Find all the outgoing edges from a vertex with a given predicate (or null) | |
/// </summary> | |
/// <remarks> | |
/// A single step in the graph away from a node | |
/// </remarks> | |
public IEnumerable<Edge> Follow(TNode start, TRelation predicate = default(TRelation)) | |
{ | |
PredicateNext startLink = null; | |
if (StartIndexedEdges.TryGetValue(start, out startLink)) | |
{ | |
foreach (var pn in startLink.Where(pn => | |
EqualityComparer<TRelation>.Default.Equals(predicate, default(TRelation)) || pn.Predicate.Equals(predicate))) | |
{ | |
yield return new Edge(start, pn.Predicate, pn.End); | |
} | |
} | |
} | |
/// <summary> | |
/// Find all the outgoing edges from a vertex with a given predicate (or null) | |
/// </summary> | |
public IEnumerable<Edge> Back(TNode end, TRelation predicate) | |
{ | |
PredicatePrevious endLink = null; | |
if (EndIndexedEdges.TryGetValue(end, out endLink)) | |
{ | |
foreach (var pn in endLink.Where(pn => | |
EqualityComparer<TRelation>.Default.Equals(predicate, default(TRelation)) || pn.Predicate.Equals(predicate))) | |
{ | |
yield return new Edge(pn.Start, pn.Predicate, end); | |
} | |
} | |
} | |
/// <summary> | |
/// Find all the outgoing edges from a vertex with a given predicate (or null) and keep following edges of that type | |
/// match only nodes of type T. Return the results as a tree (can be flattened using SelectMany). | |
/// </summary> | |
public Graph<T, TRelation> Successors<T>(TNode start, TRelation predicate) | |
where T : class, TNode, IEquatable<T> | |
{ | |
var visited = new HashSet<TNode>(); | |
var stack = new Stack<TNode>(); | |
stack.Push(start); | |
var result = new Graph<T, TRelation>(); | |
while (stack.Count > 0) | |
{ | |
start = stack.Pop(); | |
var outgoing = this.Follow(start, predicate); | |
foreach (var edge in outgoing) | |
{ | |
T inEnd = edge.Start as T; | |
T outEnd = edge.End as T; | |
if (inEnd == null || outEnd == null) continue; | |
result.AddStatement(new Graph<T, TRelation>.Edge(inEnd, edge.Predicate, outEnd)); | |
if (!visited.Contains(outEnd)) | |
{ | |
stack.Push(outEnd); | |
visited.Add(outEnd); | |
} | |
} | |
} | |
return result; | |
} | |
/// <summary> | |
/// Perform a search of the graph following a given predicate looking for nodes of type T | |
/// </summary> | |
public IEnumerable<T> Search<T>(TNode start, TRelation predicate, ISearchOrder<TNode> stack) where T : class, TNode | |
{ | |
var visited = new HashSet<TNode>(); | |
stack.Enqueue(start); | |
while (stack.Count > 0) | |
{ | |
start = stack.Dequeue(); | |
var outgoing = this.Follow(start, predicate); | |
foreach (var edge in outgoing) | |
{ | |
T outEnd = edge.End as T; | |
if (outEnd == null) continue; | |
if (!visited.Contains(outEnd)) | |
{ | |
stack.Enqueue(outEnd); | |
visited.Add(outEnd); | |
yield return outEnd; | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Union two graphs | |
/// </summary> | |
public Graph<TNode, TRelation> Union(Graph<TNode, TRelation> other) | |
{ | |
var result = new Graph<TNode, TRelation>(); | |
foreach (var edge in this.Edges.Concat(other.Edges).ToList()) // ToList for debugging | |
{ | |
result.AddStatement(edge); | |
} | |
return result; | |
} | |
/// <summary> | |
/// Intersect two graphs | |
/// </summary> | |
public Graph<TNode, TRelation> Intersect(Graph<TNode, TRelation> other) | |
{ | |
var result = new Graph<TNode, TRelation>(); | |
// As a struct, this should just work - if they have the same start, Predicate and end | |
var commonEdges = this.Edges.Intersect(other.Edges); | |
foreach (var edge in commonEdges) | |
{ | |
result.AddStatement(edge); | |
} | |
return result; | |
} | |
/// <summary> | |
/// Topological sort approx | |
/// </summary> | |
public IEnumerable<TNode> TopologicalSortApprox() | |
{ | |
var nodesWithNoIncomingLinks = Nodes.Except(StartIndexedEdges.SelectMany(e => e.Value).Select(pn => pn.End)).ToList(); | |
// L ← Empty list that will contain the sorted elements | |
var l = new List<TNode>(); | |
//S ← Set of all nodes with no incoming edges | |
var s = new Queue<TNode>(nodesWithNoIncomingLinks); | |
var visited = new HashSet<TNode>(nodesWithNoIncomingLinks); | |
//while S is non - empty do | |
while (s.Count > 0) | |
{ | |
// remove a node n from S | |
var n = s.Dequeue(); | |
// add n to tail of L | |
l.Add(n); | |
// for each node m with an edge e from n to m do | |
foreach (var e in this.Follow(n, default(TRelation))) | |
{ | |
if (visited.Contains(e.End)) | |
continue; | |
// remove edge e from the graph | |
// if m has no other incoming edges then | |
// insert m into S | |
s.Enqueue(e.End); | |
visited.Add(e.End); | |
} | |
} | |
//if graph has edges then | |
// return error(graph has at least one cycle) | |
//else | |
// return L(a topologically sorted order) | |
return l; | |
} | |
public IEnumerator<TNode> GetEnumerator() | |
{ | |
return this.GetNodes<TNode>().GetEnumerator(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return GetEnumerator(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using NUnit.Framework; | |
using AboditUnits.Graph; | |
using FluentAssertions; | |
namespace AboditUnitsTest | |
{ | |
[TestFixture] | |
public class GraphTests | |
{ | |
Graph<string, Relation> A; | |
Graph<string, Relation> B; | |
Graph<string, Relation> C; | |
static readonly string a = "a"; | |
static readonly string b = "b"; | |
static readonly string c = "c"; | |
static readonly string d = "d"; | |
static readonly string e = "e"; | |
static readonly string f = "f"; | |
public GraphTests() | |
{ | |
A = new Graph<string, Relation>(); | |
A.AddStatement(a, Relation.RDFSType, b); | |
A.AddStatement(a, Relation.RDFSType, c); | |
A.AddStatement(b, Relation.RDFSType, d); | |
A.AddStatement(c, Relation.RDFSType, e); | |
A.AddStatement(d, Relation.RDFSType, f); | |
A.AddStatement(e, Relation.RDFSType, f); | |
Console.WriteLine("A"); | |
foreach (var edge in A.Edges) | |
{ | |
Console.WriteLine(" " + edge); | |
} | |
B = new Graph<string, Relation>(); | |
B.AddStatement(a, Relation.RDFSType, b); | |
B.AddStatement(a, Relation.RDFSType, c); | |
B.AddStatement(b, Relation.RDFSType, d); | |
Console.WriteLine("B"); | |
foreach (var edge in B.Edges) | |
{ | |
Console.WriteLine(" " + edge); | |
} | |
C = new Graph<string, Relation>(); | |
C.AddStatement(a, Relation.RDFSType, f); | |
Console.WriteLine("C"); | |
foreach (var edge in C.Edges) | |
{ | |
Console.WriteLine(" " + edge); | |
} | |
} | |
[Test] | |
public void CanIntersectGraphs() | |
{ | |
var AwithA = A.Intersect(A); | |
var AwithB = A.Intersect(B); | |
var BwithA = B.Intersect(A); | |
var BwithC = B.Intersect(C); | |
var CwithB = C.Intersect(B); | |
AwithA.Nodes.Should().HaveCount(A.Nodes.Count(), "Intersection with self should preserve graph nodes"); | |
AwithA.Edges.Should().HaveCount(A.Edges.Count(), "Intersection with self should preserve graph edges"); | |
AwithB.Nodes.Should().HaveCount(B.Nodes.Count(), "B is a subgraph of A"); | |
BwithA.Nodes.Should().HaveCount(B.Nodes.Count(), "A is a supergraph of B"); | |
BwithC.Nodes.Should().HaveCount(0, "B is disjoint from C"); | |
CwithB.Nodes.Should().HaveCount(0, "C is disjoint from B"); | |
} | |
[Test] | |
public void UnionWithSelfIsIdentity() | |
{ | |
var AwithA = A.Union(A); | |
AwithA.Nodes.Should().HaveCount(A.Nodes.Count(), "Union with self should preserve graph nodes"); | |
AwithA.Edges.Should().HaveCount(A.Edges.Count(), "Union with self should preserve graph edges"); | |
} | |
[Test] | |
public void UnionWithSubgraph() | |
{ | |
var AwithB = A.Union(B); | |
var BwithA = B.Union(A); | |
AwithB.Nodes.Should().HaveCount(A.Nodes.Count(), "B is a subgraph of A"); | |
BwithA.Nodes.Should().HaveCount(A.Nodes.Count(), "A is a supergraph of B"); | |
} | |
[Test] | |
public void UnionWithDisjoint() | |
{ | |
var BwithC = B.Union(C); | |
var CwithB = C.Union(B); | |
BwithC.Nodes.Should().HaveCount(B.Nodes.Count() + C.Nodes.Count() - 1, "B is disjoint from C but shares one node"); | |
CwithB.Nodes.Should().HaveCount(B.Nodes.Count() + C.Nodes.Count() - 1, "C is disjoint from B but shares one node"); | |
BwithC.Edges.Should().HaveCount(B.Edges.Count() + C.Edges.Count(), "B is disjoint from C"); | |
CwithB.Edges.Should().HaveCount(B.Edges.Count() + C.Edges.Count(), "B is disjoint from C"); | |
} | |
[Test] | |
public void TopoSortSimple() | |
{ | |
var aSorted = string.Join("", A.TopologicalSortApprox()); | |
aSorted.Should().Be("abcdef"); | |
//var fSorted = string.Join("", A.TopologicalSortApprox()); | |
//fSorted.Should().Be("f"); | |
//var cSorted = string.Join("", A.TopologicalSortApprox()); | |
//cSorted.Should().Be("cef"); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
namespace AboditUnits.Graph | |
{ | |
public interface ISearchOrder<T> | |
{ | |
int Count { get; } | |
T Dequeue(); | |
void Enqueue(T value); | |
} | |
public class BreadthFirstSearch<T> : ISearchOrder<T> | |
{ | |
private readonly Queue<T> queue = new Queue<T>(); | |
public int Count => queue.Count; | |
public T Dequeue() | |
{ | |
return queue.Dequeue(); | |
} | |
public void Enqueue(T value) | |
{ | |
queue.Enqueue(value); | |
} | |
} | |
public class DepthFirstSearch<T> : ISearchOrder<T> | |
{ | |
private readonly Stack<T> stack = new Stack<T>(); | |
public int Count => stack.Count; | |
public T Dequeue() | |
{ | |
return stack.Pop(); | |
} | |
public void Enqueue(T value) | |
{ | |
stack.Push(value); | |
} | |
} | |
public class RandomFirstSearch<T> : ISearchOrder<T> | |
{ | |
private readonly Random r = new Random(); | |
private readonly List<T> queue = new List<T>(); | |
public int Count => queue.Count; | |
public T Dequeue() | |
{ | |
int index = r.Next(queue.Count); | |
var result = queue[index]; | |
queue.RemoveAt(index); | |
return result; | |
} | |
public void Enqueue(T value) | |
{ | |
queue.Add(value); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
namespace AboditUnits.Graph | |
{ | |
public partial class Graph<TNode, TRelation> | |
{ | |
/// <summary> | |
/// A predicate, object pair pointing forwards (stored in a linked list) | |
/// </summary> | |
[DebuggerVisualizer("--{Predicate}-->{End}")] | |
public class PredicateNext : IEnumerable<PredicateNext> | |
{ | |
const int limit = 10000; | |
public TRelation Predicate; | |
public TNode End; | |
public PredicateNext Next { get; set; } | |
public PredicateNext(TRelation predicate, TNode end) | |
{ | |
this.Predicate = predicate; | |
this.End = end; | |
} | |
public IEnumerable<PredicateNext> Chain() | |
{ | |
var current = this; | |
int i = limit; | |
while (current != null) | |
{ | |
yield return current; | |
current = current.Next; | |
if (i-- < 0) throw new Exception("Infinite loop possible"); | |
} | |
} | |
public IEnumerator<PredicateNext> GetEnumerator() | |
{ | |
return this.Chain().GetEnumerator(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return GetEnumerator(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
namespace AboditUnits.Graph | |
{ | |
public partial class Graph<TNode, TRelation> | |
{ | |
/// <summary> | |
/// A predicate, object pair pointing backwards (stored in a linked list) | |
/// </summary> | |
[DebuggerVisualizer("{Start}<--{Predicate}--")] | |
public class PredicatePrevious : IEnumerable<PredicatePrevious> | |
{ | |
const int limit = 10000; | |
public TRelation Predicate; | |
public TNode Start; | |
public PredicatePrevious Next; | |
public PredicatePrevious(TRelation predicate, TNode start) | |
{ | |
this.Predicate = predicate; | |
this.Start = start; | |
} | |
public IEnumerable<PredicatePrevious> Chain() | |
{ | |
var current = this; | |
int i = limit; | |
while (current != null) | |
{ | |
yield return current; | |
current = current.Next; | |
if (i-- < 0) throw new Exception("Infinite loop possible"); | |
} | |
} | |
public IEnumerator<PredicatePrevious> GetEnumerator() | |
{ | |
return this.Chain().GetEnumerator(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return GetEnumerator(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
namespace AboditUnits.Graph | |
{ | |
/// <summary> | |
/// A relationship between two nodes in a graph, e.g. parent, child, antonym, synonym, ... | |
/// </summary> | |
[DebuggerDisplay("{name}")] | |
public class Relation : IEquatable<Relation> | |
{ | |
public string Name { get; } | |
private Relation(string name) { this.Name = name; } | |
private static readonly Dictionary<string, Relation> identityMap = new Dictionary<string, Relation>(); | |
public static Relation GetByName(string name) | |
{ | |
Relation result; | |
if (!identityMap.TryGetValue(name, out result)) | |
{ | |
result = new Relation(name); | |
identityMap.Add(name, result); | |
} | |
return result; | |
} | |
public static readonly Relation Antonym = GetByName("antonym"); | |
public static readonly Relation Synonym = GetByName("synonym"); | |
public static readonly Relation MemberMeronymOf = GetByName("memberMeronymOf"); // mereonym | |
public static readonly Relation PartMeronymOf = GetByName("partMeronymOf"); // mereonym | |
public static readonly Relation HolonymOf = GetByName("holonymOf"); // holonym | |
public static readonly Relation SimilarTo = GetByName("similarTo"); | |
public static readonly Relation ClassifiedByTopic = GetByName("classifiedByTopic"); | |
public static readonly Relation ClassifiedByRegion = GetByName("classifiedByRegion"); | |
public static readonly Relation ClassifiedByUsage = GetByName("classifiedByUsage"); | |
public static readonly Relation PertainsTo = GetByName("pertainsTo"); | |
public static readonly Relation DerivationallyRelated = GetByName("derivationallyRelated"); | |
public static readonly Relation Related = GetByName("related"); | |
public static readonly Relation Domain = GetByName("domain"); | |
public static readonly Relation Range = GetByName("range"); | |
public static readonly Relation WordFor = GetByName("wordFor"); | |
public static readonly Relation Superlative = GetByName("superlative"); | |
public static readonly Relation Comparative = GetByName("comparative"); | |
public static readonly Relation NounForm = GetByName("noun"); | |
public static readonly Relation VerbForm = GetByName("verb"); | |
public static readonly Relation AdjectiveForm = GetByName("adjective"); | |
public static readonly Relation AdverbForm = GetByName("adverb"); | |
public static readonly Relation PastTense = GetByName("pastTense"); | |
public static readonly Relation FutureTense = GetByName("futureTense"); | |
public static readonly Relation PastParticiple = GetByName("pastParticple"); | |
public static readonly Relation PresentParticiple = GetByName("presentParticiple"); | |
public static readonly Relation FirstPersonSingular = GetByName("firstPersonSingular"); | |
public static readonly Relation FirstPersonPlural = GetByName("firstPersonPlural"); | |
public static readonly Relation SingularForm = GetByName("singular"); | |
public static readonly Relation PluralForm = GetByName("plural"); | |
public static readonly Relation RDFSType = GetByName("type"); | |
public bool Equals(Relation other) | |
{ | |
// Can use reference equals since identity mapped | |
return ReferenceEquals(this, other); | |
} | |
public override string ToString() | |
{ | |
return "--" + this.Name + "-->"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment