aac-metrics 0.2.1 → 0.2.3

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: 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