ruleby 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -73,6 +73,17 @@ module Ruleby
73
73
  attr_accessor:pattern
74
74
  attr_reader:action, :name
75
75
 
76
+
77
+ def when(&block)
78
+ wb = WhenBuilder.new
79
+ yield wb
80
+ @pattern = wb.pattern
81
+ end
82
+
83
+ def then(&block)
84
+ @action = Core::Action.new(&block)
85
+ end
86
+
76
87
  def when=(pattern)
77
88
  @pattern = pattern
78
89
  end
@@ -128,7 +139,11 @@ module Ruleby
128
139
  @facts = Array.new
129
140
  end
130
141
 
131
- attr_reader :facts
142
+ def each_fact
143
+ @facts.each do |f|
144
+ yield(f)
145
+ end
146
+ end
132
147
 
133
148
  def assert_fact(fact)
134
149
  raise 'The fact asserted cannot be nil!' unless fact.object
@@ -156,12 +171,27 @@ module Ruleby
156
171
  end
157
172
 
158
173
  class Engine
159
- def initialize()
174
+ def initialize(wm=WorkingMemory.new)
160
175
  @root = nil
161
- @working_memory = WorkingMemory.new
176
+ @working_memory = wm
162
177
  @conflict_resolver = RulebyConflictResolver.new
163
178
  end
164
-
179
+ #assert fact
180
+ def assert(object,&block)
181
+ fact_helper(object,:plus,&block)
182
+ end
183
+ #retract fact
184
+ def retract(object,&block)
185
+ fact_helper(object,:minus,&block)
186
+ end
187
+ #modify fact
188
+ # retract
189
+ # assert
190
+ def modify(object,&block)
191
+ retract(object,&block)
192
+ assert(object,&block)
193
+ end
194
+
165
195
  def assert_fact(fact)
166
196
  wm_fact = @working_memory.assert_fact fact
167
197
  @root.assert_fact wm_fact if @root != nil
@@ -173,8 +203,8 @@ module Ruleby
173
203
  end
174
204
 
175
205
  def match(agenda=@root.match, used_agenda=[], activation_counter=0)
176
- agenda = @conflict_resolver.resolve agenda
177
- if(agenda && agenda.length > 0)
206
+ while (agenda.length > 0)
207
+ agenda = @conflict_resolver.resolve agenda
178
208
  activation = agenda.pop
179
209
  used_agenda.push activation
180
210
  activation.fire self
@@ -197,15 +227,23 @@ module Ruleby
197
227
  end
198
228
  end
199
229
  agenda.delete_if {|a| new_agenda.index(a) == nil}
200
-
201
- match(agenda, used_agenda, activation_counter+1)
230
+
231
+ activation_counter = activation_counter+1
202
232
  end
203
233
  end
204
234
 
205
235
  def print
206
236
  @working_memory.print
207
237
  @root.print
208
- end
238
+ end
239
+
240
+ private
241
+ def fact_helper(object, sign=:plus, &block)
242
+ f = Core::Fact.new object, sign
243
+ yield f if block_given?
244
+ assert_fact f
245
+ f
246
+ end
209
247
  end
210
248
  end
211
249
  end
@@ -1,100 +1,19 @@
1
1
  module Ruleby
2
2
  module Core
3
-
4
- # This acts as a base class for all Nodes that wrap a pattern and
5
- # output to one or more other Nodes.
6
- class Node
7
-
8
- def initialize(pattern)
9
- # the pattern that this node wraps
10
- @pattern = pattern
11
-
12
- # The nodes that this node outputs to (ones that are below it in the
13
- # network). Currently, there will only be a single value in each list
14
- # but in the future, out network will be optimize to remove repeated
15
- # nodes. Thus each node can output to multiple nodes.
16
- @out_nodes = []
17
- end
18
-
19
- attr_reader :pattern
20
- attr_reader :out_nodes
21
-
22
- # This method is called when a new MatchResult is added to the system. It
23
- # propagates the new MR by invoking the assert method on all nodes that are
24
- # below this Node in the network.
25
- def propagate_assert(match_result, fact)
26
- @out_nodes.each do |out_node|
27
- out_node.assert(self, match_result, fact)
28
- end
29
- end
30
-
31
- # This method is called when an existing MatchResult is remvoed from the
32
- # system. It propagates the removal by invoking the retract method on all
33
- # nodes that are below this Node in the network.
34
- def propagate_retract(fact)
35
- @out_nodes.each do |out_node|
36
- out_node.retract(self, fact)
37
- end
38
- end
39
-
40
- # This method is used to progagate a new MatchResult. The difference
41
- # in this method is that it propagates MatchResults that were
42
- # generated by a NotNode
43
- def propagate_assert_neg(neg_match_result, fact)
44
- @out_nodes.each do |out_node|
45
- out_node.assert_neg(self, neg_match_result, fact)
46
- end
47
- end
48
-
49
- # This method is used to progagate the removal of a MatchResult. The
50
- # difference in this method is that it removes MatchResults that were
51
- # generated by a NotNode
52
- def propagate_retract_neg(fact)
53
- @out_nodes.each do |out_node|
54
- out_node.retract_neg(self, fact)
55
- end
56
- end
57
-
58
- # This method determines if all common tags have equal values. If any
59
- # values are not equal then the method returns false.
60
- def resolve(mr1, mr2)
61
- mr1.variables.each_key do |t1|
62
- mr2.variables.each_key do |t2|
63
- if t2 == t1 && mr1.fact_hash[t1] != mr2.fact_hash[t2]
64
- return false
65
- end
66
- end
67
- end
68
- return true
69
- end
70
-
71
- # Determines if a given MatchResult is unmatched by the any of the given
72
- # NotMatchResults.
73
- # mr - a MatchResult
74
- # nmrs - a list of MatchResults from a NotNode
75
- def matches_not?(mr,nmrs)
76
- nmrs.each do |nmr|
77
- return true if resolve(mr,nmr)
78
- end
79
- return false
80
- end
81
- end
82
3
 
