aac-metrics 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29c12bbac8e7d433302f16f6f7061aa66992875774c9de39374ae62d1e935c02
4
- data.tar.gz: 508c9d5ce1db72a6274f3709cadd4ebcd3e703e79e6888647dbcaadbcc672e60
3
+ metadata.gz: 554fc52ede0b5dffdcf9315ec279a9f42a3ef2a44a324195018eda56538336b1
4
+ data.tar.gz: 2b082df38edf52ddf495045969db894ef02f7718641658d8d1ca4b012456e14c
5
5
  SHA512:
6
- metadata.gz: 184f934c0be547f2e8ae6ed09060911398827eaefd32bbf3684ab6f9a1486f3961701017b7fb4271b46798efaf89a4431f43a59676401c0a1d7d5de302fda613
7
- data.tar.gz: 77b725d14521a3bb1659960b51b108326a56b44427956b0a03082312e070be8e7908f731df6dfb45f99456c09e572cbc25a16d172a672e5262d979f5fa20ec18
6
+ metadata.gz: 58c3c7e8128b0bee8754baa3b218a36491cf985ed752b917733a0508bc9a4c8abe8c3e2c40ddcb7cd9098bea51a6c3d9143f5ba4cbd2d9098f59f16899bf1e0b
7
+ data.tar.gz: c9e2ec540feee357aaf6b1a20b11e5b938d2f4f8d28ba6d03eee80245167b1eb339ab5a2e1440588db6a31cffbe27bee37a44d6ef35f0078f5381d363c06226c
@@ -37,6 +37,29 @@ module AACMetrics::Loader
37
37
  end
38
38
  if obfset
39
39
  json = JSON.parse(File.read(obfset))
40
+ relations_hash = {}
41
+ json.each do |board|
42
+ board['grid']['order'].each_with_index do |row, row_idx|
43
+ row.each_with_index do |id, col_idx|
44
+ button = id && board['buttons'].detect{|b| b['id'] == id }
45
+ if button && button['label'] && !button['clone_id']
46
+ ref = "#{board['grid']['rows']}x#{board['grid']['columns']}-#{row_idx}.#{col_idx}"
47
+ pre = 'c'
48
+ relations_hash["cpre#{ref}-#{button['label']}"] ||= []
49
+ relations_hash["cpre#{ref}-#{button['label']}"] << [board, button]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ relations_hash.each do |id, cells|
55
+ if cells.length > 1
56
+ cells.each do |board, button|
57
+ board['clone_ids'] ||= []
58
+ board['clone_ids'] << id
59
+ button['clone_id'] ||= id
60
+ end
61
+ end
62
+ end
40
63
  elsif analysis
41
64
  json = JSON.parse(File.read(analysis))
42
65
  end
@@ -84,6 +107,7 @@ module AACMetrics::Loader
84
107
  path = paths.shift
85
108
  visited_paths[path] = idx
