activefacts 1.3.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,304 @@
1
+ module ActiveFacts
2
+ module Metamodel
3
+ class QueryEvaluator
4
+ AFMM = ActiveFacts::Metamodel
5
+
6
+ def initialize query, population = nil
7
+ @query = query
8
+
9
+ raise "Query must be in the specified model" unless @query
10
+
11
+ # In theory a query can span more than one vocabulary. I don't do that!
12
+ @vocabulary = @query.all_variable.to_a.first.object_type.vocabulary
13
+
14
+ set_population(population)
15
+ end
16
+
17
+ def set_population population
18
+ if population.is_a?(String) || population == nil
19
+ @population = @constellation.Population[[[@vocabulary.name], population ||= '']]
20
+ raise "Population #{population.inspect} does not exist" unless @population
21
+ elsif population.is_a?(AFMM::Population)
22
+ raise "Population #{population.name} is not in the correct vocabulary #{@vocabulary.name}" unless population.vocabulary == @vocabulary
23
+ @population = population
24
+ else
25
+ raise "Population #{population.inspcct} must be a Population or a Population name"
26
+ end
27
+ end
28
+
29
+ def all_variables
30
+ @query.all_variable.to_a
31
+ end
32
+
33
+ attr_reader :free_variables
34
+
35
+ def evaluate
36
+ @result_projection = {} # A set of {hash of Variable to Instance}
37
+ @free_variables = all_variables
38
+ hypothesis = trivial_bindings @free_variables
39
+ trace :query, "Evaluating query with free variables #{@free_variables.map{|v|v.object_type.name}.inspect} and hypothesis #{hypothesis.inspect}" do
40
+ query_level(@free_variables, hypothesis)
41
+ end
42
+ @result_projection
43
+ end
44
+
45
+ # From the array of variables, delete those that are already bound and return the hypothesis hash
46
+ def trivial_bindings variables
47
+ hypothesis = {}
48
+
49
+ variables.each do |var|
50
+ next if var.value == nil
51
+ # Look up the instances for this bound variables
52
+ instance = lookup_instance_by_value(var.object_type, var.value)
53
+ trace :result, "Binding was #{instance ? '' : 'un'}successful for #{var.object_type.name} #{var.value.inspect}"
54
+ hypothesis[var] = instance
55
+ end
56
+
57
+ hypothesis.each do |v, i|
58
+ variables.delete(v)
59
+ end
60
+
61
+ trace :result, "starting hypothesis: {#{hypothesis.map{|var, instance| [var.object_type.name, instance.verbalise] }*'=>'}}"
62
+ trace :result, "free variables are #{variables.map{|v| v.object_type.name}.inspect}"
63
+
64
+ hypothesis
65
+ end
66
+
67
+ def lookup_instance_by_value(object_type, value)
68
+ pi = object_type.is_a?(ActiveFacts::Metamodel::EntityType) && object_type.preferred_identifier
69
+
70
+ # A multi-role identifier cannot be satisfied by a single value:
71
+ return nil if pi && pi.role_sequence.all_role_ref.size > 1
72
+
73
+ if pi
74
+ identifying_role = pi.role_sequence.all_role_ref.single.role
75
+ identifying_instance = lookup_instance_by_value(identifying_role.object_type, value)
76
+ return nil unless identifying_instance
77
+ identifying_role_values =
78
+ identifying_instance.all_role_value.select{|rv| rv.role == identifying_role }
79
+ raise "Faulty Population has duplicate #{object_type.name} instance #{value}" if identifying_role_values.size > 1
80
+ identifying_role_value = identifying_role_values[0]
81
+ return nil unless identifying_role_value # The value exists, but doesn't identify an existing entity
82
+ identified_instance =
83
+ (identifying_role_value.fact.all_role_value.to_a-[identifying_role_values])[0].instance
84
+ else
85
+ object_type.all_instance.detect do |instance|
86
+ instance.population == @population and instance.value == value
87
+ end
88
+ end
89
+ end
90
+
91
+ def conforms_to_plays_and_steps(hypothesis, variable, instance)
92
+ variable.all_play.to_a.map do |play|
93
+ # No play may fail (be in a step with/out a matching fact)
94
+ step = play.step
95
+
96
+ # REVISIT: We can skip steps that have been checked in an outer loop
97
+ # REVISIT: We need to check AlternateSets here.
98
+
99
+ # We cannot check a step that involves an unbound variable.
100
+ # Allow it for now (there will be nil instead of a matching fact)
101
+ next if step.all_play.to_a.any? do |other_play|
102
+ # REVISIT: Unless the step is optional
103
+ !(hypothesis.has_key?(other_play.variable) || other_play.variable == variable)
104
+ end
105
+
106
+ # Select all the RoleValues where this instance plays the required role.
107
+ # REVISIT: We've already selected instances in the correct population.
108
+ # Did RoleValues have to be further qualified? That might be redundant.
109
+ role_values = instance.all_role_value.select{|rv| rv.role == play.role }
110
+
111
+ satisfying_fact = nil
112
+
113
+ trace :result, "Trying #{role_values.size} values of #{play.role.object_type.name} to see whether #{instance.verbalise} participates in #{step.fact_type.default_reading} with bound variables #{hypothesis.map{|v,i| "#{v.object_type.name}=>#{i.verbalise}"}*', '}" do
114
+
115
+ role_values.any? do |rv|
116
+ fact = rv.fact
117
+ next false unless fact.population == @population
118
+
119
+ # If the step designates an objectification, this fact must be that objectification
120
+ if step.objectification_variable
121
+ next false unless fact.instance && step.objectification_variable.object_type == fact.instance.object_type
122
+ # REVISIT: fact.instance must be saved in the hypothesis under step.objectification_variable
123
+ end
124
+
125
+ # Succeed if this fact has all its role_values set to our bound variables
126
+ if fact.all_role_value.all? do |rv|
127
+ relevant_play = step.all_play.detect{|p| p.role == rv.role}
128
+ relevant_variable = relevant_play.variable
129
+ relevant_value = variable == relevant_variable ? instance : hypothesis[relevant_variable]
130
+ match = relevant_value == rv.instance
131
+ trace :result, "#{relevant_play.variable.object_type.name} (bound to #{hypothesis[relevant_play.variable].verbalise}) #{match ? 'MATCHES' : 'DOES NOT MATCH'} role value #{rv.instance.verbalise}" if match
132
+ match
133
+ end # of fact.all_role_value.all?
134
+ satisfying_fact = fact
135
+ satisfying_objectification_variable = step.objectification_variable
136
+ end
137
+ end # of role_values.any?
138
+ end # of trace
139
+
140
+ if satisfying_fact
141
+ trace :result, "#{instance.verbalise} #{
142
+ satisfying_fact ? 'participates' : 'does not participate'
143
+ } in #{step.is_disallowed ? 'disallowed ' : ''}step over #{step.fact_type.default_reading} "+
144
+ "with bound variables #{hypothesis.map{|v,i| "#{v.object_type.name}=>#{i.verbalise}"}*', '}"
145
+ end
146
+
147
+ if step.is_disallowed
148
+ satisfying_fact = !satisfying_fact # yield true if no fact satisfied the step, as sought
149
+ end
150
+ return nil unless satisfying_fact # This play fails the hypothesis
151
+
152
+ [ play, satisfying_fact ] +
153
+ Array(step.objectification_variable ? satisfying_fact.instance : nil)
154
+ end # of all_play
155
+ end # of conforms_to_plays_and_steps
156
+
157
+ def query_level unbound_variables = [], hypothesis = {}
158
+ # unbound_variables is an array of the remaining dimensions of the search
159
+ # hypothesis is a hash of Variable to Instance containing the values we think will satisfy the query
160
+
161
+ # Choose an unbound variable and test the instances of its object type
162
+ # This is a dumb choice for now. We should choose a variable that's
163
+ # closely connected to a bound variable, to narrow it down.
164
+ variable = unbound_variables[0]
165
+ return unless variable
166
+
167
+ # Set candidate_instances to the instances of this variable:
168
+ if hypothesis.has_key?(variable)
169
+ # REVISIT: I thought we were searching an unbound variable here? When does this happen:
170
+ candidate_instances = Array(hypothesis[variable])
171
+ else
172
+ candidate_instances = variable.object_type.all_instance
173
+ # candidate_instances = find_candidate_sets variable, hypothesis
174
+ end
175
+
176
+ population_candidates = candidate_instances.reject do |instance|
177
+ instance.population != @population # Wrong population
178
+ end
179
+
180
+ trace :result, "Query search through #{population_candidates.size} #{variable.role_name || variable.object_type.name}" do
181
+ population_candidates.each do |instance|
182
+ # filter by population:
183
+ trace :result, "Considering #{instance.verbalise}" do
184
+
185
+ # Check whether this instance satisfies the query steps of this play
186
+ played = conforms_to_plays_and_steps(hypothesis, variable, instance)
187
+ unless played
188
+ trace :result, "Does not conform to all steps"
189
+ next
190
+ else
191
+ trace :result, "Conforms to steps #{played.inspect}" unless played.empty?
192
+ end
193
+
194
+ # If this variable is the result of an objectification, the objectified fact
195
+ # has role players that might fill some remaining variables:
196
+ implications = {variable => instance}
197
+ if variable.step
198
+ objectification_plays = variable.step.all_play
199
+
200
+ # Ensure that the objectified instances don't contradict some bound variable:
201
+ next unless objectification_plays.to_a.all? do |play|
202
+ objectified_instance = instance.fact.all_role_value.select{|rv| rv.role == play.role}[0].instance
203
+ trace :result, "objectified instance includes #{play.variable.object_type.name}, checking it conforms" do
204
+ if hypothesis[play.variable]
205
+ unless hypothesis[play.variable] == objectified_instance
206
+ trace :result, "Objectified player #{objectified_instance.verbalise.inspect} does not match our hypothesis"
207
+ next false
208
+ end
209
+ true
210
+ else
211
+ implications[play.variable] = objectified_instance
212
+ conforms = conforms_to_plays_and_steps(hypothesis, play.variable, objectified_instance)
213
+ trace :result, "Objectified player #{objectified_instance.verbalise.inspect} does not conform to all steps" unless conforms
214
+ conforms
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ # Some of the variable's Plays might be matched by objectified facts.
221
+ # Record these values of the objectification variables.
222
+ next unless played.compact.all? do |playing|
223
+ play, fact, objectifying_variable = *playing
224
+ next true unless objectifying_variable
225
+ if i = hypothesis[objectifying_variable] and i != fact.instance
226
+ trace :result, "Objectification of #{fact.instance.verbalise.inspect} does not match our hypothesis"
227
+ next false
228
+ end
229
+ implications[objectifying_variable] = fact.instance
230
+ end
231
+
232
+ # accept the implications of this variable->instance assignment:
233
+ new_hypothesis = hypothesis.dup.merge(implications)
234
+ remaining_variables = unbound_variables - implications.keys
235
+
236
+ new_hypothesis.freeze
237
+ if remaining_variables.empty?
238
+ # This is a complete result set (no unbound variables) so record it:
239
+ @result_projection[new_hypothesis] = true
240
+ end
241
+ query_level(remaining_variables, new_hypothesis)
242
+ end # trace each instance
243
+ end # each instance
244
+ end # trace search through instances
245
+ end # query_level
246
+
247
+ # This method is an exploration into finding a smaller search space for the given variable.
248
+ # It relies on the values of bindings being Instance objects.
249
+ def find_candidate_sets variable, bindings
250
+ trace :result, "Looking for a smaller candidate set by traversing from these bound variables: #{bindings.keys.map{|v| (v.object_type.name+v.ordinal.to_s).inspect }*', '}" do
251
+ # If this variable is connected via a single step to one or more bound variables,
252
+ # that's probably a better place to search than object_type.all_instance.
253
+ bindable_plays =
254
+ variable.all_play.map do |play|
255
+ steps =
256
+ play.all_step_as_output_play.to_a + play.all_step_as_input_play.to_a + [play.step].compact
257
+ trace :result, "Considering steps over #{steps.map{|s|s.fact_type.default_reading.inspect}*', '}"
258
+ bound_steps =
259
+ steps.reject{|s| (s.all_play-[play]).detect{|p|
260
+ # trace :result, "Rejecting steps over #{s.fact_type.default_reading.inspect} because #{p.variable.object_type.name}#{p.variable.ordinal} is not bound" unless bindings[p.variable]
261
+ !bindings[p.variable]}
262
+ }
263
+ trace :result, "Bound steps are #{bound_steps.map{|s|s.fact_type.default_reading.inspect}*', '}"
264
+ counterpart_plays =
265
+ bound_steps.map{|s| (s.all_play-[play]) }.flatten.uniq
266
+ trace :result, "bound_steps.size = #{bound_steps.size}, counterpart_plays.size = #{counterpart_plays.size}"
267
+
268
+ # Consider only counterpart_plays for bound variables:
269
+ #counterpart_plays.reject!{|cp| !bindings[cp.variable] }
270
+
271
+ counterpart_plays
272
+ end.flatten.uniq
273
+
274
+ trace :result, "Connections of #{variable.object_type.name}#{variable.ordinal}:" do
275
+ if bindable_plays.empty?
276
+ trace :result, "NONE"
277
+ else
278
+ # Show the candidate instances here:
279
+ if trace :result
280
+ bindable_plays.each do |pl|
281
+ # We have bound instances of pl.variable, and the step(s) from this play reach the variable
282
+ trace :result, "Candidate facts which include this play are" do
283
+
284
+ bound_instance =
285
+ bindings[pl.variable] # This is the instance under consideration
286
+ bound_instance.
287
+ all_role_value.select{|rv| # Select its RoleValues for roles in this play's step
288
+ pl.all_step.detect{|s| s.fact_type == rv.fact.fact_type }
289
+ }.
290
+ each{|rv| # Verbalise other role_values of this fact
291
+ trace :result, "#{rv.fact.verbalise.inspect}"
292
+ }
293
+ end
294
+ trace :result, "-> Role of #{pl.variable.object_type.name}#{pl.variable.ordinal} in #{pl.role.fact_type.default_reading}"
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ end # QueryEvaluator
303
+ end # Metamodel
304
+ end # ActiveFacts
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefacts
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clifford Heath
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-25 00:00:00.000000000 Z
11
+ date: 2015-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activefacts-api
@@ -288,10 +288,13 @@ files:
288
288
  - lib/activefacts/cql/compiler/value_type.rb