83
4
  # This class acts as the root-node of the network. It contains the logic
84
5
  # for building the node-network from a set of rules, and updating it
85
6
  # according to the working memory
86
- class RootNode
87
-
7
+ class RootNode
88
8
  def initialize(working_memory)
89
9
  @working_memory = working_memory
90
-
91
- # we need to use our own data structure (NodeNetworkArray) for the most
92
- # efficient indexing of nodes. Also, it creates some mappings that will
93
- # be used later on to avoid repeatedly iterating over lists.
94
- @object_nodes = NodeNetworkArray.new
95
-
96
- @join_nodes = Array.new
97
- @terminal_nodes = Array.new
10
+
11
+ # TODO once node sharing is implemented, @type_nodes will be a
12
+ # Class=>TypeNode Hash.
13
+ @type_nodes = []
14
+ @atom_nodes = []
15
+ @join_nodes = []
16
+ @terminal_nodes = []
98
17
  end
99
18
 
100
19
  # This method is invoked when a new rule is added to the system. The
@@ -102,8 +21,7 @@ module Ruleby
102
21
  def assert_rule(rule)
103
22
  pattern = rule.pattern
104
23
  terminal_node = TerminalNode.new rule
105
- node = build_network(pattern, terminal_node)
106
- terminal_node.parent_node = node # only used to print the network
24
+ build_network(pattern, terminal_node)
107
25
  @terminal_nodes.push terminal_node
108
26
  end
109
27
 
@@ -117,140 +35,147 @@ module Ruleby
117
35
  # is above (i.e. it outputs to) the given node.
118
36
  def build_network(pattern, out_node, side=nil)
119
37
  if (pattern.kind_of?(ObjectPattern))
120
- return create_object_node(pattern, out_node, side)
38
+ atom_node = create_atom_nodes(pattern, out_node, side)
39
+ #out_node.parent_nodes.push atom_node # only used to print network
40
+ return atom_node
121
41
  else
122
42
  join_node = create_join_node(pattern, out_node, side)
123
43
  build_network(pattern.left_pattern, join_node, :left)
124
44
  build_network(pattern.right_pattern, join_node, :right)
45
+ out_node.parent_nodes.push join_node # only used to print network
125
46
  return join_node
126
47
  end
127
48
  end
128
49
  private:build_network
129
50
 
130
- # Creates an ObjectNode, puts it at the top of the network, and stores
131
- # the node below it into its memory. It also checks the working memory to
132
- # see if its conditions are satisfied by the exisiting facts.
133
- # pattern - the ObjectPattern that the created node wraps
134
- # out_node - the Node that this pattern is directly above in the network
135
- # side - if the out_node is a JoinNode, this marks the side of the new node
136
- def create_object_node(pattern, out_node, side)
137
- obj_node = nil
138
- if (pattern.kind_of?(NotPattern))
139
- obj_node = NotNode.new(pattern)
140
- else
141
- obj_node = ObjectNode.new(pattern)
142
- end
51
+ # This method is used to create the atom nodes that make up the given
52
+ # pattern's network. It returns the node that is at the top of the network
53
+ # for the pattern.
54
+ # pattern - the Pattern that the created node wraps
55
+ # out_node - the Node that this pattern is directly above in thw network
56
+ # side - if the out_node is a JoinNode, this marks the side of the new node
57
+ def create_atom_nodes(pattern, out_node, side)
58
+ type_node = create_type_node(pattern.atoms[0])
143
59
 
144
- if side==:left
145
- out_node.left_input = obj_node
146
- elsif side==:right
147
- out_node.right_input = obj_node
148
- end
60
+ parent_node = type_node
61
+ for i in (1..(pattern.atoms.size-1))
62
+ node = nil
63
+ if pattern.atoms[i].kind_of?(SelfReferenceAtom)
64
+ node = create_self_reference_node(pattern.atoms[i])
65
+ elsif pattern.atoms[i].kind_of?(ReferenceAtom)
66
+ node = create_reference_node(pattern.atoms[i])
67
+ out_node.ref_nodes.push node
68
+ else
69
+ node = create_property_node(pattern.atoms[i])
70
+ end
71
+ parent_node.out_nodes.push node
72
+ node.parent_nodes.push parent_node
73
+ parent_node = node
74
+ end
75
+ out_node.parent_nodes.push parent_node
149
76
 
150
- # TODO fix this opmtimization so it works
151
- #index = @object_nodes.has_node(obj_node)
152
- #if (index == nil)
153
- @object_nodes.add(obj_node)
154
- #else
155
- #obj_node = @object_nodes[obj_node.pattern.head,index]
156
- # TODO maybe create and push new "alias" node where the @tags
157
- # do not have to be the same, but the patterns are the same
158
- #end
159
-
160
- obj_node.out_nodes.push out_node
161
- compare_node_to_wm(obj_node)
162
- return obj_node
77
+ if out_node.kind_of?(JoinNode)
78
+ adapter_node = create_adapter_node(side)
79
+ parent_node.out_nodes.push adapter_node
80
+ parent_node = adapter_node
81
+ end
82
+
83
+ parent_node.out_nodes.push out_node
84
+ compare_to_wm(type_node)
85
+ return type_node
163
86
  end
164
- private:create_object_node
87
+ private:create_atom_nodes
165
88
 
166
89
  # Creates a JoinNode, puts it at the middle of the network, and stores
167
90
  # the node below it into its memory.
