wongi-engine 0.4.0.pre.alpha3 → 0.4.0.pre.alpha4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3401b29a9b186b8594549d45feddda88e885dae9a03d42f7006ad7e6fecbd0b
4
- data.tar.gz: c315eb40f0c2a05d6a5995016fd700b6ecdb7795588fa929b81b68d86722c481
3
+ metadata.gz: 566bc82ac763603c9e6a5556677364af9e57449bcb839c00865c3dd8cefbc1dc
4
+ data.tar.gz: d8285247fbe76bcf603740f7e6a6dd603226ea5c1e47564223b851c087a148e6
5
5
  SHA512:
6
- metadata.gz: ac9b80adde1174fec2361b6411d500a2c5c3c4c2659bac11d42bc630b088ccede7e3bf7d407560d099206daec5da97b0379d4c949e9891c9981d2f5847a815aa
7
- data.tar.gz: 03fa343a219317c31384446debde04894d00841bd6b780a45af31968fc5a1af8fb5ca04f2d6bf17bd445e046ab8de76828c797da47ac89f68715405e128a404a
6
+ metadata.gz: a88bbad2800cf1701d66277cbffc646fe92e0a283bb6da8f3bc828fbfdea001d3f4040be35a039372d7db4680c7acb763ccfe91e94f233be90b5182697f24602
7
+ data.tar.gz: b00126ee326301079e7c257548b6ce650682f0d8637056445760a3f0cc25da96411d4be8fc50edea8f343cdf1523f9ab82ec8d719a20a232128e7622f99ae331
@@ -1,41 +1,37 @@
1
1
  module Wongi::Engine
2
2
  class AggregateNode < BetaNode
3
- attr_reader :alpha, :tests, :assignment_pattern, :map, :function, :assign
3
+ attr_reader :var, :over, :partition, :aggregate, :map
4
4
 
5
- def initialize(parent, alpha, tests, assignment, map, function, assign)
5
+ def initialize(parent, var, over, partition, aggregate, map)
6
6
  super(parent)
7
- @alpha = alpha
8
- @tests = tests
9
- @assignment_pattern = assignment
10
- @map = map
11
- @function = function
12
- @assign = assign
7
+ @var = var
8
+ @over = over
9
+ @partition = make_partition_fn(partition)
10
+ @aggregate = make_aggregate_fn(aggregate)
11
+ @map = make_map_fn(map)
13
12
  end
14
13
 
15
- def equivalent?(alpha, tests, assignment_pattern)
16
- return false if self.alpha != alpha
17
- return false if self.assignment_pattern != assignment_pattern
18
- return true if self.tests.empty? && tests.empty?
19
- return false if self.tests.length != tests.length
14
+ def make_partition_fn(partition)
15
+ return nil if partition.nil?
20
16
 
21
- self.tests.all? { |my_test|
22
- tests.any? { |new_test|
23
- my_test.equivalent? new_test
24
- }
25
- }
17
+ if Template.variable?(partition)
18
+ ->(token) { token[partition] }
19
+ elsif partition.is_a?(Array) && partition.all? { Template.variable?(_1) }
20
+ ->(token) { token.values_at(*partition) }
21
+ else
22
+ partition
23
+ end
26
24
  end
27
25
 
28
- def alpha_activate(wme)
29
- # we need to re-run all WMEs through the aggregator, so the new incoming one doesn't matter
30
- tokens.each do |token|
31
- evaluate(wme: wme, token: token)
32
- end
26
+ def make_aggregate_fn(agg)
27
+ agg
33
28
  end
34
29
 
35
- def alpha_deactivate(wme)
36
- # we need to re-run all WMEs through the aggregator, so the new incoming one doesn't matter
37
- tokens.each do |token|
38
- evaluate(wme: wme, token: token)
30
+ def make_map_fn(map)
31
+ if map.nil?
32
+ ->(token) { token[over] }
33
+ else
34
+ map
39
35
  end
40
36
  end
41
37
 
@@ -43,42 +39,37 @@ module Wongi::Engine
43
39
  return if tokens.find { |t| t.duplicate? token }
44
40
 
45
41
  overlay.add_token(token)
46
- evaluate(wme: nil, token: token)
42
+ evaluate
47
43
  end
48
44
 
49
45
  def beta_deactivate(token)
50
46
  overlay.remove_token(token)