289
289
  - lib/activefacts/cql/nodes.rb
290
290
  - lib/activefacts/cql/parser.rb
291
+ - lib/activefacts/dependency_analyser.rb
291
292
  - lib/activefacts/generate/absorption.rb
293
+ - lib/activefacts/generate/composition.rb
292
294
  - lib/activefacts/generate/cql.rb
293
295
  - lib/activefacts/generate/dm.rb
294
296
  - lib/activefacts/generate/help.rb
297
+ - lib/activefacts/generate/helpers/inject.rb
295
298
  - lib/activefacts/generate/helpers/oo.rb
296
299
  - lib/activefacts/generate/helpers/ordered.rb
297
300
  - lib/activefacts/generate/helpers/rails.rb
@@ -304,7 +307,12 @@ files:
304
307
  - lib/activefacts/generate/ruby.rb
305
308
  - lib/activefacts/generate/sql/mysql.rb
306
309
  - lib/activefacts/generate/sql/server.rb
310
+ - lib/activefacts/generate/stats.rb
307
311
  - lib/activefacts/generate/text.rb
312
+ - lib/activefacts/generate/topics.rb
313
+ - lib/activefacts/generate/traits/oo.rb
314
+ - lib/activefacts/generate/traits/ordered.rb
315
+ - lib/activefacts/generate/traits/ruby.rb
308
316
  - lib/activefacts/generate/transform/surrogate.rb
309
317
  - lib/activefacts/generate/version.rb
310
318
  - lib/activefacts/input/cql.rb
@@ -323,6 +331,7 @@ files:
323
331
  - lib/activefacts/vocabulary.rb
324
332
  - lib/activefacts/vocabulary/extensions.rb
325
333
  - lib/activefacts/vocabulary/metamodel.rb
334
+ - lib/activefacts/vocabulary/query_evaluator.rb
326
335
  - lib/activefacts/vocabulary/verbaliser.rb
327
336
  - script/txt2html
328
337
  - spec/absorption_spec.rb