168
- # pattern - the Pattern that the created node wraps
169
- # out_node - the Node that this pattern is directly above in thw network
170
- # side - if the out_node is a JoinNode, this marks the side of the new node
171
- def create_join_node(pattern, out_node, side)
172
- join_node = JoinNode.new(pattern)
173
-
174
- if side==:left
175
- out_node.left_input = join_node
176
- elsif side==:right
177
- out_node.right_input = join_node
91
+ # pattern - the Pattern that the created node wraps
92
+ # out_node - the Node that this pattern is directly above in thw network
93
+ # side - if the out_node is a JoinNode, this marks the side of the new node
94
+ def create_join_node(pattern, out_node, side)
95
+ join_node = nil
96
+ if (pattern.left_pattern.kind_of?(NotPattern))
97
+ raise 'NotPatterns at the being of a rule are not yet supported'
98
+ elsif (pattern.right_pattern.kind_of?(NotPattern))
99
+ join_node = NotNode.new
100
+ else
101
+ join_node = JoinNode.new
178
102
  end
179
103
 
180
- # TODO opmtimization the join_nodes list (just like object_nodes)
181
- @join_nodes.push(join_node)
182
-
183
- join_node.out_nodes.push out_node
104
+ @join_nodes.push(join_node)
105
+ parent_node = join_node
106
+ if out_node.kind_of?(JoinNode)
107
+ adapter_node = create_adapter_node(side)
108
+ parent_node.out_nodes.push adapter_node
109
+ parent_node = adapter_node
110
+ end
111
+ parent_node.out_nodes.push out_node
184
112
  return join_node
185
113
  end
186
114
  private:create_join_node
115
+
116
+ def create_type_node(atom)
117
+ node = TypeNode.new atom
118
+ @type_nodes.push node
119
+ return node
120
+ end
121
+ private:create_type_node
122
+
123
+ def create_property_node(atom)
124
+ node = PropertyNode.new atom
125
+ @atom_nodes.push node
126
+ return node
127
+ end
128
+ private:create_property_node
129
+
130
+ def create_self_reference_node(atom)
131
+ node = SelfReferenceNode.new atom
132
+ @atom_nodes.push node
133
+ return node
134
+ end
135
+ private:create_self_reference_node
136
+
137
+ def create_reference_node(atom)
138
+ node = ReferenceNode.new atom
139
+ @atom_nodes.push node
140
+ return node
141
+ end
142
+ private:create_reference_node
143
+
144
+ def create_adapter_node(side)
145
+ if side == :left
146
+ return LeftAdapterNode.new
147
+ else
148
+ return RightAdapterNode.new
149
+ end
150
+ end
151
+ private:create_adapter_node
187
152
 
188
153
  # When a new fact is added to working memory, or an existing one is removed
189
154
  # this method is called. It finds any nodes that depend on it, and updates
190
155
  # them accordingly.
191
156
  def assert_fact(fact)
192
- @object_nodes.each fact.object.class do |object_node|
193
- altered = false
157
+ # TODO implement node sharing. Instead of iterating over the @type_nodes
158
+ # list, we will just get the one that matches the type of this fact
159
+ @type_nodes.each do |node|
194
160
  if fact.token == :plus
195
- altered = object_node.assert fact
161
+ node.assert(MatchContext.new(fact))
196
162
  else
197
- altered = object_node.retract fact
198
- end
199
-
200
- # BUG this may cause there to be multiple MatchResults for the same
201
- # fact. It will be as if there were 2 of the same fact in working
202
- # memory. Right now it is okay because we are removing duplicate
203
- # MatchResults. But this means we cannot have duplicate facts in
204
- # working memory. The following WILL need to be addressed at
205
- # some point.
206
- #
207
- # QUESTION a possible solution would be to pass an 'is_new' parameter to
208
- # the refresh_node method. This param could represent that we are
209
- # refreshing (as opposed to calling it from assert_rule - which would
210
- # be the first time).
211
- if altered
212
- check_references(object_node)
163
+ node.retract fact
213
164
  end
214
- end
215
- end
216
-
217
- def check_references(object_node)
218
- object_node.referenced_by.each do |ref_node|
219
- # HACK this is a very costly operation because we will have to iterate
220
- # over ALL facts in working memory to see if ANY nodes were affected
221
- # by the fact asserted/retracted here. So patterns using ReferenceAtoms
222
- # become very expensive.
223
- altered = compare_node_to_wm(ref_node)
224
-
225
- # call this method recursively until we reach a node that is not
226
- # referenced by any other nodes
227
- if altered
228
- check_references(ref_node)
229
- end
230
- end
165
+ end
231
166
  end
232
167
 
233
- # This method is used to update each ObjectNode in the given list based on
168
+ # This method is used to update each TypeAtomNode based on
234
169
  # the facts in working memory. It can be a costly operation because it
235
- # iterates over EVERY fact in working memory. It should be used only when
236
- # needed.
237
- def compare_node_to_wm(obj_node)
238
- altered = false
239
- facts = @working_memory.facts
240
- if (facts.empty? && obj_node.kind_of?(NotNode))
241
- # HACK its kind of lame to have to do this here, maybe the NotNode
242
- # should have this value by default
243
- obj_node.assert_input_true
244
- else
245
- # check working memory to see if this node is satisfied by any fact(s)
246
- facts.each do |fact|
247
- inner_altered = obj_node.assert fact
248
- altered = true if inner_altered
249
- end
170
+ # iterates over EVERY fact in working memory. It should only be used when a
171
+ # new rule is added.
172
+ def compare_to_wm(type_node)
173
+ @working_memory.each_fact do |fact|
174
+ type_node.assert MatchContext.new(fact)
250
175
  end
251
- return altered
252
176
  end
253
-
177
+ private:compare_to_wm
178
+
254
179
  # When invoked, this method returns a list of all Action|MatchContext pairs
