turbine-graph 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,125 @@
1
+ require 'turbine'
2
+
3
+ module Turbine
4
+
5
+ # This example is taken from a TV show called 'Modern Family' with complex
6
+ # family relations, in other words: ideal to test a complex graph structure.
7
+ #
8
+ # http://en.wikipedia.org/wiki/List_of_Modern_Family_characters
9
+ def self.family_stub
10
+ graph = Turbine::Graph.new
11
+
12
+ phil = graph.add(Turbine::Node.new(:phil, gender: :male))
13
+ claire = graph.add(Turbine::Node.new(:claire, gender: :female))
14
+ haley = graph.add(Turbine::Node.new(:haley, gender: :female))
15
+ alex = graph.add(Turbine::Node.new(:alex, gender: :female))
16
+ luke = graph.add(Turbine::Node.new(:luke, gender: :male))
17
+
18
+ jay = graph.add(Turbine::Node.new(:jay, gender: :male))
19
+ gloria = graph.add(Turbine::Node.new(:gloria, gender: :female))
20
+ manny = graph.add(Turbine::Node.new(:manny, gender: :male))
21
+ unnamed = graph.add(Turbine::Node.new(:unnamed))
22
+
23
+ mitchell = graph.add(Turbine::Node.new(:mitchell, gender: :male))
24
+ cameron = graph.add(Turbine::Node.new(:cameron, gender: :male))
25
+ lily = graph.add(Turbine::Node.new(:lily, gender: :female))
26
+
27
+ dede = graph.add(Turbine::Node.new(:dede, gender: :female))
28
+ javier = graph.add(Turbine::Node.new(:javier, gender: :male))
29
+
30
+ frank = graph.add(Turbine::Node.new(:frank, gender: :male))
31
+ sarah = graph.add(Turbine::Node.new(:sarah, gender: :female))
32
+
33
+ # Dunphy -----------------------------------------------------------------
34
+
35
+ # Phil
36
+ phil.connect_to(claire, :spouse)
37
+ phil.connect_to(haley, :child)
38
+ phil.connect_to(alex, :child)
39
+ phil.connect_to(luke, :child)
40
+
41
+ # Claire
42
+ claire.connect_to(phil, :spouse)
43
+ claire.connect_to(haley, :child)
44
+ claire.connect_to(alex, :child)
45
+ claire.connect_to(luke, :child)
46
+
47
+ # Pritchett --------------------------------------------------------------
48
+
49
+ # Jay
50
+ jay.connect_to(gloria, :spouse)
51
+ jay.connect_to(claire, :child)
52
+ jay.connect_to(mitchell, :child)
53
+ jay.connect_to(unnamed, :child)
54
+ jay.connect_to(dede, :divorced)
55
+
56
+ # Gloria
57
+ gloria.connect_to(jay, :spouse)
58
+ gloria.connect_to(manny, :child)
59
+ gloria.connect_to(unnamed, :child)
60
+ gloria.connect_to(javier, :divorced)
61
+
62
+ # Tucker-Pritchett -------------------------------------------------------
63
+
64
+ mitchell.connect_to(cameron, :spouse)
65
+ mitchell.connect_to(lily, :child)
66
+
67
+ cameron.connect_to(mitchell, :spouse)
68
+ cameron.connect_to(lily, :child)
69
+
70
+ # Others -----------------------------------------------------------------
71
+
72
+ dede.connect_to(claire, :child)
73
+ dede.connect_to(mitchell, :child)
74
+
75
+ javier.connect_to(manny, :child)
76
+
77
+ frank.connect_to(phil, :child)
78
+ frank.connect_to(sarah, :spouse)
79
+
80
+ sarah.connect_to(phil, :child)
81
+ sarah.connect_to(frank, :spouse)
82
+
83
+ graph
84
+ end
85
+ end
86
+
87
+ # Find out that Manny is Jay's step-child with:
88
+ #
89
+ # jay = Turbine.stub.node(:jay)
90
+ # jay.out(:spouse).out(:child) - jay.out(:child)
91
+ #
92
+ # # => #<Turbine::Collection {#<Turbine::Node key=:manny>}>
93
+ #
94
+ # ... or that Alex has two siblings:
95
+ #
96
+ # alex = Turbine.stub.node(:alex)
97
+ # alex.in(:child).out(:child) - [alex]
98
+ #
99
+ # ... or that Jay and Gloria have a single "common" child:
100
+ #
101
+ # graph = Turbine.stub
102
+ # jay, gloria = graph.node(:jay), graph.node(:gloria)
103
+ #
104
+ # jay.out(:child) & gloria.out(:child)
105
+ #
106
+ # # => #<Turbine::Collection {#<Turbine::Node key=:unnamed>}>
107
+ #
108
+ # ... or who are Luke's uncles and aunts:
109
+ #
110
+ # graph = Turbine.stub
111
+ # luke = graph.node(:luke)
112
+ #
113
+ # parents = luke.in(:child)
114
+ # grandparents = parents.in(:child)
115
+ #
116
+ # # Remove Claire, Luke's mother:
117
+ # uncles_and_aunts = grandparents.out(:child) - parents
118
+ #
119
+ # # And add the uncle and aunt spouses for the full list.
120
+ # uncles_and_aunts + uncles_and_aunts.out(:spouse)
121
+ #
122
+ # # => #<Turbine::Collection {
123
+ # #<Turbine::Node key=:mitchell>,
124
+ # #<Turbine::Node key=:unnamed>,
125
+ # #<Turbine::Node key=:cameron>}>
data/lib/turbine.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'tsort'
2
+ require 'set'
3
+ require 'forwardable'
4
+
5
+ # On with the library...
6
+ require 'turbine/properties'
7
+ require 'turbine/algorithms/tarjan'
8
+ require 'turbine/algorithms/filtered_tarjan'
9
+ require 'turbine/edge'
10
+ require 'turbine/errors'
11
+ require 'turbine/graph'
12
+ require 'turbine/pipeline/dsl'
13
+ require 'turbine/pipeline/segment'
14
+ require 'turbine/pipeline/trace'
15
+ require 'turbine/pipeline/expander'
16
+ require 'turbine/pipeline/filter'
17
+ require 'turbine/pipeline/journal'
18
+ require 'turbine/pipeline/journal_filter'
19
+ require 'turbine/pipeline/pump'
20
+ require 'turbine/pipeline/transform'
21
+ require 'turbine/pipeline/traversal'
22
+ require 'turbine/pipeline/sender'
23
+ require 'turbine/pipeline/split'
24
+ require 'turbine/pipeline/unique'
25
+ require 'turbine/traversal/base'
26
+ require 'turbine/traversal/breadth_first'
27
+ require 'turbine/traversal/depth_first'
28
+ require 'turbine/node'
29
+ require 'turbine/version'
@@ -0,0 +1,33 @@
1
+ module Turbine
2
+ module Algorithms
3
+ # Internal: A wrapper around the Ruby stdlib implementation of Tarjan's
4
+ # strongly connected components and topological sort algorithms. Restricts
5
+ # the sort to only those edges which match the filter.
6
+ class FilteredTarjan < Tarjan
7
+
8
+ # Public: Creates a FilteredTarjan instance. If you simply wish to
9
+ # filter by the edge label, the standard Tarjan class will do this with
10
+ # better performance.
11
+ #
12
+ # graph - A Turbine graph whose nodes are to be sorted.
13
+ # &filter - Block which sleects the edges used to perform the sort.
14
+ #
15
+ # Returns a FilteredTarjan.
16
+ def initialize(graph, &filter)
17
+ super(graph, nil)
18
+ @filter = filter
19
+ end
20
+
21
+ #######
22
+ private
23
+ #######
24
+
25
+ # Internal: Used by TSort to iterate through each +out+ node.
26
+ #
27
+ # Returns nothing.
28
+ def tsort_each_child(node)
29
+ node.out_edges.select(&@filter).each { |edge| yield edge.to }
30
+ end
31
+ end # FilteredTarjan
32
+ end # Algorithms
33
+ end # Turbine
@@ -0,0 +1,50 @@
1
+ module Turbine
2
+ module Algorithms
3
+ # Internal: A wrapper around the Ruby stdlib implementation of Tarjan's
4
+ # strongly connected components and topological sort algorithms.
5
+ class Tarjan
6
+ include TSort
7
+
8
+ # Public: Creates a new Tarjan instance used for topologically sorting
9
+ # graphs.
10
+ #
11
+ # graph - A Turbine graph whose nodes are to be sorted.
12
+ # label - An optional label which will be used when traversing from a
13
+ # node to its +out+ nodes.
14
+ #
15
+ # For example
16
+ #
17
+ # Turbine::Algorithms::Tarjan.new(graph).tsort
18
+ # # => [ [ #<Node key=one>, #<Node key=two>, #<Node key=three> ],
19
+ # [ #<Node key=five> ],
20
+ # [ #<Node key=six>, #<Node key=seven> ] ]
21
+ #
22
+ # Turbine::Algorithms::Tarjan.new(graph, :spouse).tsort
23
+ # # => [ ... ]
24
+ #
25
+ # Returns a Tarjan.
26
+ def initialize(graph, label = nil)
27
+ @nodes = graph.nodes
28
+ @label = label
29
+ end
30
+
31
+ #######
32
+ private
33
+ #######
34
+
35
+ # Internal: Used by TSort to iterate through each node in the graph.
36
+ #
37
+ # Returns nothing.
38
+ def tsort_each_node
39
+ @nodes.each { |node| yield node }
40
+ end
41
+
42
+ # Internal: Used by TSort to iterate through each +out+ node.
43
+ #
44
+ # Returns nothing.
45
+ def tsort_each_child(node)
46
+ node.nodes(:out, @label).each { |out| yield out }
47
+ end
48
+ end # Tarjan
49
+ end # Algorithms
50
+ end # Turbine
@@ -0,0 +1,94 @@
1
+ module Turbine
2
+ # Edges represent a connection between an +from+ node and an +to+ node.
3
+ #
4
+ # Note that simply creating an Edge does not actually establish the
5
+ # connection between the two nodes. Rather than creating the Edge
6
+ # instance manually, Node can do this for you with +Node#connect_to+ and
7
+ # +Node#connect_via+:
8
+ #
9
+ # jay = Turbine::Node.new(:jay)
10
+ # gloria = Turbine::Node.new(:gloria)
11
+ #
12
+ # jay.connect_to(gloria, :spouse)
13
+ # gloria.connect_to(jay, :spouse)
14
+ #
15
+ # However, if you want to do it manually (perhaps with a subclass of Edge),
16
+ # you should use +Node#connect_via+:
17
+ #
18
+ # jay = Turbine::Node.new(:jay)
19
+ # gloria = Turbine::Node.new(:gloria)
20
+ #
21
+ # jay_married_to_gloria = Turbine::Edge.new(jay, gloria, :spouse)
22
+ # gloria_married_to_jay = Turbine::Edge.new(gloria, jay, :spouse)
23
+ #
24
+ # jay.connect_via(jay_married_to_gloria)
25
+ # gloria.connect_via(jay_married_to_gloria)
26
+ #
27
+ # jay.connect_via(gloria_married_to_jay)
28
+ # gloria.connect_via(gloria_married_to_jay)
29
+ #
30
+ class Edge
31
+ include Properties
32
+
33
+ # Public: The node which the edge leaves; the edge is an +out_edge+ on
34
+ # this node.
35
+ attr_reader :from
36
+
37
+ # Public: The node to which the edge points; the edge is an +in_edge+ on
38
+ # this node.
39
+ attr_reader :to
40
+
41
+ # Public: Returns the optional label assigned to the edge.
42
+ attr_reader :label
43
+
44
+ # Attribute aliases.
45
+ alias_method :parent, :from
46
+ alias_method :child, :to
47
+
48
+ # Public: Creates a new Edge.
49
+ #
50
+ # from_node - The Node from which the edge originates.
51
+ # in_node - The Node to which the edge points.
52
+ # label - An optional label for describing the nature of the
53
+ # relationship between the two nodes.
54
+ # properties - Optional key/value properties to be associated with the
55
+ # edge.
56
+ #
57
+ def initialize(from_node, to_node, label = nil, properties = nil)
58
+ @from = from_node
59
+ @to = to_node
60
+ @label = label
61
+
62
+ self.properties = properties
63
+ end
64
+
65
+ # Public: Determines if the +other+ edge is similar to this one.
66
+ #
67
+ # Two edges are considered similar if have the same +to+ and +from+ nodes,
68
+ # and their label is identical.
69
+ #
70
+ # Returns true or false.
71
+ def similar?(other)
72
+ other && other.to == @to && other.from == @from && other.label == @label
73
+ end
74
+
75
+ # Public: Returns a human-readable version of the edge.
76
+ def inspect
77
+ "#<#{ self.class.name } #{ to_s }>".sub('>', "\u27A4")
78
+ end
79
+
80
+ # Public: Returns a human-readable version of the edge by showing the from
81
+ # and to nodes, connected by the label.
82
+ def to_s
83
+ "#{ @from.key.inspect } -#{ @label.inspect }-> #{ @to.key.inspect }"
84
+ end
85
+
86
+ # Internal: A low-level method which retrieves the node in a given
87
+ # direction. Used for compatibility with Pipeline.
88
+ #
89
+ # Returns a Node.
90
+ def nodes(direction, *)
91
+ direction == :to ? @to : @from
92
+ end
93
+ end # Edge
94
+ end # Turbine
@@ -0,0 +1,74 @@
1
+ module Turbine
2
+ # Error class which serves as a base for all errors which occur in Turbine.
3
+ TurbineError = Class.new(StandardError)
4
+
5
+ # Internal: Creates a new error class which inherits from TurbineError,
6
+ # whose message is created by evaluating the block you give.
7
+ #
8
+ # For example
9
+ #
10
+ # MyError = error_class do |weight, limit|
11
+ # "#{ weight } exceeds #{ limit }"
12
+ # end
13
+ #
14
+ # raise MyError.new(5000, 2500)
15
+ # # => #<Turbine::MyError: 5000 exceeds 2500>
16
+ #
17
+ # Returns an exception class.
18
+ def self.error_class(superclass = TurbineError, &block)
19
+ Class.new(superclass) do
20
+ def initialize(*args) ; super(make_message(*args)) ; end
21
+ define_method(:make_message, &block)
22
+ end
23
+ end
24
+
25
+ # Added a node to a graph, when one already exists with the same key.
26
+ DuplicateNodeError = error_class do |key|
27
+ "Graph already has a node with the key #{ key.inspect }"
28
+ end
29
+
30
+ # Attempted an operation on a Node which does not exist.
31
+ NoSuchNodeError = error_class do |key|
32
+ "Graph does not contain a node with the key #{ key.inspect }"
33
+ end
34
+
35
+ # Added an edge between two nodes which was too similar to an existing edge.
36
+ # See Edge#similar?
37
+ DuplicateEdgeError = error_class do |node, edge|
38
+ "Another edge already exists on #{ node.inspect } which is too similar " \
39
+ "to #{ edge.inspect }"
40
+ end
41
+
42
+ # Attempted to set properties on an object, when the properties were not in
43
+ # the form of a hash, or were otherwise invalid in some way.
44
+ InvalidPropertiesError = error_class do |model, properties|
45
+ "Tried to assign properties #{ properties.inspect } on " \
46
+ "#{ model.inspect } - it must be a Hash, or subclass of Hash"
47
+ end
48
+
49
+ # Tried to access values from a non-existant Journal segment.
50
+ NoSuchJournalError = error_class do |name|
51
+ "No such upstream journal: #{ name.inspect }"
52
+ end
53
+
54
+ # Attempted to get trace information from a pipeline when tracing has not
55
+ # been enabled.
56
+ TracingNotEnabledError = error_class do |segment|
57
+ "You cannot get trace information from the #{ segment.inspect } " \
58
+ "segment as tracing is not enabled"
59
+ end
60
+
61
+ # Tried to enable tracing on a segment which doesn't support it.
62
+ NotTraceableError = error_class do |segment|
63
+ "You cannot enable tracing on pipelines with #{ segment.class.name } " \
64
+ "segments"
65
+ end
66
+
67
+ # Tried to topologically sort a graph which contains loops.
68
+ class CyclicError < TurbineError
69
+ def initialize(orig_exception)
70
+ set_backtrace(orig_exception.backtrace)
71
+ super(orig_exception.message)
72
+ end
73
+ end
74
+ end # Turbine
@@ -0,0 +1,113 @@
1
+ module Turbine
2
+ # Contains nodes and edges.
3
+ class Graph
4
+
5
+ # Public: Creates a new graph.
6
+ def initialize
7
+ @nodes = {}
8
+ end
9
+
10
+ # Public: Adds the +node+ to the graph.
11
+ #
12
+ # node - The node to be added.
13
+ #
14
+ # Raises a DuplicateNodeError if the graph already contains a node with
15
+ # the same key.
16
+ #
17
+ # Returns the node.
18
+ def add(node)
19
+ if @nodes.key?(node.key)
20
+ raise DuplicateNodeError.new(node.key)
21
+ end
22
+
23
+ @nodes[node.key] = node
24
+ end
25
+
26
+ # Public: Removes the +node+ from the graph and disconnects any nodes
27
+ # which have edges to the +node+.
28
+ #
29
+ # node - The node to be deleted.
30
+ #
31
+ # Raises a NoSuchNodeError if the graph does not contain the given +node+.
32
+ #
33
+ # Returns the node.
34
+ def delete(node)
35
+ unless @nodes.key?(node.key)
36
+ raise NoSuchNodeError.new(node.key)
37
+ end
38
+
39
+ (node.edges(:out) + node.edges(:in)).each do |edge|
40
+ edge.from.disconnect_via(edge)
41
+ edge.to.disconnect_via(edge)
42
+ end
43
+
44
+ @nodes.delete(node.key)
45
+ end
46
+
47
+ # Public: Retrieves the node whose key is +key+.
48
+ #
49
+ # key - The key of the desired node.
50
+ #
51
+ # Returns the node, or nil if no such node is known.
52
+ def node(key)
53
+ @nodes[key]
54
+ end
55
+
56
+ # Public: All of the nodes in an array.
57
+ #
58
+ # Generally speaking, the nodes will be returned in the same order as
59
+ # they were added to the graph, however this may very depending on your
60
+ # Ruby implementation.
61
+ #
62
+ # Returns an array of nodes.
63
+ def nodes
64
+ @nodes.values
65
+ end
66
+
67
+ # Public: Topologically sorts the nodes in the graph so that nodes with
68
+ # no in edges appear at the beginning of the array, and those deeper
69
+ # within the graph are at the end.
70
+ #
71
+ # label - An optional label used to limit the edges used when traversing
72
+ # from one node to its outward nodes.
73
+ #
74
+ # Raises CyclicError if the graph contains loops.
75
+ #
76
+ # Returns an array.
77
+ def tsort(label = nil)
78
+ Algorithms::Tarjan.new(self, label).tsort
79
+ rescue TSort::Cyclic => exception
80
+ raise CyclicError.new(exception)
81
+ end
82
+
83
+ # Public: Uses Tarjan's strongly-connected components algorithm to detect
84
+ # nodes which are interrelated.
85
+ #
86
+ # label - An optional label used to limit the edges used when traversing
87
+ # from one node to its outward nodes.
88
+ #
89
+ # For example
90
+ #
91
+ # graph.strongly_connected_components
92
+ # # => [ [ #<Node key=one> ],
93
+ # [ #<Node key=two>, #<Node key=three>, #<Node key=four> ],
94
+ # [ #<Node key=five>, #<Node key=six ] ]
95
+ #
96
+ # Returns an array.
97
+ def strongly_connected_components(label = nil)
98
+ Algorithms::Tarjan.new(self, label).strongly_connected_components
99
+ end
100
+
101
+ # Public: A human-readable version of the graph.
102
+ #
103
+ # Returns a string.
104
+ def inspect
105
+ edge_count = @nodes.values.each_with_object(Set.new) do |node, edges|
106
+ edges.merge(node.edges(:out))
107
+ end.length
108
+
109
+ "#<#{self.class} (#{ @nodes.length } nodes, #{ edge_count } edges)>"
110
+ end
111
+
112
+ end # Graph
113
+ end # Turbine