aac-metrics 0.2.1 → 0.2.3

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: bddb8e430a21868a5b26312fb5924f36b914446dc212d849330462768fd9b233
4
- data.tar.gz: c5ddd0e2b835678f0dabf8743099b29d462612d94a4aa4dd261e1cb7337eaa02
3
+ metadata.gz: 9d8e0a136e07e00454338b3ddafc3d3374a0b38cb19f915110aef990dfea3f20
4
+ data.tar.gz: dcc8109cc6c77156e12205e7f518059696aa271d622b085ea0399877dd488542
5
5
  SHA512:
6
- metadata.gz: 876e09d6be2b2e2b7579c11bb06367296ee7fbba627db0f8d421b4e60d2b7042d9a41433f9b1af10c78a6fc8ac84077170224e059a5a5ba30db6b217cab56936
7
- data.tar.gz: 5072598db0226690da77985e4e1ed760b24f0d0edc2bc99a5961e59d7f8e8bfa549073f4d0b223559ba48d7d2348556041d8b513944e5427c9c158db3b467f6c
6
+ metadata.gz: 0de1a12d33d59f0440bab00fb60e426ced9c9cb95c2311931ac0a67a10032e1752b8803b91ef5fb5e80e1f0aa610575ae56917ef42ad7e15cbcb0ac04d4be31b
7
+ data.tar.gz: f372e88fcac486795074d2f425da2d9ff1651b7ad5e6af66b6df46b3e573c927ba30106465bdace6a350c3d6704b425f65bd4b1fb7b60a0ce4142fd2ba9eb83b
data/README.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  A tool for analysing and comparing grid-based AAC board sets
4
4
 
5
+ To compare two datasets
6
+ `ruby setup.rb qc24 qc112`
7
+
8
+ To compare two datasets and export the minimal obfset for the first
9
+ `ruby setup.rb qc24 qc112 export`
10
+ `ruby setup.rb qc24 qc112 render` # updates interactive preview in sets/preview.html
11
+
12
+ To generate an obfset from an external data source
13
+ `ruby lib/ingester.rb path/to/manifest-from-unzipped-obz.json`
14
+ `ruby lib/ingester.rb path/to/compiled-set.obfset`
15
+ `ruby lib/ingester.rb path/to/root-file.obf`
16
+
17
+
18
+ NOTE: if you add an obfset to sets, it will be accessible
19
+ simply by its prefix. If you add ".common" to the filename
20
+ similar to the existing files, it will be added to the
21
+ corpus of "common" word sets.
22
+
5
23
  ## License
6
24
 
7
25
  Licensed under the MIT License.
@@ -20,6 +20,7 @@ module AACMetrics::Loader
20
20
  }
21
21
  if button['load_board'] && button['load_board']['id']
22
22
  new_button['load_board'] = {'id' => button['load_board']['id']}
23
+ new_button['load_board']['temporary_home'] = button['load_board']['temporary_home'] if button['load_board']['temporary_home'] == true || button['load_board']['temporary_home'] == 'prior'
23
24
  new_button['load_board']['add_to_sentence'] = true if button['load_board']['add_to_sentence']
24
25
  end
25
26
  new_board['buttons'].push(new_button)
@@ -73,11 +74,11 @@ module AACMetrics::Loader
73
74
  if json.is_a?(Array)
74
75
  json.each do |brd|
75
76
  brd['buttons'].each do |btn|
76
- if btn['label'].match(/^\$/)
77
+ if (btn['label'] || '').match(/^\$/)
77
78
  word = base[btn['label'].sub(/^\$/, '')]
78
79
  btn['label'] = word if word
79
80
  end
80
- btn['label'] = btn['label'].gsub(/’/, '')
81
+ btn['label'] = (btn['label'] || '').gsub(/’/, '')
81
82
  end
82
83
  end
83
84
  elsif json.is_a?(Hash)
@@ -159,6 +160,12 @@ module AACMetrics::Loader
159
160
  # record load_board reference
160
161
  btn_idx += 1
161
162
  if btn['load_board']
163
+ if !btn['load_board']['path'] && btn['load_board']['id']
164
+ bpath = File.join(File.dirname(path), btn['load_board']['id'] + '.obf')
165
+ if File.exists?(bpath)
166
+ btn['load_board']['path'] = bpath
167
+ end
168
+ end
162
169
  if btn['load_board']['path']
163
170
  if visited_paths[btn['load_board']['path']]
