omf_rete 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .project
2
+ Rakefile-back
3
+ examples/of.rb
4
+ lib/omf_rete/UNUSED/tuple_set.rb
data/README.md ADDED
@@ -0,0 +1,182 @@
1
+
2
+ = Introduction
3
+
4
+ This library implements a tuple store with a query and subscribe mechanism.
5
+ A subscribe is effectively a standing query which executes a block whenever
6
+ a newly added tuple together with the store's content fullfills the filter
7
+ specification.
8
+
9
+ The store holds same sized tuples with each value being assigned a name and
10
+ type at creation to support varous convenience functions to create and retrieve
11
+ tuples.
12
+
13
+ The following code snippet creates a simple RDF store and adds a few triplets
14
+ to it.
15
+
16
+ store = OMF::Rete::Store.new(3)
17
+ store.add('myFridge', 'contains', 'milk')
18
+
19
+ A filter consists of an array of tuple +patterns+ and a +block+ to be called when the store
20
+ contains a set of tuples matching the +pattern+.
21
+
22
+ The following filter only looks for a single, specific tuple. The supplied block is called
23
+ immediately if the tuple already exists in the store, or when such a tuple would be added at a later
24
+ stage.
25
+
26
+ store.subscribe(:report_problem, [
27
+ ['myFridge', 'status', 'broken']
28
+ ]) do |m|
29
+ puts "My fridge is broken"
30
+ end
31
+
32
+ The following filter contains two +patterns+ and therefore both need to be matched at the same
33
+ time in order for the block to fire. Note, that the order these tuples are added to the store
34
+ or the interval between is irrelevant.
35
+
36
+ store.subscribe(:save_milk, [
37
+ ['myFridge', 'status', 'broken'],
38
+ ['myFridge', 'contains', 'milk'],
39
+ ]) do |m|
40
+ puts "Save the milk from my fridge"
41
+ end
42
+
43
+
44
+ So far the filter pattern were fully specified. The <tt>:_</tt> symbol can be used as a wildcard identifier.
45
+ The following code snippet reports anything which is broken.
46
+
47
+ store.subscribe(:something_broken, [
48
+ [:_, 'status', 'broken']
49
+ ]) do |m|
50
+ puts "Something is broken"
51
+ end
52
+
53
+ _Not implemented yet_
54
+ Similar to OMF::Rete::Store#addNamed we can describe a pattern with a hash. Any value not named is automatically
55
+ wildcarded. Therefore, an alternative represenation of the previous filter is as follows:
56
+
57
+ store.subscribe(:something_broken, [
58
+ {:pred => 'status', :obj => 'broken'}
59
+ ]) do |m|
60
+ puts "Something is broken"
61
+ end
62
+
63
+ The +match+ argument to the block holds the context of the match and specifically, the tuples involved
64
+ in the match.
65
+
66
+ store.subscribe(:something_broken, [
67
+ [:_, 'status', 'broken']
68
+ ]) do |match|
69
+ what = match.tuples[0][:subject]
70
+ puts "#{what} is broken"
71
+ end
72
+
73
+ <tt>match.tuples</tt> returns an area of tuples one for each pattern. The matched tuple for the first pattern is at index 0,
74
+ the second one at index 1, and so on. Individual values of a tuple can be retrieved through the initially declared
75
+ value name (see OMF::Rete::Tuple#[]).
76
+
77
+ Let us assume we are monitoring many fridges, so if we want to report broken ones with milk inside, we need to ensure
78
+ that the +subject+ in both patterns in our second example are identical. Or in more technical terms, we need to +bind+ or +join+
79
+ values across patterns. A binding variable is identified by a symbol with a trailing <b>?</b>.
80
+
81
+ store.subscribe(:save_milk, [
82
+ [:fridge?, 'status', 'broken'],
83
+ [:fridge?, 'contains', 'milk'],
84
+ ]) do |match|
85
+ fridge = match[:fridge]
86
+ puts "Save the milk from #{fridge}"
87
+ end
88
+
89
+ <tt>match[bindingName]</tt> (without the '?') returns the value bound to <tt>:fridge?</tt> for this match.
90
+ Obviously <tt>match.tuples[0][:subject]</tt> will return the same value.
91
+
92
+ == Functions
93
+
94
+ Pattern matches alone are not always sufficient. For instance, let us assume that we have also stored the age in years
95
+ of each monitored fridge and want to replace each broken one which is older than 10 years. To describe such a filter
96
+ we introduce functions (or what in SPARQL is refered to as a FILTER) which allow us to restrict bound values.
97
+
98
+ Functions are identified by the <tt>:PROC</tt> symbol in the first position of a pattern, followed by the function
99
+ name, and the list of parameters. Effectively, a function filters the values previosuly bound to a variable to those
100
+ for which the function returns true.
101
+
102
+ store.subscribe(:replace_old_ones, [
103
+ [:fridge?, 'status', 'broken'],
104
+ [:fridge?, 'age', :age?],
105
+ [:PROC, :greater, :age?, 10]
106
+ ]) do |match|
107
+ puts "Replace #{match[:fridge]}"
108
+ end
109
+
110
+ <b>Design Note:</b> A more generic solution based on a 'lambda' is most likely cleaner. This is effectively
111
+ identical to the final block, except that the block should return +true+ for tuples passing the filter,
112
+ and +false+ for all others. To further simplify this and also reduce the search space, we can define a
113
+ +filter+ function which takes a list of bound variables and calls the associated block with specific bindings.
114
+
115
+ store.subscribe(:replace_old_ones, [
116
+ [:fridge?, 'status', 'broken'],
117
+ [:fridge?, 'age', :age?],
118
+ filter(:age?) { |age| age > 10 }
119
+ ]) do |match|
120
+ puts "Replace #{match[:fridge]}"
121
+ end
122
+
123
+ == Set Operators
124
+
125
+ Let us assume we want the store to not only reflect the current facts but the entire history of a system. We
126
+ can achieve that by adding a timestamp to each fact and never retract facts.
127
+
128
+ store = OMF::Rete::Store.new(:subj => String, :pred => String, :obj => Object, :tstamp => Time)
129
+
130
+ This now allows us to capture that a fridge broke on a specific date and was fixed some times later.
131
+
132
+ store.add('myFridge', 'status', 'broken', '2008-12-20')
133
+ store.add('myFridge', 'status', 'ok', '2008-12-22')
134
+
135
+ However, how can we now determine that a specific fridge is CURRENTLY broken? The pattern
136
+ <tt>[:f?, 'status' 'broken']</tt> will identify all fridges which are currently broken, as well as those
137
+ which broke in the past but are ok now. What we need is a way to describe sets and a filter to select a single tuple
138
+ from each set. In our example, each set would contain all the status messages for a specific fridge, while
139
+ the filter picks the one with the most recent timestamp.
140
+
141
+ The current syntax achieves this through special match values. For instance, <tt>:LATEST</tt> for <tt>Time</tt>
142
+ types picks the most recent fact.
143
+
144
+ [:fridge?, 'status', :_, :LATEST]
145
+
146
+ To find all currently broken fridges we need to bind this to all broken status facts.
147
+
148
+ store.subscribe(:broken_lately, [
149
+ [:fridge?, 'status', :_, :LATEST],
150
+ [:fridge?, 'status', 'broken']
151
+ ]) do |match|
152
+ puts "#{match[:fridge]} is broken"
153
+ end
154
+
155
+ <b>Design Note:</b> This seems to be a fairly ad-hoc syntax. Is there a better one? This assumes that there is no join
156
+ on any of the bound variables, they are simply keys for the sets. But overloading functionality always adds complexity.
157
+
158
+ == Negated Conditions
159
+
160
+ Now let us consider we know that our fridge is broken and we want to monitor any future status updates.
161
+ There may be many different status types and we are interested in all of them as long as they are
162
+ different to 'broken'. In other words, we need a way to describe what is refered to as a 'negated
163
+ condition' and is defined by a leading <tt>:NOT</tt>, followed by one or multiple patterns describing
164
+ what should NOT be in the store.
165
+
166
+ store.subscribe(:find_latest, [
167
+ ['My Fridge', :status, :_, :LATEST],
168
+ [:NOT, ['My Fridge', 'status', 'broken']]
169
+ ]) do |match|
170
+ puts "Status for my fridge changed to '#{match.tuples[0][:obj]}."
171
+ end
172
+
173
+ Please note that the above example fails to report when my fridge is reported as broken again.
174
+
175
+ = Implementation
176
+
177
+
178
+
179
+
180
+
181
+
182
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'rake/testtask'
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => :test
5
+
6
+
7
+ #
8
+ # TESTING
9
+
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << "tests"
12
+ t.test_files = FileList['tests/test.rb']
13
+ t.verbose = true
14
+ end
@@ -0,0 +1,68 @@
1
+
2
+ module OMF::Rete
3
+ #
4
+ # This class maintains a set of tuples and
5
+ # supports a block being attached which is
6
+ # being called whenever a tuple is added or
7
+ # removed.
8
+ #
9
+ # The TupleSet is defined by a +description+.
10
+ #
11
+ # The +description+ is an array of the
12
+ # same length as the tuples maintained. Each element,
13
+ # if not nil, names the binding variable associated with it.
14
+ # The position of a binding can be retrieved with
15
+ # +index_for_binding+.
16
+ #
17
+ class AbstractTupleSet
18
+
19
+ attr_reader :description
20
+ attr_accessor :source
21
+
22
+ def initialize(description, source = nil)
23
+ @description = description
24
+ @source = source
25
+ end
26
+
27
+ def addTuple(tuple)
28
+ raise 'Abstract class'
29
+ end
30
+
31
+ # Call block for every tuple stored in this set currently and
32
+ # in the future. In other words, the block may be called even after this
33
+ # method returns.
34
+ #
35
+ # The block will be called with one parameters, the
36
+ # tuple added.
37
+ #
38
+ def on_add(&block)
39
+ raise 'Abstract class'
40
+ end
41
+
42
+ # Return all stored tuples in an array.
43
+ def to_a
44
+ raise 'Abstract class'
45
+ end
46
+
47
+ # Retunr the index into the tuple for the binding variable +bname+.
48
+ #
49
+ # Note: This index is different to the set index used in +IndexedTupleSet+
50
+ #
51
+ def index_for_binding(bname)
52
+ @description.find_index do |el|
53
+ el == bname
54
+ end
55
+ end
56
+
57
+ def binding_at(index)
58
+ @description[index]
59
+ end
60
+
61
+ def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
62
+ raise 'Abstract class'
63
+ end
64
+
65
+
66
+ end # class
67
+ end # module
68
+
@@ -0,0 +1,129 @@
1
+ require 'omf_rete/abstract_tuple_set'
2
+
3
+ module OMF::Rete
4
+ #
5
+ # This class maintains a set of tuples and
6
+ # supports a block being attached which is
7
+ # being called whenever a tuple is added or
8
+ # removed.
9
+ #
10
+ # The IndexedTupleSet is defined by a +description+ and an
11
+ # +indexPattern+.
12
+ #
13
+ # The +description+ is an array of the
14
+ # same length as the tuples maintained. Each element,
15
+ # if not nil, names the binding variable associated with it.
16
+ # The position of a binding can be retrieved with
17
+ # +index_for_binding+.
18
+ #
19
+ # The +indexPattern+ describes which elements of the inserted
20
+ # tuple are being combined in an array to form the index
21
+ # key for each internal tuple. The elements in the +indexPattern+
22
+ # are described by the binding name.
23
+ #
24
+ #
25
+ class IndexedTupleSet < AbstractTupleSet
26
+
27
+ attr_reader :indexPattern
28
+ attr_writer :transient # if true only process tuple but don't store it
29
+
30
+ def initialize(description, indexPattern, source = nil, opts = {})
31
+ super description, source
32
+ if (indexPattern.length == 0)
33
+ raise "Expected index to be non-nil (#{description.join(', ')})"
34
+ end
35
+ @indexPattern = indexPattern
36
+ @indexMap = indexPattern.collect do |bname|
37
+ index_for_binding(bname)
38
+ end
39
+
40
+ @index = {}
41
+ end
42
+
43
+ def addTuple(tuple)
44
+ key = @indexMap.collect do |ii|
45
+ tuple[ii]
46
+ end
47
+
48
+ if @transient
49
+ @onAddBlockWithIndex.call(key, tuple) if @onAddBlockWithIndex
50
+ @onAddBlock.call(tuple) if @onAddBlock
51
+ else
52
+ vset = (@index[key] ||= Set.new)
53
+ if vset.add?(tuple)
54
+ # new value
55
+ @onAddBlockWithIndex.call(key, tuple) if @onAddBlockWithIndex
56
+ @onAddBlock.call(tuple) if @onAddBlock
57
+ end
58
+ end
59
+ tuple # return added tuple
60
+ end
61
+
62
+ # Call block for every tuple stored in this set currently and
63
+ # in the future. In other words, the block may be called even after this
64
+ # method returns.
65
+ #
66
+ # The block will be called with one parameters, the
67
+ # tuple added.
68
+ #
69
+ # Note: Only one +block+ can be registered at a time
70
+ #
71
+ def on_add(&block)
72
+ @index.each do |index, values|
73
+ values.each do |v|
74
+ block.call(v)
75
+ end
76
+ end
77
+ @onAddBlock = block
78
+ end
79
+
80
+
81
+ # Call block for every tuple stored in this set currently and
82
+ # in the future. In other words, the block may be called even after this
83
+ # method returns.
84
+ #
85
+ # The block will be called with two parameters, the index of the tuple followed by the
86
+ # tuple itself.
87
+ #
88
+ # Note: Only one +block+ can be registered at a time
89
+ #
90
+ def on_add_with_index(&block)
91
+ @index.each do |index, values|
92
+ values.each do |v|
93
+ block.call(index, v)
94
+ end
95
+ end
96
+ @onAddBlockWithIndex = block
97
+ end
98
+
99
+ # Return the set of tuples index by +key+.
100
+ # Will return nil if nothing is stored for +key+
101
+ #
102
+ def [](key)
103
+ res = @index[key]
104
+ res
105
+ end
106
+
107
+ # Return all stored tuples in an array.
108
+ def to_a
109
+ a = []
110
+ @index.each_value do |s|
111
+ s.each do |t|
112
+ a << t
113
+ end
114
+ end
115
+ a
116
+ end
117
+
118
+ def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
119
+ out.write(" " * offset)
120
+ desc = @description.collect do |e| e || '*' end
121
+ out.write("ts: [#{desc.join(', ')}]")
122
+ ind = @indexMap.collect do |i| @description[i] end
123
+ out.write(" (index: [#{ind.sort.join(', ')}])#{sep}")
124
+ @source.describe(out, offset + incr, incr, sep) if @source
125
+ end
126
+
127
+ end # class IndexedTupleSet
128
+ end # module
129
+
@@ -0,0 +1,113 @@
1
+ require 'omf_rete/indexed_tuple_set'
2
+
3
+ module OMF::Rete
4
+
5
+ # This class implements the join operation between two
6
+ # +IndexedTupleSets+ feeding into a third, result tuple set.
7
+ # The size of both incoming tuple sets needs to be identical and they
8
+ # are supposed to be indexed on the same list of variables as this is
9
+ # what they wil be joined at.
10
+ #
11
+ # Implementation Note: We first calculate a +combinePattern+
12
+ # from the +description+ of the result set.
13
+ # The +combinePattern+ describes how to create a joined tuple to insert
14
+ # into the result tuple set. The +combinePattern+ is an array of
15
+ # the same size as the result tuple. Each element is a 2-array
16
+ # with the first element describing the input set (0 .. left, 1 .. right)
17
+ # and the second one the index from which to take the value.
18
+ #
19
+ #
20
+ class JoinOP
21
+ def initialize(leftSet, rightSet, resultSet)
22
+ @resultSet = resultSet
23
+ @left = leftSet
24
+ @right = rightSet
25
+
26
+ @combinePattern = resultSet.description.collect do |bname|
27
+ side = 0
28
+ unless (i = leftSet.index_for_binding(bname))
29
+ side = 1
30
+ unless (i = rightSet.index_for_binding(bname))
31
+ raise "Can't find binding '#{bname}' in either streams. Should never happen"
32
+ end
33
+ end
34
+ #description << bname
35
+ [side, i]
36
+ end
37
+ @resultLength = @combinePattern.length
38
+
39
+ leftSet.on_add_with_index do |index, ltuple|
40
+ if (rs = rightSet[index])
41
+ rs.each do |rtuple|
42
+ add_result(ltuple, rtuple)
43
+ end
44
+ end
45
+ end
46
+ rightSet.on_add_with_index do |index, rtuple|
47
+ if (ls = leftSet[index])
48
+ ls.each do |ltuple|
49
+ add_result(ltuple, rtuple)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Supporting 'check_for_tuple'
55
+ @left_pattern = @left.description.map do |bname|
56
+ @resultSet.index_for_binding(bname)
57
+ end
58
+ @right_pattern = @right.description.map do |bname|
59
+ @resultSet.index_for_binding(bname)
60
+ end
61
+
62
+ end
63
+
64
+ # Check if +tuple+ can be produced by this join op. We first
65
+ # check if we can find a match on one side and then request
66
+ # from the other side all the tuples which would lead to full
67
+ # join.
68
+ #
69
+ def check_for_tuple(tuple)
70
+ ltuple = @left_pattern.map {|i| tuple[i]}
71
+ if @left.check_for_tuple(ltuple)
72
+ rtuple = @right_pattern.map {|i| tuple[i]}
73
+ if @right.check_for_tuple(rtuple)
74
+ return true
75
+ end
76
+ end
77
+ return false
78
+ end
79
+
80
+ def description()
81
+ @resultSet.description
82
+ end
83
+
84
+ def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
85
+ out.write(" " * offset)
86
+ result = @combinePattern.collect do |side, index|
87
+ (side == 0) ? @left.binding_at(index) : @right.binding_at(index)
88
+ end
89
+ out.write("join: [#{@left.indexPattern.sort.join(', ')}] => [#{result.sort.join(', ')}]#{sep}")
90
+ @left.describe(out, offset + incr, incr, sep)
91
+ @right.describe(out, offset + incr, incr, sep)
92
+ end
93
+
94
+ private
95
+
96
+ def add_result(ltuple, rtuple)
97
+ unless @resultLength
98
+ i = 2
99
+ end
100
+ result = Array.new(@resultLength)
101
+ i = 0
102
+ @combinePattern.each do |setId, index|
103
+ t = setId == 0 ? ltuple : rtuple
104
+ result[i] = t[index]
105
+ i += 1
106
+ end
107
+ @resultSet.addTuple(result)
108
+ end
109
+
110
+
111
+
112
+ end # class
113
+ end # module
@@ -0,0 +1,57 @@
1
+ module OMF::Rete
2
+ module Planner
3
+
4
+
5
+ # This class is the super class for all plans
6
+ #
7
+ #
8
+ class AbstractPlan
9
+
10
+ attr_reader :cover_set, :result_set
11
+
12
+ #
13
+ # coverSet -- set of source plans covered by this plan
14
+ # resultSet -- set of bindings provided by this source
15
+ #
16
+ def initialize(coverSet, resultSet)
17
+ @cover_set = coverSet
18
+ @result_set = resultSet
19
+ @is_used = false
20
+ @is_complete = false
21
+ end
22
+
23
+ def result_description
24
+ @result_set.to_a.sort
25
+ end
26
+
27
+
28
+
29
+ # Return true if this plan is a complete one.
30
+ #
31
+ # A complete plan covers (@coverSet) all leaf plans.
32
+ #
33
+ def complete?()
34
+ @is_complete
35
+ end
36
+
37
+ # Set this plan to be complete
38
+ #
39
+ def complete()
40
+ @is_complete = true
41
+ end
42
+
43
+ # Return true if used by some higher plan
44
+ #
45
+ def used?()
46
+ @is_used
47
+ end
48
+
49
+ # Informs the plan that it is used by some higher plan
50
+ #
51
+ def used()
52
+ @is_used = true
53
+ end
54
+ end # PlanBuilder
55
+
56
+ end # Planner
57
+ end # module
@@ -0,0 +1,49 @@
1
+
2
+ require 'omf_rete/planner/plan_builder'
3
+ require 'omf_rete/planner/abstract_plan'
4
+ require 'set'
5
+
6
+ module OMF::Rete
7
+ module Planner
8
+
9
+ # This class represents a filter operation on a binding stream.
10
+ #
11
+ #
12
+ class FilterPlan
13
+ attr_reader :description
14
+
15
+ #
16
+ # resultSet - set of bindings provided by this source
17
+ #
18
+ def initialize(projectPattern, outDescription = nil, &block)
19
+ @projectPattern = projectPattern
20
+ @description = outDescription #|| projectPattern.sort
21
+ @block = block
22
+ end
23
+
24
+
25
+ def materialize(description, source, opts)
26
+ # A filter has the same in as well as out description as it doesn't change
27
+ # the tuple just potentially drop it.
28
+ #
29
+ pts = FilterTupleStream.new(@projectPattern, description, &@block)
30
+ pts.source = source
31
+ # if (in_description == @projectPattern)
32
+ # pts.on_add &@block
33
+ # else
34
+ # projectIndex = @projectPattern.collect do |bname|
35
+ # pts.index_for_binding(bname)
36
+ # end
37
+ # pts.on_add do |*t|
38
+ # pt = projectIndex.collect do |index|
39
+ # t[index]
40
+ # end
41
+ # @block.call(*pt)
42
+ # end
43
+ # end
44
+ pts
45
+ end
46
+ end # FilterPlan
47
+
48
+ end # Planner
49
+ end # module