255
180
  # as Activations. The list is generated when facts and rules are asserted,
256
181
  # so no comparisions are done here (i.e. no Backward Chaining).
@@ -258,7 +183,7 @@ module Ruleby
258
183
  agenda = Array.new
259
184
  @terminal_nodes.each do |node|
260
185
  if (node.satisfied?)
261
- agenda.concat node.activations.values.values
186
+ agenda.concat node.activations.values
262
187
  end
263
188
  end
264
189
  return agenda
@@ -267,418 +192,432 @@ module Ruleby
267
192
  def print
268
193
  puts 'NETWORK:'
269
194
  @terminal_nodes.each do |node|
270
- node.print(' ')
195
+ node.print(' ')
196
+ end
197
+ end
198
+ end
199
+
200
+ # Any node in the network that needs to be printed extends this class. It
201
+ # provides handles to the nodes above it in network. These are not used for
202
+ # matching (i.e. no backward-chaining).
203
+ class Printable
204
+ def initialize
205
+ # this only used for printing the network, not for matching
206
+ @parent_nodes = []
207
+ end
208
+ attr_reader:parent_nodes
209
+ def print(tab)
210
+ puts tab + to_s
211
+ @parent_nodes.each do |out_node|
212
+ out_node.print(' '+tab)
271
213
  end
272
214
  end
273
215
  end
274
216
 
275
- # This class represents a one-input node. It essenatially acts as a wrapper
276
- # for an ObjectPattern, and provides the logic for matching facts.
277
- class ObjectNode < Node
278
- def initialize(pattern)
217
+ # Base Node class used by all nodes in the network that do some kind
218
+ # of matching.
219
+ class Node < Printable
220
+
221
+ # This method determines if all common tags have equal values. If any
222
+ # values are not equal then the method returns false.
223
+ def resolve(mr1, mr2)
224
+ mr1.variables.each_key do |t1|
225
+ mr2.variables.each_key do |t2|
226
+ if t2 == t1 && mr1.fact_hash[t1] != mr2.fact_hash[t2]
227
+ return false
228
+ end
229
+ end
230
+ end
231
+ return true
232
+ end
233
+ end
234
+
235
+ # This is the base class for all nodes in the network that output to some
236
+ # other node (i.e. they are not at the bottom). It contains methods for
237
+ # propagating match results.
238
+ class ParentNode < Node
239
+ def initialize
279
240
  super
280
-
281
- # a list of Nodes that reference this node. This will be used so that we
282
- # can update them if this node get some new matches (or loses matches).
283
- @referenced_by = []
284
-
285
- # The atoms need a handle to this node.
286
- @pattern.atoms.each { |a| a.parent = self }
287
-
288
- # In this hash, the key is a fact.id, and each value is a list of
289
- # MatchResults. Each MR is from a node that this node references.
290
- @ref_results_hash = {}
291
-
292
- # In this hash, the key is a fact.id, and each value is a single
293
- # MatchResult. The MRs represent the match for each fact.
294
- @obj_results_hash = {}
295
- end
296
-
297
- attr:referenced_by,true
241
+ @out_nodes = []
242
+ end
243
+ attr_reader:out_nodes
298
244
 
299
- def satisfied?
300
- return !@obj_results_hash.empty?
245
+ def assert(context)
246
+ propagate_assert(context)
301
247
  end
302
248
 
303
- def obj_results
304
- return @obj_results_hash.values
249
+ def retract(fact)
250
+ propagate_retract(fact)
305
251
  end
306
252
 
307
- def ref_results(tag)
308
- list = []
309
- @ref_results_hash.each_value do |ref_result|
310
- list.concat ref_result[tag] if ref_result[tag] != ref_result.default
253
+ def propagate_assert(context)
254
+ @out_nodes.each do |out_node|
255
+ out_node.assert(context)
311
256
  end
312
- return list
313
- end
314
-
315
- # This method initiates the matching of this pattern to the given fact
316
- # (specificily to the object it wraps). If the pattern matches the object
317
- # then one or more MatchResults will be created and added to the system.
318
- # returns:boolean - true if this node was altered by the assertion
319
- def assert(fact)
320
- obj_result = MatchResult.new
321
- ref_results = {}
322
- results = [MatchResult.new]
323
-
324
- @pattern.atoms.each do |atom|
325
- r = atom.match? fact
326
- if r.empty? || !r[0].is_match
327
- return false
328
- else
329
- # Build the MatchResult that will be added to each atom
330
- # after we have gone through all atoms. We must wait
331
- # till after we check all atoms because we only add it
332
- # if ALL atoms have a match for this object.
333
- obj_result[atom.tag] = r[0][atom.tag]
334
- obj_result.is_match = r[0].is_match
335
- obj_result.fact_hash = obj_result.fact_hash.update r[0].fact_hash
336
- obj_result.recency = obj_result.recency | r[0].recency
337
-
338
- ref_results[atom.tag] = []
339
- results = build_results results, ref_results, r, atom
340
- end
341
- ref_results[atom.tag].each do |ref_result|
342
- # update each ReferenceAtom's MatchResult with the core result for this fact
343
- ref_result.update obj_result
344
- end
257
+ end
258
+
259
+ def propagate_retract(fact)
260
+ @out_nodes.each do |out_node|
261
+ out_node.retract(fact)
345
262
  end