164
171
  new_btn['load_board'] = {'id' => "brd#{visited_paths[btn['load_board']['path']]}"}
@@ -176,9 +183,14 @@ module AACMetrics::Loader
176
183
  new_btn['load_board'] = {'tmp_path' => btn['load_board']['data_url']}
177
184
  end
178
185
  else
186
+ puts path
179
187
  puts "Link found with no access #{btn['load_board'].to_json}"
180
188
  end
189
+ new_btn['load_board']['temporary_home'] = true if new_btn['load_board'] && btn['load_board']['temporary_home']
190
+ new_btn['load_board']['temporary_home'] = true if new_btn['load_board'] && btn['ext_coughdrop_home_lock']
181
191
  new_btn['load_board']['add_to_sentence'] = true if new_btn['load_board'] && btn['load_board']['add_to_sentence']
192
+ new_btn['load_board']['add_to_sentence'] = true if new_btn['load_board'] && btn['ext_coughdrop_add_to_vocalization']
193
+ new_btn['load_board']['add_to_sentence'] = true if new_btn['load_board'] && btn['ext_coughdrop_add_vocalization']
182
194
  elsif btn['action']
183
195
  # TODO: track keyboard actions and don't
184
196
  # treat action buttons for metrics
@@ -1,14 +1,8 @@
1
1
  # TODO:
2
2
  # Qualitative evaluation criteria:
3
- # - this set looks easy to learn for communicators
4
- # - this set looks easy to learn for supporters
5
- # - this vocabulary organization of this set makes sense
6
3
  # - this set provides clear locations for user-specific words to be added
7
4
  # - this set supports the use of grammatical forms (tenses and other inflections)
8
5
  # - this set provides predefined simplification for beginning communicators
9
- # - this set allows for long-term vocabulary growth over time
10
- # - this vocabulary looks like it will work well for young users
11
- # - this vocabulary looks like it will work well for adult users
12
6
 
13
7
  # Effort algorithms for scanning/eyes
14
8
  module AACMetrics::Metrics
@@ -30,14 +24,16 @@ module AACMetrics::Metrics
30
24
  # as the link used to get there if they share an id
31
25
  def self.analyze(obfset, output=true, include_obfset=false)
32
26
  locale = nil
33
- buttons = []
27
+ buttons = nil
34
28
  set_refs = {}
35
29
  grid = {}
30
+ alt_scores = {}
36
31
 
37
32
  if obfset.is_a?(Hash) && obfset['buttons']
38
33
  locale = obfset['locale'] || 'en'
39
34
  set_refs = obfset['reference_counts']
40
35
  grid = obfset['grid']
36
+ alt_scores = obfset['alternates']
41
37
  buttons = []
42
38
  obfset['buttons'].each do |btn|
