module AACMetrics::Metrics

TODO: Scores for average effort level for word sets (spelling if that's th only way) Effort scores for sentence corpus Effort algorithms for scanning/eyes

Public Class Methods

analyze(obfset, output=true) click to toggle source
# File lib/aac-metrics/metrics.rb, line 7
def self.analyze(obfset, output=true)
  locale = nil
  buttons = []
  total_boards = 1
  
  if obfset.is_a?(Hash) && obfset['buttons']
    locale = obfset['locale'] || 'en'
    buttons = []
    obfset['buttons'].each do |btn|
      buttons << {
        id: btn['id'],
        label: btn['label'],
        level: btn['level'],
        effort: btn['effort']
      }
    end
    total_boards = obfset['total_boards']
  else
    visited_board_ids = {}
    to_visit = [{board: obfset[0], level: 0, entry_x: 1.0, entry_y: 1.0}]
    locale = obfset[0]['locale']
    known_buttons = {}
    sqrt2 = Math.sqrt(2)
    while to_visit.length > 0
      board = to_visit.shift
      visited_board_ids[board[:board]['id']] = board[:level]
      puts board[:board]['id'] if output
      btn_height = 1.0  / board[:board]['grid']['rows'].to_f
      btn_width = 1.0  / board[:board]['grid']['columns'].to_f
      board_effort = 0
      # add effort for level of complexity when new board is rendered
      board_effort += 0.003 * board[:board]['grid']['rows'] * board[:board]['grid']['columns']
      # add effort for number of visible buttons
      board_effort += 0.007 * board[:board]['grid']['order'].flatten.length
      prior_buttons = 0

      board[:board]['grid']['rows'].times do |row_idx|
        board[:board]['grid']['columns'].times do |col_idx|
          button_id = (board[:board]['grid']['order'][row_idx] || [])[col_idx]
          button = board[:board]['buttons'].detect{|b| b['id'] == button_id }
          prior_buttons += 0.1 if !button
          next unless button
          x = (btn_width / 2)  + (btn_width * col_idx)
          y = (btn_height / 2) + (btn_height * row_idx)
          # prior_buttons = (row_idx * board[:board]['grid']['columns']) + col_idx
          effort = 0
          effort += board_effort
          # add effort for percent distance from entry point
          distance = Math.sqrt((x - board[:entry_x]) ** 2 + (y - board[:entry_y]) ** 2) / sqrt2
          effort += distance
          if distance > 0.1 || (board[:entry_x] == 1.0 && board[:entry_y] == 1.0)
            # add small effort for every prior (visible) button when visually scanning
            effort += prior_buttons * 0.001
          else
            # ..unless it's right by the previous button, then
            # add tiny effort for local scan
            effort += distance * 0.5
          end
          # add cumulative effort from previous sequence
          effort += board[:prior_effort] || 0
          prior_buttons += 1

          if button['load_board']
            try_visit = false
            # For linked buttons, only traverse if
            # the board hasn't been visited, or if
            # we're not visiting it at a lower level
            if visited_board_ids[button['load_board']['id']] == nil
              try_visit = true 
            elsif visited_board_ids[button['load_board']['id']] > board[:level] + 1
              try_visit = true 
            end
            if to_visit.detect{|b| b[:board]['id'] == button['load_board']['id'] && b[:level] <= board[:level] + 1 }
              try_visit = false
            end
            if try_visit
              next_board = obfset.detect{|brd| brd['id'] == button['load_board']['id'] }
              if next_board
                to_visit.push({
                  board: next_board,
                  level: board[:level] + 1,
                  prior_effort: effort + 1.0,
                  entry_x: x,
                  entry_y: y
                })
              end
            end
          else
            word = button['label']
            existing = known_buttons[word]
            if !existing || board[:level] < existing[:level]
              known_buttons[word] = {
                id: "#{button['id']}::#{board[:board]['id']}",
                label: word,
                level: board[:level],
                effort: effort
              }
            end
          end
        end
      end
    end
    buttons = known_buttons.to_a.map(&:last)
    total_boards = visited_board_ids.keys.length
  end
  buttons = buttons.sort_by{|b| [b[:effort] || 1, b[:label] || ""] }
  clusters = {}
  buttons.each do |btn| 
    clusters[btn[:level]] ||= []
    clusters[btn[:level]] << btn
  end
  {
    analysis_version: AACMetrics::VERSION,
    locale: locale,
    total_boards: total_boards,
    total_buttons: buttons.length,
    buttons: buttons,
    levels: clusters
  }
end
analyze_and_compare(obfset, compset) click to toggle source
# File lib/aac-metrics/metrics.rb, line 128
def self.analyze_and_compare(obfset, compset)
  target = AACMetrics::Metrics.analyze(obfset, false)
  res = {}.merge(target)

  compare = AACMetrics::Metrics.analyze(compset, false)
  res[:comp_boards] = compare[:total_boards]
  res[:comp_buttons] = compare[:total_buttons]
  
  compare_words = []
  compare_buttons = {}
  comp_efforts = {}
  compare[:buttons].each do |btn|
    compare_words << btn[:label]
    compare_buttons[btn[:label]] = btn
    comp_efforts[btn[:label]] = btn[:effort]
  end

  efforts = {}
  target_efforts = {}
  target_words = []
  res[:buttons].each{|b| 
    target_words << b[:label]
    target_efforts[b[:label]] = b[:effort]
    efforts[b[:label]] = b[:effort] 
    comp = compare_buttons[b[:label]]
    if comp
      b[:comp_level] = comp[:level]
      b[:comp_effort] = comp[:effort]
    end
  }
  compare[:buttons].each{|b| 
    if efforts[b[:label]]
      efforts[b[:label]] += b[:effort] 
      efforts[b[:label]] /= 2
    else
      efforts[b[:label]] ||= b[:effort] 
    end
  }
  
  core_lists = AACMetrics::Loader.core_lists(target[:locale])
  common_words_obj = AACMetrics::Loader.common_words(target[:locale])
  synonyms = AACMetrics::Loader.synonyms(target[:locale])
  common_words_obj['efforts'].each{|w, e| efforts[w] ||= e }
  common_words = common_words_obj['words']
  
  too_easy = []
  too_hard = []
  target[:buttons].each do |btn|
    if btn[:effort] && common_words_obj['efforts'][btn[:label]]
      if btn[:effort] < common_words_obj['efforts'][btn[:label]] - 5
        too_easy << btn[:label]
      elsif btn[:effort] > common_words_obj['efforts'][btn[:label]] + 3
        too_hard << btn[:label]
      end
    end
  end

  
  missing = (compare_words - target_words).sort_by{|w| efforts[w] }
  missing = missing.select do |word|
    !synonyms[word] || (synonyms[word] & target_words).length == 0
  end
  extras = (target_words - compare_words).sort_by{|w| efforts[w] }
  extras = extras.select do |word|
    !synonyms[word] || (synonyms[word] & compare_words).length == 0
  end
  # puts "MISSING WORDS (#{missing.length}):"
  res[:missing_words] = missing
  # puts missing.join('  ')
  # puts "EXTRA WORDS (#{extras.length}):"
  res[:extra_words] = extras
  # puts extras.join('  ')
  overlap = (target_words & compare_words & common_words)
  # puts "OVERLAPPING WORDS (#{overlap.length}):"
  res[:overlapping_words] = overlap
  # puts overlap.join('  ')
  missing = (common_words - target_words)
  missing = missing.select do |word|
    !synonyms[word] || (synonyms[word] & target_words).length == 0
  end
  common_effort = 0
  comp_effort = 0
  common_words.each do |word|
    effort = target_efforts[word]
    if !effort && synonyms[word]
      synonyms[word].each do |syn|
        effort ||= target_efforts[syn]
      end
    end
    effort ||= 2 + (word.length * 2.5)
    common_effort += effort

    effort = comp_efforts[word]
    if !effort && synonyms[word]
      synonyms[word].each do |syn|
        effort ||= comp_efforts[syn]
      end
    end
    effort ||= 2 + (word.length * 2.5)
    comp_effort += effort
  end
  common_effort = common_effort.to_f / common_words.length.to_f
  comp_effort = comp_effort.to_f / common_words.length.to_f
  # puts "MISSING FROM COMMON (#{missing.length})"
  res[:missing] = {
    :common => {name: "Common Word List", list: missing}
  }
  res[:cores] = {
    :common => {name: "Common Word List", list: common_words, average_effort: common_effort, comp_effort: comp_effort}
  }
  # puts missing.join('  ')
  core_lists.each do |list|
    puts list['id']
    missing = []
    list_effort = 0
    comp_effort = 0
    list['words'].each do |word|
      words = [word] + (synonyms[word] || [])
      if (target_words & words).length == 0
        missing << word
      end
      effort = target_efforts[word]
      if !effort
        words.each{|w| effort ||= target_efforts[w] }
      end
      effort ||= 2 + (word.length * 2.5)
      list_effort += effort
      effort = comp_efforts[word]
      if !effort
        words.each{|w| effort ||= comp_efforts[w] }
      end
      effort ||= 2 + (word.length * 2.5)
      comp_effort += effort
    end
    if missing.length > 0
      # puts "MISSING FROM #{list['id']} (#{missing.length}):"
      res[:missing][list['id']] = {name: list['name'], list: missing, average_effort: list_effort}
      # puts missing.join('  ')
    end
    list_effort = list_effort.to_f / list['words'].length.to_f
    comp_effort = comp_effort.to_f / list['words'].length.to_f
    res[:cores][list['id']] = {name: list['name'], list: list['words'], average_effort: list_effort, comp_effort: comp_effort}
  end
  # puts "CONSIDER MAKING EASIER"
  res[:high_effort_words] = too_hard
  # puts too_hard.join('  ')
  # puts "CONSIDER LESS PRIORITY"
  res[:low_effort_words] = too_easy
  # puts too_easy.join('  ')
  res
end