346
-
347
- @ref_results_hash[fact.id] = ref_results
348
- @obj_results_hash[fact.id] = obj_result
349
-
350
- # HACK delete repeated MatchResults. Instead of doing this
351
- # we need to be sure that every MatchResult added is unique (or is
352
- # intended to be a duplicate).
353
- results.each do |r1|
354
- results.delete_if do |r2|
355
- r1 == r2 && r1.object_id != r2.object_id
356
- end
357
- end
358
-
359
- results.each do |mr|
360
- propagate_assert mr, fact
361
- end
362
- return !results.empty?
363
- end
364
-
365
- # This method is called when a fact is removed from the system. If the
366
- # patterned contained in the node was matched to the given fact, then we
367
- # removed those MatchResults from our local cache, and propagate the change.
368
- # returns:boolean - true if this node was altered by the retraction
369
- def retract(fact)
370
- @ref_results_hash.delete(fact.id)
371
- result = @obj_results_hash.delete(fact.id)
372
- if result != @obj_results_hash.default
373
- propagate_retract fact
374
- return true
263
+ end
264
+ end
265
+
266
+ # This is a base class for all single input nodes that match facts based on
267
+ # some properties. It is essentially a wrapper for an Atom.
268
+ class AtomNode < ParentNode
269
+ def initialize(atom)
270
+ super()
271
+ @atom = atom
272
+ end
273
+ attr_reader:atom
274
+
275
+ def retract(fact)
276
+ # These nodes do not have a memory, so there is no need to do anything
277
+ # when a fact is retracted. Just pass it on to the two-input nodes.
278
+ propagate_retract(fact)
279
+ end
280
+ end
281
+
282
+ # This node class is used to match the type of a fact.
283
+ class TypeNode < AtomNode
284
+ def assert(context)
285
+ mr = match(context.fact)
286
+ if mr.is_match
287
+ context.match.update mr
288
+ propagate_assert(context)
375
289
  end
376
- return false
377
- end
378
-
379
- # This internal method is used to build the result set for matching fact-
380
- # pattern combination.
381
- def build_results(results, ref_results, r, atom)
382
- inner_results = []
383
- r.each do |each_r|
384
- # Update each MatchResult already in the list with this MatchResult
385
- # if the two resolve. Then add the result to the list for this atom.
386
- results.each do |result|
387
- if resolve(result, each_r)
388
- ref_results[atom.tag].push each_r
389
- mr = result.dup
390
- mr.update(each_r)
391
- inner_results.push mr
392
- end
393
- end
394
- end
395
- return inner_results
396
- end
397
- private:build_results
398
-
399
- def ==(node)
400
- if (node.kind_of?(ObjectNode))
401
- return @pattern == node.pattern
290
+ end
291
+
292
+ def match(fact)
293
+ mr = MatchResult.new
294
+ val = fact.object.send("#{@atom.name}")
295
+ if @atom.proc.call(val)
296
+ mr.is_match = true
297
+ mr.recency.push fact.recency
298
+ mr.fact_hash[@atom.tag] = fact.id
299
+ mr[@atom.tag] = fact.object
300
+ end
301
+ return mr
302
+ end
303
+ private:match
304
+ end
305
+
306
+ # This node class is used for matching properties of a fact.
307
+ class PropertyNode < AtomNode
308
+ def assert(context)
309
+ mr = match(context.fact)
310
+ if mr.is_match
311
+ # TODO for node sharing will we have to update multiple matches
312
+ context.match.update mr
313
+ propagate_assert(context)
402
314
  end
403
315
  end
404
316
 
405
- def print(tab)
406
- puts tab + to_s + " (#{satisfied?}) #{@pattern} -> #{@out_nodes}"
317
+ def match(fact)
318
+ mr = MatchResult.new
319
+ val = fact.object.send("#{@atom.name}")
320
+ if @atom.proc.call(val)
321
+ mr.is_match = true
322
+ mr.fact_hash[@atom.tag] = fact.id
323
+ mr.recency.push fact.recency
324
+ mr[@atom.tag] = val
325
+ end
326
+ return mr
407
327
  end
328
+ private:match
408
329
  end
409
330
 
410
- # This class represents a one-input node. It essenatially acts as a wrapper
411
- # for an NotPattern, and provides the logic for matching facts. This class
412
- # is similar to ObjectNode (and in fact extends it), but some of the matching
413
- # logic is different. This is because when this matches facts that need to
414
- # not exist in working memory.
415
- class NotNode < ObjectNode
416
- def satisfied?
417
- return true # there is always something not there...
331
+ # This node class is used to match properties of one with the properties
332
+ # of any other already matched fact. It differs from the other AtomNodes
333
+ # because it does not perform any inline matching. The match method is only
334
+ # invoked by the two input node.
335
+ class ReferenceNode < AtomNode
336
+ def match(left_context,right_fact)
337
+ val = right_fact.object.send("#{@atom.name}")
338
+ args = [val]
339
+ # TODO for node sharing we will have to compare against multiple matches
340
+ match = left_context.match.dup
341
+ @atom.vars.each do |var|
342
+ args.push match.variables[var]
343
+ end
344
+ if @atom.proc.call(*args)
345
+ m = MatchResult.new(match.variables.clone, true, match.fact_hash.clone, match.recency)
346
+ m.recency.push right_fact.recency
347
+ m.fact_hash[@atom.tag] = right_fact.id
348
+ m.variables[@atom.tag] = val
349
+ return m
350
+ end
351
+ return MatchResult.new
418
352
  end
419
-
420
- # This method initiates the matching of this pattern to the given fact
421
- # (really to the object it wraps). If the pattern matches the object
422
- # then one or more MatchResults will be created and added to the system.
423
- # returns:boolean - false because no nodes can reference a NotNode
424
- def assert(fact)
425
- ref_results = {}
426
- results = [MatchResult.new]
427
-
428
- @pattern.atoms.each do |atom|
429
- r = atom.match? fact
430
- if r.empty? || !r[0].is_match
431
- # QUESTION do we need to add this for every unmatched fact?
432
- propagate_assert MatchResult.new({},true), fact
433
- return false
434
- else
435
- ref_results[atom.tag] = []
436
- results = build_results results, ref_results, r, atom
437
- end
353
+ end
354
+
355
+ # This node class is used to match properties of a fact with other properties
356
+ # of itself. Unlike ReferenceAtom it does perform inline matching.
357
+ class SelfReferenceNode < ReferenceNode
358
+ def assert(context)
359
+ mr = match(context, context.fact)
360
+ if mr.is_match
361
+ context.match.update mr
362
+ propagate_assert(context)
438
363
  end
439
-
440
- @ref_results_hash[fact.id] = ref_results
441
-
442
- # HACK delete repeated MatchResults. Instead of doing this
443
- # we need to be sure that every MatchResult added is unique.
444
- results.each do |r1|
445
- results.delete_if do |r2|
446
- r1 == r2 && r1.object_id != r2.object_id
447
- end
448
- end
449
-
450
- results.each do |mr|
451
- # TODO we need to extend the atoms so that NotNodes's results don't
452
- # have the variables that we are removing below
453
- mr.keys.each do |key|
454
- delete = true
455
- @pattern.atoms.each do |a|
456
- if (a.kind_of?(ReferenceAtom) && a.vars.index(key) != nil)
457
- delete = false
458
- end
459
- end
460
- mr.delete key if delete
461
- end
462
-
463
- propagate_assert_neg mr, fact
464
- end
465
- return false # nothing can reference a NotNode... easy
466
- end
467
-
468
- # This method is called when a fact is removed from the system. If the
469
- # patterned contained in the node was matched to the given fact, then we
470
- # removed those MatchResults from our local cache, and propagate the change.
471
- # returns:boolean - always false because no other nodes will ever care
472
- def retract(fact)
473
- unless @ref_results_hash.delete(fact.id) == @ref_results_hash.default
474
- propagate_retract_neg fact
364
+ end
365
+ end
366
+
367
+ # This class is used to plug nodes into the left input of a two-input JoinNode
368
+ class LeftAdapterNode < ParentNode
369
+ def propagate_assert(context)
370
+ @out_nodes.each do |out_node|
371
+ out_node.assert_left(context)
475
372
  end
476
- return false # nothing can reference a NotNode... easy
477
- end
478
-
479
- # This method forces the node to be true (regardless of the facts in
480
- # working memory). This is need when the working memory is empty.
481
- def assert_input_true
482
- propagate_assert MatchResult.new({},true), nil
483
373
  end
484
374
 
485
- def print(tab)
486
- puts tab + to_s + " (#{satisfied?}) #{@pattern} -> #{@out_nodes}"
375
+ def propagate_retract(fact)
376
+ @out_nodes.each do |out_node|
377
+ out_node.retract_left(fact)
378
+ end
487
379
  end
488
380
  end
489
-
490
- # This class represents a two-input node. It acts as a wrapper for a
491
- # composite pattern, and keeps a memory of whether or not the left and
492
- # right values have been satisfied.
493
- class JoinNode < Node
494
381
 
495
- # the 'pattern' parameter denotes the boolean operator that this class
496
- # will use to compute if this join_node has been satisfied.
497
- def initialize(pattern)
498
- super pattern
499
- @left_memory = MatchContext.new
500
- @right_memory = MatchContext.new
501
- @local_memory = MatchContext.new
502
- @not_memory = MatchContext.new
503
- @left_input = nil
504
- @right_input = nil
505
- end
506
-
507
- attr :left_input, true
508
- attr :right_input, true
509
- attr :local_memory, true
510
-
511
- def satisfied?
512
- return @local_memory.satisfied?
513
- end
514
-
515
- def assert(in_node, mr, fact)
516
- left = []; right = []
517
- if (in_node.object_id == @left_input.object_id)
518
- return if @left_memory.contains?(mr)
519
- @left_memory.add mr.fact_ids, mr
520
- left = [mr]
521
- right = @right_memory.match_results
522
- elsif (in_node.object_id == @right_input.object_id)
523
- return if @right_memory.contains?(mr)
524
- @right_memory.add mr.fact_ids, mr
525
- left = @left_memory.match_results
526
- right = [mr]
527
- else
528
- return
529
- end
530
- list = filter_match_results(left, right)
531
- list.each do |new_mr|
532
- # QUESTION can we just do this inside of filter_match_results?
533
- @local_memory.add new_mr.fact_ids, new_mr
534
- propagate_assert new_mr, fact
535
- end
536
- end
537
-
538
- def retract(in_node, fact)
539
- if in_node.object_id == @left_input.object_id
540
- @left_memory.remove fact.id
541
- elsif in_node.object_id == @right_input.object_id
542
- @right_memory.remove fact.id
543
- else
544
- return
382
+ # This class is used to plug nodes into the right input of a two-input JoinNode
383
+ class RightAdapterNode < ParentNode
384
+ def propagate_assert(context)
385
+ @out_nodes.each do |out_node|
386
+ out_node.assert_right(context)
545
387
  end
546
- @local_memory.remove fact.id
547
- propagate_retract fact
548
388
  end
389
+
390
+ def propagate_retract(fact)
391
+ @out_nodes.each do |out_node|
392
+ out_node.retract_right(fact)
393
+ end
394
+ end
395
+ end
396
+
397
+ # This class is a two-input node that is used to create a cross-product of the
398
+ # two network branches above it. It keeps a memory of the left and right
399
+ # inputs and compares new facts to each.
400
+ class JoinNode < ParentNode
401
+ def initialize
402
+ super
403
+ @left_memory = {}
404
+ @right_memory = {}
405
+ @ref_nodes = []
406
+ end
407
+ attr:ref_nodes,true
549
408
 