43
39
  buttons << {
@@ -51,6 +47,7 @@ module AACMetrics::Metrics
51
47
  end
52
48
  total_boards = obfset['total_boards']
53
49
  else
50
+ start_boards = [obfset[0]]
54
51
  visited_board_ids = {}
55
52
  to_visit = [{board: obfset[0], level: 0, entry_x: 1.0, entry_y: 1.0}]
56
53
  set_refs = {}
@@ -87,6 +84,17 @@ module AACMetrics::Metrics
87
84
  set_refs[id] += 1
88
85
  end
89
86
  end
87
+ board['buttons'].each do |link_btn|
88
+ if link_btn['load_board'] && link_btn['load_board']['id'] && link_btn['load_board']['temporary_home']
89
+ # TODO: buttons can have multiple efforts, depending
90
+ # on if they are navigable from a temporary_home
91
+ if link_btn['load_board']['temporary_home'] == 'prior'
92
+ start_boards << board
93
+ elsif link_btn['load_board']['temporary_home'] == true
94
+ start_boards << obfset.detect{|b| b['id'] == link_btn['load_board']['id']}
95
+ end
96
+ end
97
+ end
90
98
  end
91
99
  # If the average grid size is much different than the root
92
100
  # grid size, only then use the average as the size for this board set
@@ -99,7 +107,53 @@ module AACMetrics::Metrics
99
107
  loc = id.split(/-/)[1]
100
108
  set_pcts[id] = cnt.to_f / (cell_refs[loc] || obfset.length).to_f
101
109
  end
102
- locale = obfset[0]['locale']
110
+
111
+ total_boards = nil
112
+ locale = nil
113
+ clusters = nil
114
+ # TODO: this list used to be reversed, but I don't know why.
115
+ # What we want is for these analyses to be run for the root
116
+ # board, don't we?
117
+ # puts JSON.pretty_generate(obfset[0])
118
+ start_boards.uniq.each do |brd|
119
+ analysis = analyze_for(obfset, brd, set_pcts, output)
120
+ buttons ||= analysis[:buttons]
121
+ if brd != obfset[0]
122
+ alt_scores[brd['id']] = {
123
+ buttons: analysis[:buttons],
124
+ levels: analysis[:levels]
125
+ }
126
+ end
127
+ total_boards ||= analysis[:total_boards]
128
+ clusters ||= analysis[:levels]
129
+ locale ||= analysis[:locale]
130
+ end
131
+ end
132
+ res = {
133
+ analysis_version: AACMetrics::VERSION,
134
+ locale: locale,
135
+ total_boards: total_boards,
136
+ total_buttons: buttons.map{|b| b[:count] || 1}.sum,
137
+ total_words: buttons.map{|b| b[:label] }.uniq.length,
138
+ reference_counts: set_refs,
139
+ grid: {
140
+ rows: root_rows,
141
+ columns: root_cols
142
+ },
143
+ buttons: buttons,
144
+ levels: clusters,
145
+ alternates: alt_scores
146
+ }
147
+ if include_obfset
148
+ res[:obfset] = obfset
149
+ end
150
+ res
151
+ end
152
+
153
+ def self.analyze_for(obfset, brd, set_pcts, output)
154
+ visited_board_ids = {}
155
+ to_visit = [{board: brd, level: 0, entry_x: 1.0, entry_y: 1.0}]
156
+ locale = brd['locale'] || 'en'
103
157
  known_buttons = {}
104
158
  while to_visit.length > 0
105
159
  board = to_visit.shift
@@ -173,7 +227,7 @@ module AACMetrics::Metrics
173
227
  button_id = (board[:board]['grid']['order'][row_idx] || [])[col_idx]
174
228
  button = board[:board]['buttons'].detect{|b| b['id'] == button_id }
175
229
  # prior_buttons += 0.1 if !button
176
- next unless button
230
+ next unless button && (button['label'] || button['vocalization'] || '').length > 0
177
231
  x = (btn_width / 2) + (btn_width * col_idx)
178
232
  y = (btn_height / 2) + (btn_height * row_idx)
179
233
  # prior_buttons = (row_idx * board[:board]['grid']['columns']) + col_idx
@@ -271,10 +325,14 @@ module AACMetrics::Metrics
271
325
  next_board = obfset.detect{|brd| brd['id'] == button['load_board']['id'] }
272
326
  change_effort = BOARD_CHANGE_PROCESSING_EFFORT
273
327
  if next_board
328
+ temp_home_id = board[:temporary_home_id]
329
+ temp_home_id = board[:board]['id'] if button['load_board']['temporary_home'] == 'prior'
330
+ temp_home_id = button['load_board']['id'] if button['load_board']['temporary_home'] == true
274
331
  to_visit.push({
275
332
  board: next_board,
276
333
  level: board[:level] + 1,
277
334
  prior_effort: effort + change_effort,
335
+ temporary_home_id: temp_home_id,
278
336
  entry_x: x,
279
337
  entry_y: y,
280
338
  entry_clone_id: button['clone_id'],
@@ -286,29 +344,35 @@ module AACMetrics::Metrics
286
344
  if !button['load_board'] || button['load_board']['add_to_sentence']
287
345
  word = button['label']
288
346
  existing = known_buttons[word]
289
- if !existing || effort < existing[:effort] #board[:level] < existing[:level]
290
- if board_pcts[button['clone_id']]
291
- effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.3 / board_pcts[button['clone_id']]].min
292
- elsif board_pcts[button['semantic_id']]
293
- effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.5 / board_pcts[button['semantic_id']]].min
294
- end
295
-
296
- known_buttons[word] = {
347
+ if board_pcts[button['clone_id']]
348
+ effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.3 / board_pcts[button['clone_id']]].min
349
+ elsif board_pcts[button['semantic_id']]
350
+ effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.5 / board_pcts[button['semantic_id']]].min
351
+ end
352
+ if !existing || effort < existing[:effort]
353
+ ww = {
297
354
  id: "#{button['id']}::#{board[:board]['id']}",
298
355
  label: word,
299
356
  level: board[:level],
300
357
  effort: effort,
358
+ count: ((existing || {})[:count] || 0) + 1
301
359
  }