51
- beta_deactivate_children(token: token)
47
+ beta_deactivate_children(token:)
48
+ evaluate
52
49
  end
53
50
 
54
51
  def refresh_child(child)
55
- tokens.each do |token|
56
- evaluate(wme: nil, token: token, child: child)
57
- end
52
+ evaluate(child: child)
58
53
  end
59
54
 
60
- def evaluate(wme:, token:, child: nil)
61
- # clean up previous decisions
62
- # # TODO: optimise: only clean up if the value changed
63
- beta_deactivate_children(token: token)
64
-
65
- template = specialize(alpha.template, tests, token)
66
- candidates = select_wmes(template) { |asserted_wme| matches?(token, asserted_wme) }
67
-
68
- return if candidates.empty?
69
-
70
- mapped = candidates.map(&map)
71
- value = if function.is_a?(Symbol) && mapped.respond_to?(function)
72
- mapped.send(function)
73
- else
74
- function.call(mapped)
75
- end
76
- assignments = { assign => value }
77
- if child
78
- child.beta_activate(Token.new(child, token, wme, assignments))
55
+ def evaluate(child: nil)
56
+ groups = if partition
57
+ tokens.group_by(&partition).values
79
58
  else
80
- children.each do |beta|
81
- beta.beta_activate(Token.new(beta, token, wme, assignments))
59
+ # just a single group of everything
60
+ [tokens]
61
+ end
62
+
63
+ groups.each do |tokens|
64
+ aggregated = self.aggregate.call(tokens.map(&self.map))
65
+ assignment = { var => aggregated }
66
+ children = child ? [child] : self.children
67
+ tokens.each do |token|
68
+ # TODO: optimize this to work with a diff of actual changes
69
+ beta_deactivate_children(token:, children:)
70
+ children.each do |beta|
71
+ beta.beta_activate(Token.new(beta, token, nil, assignment))
72
+ end
82
73
  end
83
74
  end
84
75
  end
@@ -58,12 +58,9 @@ module Wongi::Engine
58
58
  end
59
59
  end
60
60
 
61
- def aggregate_node(condition, tests, assignment, map, function, assign)
62
- declare(assign)
63
- alpha = rete.compile_alpha(condition)
64
- self.node = AggregateNode.new(node, alpha, tests, assignment, map, function, assign).tap do |node|
65
- alpha.betas << node unless alpha_deaf
66
- end.tap(&:refresh)
61
+ def aggregate_node(var, over, partition, aggregate, map)
62
+ declare(var)
63
+ self.node = AggregateNode.new(node, var, over, partition, aggregate, map).tap(&:refresh)
67
64
  end
68
65
 
69
66
  def or_node(variants)
@@ -1,20 +1,18 @@
1
1
  module Wongi::Engine
2
2
  module DSL::Clause
3
- class Aggregate < Has
4
- attr_reader :map, :function, :assign
3
+ class Aggregate
4
+ attr_reader :var, :over, :partition, :aggregate, :map
5
5
 
6
- def initialize(s, p, o, options = {})
7
- member = options[:on]
6
+ def initialize(var, options = {})
7
+ @var = var
8
+ @over = options[:over]
9
+ @partition = options[:partition]
10
+ @aggregate = options[:using]
8
11
  @map = options[:map]
9
- @function = options[:function]
10
- @assign = options[:assign]
11
- @map ||= ->(wme) { wme.send(member) }
12
- super
13
12
  end
14
13
 
15
14
  def compile(context)
16
- tests, assignment = parse_variables(context)
17
- context.tap { |c| c.aggregate_node(self, tests, assignment, map, function, assign) }
15
+ context.tap { |c| c.aggregate_node(var, over, partition, aggregate, map) }
18
16
  end
19
17
  end
20
18
  end
@@ -89,19 +89,29 @@ module Wongi::Engine::DSL
89
89
  clause :aggregate
90
90
  accept Clause::Aggregate
91
91
 