550
- def assert_neg(in_node, nmr, fact)
551
- return if @not_memory.contains?(nmr)
552
- @not_memory.add nmr.fact_ids, nmr
553
- @local_memory.delete_if do |mr|
554
- matches_not?(mr,[nmr])
409
+ def retract_left(fact)
410
+ @left_memory.delete(fact.id)
411
+ propagate_retract(fact)
412
+ end
413
+
414
+ def retract_right(fact)
415
+ @right_memory.delete(fact.id)
416
+ propagate_retract(fact)
417
+ end
418
+
419
+ def assert_left(context)
420
+ add_to_left_memory(context)
421
+ @right_memory.values.each do |right_context|
422
+ mr = match_ref_nodes(context,right_context)
423
+ if (mr.is_match)
424
+ # TODO for node sharing we will have to update multiple matches
425
+ new_context = MatchContext.new context.fact, mr
426
+ propagate_assert(new_context)
427
+ end
555
428
  end
556
- propagate_assert_neg nmr, fact
557
429
  end
558
430
 
559
- def retract_neg(in_node, fact)
560
- result = @not_memory.remove fact.id
561
- return if result == @not_memory.default
562
-
563
- # TODO keep a cache of MatchResults that were left out because of
564
- # certain not_match_results. Hash these by the fact.id of those NMRs.
565
- list = filter_match_results(@left_memory.match_results, @right_memory.match_results)
566
- list.each do |mr|
567
- unless @local_memory.contains?(mr)
568
- @local_memory.push mr
569
- propogate_assert mr, nil
570
- end
571
- end
572
- propagate_retract_neg fact
573
- end
574
-
575
- def ==(node)
576
- if (node.kind_of?(JoinNode))
577
- return @pattern == node.pattern
431
+ def assert_right(context)
432
+ @right_memory[context.fact.id] = context
433
+ @left_memory.values.flatten.each do |left_context|
434
+ mr = match_ref_nodes(left_context,context)
435
+ if (mr.is_match)
436
+ # TODO for node sharing we will have to update multiple matches
437
+ new_context = MatchContext.new context.fact, mr
438
+ propagate_assert(new_context)
439
+ end
578
440
  end
579
441
  end
580
442
 
581
- def print(tab)
582
- puts tab + "JoinNode:#{satisfied?}(#{@left_memory},#{@right_memory})(#{@not_memory}) -> #{@out_nodes}"
583
- @left_input.print(tab + ' ')
584
- @right_input.print(tab + ' ')
585
- end
586
-
587
- # This method filters the match results that are given by the left and
588
- # right patterns. It does this by merging the two. It needs to be
589
- # refactored, and probably moved into the Pattern that this JoinNode wraps
590
- # (so we can take a different approach for OrPatterns in the future).
591
- def filter_match_results(left, right)
592
- mc = []
593
- left.each do |left_mr|
594
- right.each do |right_mr|
595
- if right_mr.keys.empty?
596
- # HACK we're doing this because we have an empty MatchResult
597
- # from a NotNode.
598
- # QUESTION do we need to account for these on the left too?
599
- mc.push left_mr.dup unless matches_not?(left_mr,@not_memory.match_results)
600
- else
601
- if resolve left_mr, right_mr
602
- mr = left_mr.dup.update(right_mr.dup)
603
- mr.fact_hash.update left_mr.fact_hash
604
- mr.fact_hash.update right_mr.fact_hash
605
- mc.push mr unless matches_not?(mr,@not_memory.match_results)
606
- # TODO cache the mr's that fail the test above, and add them in
607
- # if the fact that generated that not_match_result is retracted?
608
- end
443
+ def match_ref_nodes(left_context,right_context)
444
+ mr = right_context.match.dup
445
+ if @ref_nodes.empty?
446
+ return left_context.match.dup.update(mr)
447
+ else
448
+ # TODO for node sharing we will have to have a list of matches
449
+ @ref_nodes.each do |ref_node|
450
+ ref_mr = ref_node.match(left_context, right_context.fact)
451
+ if ref_mr.is_match
452
+ mr.update ref_mr
453
+ else
454
+ return MatchResult.new
609
455
  end
610
456
  end
611
- end
612
- return mc
613
- end
614
- private:filter_match_results
615
- end
616
-
617
- # This class represents a final node in the network. It has no outputs. It
618
- # is responsible for creating, and maintaining the list of activations for the
619
- # node network.
620
- class TerminalNode < Node
621
-
622
- def initialize(rule)
623
- super(nil)
624
- @rule = rule
625
- @activations = DoubleHash.new
626
- @parent_node = nil #only used for printing the network
457
+ return mr
458
+ end
627
459
  end
460
+ private:match_ref_nodes
628
461
 
629
- attr_reader :rule
630
- attr :parent_node, true
631
- attr_reader :activations
462
+ def add_to_left_memory(context)
463
+ lm = @left_memory[context.fact.id]
464
+ lm = [] unless lm
465
+ lm.push context
466
+ @left_memory[context.fact.id] = lm
467
+ # QUESTION for a little while we were having trouble with duplicate contexts
468
+ # being added to the left_memory. Double check that this is not happening
469
+ end
632
470
 
633
- def assert(in_node,mr,fact)
634
- if verify_by_tags(mr, @rule.pattern.atom_tags)
635
- @activations.add mr.fact_ids, Activation.new(@rule.action, mr)
471
+ def propagate_retract_resolve(match)
472
+ @out_nodes.each do |o|
473
+ o.retract_resolve(match)
636
474
  end
637
475
  end
638
476
 
639
- def retract(in_node,fact)
640
- @activations.remove(fact.id)
477
+ def retract_resolve(match)
478
+ # in this method we retract an existing match from memory if it resolves
479
+ # with the match given. It would probably be better to check if it
480
+ # resolves with a list of facts. But the system is not set up for
481
+ # that yet.
482
+ @left_memory.each do |fact_id,contexts|
483
+ value.delete_if do |left_context|
484
+ resolve(left_context.match, match)
485
+ end
486
+ end
641
487
  end