86
109
  new_json = {
110
+ "format" => 'obs',
87
111
  "id" => "brd#{idx}",
88
112
  "buttons" => [],
89
113
  "grid" => {},
@@ -158,28 +182,29 @@ module AACMetrics::Loader
158
182
  # treat action buttons for metrics
159
183
  new_btn = nil
160
184
  end
161
- # temporarily save semantic_id and possible clone_id for later use
185
+ # temporarily save semantic_id and possibly clone_id for later use
162
186
  # 1. Buttons in the same location with the same
163
187
  # semantic_id should be marked in the obfset as having
164
188
  # the same semantic_id
165
189
  # 2. Buttons in the same location with the same label & voc
166
190
  # and same load_board setting
167
191
  # should be marked in the obfset as having the same clone_id
168
- ref = "#{new_json['grid']['rows']}x#{new_json['grid']['columns']}-#{row_ids}.#{col_id}"
192
+ ref = "#{new_json['grid']['rows']}x#{new_json['grid']['columns']}-#{row_idx}.#{col_idx}"
169
193
  if btn['semantic_id']
170
194
  relations_hash["s#{ref}-#{btn['semantic_id']}"] ||= []
171
195
  relations_hash["s#{ref}-#{btn['semantic_id']}"] << [new_json['id'], new_btn['id']]
172
196
  end
173
- if new_btn['label']
174
- # TODO: currently doesn't enforce same-location on links, just whether it's a linked button or not
175
- pre = new_btn['load_board'] ? 'cl' : 'c'
197
+ if new_btn && new_btn['label']
198
+ #pre = new_btn['load_board'] ? 'cl' : 'c'
199
+ pre = 'c'
176
200
  relations_hash["#{pre}#{ref}-#{new_btn['label']}"] ||= []
177
- relations_hash["#{pre}#{ref}-#{new_btn['label']}"] ||= [new_json['id'], new_btn['id']]
201
+ relations_hash["#{pre}#{ref}-#{new_btn['label']}"] << [new_json['id'], new_btn['id']]
178
202
  end
179
- if do_ingest && new_btn['label']
203
+ if do_ingest && new_btn && new_btn['label']
180
204
  str = new_btn['label'].downcase.sub(/^\s+/, '').sub(/\s+$/, '')
181
205
  if str.scan(/\s+/).length < 2
182
206
  word_hash = Digest::MD5.hexdigest(str)[0, 10]
207
+ # Will need to re-evaluate hash process if it ever finds a collision with an already-saved word
183
208
  raise "collision!" if words[word_hash] && words[word_hash] != str
184
209
  if add_words || words[word_hash]
185
210
  words[word_hash] = str
@@ -197,9 +222,18 @@ module AACMetrics::Loader
197
222
  boards << new_json
198
223
  end
199
224
  end
225
+ # finalize all board paths once done iterating
226
+ boards.each do |brd|
227
+ brd['buttons'].each do |btn|
228
+ if btn['load_board'] && btn['load_board']['tmp_path']
229
+ btn['load_board']['id'] = "brd#{visited_paths[btn['load_board']['tmp_path']]}" if visited_paths[btn['load_board']['tmp_path']]
230
+ btn['load_board'].delete('tmp_path')
231
+ end
232
+ end
233
+ end
200
234
  # any semantic_id or clone_id repeats must be recorded
201
235
  relations_hash.each do |id, btns|
202
- if btns && btns.length > 0
236
+ if btns && btns.length > 1
203
237
  btns.each do |brd_id, btn_id|
204
238
  brd = boards.detect{|b| b['id'] == brd_id }
205
239
  if brd && brd['buttons']
@@ -220,32 +254,18 @@ module AACMetrics::Loader
220
254
  #
221
255
  end
222
256
  end
223
- boards.each do |brd|
224
- brd['buttons'].each do |btn|
225
- if btn['load_board'] && btn['load_board']['tmp_path']
226
- btn['load_board']['id'] = "brd#{visited_paths[btn['load_board']['tmp_path']]}" if visited_paths[btn['load_board']['tmp_path']]
227
- btn['load_board'].delete('tmp_path')
228
- end
229
- end
230
- end
231
257
  # TODO: record whether the board set is expected to have auto-home
232
258
  {boards: boards, words: words, words_path: words_path}
233
259
  end
234
260
 
235
- # TODO: Qualitative assessments of common vocabularies,
236
- # gather perspectives on what makes a "good" vocabulary
237
- # and collect reviews from field experts, also free
238
- # response sections.
239
- # Some criteria:
240
- # - works well for age group X, Y, Z
241
- # - works well for a beginning communicator
242
- # - allows long-term growth as-is
243
- # - comprehensive core
244
- # -
245
-
246
261
  def self.ingest(fn, token=nil)
247
262
  output = nil
248
263
  boards = nil
264
+ if fn.match(/manifest.json/)
265
+ json = JSON.parse(File.read(fn))
266
+ root_fn = json['root']
267
+ fn = fn.sub(/manifest.json/, root_fn)
268
+ end
249
269
  if fn.match(/\.obfset$/)
250
270
  boards = retrieve(fn, false)
251
271
  output = fn
@@ -268,7 +288,8 @@ module AACMetrics::Loader
268
288
  end
269
289
  end
270
290
  if boards
271
- analysis = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'sets', fn.sub(/\.obfset$/, '.analysis')))
291
+ analysis = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'sets', output.sub(/\.obfset$/, '.analysis')))
292
+ analysis = output.sub(/\.obfset$/, '.analysis')
272
293
  res = AACMetrics::Metrics.analyze(boards, false)