360
+ # If a board set has any temporary_home links,
361
+ # then that can possibly affect the effort
362
+ # score for sentences
363
+ if board[:temporary_home_id]
364
+ ww[:temporary_home_id] = board[:temporary_home_id]
365
+ end
366
+ known_buttons[word] = ww
302
367
  end
303
368
  end
304
369
  button['effort'] = effort
305
-
306
370
  end
307
371
  end
308
- end
372
+ end # end to_visit list
309
373
  buttons = known_buttons.to_a.map(&:last)
310
374
  total_boards = visited_board_ids.keys.length
311
- end
375
+
312
376
  buttons = buttons.sort_by{|b| [b[:effort] || 1, b[:label] || ""] }
313
377
  clusters = {}
314
378
  buttons.each do |btn|
@@ -320,25 +384,38 @@ module AACMetrics::Metrics
320
384
  locale: locale,
321
385
  total_boards: total_boards,
322
386
  total_buttons: buttons.length,
323
- reference_counts: set_refs,
324
- grid: {
325
- rows: root_rows,
326
- columns: root_cols
327
- },
328
387
  buttons: buttons,
329
388
  levels: clusters
330
389
  }
331
- if include_obfset
332
- res[:obfset] = obfset
333
- end
334
390
  res
335
391
  end
336
392
 
393
+ class ExtraFloat < Numeric
394
+ include Math
395
+ def initialize(float=0.0)
396
+ @float = Float(float)
397
+ end
398
+
399
+ def to_f
400
+ @float.to_f
401
+ end
402
+
403
+ def method_missing(message, *args, &block)
404
+ if block_given?
405
+ @float.public_send(message, *args, &block)
406
+ else
407
+ @float.public_send(message, *args)
408
+ end
409
+ end
410
+ end
411
+
337
412
  SQRT2 = Math.sqrt(2)
338
413
  BUTTON_SIZE_MULTIPLIER = 0.09
339
414
  FIELD_SIZE_MULTIPLIER = 0.005
340
415
  VISUAL_SCAN_MULTIPLIER = 0.015
341
416
  BOARD_CHANGE_PROCESSING_EFFORT = 1.0
417
+ BOARD_HOME_EFFORT = 1.0
418
+ COMBINED_WORDS_REMEMBERING_EFFORT = 1.0
342
419
  DISTANCE_MULTIPLIER = 0.4
343
420
  DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN = 0.1
344
421
  SKIPPED_VISUAL_SCAN_DISTANCE_MULTIPLIER = 0.5
@@ -377,6 +454,7 @@ module AACMetrics::Metrics
377
454
  compare = AACMetrics::Metrics.analyze(compset, false)
378
455
  res[:comp_boards] = compare[:total_boards]
379
456
  res[:comp_buttons] = compare[:total_buttons]
457
+ res[:comp_words] = compare[:total_words]
380
458
  res[:comp_grid] = compare[:grid]
381
459
 
382
460
  compare_words = []
@@ -386,9 +464,21 @@ module AACMetrics::Metrics
386
464
  compare[:buttons].each do |btn|
387
465
  compare_words << btn[:label]
388
466
  compare_buttons[btn[:label]] = btn
389
- comp_efforts[btn[:label]] = btn[:effort]
467
+ comp_efforts[btn[:label]] = ExtraFloat.new(btn[:effort])
468
+ comp_efforts[btn[:label]].instance_variable_set('@temp_home_id', btn[:temporary_home_id])
390
469
  comp_levels[btn[:label]] = btn[:level]
391
470
  end
471
+ compare[:alternates].each do |id, alt|
472
+ efforts = {}
473
+ levels = {}
474
+ alt[:buttons].each do |btn|
475
+ efforts[btn[:label]] = ExtraFloat.new(btn[:effort])
476
+ efforts[btn[:label]].instance_variable_set('@temp_home_id', btn[:temporary_home_id])
477
+ levels[btn[:label]] = btn[:level]
478
+ end
479
+ comp_efforts["H:#{id}"] = efforts
480
+ comp_levels["H:#{id}"] = levels
481
+ end
392
482
 
393
483
  sortable_efforts = {}
394
484
  target_efforts = {}
@@ -400,7 +490,9 @@ module AACMetrics::Metrics
400
490
  # very frequent core words and use that when available