642
488
 
643
- def assert_neg(in_node,mr,fact)
644
- @activations.delete_if { |a| matches_not?(a.match,[mr]) }
489
+ def to_s
490
+ return "#{self.class}:#{object_id} | #{@left_memory.values} | #{@right_memory}"
491
+ end
492
+ end
493
+
494
+ # This node class is used when a rule is looking for a fact that does not
495
+ # exist. It is a two-input node, and thus has some of the properties of the
496
+ # JoinNode. NOTE it has not clear how this will work if the NotPattern is
497
+ # declared as the first pattern in a rule.
498
+ class NotNode < JoinNode
499
+ def initialize
500
+ super
645
501
  end
646
502
 
647
- def retract_neg(in_node,fact)
648
- # we don't need to do anything here, because the nodes above us will
649
- # handle the assertion of new MatchResults; hence Activations
503
+ def retract_left(fact)
504
+ @left_memory.delete(fact.id)
505
+ propagate_retract(fact)
650
506
  end
651
507
 
652
- def print(tab)
653
- puts tab + 'TerminalNode: ' + satisfied?.to_s + "(#{@activations.values})(#{@match_context})"
654
- @parent_node.print(tab + ' ')
655
- end
656
-
657
- # HACK This method is used to ensure that all required atoms have a value
658
- # present in this match result. If any are missing, then the match result
659
- # is incomplete. Maybe this should be moved up to be done within the context
660
- # of matching. Each MatchContext could keep a hash/list of atoms it needs
661
- # to be satisfied, and check them off as they are.
662
- def verify_by_tags(match, tags)
663
- tags.each do |t|
664
- if t.kind_of?(Array)
665
- # for future use by the OrPattern
666
- return false unless verify_by_tags(match, t)
667
- else
668
- if t.exists
669
- unless (match.key? t.tag)
670
- return false
508
+ def retract_right(fact)
509
+ right_context = @right_memory.delete(fact.id)
510
+ unless right_context == @right_memory.default
511
+ unless @ref_nodes.empty? && !@right_memory.empty?
512
+ @left_memory.values.each do |left_context|
513
+ # TODO we should cache the matches on the left that were unmatched
514
+ # by a result from a NotPattern. We could hash them by the right
515
+ # match that caused this. In that we woould not have to re-compare
516
+ # the the left and right matches.
517
+ if match_ref_nodes(left_context,right_context)
518
+ propagate_assert(left_context)
671
519
  end
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ def assert_left(context)
526
+ add_to_left_memory(context)
527
+ unless @ref_nodes.empty? && @right_memory.empty?
528
+ propagate = true
529
+ @right_memory.values.each do |right_context|
530
+ if match_ref_nodes(context,right_context)
531
+ propagate = false
532
+ break
533
+ end
534
+ end
535
+ propagate_assert(context) if propagate
536
+ end
537
+ end
538
+
539
+ def assert_right(context)
540
+ @right_memory[context.fact.id] = context
541
+ unless @ref_nodes.empty?
542
+ @left_memory.values.flatten.each do |left_context|
543
+ if match_ref_nodes(left_context,context)
544
+ # QUESTION is there a more efficient way to retract here?
545
+ propagate_retract_resolve(left_context.match)
672
546
  end
673
- end
547
+ end
548
+ end
549
+ end
550
+
551
+ # NOTE this returns a boolean, while the other classes return a MatchResult
552
+ def match_ref_nodes(left_context,right_context)
553
+ @ref_nodes.each do |ref_node|
554
+ ref_mr = ref_node.match(left_context, right_context.fact)
555
+ unless ref_mr.is_match
556
+ return false
557
+ end
674
558
  end
675
559
  return true
676
560
  end
677
- private:verify_by_tags
561
+ private:match_ref_nodes
562
+ end
563
+
564
+ # This class represents the bottom node in the network. There is a one to one
565
+ # relation betweek TerminalNodes and Rules. A terminal node acts as a wrapper
566
+ # for a rule. The class is responsible for keeping a memory of the
567
+ # activations that have been generated by the network.
568
+ class TerminalNode < Node
569
+ def initialize(rule)
570
+ super()
571
+ @rule = rule
572
+ @activations = MultiHash.new
573
+ end
574
+ attr_reader:activations
575
+
576
+ def assert(context)
577
+ @activations.add context.match.fact_ids, Activation.new(@rule.action, context.match)
578
+ end
579
+
580
+ def retract(fact)
581
+ @activations.remove fact.id
582
+ end
678
583
 
679
584
  def satisfied?
680
585
  return !@activations.values.empty?
586
+ end
587
+
588
+ def to_s
589
+ return "TerminalNode:#{object_id} | #{@activations.values}"
590
+ end
591
+
592
+ def retract_resolve(match)
593
+ # in this method we retract an existing activation from memory if its
594
+ # match resolves with the match given. It would probably be better to
595
+ # check if it resolves with a list of facts. But the system is not set up
596
+ # for that yet.
597
+ @activations.delete_if do |activation|
598
+ resolve(activation.match, match)
599
+ end
681
600
  end
682
- end
683
- end
601
+ end
602
+
603
+ class MatchContext
604
+ def initialize(fact,mr=MatchResult.new)
605
+ @fact = fact
606
+
607
+ # TODO for node sharing this will be a pattern=>match Hash
608
+ @match = mr
609
+ end
610
+ attr_reader:fact
611
+ attr_reader:match
612
+
613
+ def to_s
614
+ return @match.to_s
615
+ end
616
+
617
+ def ==(t)
618
+ return @fact == t.fact && @match == t.match
619
+ end
620
+ end
621
+
622
+ end
684
623
  end