273
294
  f = File.open(analysis, 'w')
274
295
  f.write(JSON.pretty_generate(res))
@@ -5,34 +5,38 @@
5
5
  # - the vocabulary organization of this set makes sense
6
6
  # - this set provides clear opportunities for user-specific words to be added
7
7
  # - grammatical forms, growth over time
8
+ # - works well for age group X, Y, Z
9
+ # - works well for a beginning communicator
10
+ # - allows long-term growth as-is
11
+ # - comprehensive core
12
+
8
13
  # Effort algorithms for scanning/eyes
9
- # TODO: manual way to flag button as conceptually
10
- # related to the same-locaed button on the
11
- # prior board, allowing for a discounted penalty
12
14
  module AACMetrics::Metrics
13
- # TODO:
14
- # 1. When navigating from one board to the next, grid locations
15
+ # A. When navigating from one board to the next, grid locations
15
16
  # with the same clone_id or semantic_id should result in a
16
17
  # discount to overall search based more on the number of
17
18
  # uncloned/unsemantic buttons than the number of total buttons
18
19
  # (perhaps also factoring in the percent of board with that
19
20
  # id present in the full board set)
20
- # 2. When selecting a button with a semantic_id or clone_id,
21
+ # B. When selecting a button with a semantic_id or clone_id,
21
22
  # a discount to both search and selection should
22
23
  # be applied based on the percent of boards that
23
24
  # contain the same id at that grid location
24
- # 3.5 When selecting a button with a semantic_id or clone_id,
25
+ # C. When selecting a button with a semantic_id or clone_id,
25
26
  # if the same id was present on the previous board,
26
27
  # an additional discount to search and selection should be applied
27
- def self.analyze(obfset, output=true, include_obfset=false)
28
+ # D When selecting a button with a semantic_id or clone_id,
29
+ # apply a steep discount to the button in the same location
30
+ # as the link used to get there if they share an id
31
+ def self.analyze(obfset, output=true, include_obfset=false)
28
32
  locale = nil
29
33
  buttons = []
30
- refs = {}
34
+ set_refs = {}
31
35
  grid = {}
32
36
 
33
37
  if obfset.is_a?(Hash) && obfset['buttons']
34
38
  locale = obfset['locale'] || 'en'
35
- refs = obfset['reference_counts']
39
+ set_refs = obfset['reference_counts']
36
40
  grid = obfset['grid']
37
41
  buttons = []
38
42
  obfset['buttons'].each do |btn|
@@ -49,13 +53,14 @@ module AACMetrics::Metrics
49
53
  else
50
54
  visited_board_ids = {}
51
55
  to_visit = [{board: obfset[0], level: 0, entry_x: 1.0, entry_y: 1.0}]
52
- refs = {}
56
+ set_refs = {}
53
57
  rows_tally = 0.0
54
58
  cols_tally = 0.0
55
59
  root_rows = nil
56
60
  root_cols = nil
57
61
  # Gather repeated words/concepts
58
62
  obfset.each do |board|
63
+ # try to figure out the average grid size for board set
59
64
  root_rows ||= board['grid']['rows']
60
65
  root_cols ||= board['grid']['columns']
61
66
  rows_tally += board['grid']['rows']
@@ -63,25 +68,27 @@ module AACMetrics::Metrics
63
68
  # determine frequency within the board set
64
69
  # for each semantic_id and clone_id
65
70
  if board['clone_ids']
66
- boards['clone_ids'].each do |id|
67
- refs[id] ||= 0
68
- refs[id] += 1
71
+ board['clone_ids'].each do |id|
72
+ set_refs[id] ||= 0
73
+ set_refs[id] += 1
69
74
  end
70
75
  end
71
76
  if board['semantic_ids']
