activefacts 1.3.0 → 1.5.0

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.
@@ -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