401
491
  res[:buttons].each{|b|
402
492
  target_words << b[:label]
403
- target_efforts[b[:label]] = b[:effort]
493
+ target_efforts[b[:label]] = ExtraFloat.new(b[:effort])
494
+ target_efforts[b[:label]].instance_variable_set('@temp_home_id', b[:temporary_home_id])
495
+
404
496
  target_levels[b[:label]] = b[:level]
405
497
  sortable_efforts[b[:label]] = b[:effort]
406
498
  comp = compare_buttons[b[:label]]
@@ -409,6 +501,18 @@ module AACMetrics::Metrics
409
501
  b[:comp_effort] = comp[:effort]
410
502
  end
411
503
  }
504
+ res[:alternates].each do |id, alt|
505
+ efforts = {}
506
+ levels = {}
507
+ alt[:buttons].each do |btn|
508
+ efforts[btn[:label]] = ExtraFloat.new(btn[:effort])
509
+ efforts[btn[:label]].instance_variable_set('@temp_home_id', btn[:temporary_home_id])
510
+ levels[btn[:label]] = btn[:level]
511
+ end
512
+ target_efforts["H:#{id}"] = efforts
513
+ target_levels["H:#{id}"] = levels
514
+ end
515
+ res.delete(:alternates)
412
516
  # Effort scores are the mean of thw scores from the
413
517
  # two sets, or just a singular value if in only one set
414
518
  compare[:buttons].each{|b|
@@ -441,7 +545,6 @@ module AACMetrics::Metrics
441
545
  end
442
546
  end
443
547
  end
444
-
445
548
 
446
549
  missing = (compare_words - target_words).sort_by{|w| sortable_efforts[w] }
447
550
  missing = missing.select do |word|
@@ -519,20 +622,11 @@ module AACMetrics::Metrics
519
622
  end
520
623
 
521
624
  # Calculate the effort for the target and comp sets
522
- effort = target_efforts[word]
523
- if !effort
524
- words.each{|w| effort ||= target_efforts[w] }
525
- end
526
- # Fallback penalty for missing word
527
- effort ||= spelling_effort(word)
625
+ effort, level = best_match(word, target_efforts, nil, synonyms)
528
626
  reffort = effort
529
627
  list_effort += effort
530
628
 
531
- effort = comp_efforts[word]
532
- if !effort
533
- words.each{|w| effort ||= comp_efforts[w] }
534
- end
535
- effort ||= spelling_effort(word)
629
+ effort, level = best_match(word, comp_efforts, nil, synonyms)
536
630
  comp_effort += effort
537
631
  # puts "#{word} - #{reffort.round(1)} - #{effort.round(1)}"
538
632
  end
@@ -552,50 +646,18 @@ module AACMetrics::Metrics
552
646
  res[:care_components][:comp_core] = (comp_effort_tally / core_lists.to_a.length) * 5.0
553
647
  comp_effort_tally = res[:care_components][:comp_core]
554
648
 
555
- # TODO: Assemble or allow a battery of word combinations,
649
+ # Assemble or allow a battery of word combinations,
556
650
  # and calculate the level of effort for each sequence,
557
651
  # as well as an average level of effort across combinations.
652
+ # TODO: sets with temporary_home settings will have custom
653
+ # effort scores for subsequent words in the sentence
558
654
  res[:sentences] = []
559
655
  sentences.each do |words|
560
- target_effort_score = 0.0
561
- comp_effort_score = 0.0
562
- words.each_with_index do |word, idx|
563
- synonym_words = [word] + (synonyms[word] || [])
564
- effort = target_efforts[word] || target_efforts[word.downcase]
565
- level = target_levels[word] || target_levels[word.downcase]
566
- if !effort
567
- synonym_words.each do |w|
568
- if !effort && target_efforts[w]
569
- effort = target_efforts[w]
570
- level = target_levels[w]
571
- end
572
- end
573
- end
574
- effort ||= spelling_effort(word)
575
- if level && level > 0 && idx > 0
576
- effort += BOARD_CHANGE_PROCESSING_EFFORT
577
- end
578
- ee = effort
579
- target_effort_score += effort
580
-
581
- effort = comp_efforts[word] || comp_efforts[word.downcase]
582
- level = comp_levels[word] || comp_levels[word.downcase]
583
- if !effort
584
- synonym_words.each do |w|
585
- if !effort && comp_efforts[w]
586
- effort = comp_efforts[w]
587
- level = comp_levels[w]
588
- end
589
- end
590
- end
591
- effort ||= spelling_effort(word)
592
- if level && level > 0 && idx > 0
593
- effort += BOARD_CHANGE_PROCESSING_EFFORT
594
- end
595
- comp_effort_score += effort
596
- end
597
- target_effort_score = target_effort_score / words.length
598
- comp_effort_score = comp_effort_score / words.length
656
+ sequence = best_combo(words, target_efforts, target_levels, synonyms)
657
+ target_effort_score = sequence.map{|w, e| e }.sum.to_f / words.length.to_f
658
+ sequence = best_combo(words, comp_efforts, comp_levels, synonyms)
659
+ comp_effort_score = sequence.map{|w, e| e }.sum.to_f / words.length.to_f
660
+
599
661
  res[:sentences] << {sentence: words.join(' '), words: words, effort: target_effort_score, comp_effort: comp_effort_score}
600
662
  end
601
663
  res[:care_components][:sentences] = res[:sentences].map{|s| s[:effort] }.sum.to_f / res[:sentences].length.to_f * 3.0
@@ -607,20 +669,11 @@ module AACMetrics::Metrics
607
669
  fringe.each do |word|
608
670
  target_effort_score = 0.0
609
671
  comp_effort_score = 0.0
610
- synonym_words = [word] + (synonyms[word] || [])
611
- effort = target_efforts[word] || target_efforts[word.downcase]
612
- if !effort
613
- synonym_words.each{|w| effort ||= target_efforts[w] }
614
- end
615
- # puts "!#{word}" unless effort
616
- effort ||= spelling_effort(word)
672
+
673
+ effort, level = best_match(word, target_efforts, nil, synonyms)
617
674
  target_effort_score += effort
618
675
 
619
- effort = comp_efforts[word] || comp_efforts[word.downcase]
620
- if !effort
621
- synonym_words.each{|w| effort ||= comp_efforts[w] }
622
- end
623
- effort ||= spelling_effort(word)
676
+ effort, level = best_match(word, comp_efforts, nil, synonyms)
624
677
  comp_effort_score += effort
625
678
  res[:fringe_words] << {word: word, effort: target_effort_score, comp_effort: comp_effort_score}
626
679
  end
@@ -633,19 +686,10 @@ module AACMetrics::Metrics
633
686
  common_fringe.each do |word|
634
687
  target_effort_score = 0.0
635
688
  comp_effort_score = 0.0
636
- synonym_words = [word] + (synonyms[word] || [])
637
- effort = target_efforts[word] || target_efforts[word.downcase]
638
- if !effort
639
- synonym_words.each{|w| effort ||= target_efforts[w] }
640
- end
641
- effort ||= spelling_effort(word)
689
+ effort, level = best_match(word, target_efforts, nil, synonyms)
642
690
  target_effort_score += effort
643
691
 
644
- effort = comp_efforts[word] || comp_efforts[word.downcase]
645
- if !effort
646
- synonym_words.each{|w| effort ||= comp_efforts[w] }
647
- end
648
- effort ||= spelling_effort(word)
692
+ effort, level = best_match(word, comp_efforts, nil, synonyms)
649
693
  comp_effort_score += effort
650
694
  res[:common_fringe_words] << {word: word, effort: target_effort_score, comp_effort: comp_effort_score}
651
695
  end
@@ -657,13 +701,6 @@ module AACMetrics::Metrics
657
701
  target_effort_tally += 70 # placeholder value for future added calculations
658
702
  comp_effort_tally += 70
659
703
 
660
- # puts target_effort_tally
661
- # puts comp_effort_tally
662
- # puts (target_effort_tally - comp_effort_tally).abs
663
- # puts JSON.pretty_generate(res[:care_components])
664
- # raise 'arf'
665
-
666
-
667
704
  res[:target_effort_score] = [0.0, 350.0 - target_effort_tally].max
668
705
  res[:comp_effort_score] = [0.0, 350.0 - comp_effort_tally].max
669
706
  # puts "CONSIDER MAKING EASIER"
@@ -674,4 +711,94 @@ module AACMetrics::Metrics
674
711
  # puts too_easy.join(' ')
675
712
  res
676
713
  end
714
+
715
+ # Find the effort for a word, its synonyms, or its spelling.
716
+ # Always returns a non-nil effort score
717
+ def self.best_match(word, target_efforts, target_levels, synonyms)
718
+ synonym_words = [word] + (synonyms[word] || [])
719
+ effort = target_efforts[word] || target_efforts[word.downcase]
720
+ target_levels ||= {}
721
+ level = target_levels[word] || target_levels[word.downcase]
722
+ if !effort
723
+ synonym_words.each do |w|
724
+ if !effort && target_efforts[w]
725
+ effort = target_efforts[w]
726
+ level = target_levels[w]
727
+ end
728
+ end
729
+ end
730
+ # Fallback penalty for missing word
731
+ fallback_effort = spelling_effort(word)
732
+ effort = fallback_effort if !effort || fallback_effort < effort
733
+
734
+ [effort, level || 0]
735
+ end
736
+
737
+ def self.best_combo(words, efforts, levels, synonyms)
738
+ options = [{next_idx: 0, list: []}]
739
+ words.length.times do |idx|
740
+ options.each do |option|
741
+ home_id = option[:temporary_home_id]
742
+ if option[:next_idx] == idx
743
+ combos = forward_combos(words, idx, efforts, levels)
744
+ if home_id
745
+ # Effort of hitting home button, and processing change, plus usual
746
+ combos.each{|c| c[:effort] += BOARD_HOME_EFFORT + BOARD_CHANGE_PROCESSING_EFFORT}
747
+ more_combos = forward_combos(words, idx, efforts["H:#{home_id}"] || {}, levels["H:#{home_id}"] || {})
748
+ more_combos.each{|c| c[:temporary_home_id] ||= home_id }
749
+ combos += more_combos
750
+ end
751
+ combos.each do |combo|
752
+ if idx > 0 && combo[:level] && combo[:level] > 0
753
+ combo[:effort] += BOARD_CHANGE_PROCESSING_EFFORT
754
+ end
755
+ options << {
756
+ next_idx: idx + combo[:size],
757
+ list: option[:list] + [[combo[:partial], combo[:effort]]],
758
+ temporary_home_id: combo[:temporary_home_id]
759
+ }
760
+ end
761
+ effort, level = best_match(words[idx], efforts, levels, synonyms)
762
+ option[:temporary_home_id] = effort.instance_variable_get('@temp_home_id')
763
+ effort += BOARD_CHANGE_PROCESSING_EFFORT if idx > 0 && level && level > 0
764
+ if home_id
765
+ effort += BOARD_HOME_EFFORT + BOARD_CHANGE_PROCESSING_EFFORT
766
+ other_effort, other_level = best_match(words[idx], efforts["H:#{home_id}"] || {}, levels["H:#{home_id}"] || {}, synonyms)
767
+ new_home_id = other_effort.instance_variable_get('@temp_home_id') || home_id
768
+ other_effort += BOARD_CHANGE_PROCESSING_EFFORT if idx > 0 && other_level && other_level > 0
769
+ other_list = option[:list] + [[words[idx], other_effort]]
770
+ options << {next_idx: idx + 1, list: other_list, temporary_home_id: new_home_id}
771
+ end
772
+ option[:list] << [words[idx], effort]
773
+ option[:next_idx] = idx + 1
774
+ end
775
+ end
776
+ end
777
+ options.sort_by{|o| o[:list].map{|w, e| e}.sum }.reverse[0][:list]
778
+ end
779
+
780
+ # Checks if any buttons will work for multiple words in a sentence
781
+ def self.forward_combos(words, idx, target_efforts, target_levels)
782
+ words_left = words.length - idx
783
+ combos = []
784
+ skip = 0
785
+ temp_home_id = nil
786
+ if words_left > 1
787
+ (words_left - 1).times do |minus|
788
+ partial = words[idx, words_left - minus].join(' ')
789
+ if target_efforts[partial] || target_efforts[partial.downcase]
790
+ effort = (target_efforts[partial] || target_efforts[partial.downcase]) + COMBINED_WORDS_REMEMBERING_EFFORT
791
+ level = target_levels[partial] || target_levels[partial.downcase]
792
+ combos << {
793
+ partial: partial,
794
+ effort: effort,
795
+ temporary_home_id: effort.instance_variable_get('@temp_home_id'),
796
+ level: level,
797
+ size: words_left - minus
798
+ }
799
+ end
800
+ end
801
+ end
802
+ combos
803
+ end
677
804
  end
data/lib/ingester.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # Imports an obf dataset for future access.
2
+
1
3
  lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
4
  $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
3
5