72
- boards['semantic_ids'].each do |id|
73
- refs[id] ||= 0
74
- refs[id] += 1
77
+ board['semantic_ids'].each do |id|
78
+ set_refs[id] ||= 0
79
+ set_refs[id] += 1
75
80
  end
76
81
  end
77
82
  end
83
+ # If the average grid size is much different than the root
84
+ # grid size, only then use the average as the size for this board set
78
85
  if (rows_tally / obfset.length.to_f - root_rows).abs > 3 || (cols_tally / obfset.length.to_f - root_cols).abs > 3
79
86
  root_rows = (rows_tally / obfset.length.to_f).floor
80
87
  root_cols = (cols_tally / obfset.length.to_f).floor
81
88
  end
82
- pcts = {}
83
- refs.each do |id, cnt|
84
- pcts[id] = cnt.to_f / obfset.length.to_f
89
+ set_pcts = {}
90
+ set_refs.each do |id, cnt|
91
+ set_pcts[id] = cnt.to_f / obfset.length.to_f
85
92
  end
86
93
  locale = obfset[0]['locale']
87
94
  known_buttons = {}
@@ -102,20 +109,56 @@ module AACMetrics::Metrics
102
109
  # whose semantic_id or clone_id is repeated in the board set
103
110
  # -0.0025 (* pct of matching boards) for semantic_id
104
111
  # -0.005 (* pct of matching boards) for clone_id
112
+ reuse_discount = 0.0
105
113
  board[:board]['grid']['rows'].times do |row_idx|
106
114
  board[:board]['grid']['columns'].times do |col_idx|
107
115
  button_id = (board[:board]['grid']['order'][row_idx] || [])[col_idx]
108
116
  button = board[:board]['buttons'].detect{|b| b['id'] == button_id }
109
- if button && button['clone_id'] && pcts[button['clone_id']]
110
- board_effort -= 0.005 * pcts[button['clone_id']]
111
- elsif button && button['semantic_id'] && pcts[button['semantic_id']]
112
- board_effort -= 0.0025 * pcts[button['semantic_id']]
117
+ if button && button['clone_id'] && set_pcts[button['clone_id']]
118
+ reuse_discount += REUSED_CLONE_FROM_OTHER_DISCOUNT * set_pcts[button['clone_id']]
119
+ elsif button && button['semantic_id'] && set_pcts[button['semantic_id']]
120
+ reuse_discount += REUSED_SEMANTIC_FROM_OTHER_DISCOUNT * set_pcts[button['semantic_id']]
113
121
  end
114
122
  end
115
123
  end
116
-
124
+ board_effort -= reuse_discount
117
125
  prior_buttons = 0
118
126
 
127
+ # Calculate the percent of links to this board
128
+ # that had or were linked by clone_ids or semantic_ids
129
+ board_pcts = {}
130
+ obfset.each do |brd|
131
+ brd['buttons'].each do |link_btn|
132
+ # For every board that links to this board
133
+ if link_btn['load_board'] && link_btn['load_board']['id'] == board[:board]['id']
134
+ board_pcts['all'] ||= 0
135
+ board_pcts['all'] += 1
136
+ # Count how many of those links have a clone_id or semantic_id
137
+ if link_btn['clone_id']
138
+ board_pcts[link_btn['clone_id']] ||= 0
139
+ board_pcts[link_btn['clone_id']] += 1
140
+ end
141
+ if link_btn['semantic_id']
142
+ board_pcts[link_btn['semantic_id']] ||= 0
143
+ board_pcts[link_btn['semantic_id']] += 1
144
+ end
145
+ # Also count all the clone_ids and semantic_ids
146
+ # anywhere on the boards that link to this one
147
+ (brd['clone_ids'] || []).uniq.each do |cid|
148
+ board_pcts["upstream-#{cid}"] ||= 0
149
+ board_pcts["upstream-#{cid}"] += 1
150
+ end
151
+ (brd['semantic_ids'] || []).uniq.each do |sid|
152
+ board_pcts["upstream-#{sid}"] ||= 0
153
+ board_pcts["upstream-#{sid}"] += 1
154
+ end
155
+ end
156
+ end
157
+ end
158
+ board_pcts.each do |id, cnt|
159
+ board_pcts[id] = board_pcts[id].to_f / board_pcts['all'].to_f
160
+ end
161
+
119
162
  board[:board]['grid']['rows'].times do |row_idx|