92
- clause :least, :min
93
- body { |s, p, o, opts|
94
- aggregate s, p, o, on: opts[:on], function: :min, assign: opts[:assign]
92
+ clause :min
93
+ body { |var, opts|
94
+ aggregate var, opts.merge(using: ->(collection) { collection.min })
95
95
  }
96
96
 
97
- clause :greatest, :max
98
- body { |s, p, o, opts|
99
- aggregate s, p, o, on: opts[:on], function: :max, assign: opts[:assign]
97
+ clause :max
98
+ body { |var, opts|
99
+ aggregate var, opts.merge(using: ->(collection) { collection.max })
100
100
  }
101
101
 
102
102
  clause :count
103
- body { |s, p, o, opts|
104
- aggregate s, p, o, map: ->(_) { 1 }, function: ->(collection) { collection.inject(&:+) }, assign: opts[:assign]
103
+ body { |var, opts = {}|
104
+ aggregate var, opts.merge(using: ->(collection) { collection.count })
105
+ }
106
+
107
+ clause :sum
108
+ body { |var, opts|
109
+ aggregate var, opts.merge(using: ->(collection) { collection.inject(:+) })
110
+ }
111
+
112
+ clause :product
113
+ body { |var, opts|
114
+ aggregate var, opts.merge(using: ->(collection) { collection.inject(:*) })
105
115
  }
106
116
 
107
117
  clause :assert, :dynamic
@@ -40,6 +40,10 @@ module Wongi::Engine
40
40
  a.respond_to?(:call) ? a.call(self) : a
41
41
  end
42
42
 
43
+ def values_at(*vars)
44
+ vars.map { self[_1] }
45
+ end
46
+
43
47
  def has_var?(x)
44
48
  assignments.key? x
45
49
  end
@@ -1,5 +1,5 @@
1
1
  module Wongi
2
2
  module Engine
3
- VERSION = "0.4.0-alpha3".freeze
3
+ VERSION = "0.4.0-alpha4".freeze
4
4
  end
5
5
  end
@@ -8,190 +8,173 @@ describe 'aggregate' do
8
8
  let(:rule_name) { SecureRandom.alphanumeric(16) }
9
9
  let(:production) { engine.productions[rule_name] }
10
10
 
11
- context 'generic clause' do
12
- it 'returns a single token' do
11
+ context 'min' do
12
+ it 'works' do
13
13
  engine << rule(rule_name) do
14
14
  forall {
15
- aggregate :_, :weight, :_, on: :object, function: :min, assign: :X
15
+ has :_, :weight, :Weight
16
+ min :X, over: :Weight
17
+ has :Fruit, :weight, :X
16
18
  }
17
19
  end
18
20
 
19
- expect(production.size).to be == 0
20
-
21
21
  engine << [:apple, :weight, 5]
22
22
  expect(production.size).to be == 1
23
- expect(production.tokens.first[:X]).to be == 5
23
+ production.tokens.each do |token|
24
+ expect(token[:X]).to be == 5
25
+ expect(token[:Fruit]).to be == :apple
26
+ end
24
27
 
25
28
  engine << [:pea, :weight, 2]
26
- expect(production.size).to be == 1
27
- expect(production.tokens.first[:X]).to be == 2
28
-
29
- engine << [:melon, :weight, 15]
30
- expect(production.size).to be == 1
31
- expect(production.tokens.first[:X]).to be == 2
29
+ expect(production.size).to be == 2
30
+ production.tokens.each do |token|
31
+ expect(token[:X]).to be == 2
32
+ expect(token[:Fruit]).to be == :pea
33
+ end
32
34
 
33
35
  engine.retract [:pea, :weight, 2]
34
36
  expect(production.size).to be == 1
35
- expect(production.tokens.first[:X]).to be == 5
36
-
37
- engine.retract [:apple, :weight, 5]
38
- expect(production.size).to be == 1
39
- expect(production.tokens.first[:X]).to be == 15
40
-
41
- engine.retract [:melon, :weight, 15]
42
- expect(production.size).to be == 0
37
+ production.tokens.each do |token|
38
+ expect(token[:X]).to be == 5
39
+ expect(token[:Fruit]).to be == :apple
40
+ end
43
41
  end
44
42
  end
45
43
 
46
- context 'least' do
44
+ context 'max' do
47
45
  it 'works' do
48
46
  engine << rule(rule_name) do
49
47
  forall {
50
- least :_, :weight, :_, on: :object, assign: :X
48
+ has :_, :weight, :Weight
49
+ max :X, over: :Weight
51
50
  has :Fruit, :weight, :X
52
51
  }
53
52
  end
54
53
 
55
- engine << [:apple, :weight, 5]
56
- expect(production.size).to be == 1
57
- expect(production.tokens.first[:X]).to be == 5
58
- expect(production.tokens.first[:Fruit]).to be == :apple
59
-
60
54
  engine << [:pea, :weight, 2]
61
55
  expect(production.size).to be == 1
62
- expect(production.tokens.first[:X]).to be == 2
63
- expect(production.tokens.first[:Fruit]).to be == :pea
64
-
65
- engine.retract [:pea, :weight, 2]
66
- expect(production.size).to be == 1
67
- expect(production.tokens.first[:X]).to be == 5
68
- expect(production.tokens.first[:Fruit]).to be == :apple
69
- end
70
- end
71
-
72
- context 'min' do
73
- it 'works' do
74
- engine << rule(rule_name) do
75
- forall {
76
- min :_, :weight, :_, on: :object, assign: :X
77
- has :Fruit, :weight, :X
78
- }
56
+ production.tokens.each do |token|
57
+ expect(token[:X]).to be == 2
58
+ expect(token[:Fruit]).to be == :pea
79
59
  end
80
60
 
81
61
  engine << [:apple, :weight, 5]
82
- expect(production.size).to be == 1
83
- expect(production.tokens.first[:X]).to be == 5
84
- expect(production.tokens.first[:Fruit]).to be == :apple
85
-
86
- engine << [:pea, :weight, 2]
87
- expect(production.size).to be == 1
88
- expect(production.tokens.first[:X]).to be == 2
89
- expect(production.tokens.first[:Fruit]).to be == :pea
62
+ expect(production.size).to be == 2
63
+ production.tokens.each do |token|
64
+ expect(token[:X]).to be == 5
65
+ expect(token[:Fruit]).to be == :apple
66
+ end
90
67
 
91
- engine.retract [:pea, :weight, 2]
68
+ engine.retract [:apple, :weight, 5]
92
69
  expect(production.size).to be == 1
93
- expect(production.tokens.first[:X]).to be == 5
94
- expect(production.tokens.first[:Fruit]).to be == :apple
70
+ production.tokens.each do |token|
71
+ expect(token[:X]).to be == 2
72
+ expect(token[:Fruit]).to be == :pea
73
+ end
95
74
  end
96
75
  end
97
76
 
98
- context 'greatest' do
77
+ context 'count' do
99
78
  it 'works' do
100
79
  engine << rule(rule_name) do
101
80
  forall {
102
- greatest :_, :weight, :_, on: :object, assign: :X
103
- has :Fruit, :weight, :X
81
+ has :_, :weight, :Weight
82
+ count :Count
104
83
  }
105
84
  end
106
85
 
107
- engine << [:pea, :weight, 2]
86
+ engine << [:pea, :weight, 1]
108
87
  expect(production.size).to be == 1
109
- expect(production.tokens.first[:X]).to be == 2
110
- expect(production.tokens.first[:Fruit]).to be == :pea
88
+ production.tokens.each do |token|
89
+ expect(token[:Count]).to be == 1
90
+ end
111
91
 
112
92
  engine << [:apple, :weight, 5]
113
- expect(production.size).to be == 1
114
- expect(production.tokens.first[:X]).to be == 5
115
- expect(production.tokens.first[:Fruit]).to be == :apple
93
+ expect(production.size).to be == 2
94
+ production.tokens.each do |token|
95
+ expect(token[:Count]).to be == 2
96
+ end
97
+
98
+ engine << [:watermelon, :weight, 15]
99
+ expect(production.size).to be == 3
100
+ production.tokens.each do |token|
101
+ expect(token[:Count]).to be == 3
102
+ end
116
103
 
117
104
  engine.retract [:apple, :weight, 5]
118
- expect(production.size).to be == 1
119
- expect(production.tokens.first[:X]).to be == 2
120
- expect(production.tokens.first[:Fruit]).to be == :pea
105
+ expect(production.size).to be == 2
106
+ production.tokens.each do |token|
107
+ expect(token[:Count]).to be == 2
108
+ end
121
109
  end
122
- end
123
110
 
124
- context 'max' do
125
- it 'works' do
111
+ it 'works with a post-filter' do
126
112
  engine << rule(rule_name) do
127
113
  forall {
128
- max :_, :weight, :_, on: :object, assign: :X
129
- has :Fruit, :weight, :X
114
+ has :_, :weight, :Weight
115
+ count :Count
116
+ gte :Count, 3 # pass if at least 3 matching facts exist
130
117
  }
131
118
  end
132
119
 
133
- engine << [:pea, :weight, 2]
134
- expect(production.size).to be == 1
135
- expect(production.tokens.first[:X]).to be == 2
136
- expect(production.tokens.first[:Fruit]).to be == :pea
120
+ engine << [:pea, :weight, 1]
121
+ expect(production.size).to be == 0
137
122
 
138
123
  engine << [:apple, :weight, 5]
139
- expect(production.size).to be == 1
140
- expect(production.tokens.first[:X]).to be == 5
141
- expect(production.tokens.first[:Fruit]).to be == :apple
124
+ expect(production.size).to be == 0
125
+
126
+ engine << [:watermelon, :weight, 15]
127
+ expect(production.size).to be == 3
128
+ production.tokens.each do |token|
129
+ expect(token[:Count]).to be == 3
130
+ end
142
131
 
143
132
  engine.retract [:apple, :weight, 5]
144
- expect(production.size).to be == 1
145
- expect(production.tokens.first[:X]).to be == 2
146
- expect(production.tokens.first[:Fruit]).to be == :pea
133
+ expect(production.size).to be == 0
147
134
  end
148
135
  end
149
136
 
150
- context 'count' do
137
+ context 'partitioning by a single var' do
151
138
  it 'works' do
152
139
  engine << rule(rule_name) do
153
140
  forall {
154
- count :_, :weight, :_, assign: :Count
141
+ has :factor, :Number, :Factor
142
+ product :Product, over: :Factor, partition: :Number
155
143
  }
156
144
  end
157
145
 
158
- engine << [:pea, :weight, 1]
159
- expect(production.size).to be == 1
160
- expect(production.tokens.first[:Count]).to be == 1
161
-
162
- engine << [:apple, :weight, 5]
163
- expect(production.size).to be == 1
164
- expect(production.tokens.first[:Count]).to be == 2
165
-
166
- engine << [:watermelon, :weight, 15]
167
- expect(production.size).to be == 1
168
- expect(production.tokens.first[:Count]).to be == 3
146
+ engine << [:factor, 10, 2]
147
+ engine << [:factor, 10, 5]
148
+ engine << [:factor, 12, 3]
149
+ engine << [:factor, 12, 4]
169
150
 
170
- engine.retract [:apple, :weight, 5]
171
- expect(production.size).to be == 1
172
- expect(production.tokens.first[:Count]).to be == 2
151
+ expect(production).to have(4).tokens
152
+ production.tokens.each do |token|
153
+ expect(token[:Product]).to be_a(Integer)
154
+ expect(token[:Product]).to eq(token[:Number])
155
+ end
173
156
  end
157
+ end
174
158
 
175
- it 'works with a post-filter' do
159
+ context 'partitioning by a list' do
160
+ it 'works' do
176
161
  engine << rule(rule_name) do
177
162
  forall {
178
- count :_, :weight, :_, assign: :Count
179
- gte :Count, 3 # pass if at least 3 matching facts exist
163
+ has :factor, :Number, :Factor
164
+ product :Product, over: :Factor, partition: [:Number]
180
165
  }
181
166
  end
182
167
 
183
- engine << [:pea, :weight, 1]
184
- expect(production.size).to be == 0
185
-
186
- engine << [:apple, :weight, 5]
187
- expect(production.size).to be == 0
188
-
189
- engine << [:watermelon, :weight, 15]
190
- expect(production.size).to be == 1
191
- expect(production.tokens.first[:Count]).to be == 3
168
+ engine << [:factor, 10, 2]
169
+ engine << [:factor, 10, 5]
170
+ engine << [:factor, 12, 3]
171
+ engine << [:factor, 12, 4]
192
172
 
193
- engine.retract [:apple, :weight, 5]
194
- expect(production.size).to be == 0
173
+ expect(production).to have(4).tokens
174
+ production.tokens.each do |token|
175
+ expect(token[:Product]).to be_a(Integer)
176
+ expect(token[:Product]).to eq(token[:Number])
177
+ end
195
178
  end
196
179
  end
197
180
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wongi-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0.pre.alpha3
4
+ version: 0.4.0.pre.alpha4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valeri Sokolov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-01 00:00:00.000000000 Z
11
+ date: 2022-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry