omf_rete 0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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