120
163
  board[:board]['grid']['columns'].times do |col_idx|
121
164
  button_id = (board[:board]['grid']['order'][row_idx] || [])[col_idx]
@@ -125,13 +168,34 @@ module AACMetrics::Metrics
125
168
  x = (btn_width / 2) + (btn_width * col_idx)
126
169
  y = (btn_height / 2) + (btn_height * row_idx)
127
170
  # prior_buttons = (row_idx * board[:board]['grid']['columns']) + col_idx
171
+ # calculate the percentage of links that point to this button
172
+ # and match on semantic_id or clone_id
128
173
  effort = 0
129
- # TODO: additional discount on board search effort
130
- # if this button's semantic_id or clone_id
131
- # was also present on the prior board
132
- # board_effort * 0.5 for semantic_id
133
- # board_effort * 0.33 for clone_id
134
- effort += board_effort
174
+ # Additional discount on board search effort,
175
+ # remember that semantic_id and clone_id are
176
+ # keyed to the same grid location, so matches only
177
+ # apply to that specific location
178
+ # - if this button's semantic_id or clone_id
179
+ # was also present anywhere on the prior board
180
+ # board_effort * 0.5 for semantic_id
181
+ # board_effort * 0.33 for clone_id
182
+ # - if this button's semantic_id or clone_id
183
+ # is directly used to navigate to this board
184
+ # board_effort * 0.1 for semantic_id
185
+ # board_effort * 0.1 for clone_id
186
+ button_effort = board_effort
187
+ if board_pcts[button['semantic_id']]
188
+ # TODO: Pull out these magic numbers
189
+ button_effort = [button_effort, button_effort * SAME_LOCATION_AS_PRIOR_DISCOUNT / board_pcts[button['semantic_id']]].min
190
+ elsif board_pcts["upstream-#{button['semantic_id']}"]
191
+ button_effort = [button_effort, button_effort * RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT / board_pcts["upstream-#{button['semantic_id']}"]].min
192
+ end
193
+ if board_pcts[button['clone_id']]
194
+ button_effort = [button_effort, button_effort * SAME_LOCATION_AS_PRIOR_DISCOUNT / board_pcts[button['clone_id']]].min
195
+ elsif board_pcts["upstream-#{button['clone_id']}"]
196
+ button_effort = [button_effort, button_effort * RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT / board_pcts["upstream-#{button['clone_id']}"]].min
197
+ end
198
+ effort += button_effort
135
199
  # add effort for percent distance from entry point
136
200
  distance = distance_effort(x, y, board[:entry_x], board[:entry_y])
137
201
  # TODO: decrease effective distance if the semantic_id or clone_id:
@@ -141,6 +205,24 @@ module AACMetrics::Metrics
141
205
  # - was also present on the prior board (total)
142
206
  # distance * 0.5 for semantic_id
143
207
  # distance * 0.33 for clone_id
208
+ # - is directly used to navigate to this board
209
+ # distance * 0.1 * (pct of links that match) for semantic_id
210
+ # distance * 0.1 * (pct of links that match) for clone_id
211
+ if board_pcts[button['semantic_id']]
212
+ distance = [distance, distance * SAME_LOCATION_AS_PRIOR_DISCOUNT / board_pcts[button['semantic_id']]].min
213
+ elsif board_pcts["upstream-#{button['semantic_id']}"]
214
+ distance = [distance, distance * RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT / board_pcts["upstream-#{button['semantic_id']}"]].min
215
+ elsif set_pcts[button['semantic_id']]
216
+ distance = [distance, distance * RECOGNIZABLE_SEMANTIC_FROM_OTHER_DISCOUNT / set_pcts[button['semantic_id']]].min
217
+ end
218
+ if board_pcts[button['clone_id']]
219
+ distance = [distance, distance * SAME_LOCATION_AS_PRIOR_DISCOUNT / board_pcts[button['clone_id']]].min
220
+ elsif board_pcts["upstream-#{button['clone_id']}"]
221
+ distance = [distance, distance * RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT / board_pcts["upstream-#{button['clone_id']}"]].min
222
+ elsif set_pcts[button['clone_id']]
223
+ distance = [distance, distance * RECOGNIZABLE_CLONE_FROM_OTHER_DISCOUNT / set_pcts[button['clone_id']]].min
224
+ end
225
+
144
226
  effort += distance
145
227
  if distance > DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN || (board[:entry_x] == 1.0 && board[:entry_y] == 1.0)
146
228
  # add small effort for every prior (visible) button when visually scanning
@@ -170,31 +252,39 @@ module AACMetrics::Metrics
170
252
  end
171
253
  if try_visit
172
254
  next_board = obfset.detect{|brd| brd['id'] == button['load_board']['id'] }
173
- puts "LIKE[] #{effort}" if button['label'] == 'like'
255
+ change_effort = BOARD_CHANGE_PROCESSING_EFFORT
174
256
  if next_board
175
257
  to_visit.push({
176
258
  board: next_board,
177
259
  level: board[:level] + 1,
178
- prior_effort: effort + BOARD_CHANGE_PROCESSING_EFFORT,
260
+ prior_effort: effort + change_effort,
179
261
  entry_x: x,
180
- entry_y: y
262
+ entry_y: y,
263
+ entry_clone_id: button['clone_id'],
264
+ entry_semantic_id: button['semantic_id']
181
265
  })
182
266
  end
183
267
  end
184
268
  else
185
269
  word = button['label']
186
270
  existing = known_buttons[word]
187
- button['effort'] = effort
188
- if !existing || existing[:effort] < effort #board[:level] < existing[:level]
189
- puts "LIKE #{effort}" if button['label'] == 'like'
271
+ if !existing || effort < existing[:effort] #board[:level] < existing[:level]
272
+ if board_pcts[button['clone_id']]
273
+ effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.3 / board_pcts[button['clone_id']]].min
274
+ elsif board_pcts[button['semantic_id']]
275
+ effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.5 / board_pcts[button['semantic_id']]].min
276
+ end
277
+
190
278
  known_buttons[word] = {
191
279
  id: "#{button['id']}::#{board[:board]['id']}",
192
280
  label: word,
193
281
  level: board[:level],
194
- effort: effort
282
+ effort: effort,
195
283
  }
196
284
  end
197
285
  end
286
+ button['effort'] = effort
287
+
198
288
  end
199
289
  end
200
290
  end
@@ -212,7 +302,7 @@ module AACMetrics::Metrics
212
302
  locale: locale,
213
303
  total_boards: total_boards,
214
304
  total_buttons: buttons.length,
215
- reference_counts: refs,
305
+ reference_counts: set_refs,
216
306
  grid: {
217
307
  rows: root_rows,
218
308
  columns: root_cols
@@ -234,6 +324,13 @@ module AACMetrics::Metrics
234
324
  DISTANCE_MULTIPLIER = 0.4
235
325
  DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN = 0.1
236
326
  SKIPPED_VISUAL_SCAN_DISTANCE_MULTIPLIER = 0.5
327
+ SAME_LOCATION_AS_PRIOR_DISCOUNT = 0.1
328
+ RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT = 0.5
329
+ RECOGNIZABLE_SEMANTIC_FROM_OTHER_DISCOUNT = 0.6
330
+ RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT = 0.33
331
+ RECOGNIZABLE_CLONE_FROM_OTHER_DISCOUNT = 0.4
332
+ REUSED_SEMANTIC_FROM_OTHER_DISCOUNT = 0.0025
333
+ REUSED_CLONE_FROM_OTHER_DISCOUNT = 0.005
237
334
 
238
335
  def self.button_size_effort(rows, cols)
239
336
  BUTTON_SIZE_MULTIPLIER * (rows + cols) / 2