EHadoux/aptimizer

View on GitHub
lib/aptimizer/objects/optimizer.rb

Summary

Maintainability
F
3 days
Test Coverage
module Aptimizer
  class Optimizer
    attr_reader :premises_hash, :modif_hash

    def initialize(aps)
      fill_usage_hashes(aps)
    end

    def fill_usage_hashes(aps)
      @premises_hash = Hash.new(nil)
      @modif_hash    = Hash.new(nil)
      @user_hash     = Hash.new(nil)

      filler = ->(pred, hash) do
        case pred.type
          when :pub  then str = pred.argument1
          when :priv then str = "#{pred.argument1}#{pred.owner}"
          when :atk  then str = "#{pred.argument1}_#{pred.argument2}"
          else raise Error
        end
        if hash[str].nil?
          hash[str] = (pred.positive ? :pos : :neg)
        else
          hash[str] = :both if hash[str] != (pred.positive ? :pos : :neg)
        end
      end

      (aps.agent.rules.product([:agent]) + aps.opponent.rules.product([:opponent])).each do |r, a|
        r.premises.each  {|p| filler.(p, @premises_hash)}
        r.modifiers.each do |m|
          filler.(m.predicate, @modif_hash)
          if m.predicate.type == :pub
            arg = m.predicate.argument1
            @user_hash[arg] = a     if @user_hash[arg].nil?
            @user_hash[arg] = :both if @user_hash[arg] == :agent && a == :opponent
          end
        end
      end
    end

    def optimize(aps, verbose=true, cycle=true, optimizations=[])
      aps if optimizations.include?(:none)
      flags_list = [:irrelevant, :initial, :enthymeme, :dominated]
      (f = (optimizations - flags_list)) and (raise "Unknown flags :#{f} Known flags are #{flags_list}" unless f.empty?)
      optimizations = flags_list if optimizations.empty?
      begin
        modified = false
        optimizations.each do |opt|
          case opt
            when :irrelevant then modified = remove_irrelevant_args(aps, verbose) || modified
            when :enthymeme  then modified = remove_enthymemes(aps, verbose) || modified
            when :dominated  then modified = remove_dominated(aps, verbose) || modified
            when :initial    then modified = remove_incompatible_initial(aps, verbose) || modified
          end
          prune_empty_rules(aps, verbose)
        end
      end while modified && cycle
      aps
    end

    private

    def prune_empty_rules(aps, verbose)
      if verbose
        puts "\n--> Pruning empty rules:"
      end

      prune = ->(set) do
        removed = Array.new
        set.rules.each_with_index do |rule, r_index|
          removed << [rule, r_index] if rule.alternatives.map(&:modifiers).all?(&:empty?)
        end
        set.rules -= removed.map(&:first)
        removed
      end

      agent_removed = prune.(aps.agent)
      unless agent_removed.empty?
        if verbose
          puts "Agent rules removed: #{agent_removed.map{|_,r| aps.agent.action_names[r]}}"
        end
        agent_removed.reverse_each {|r| aps.agent.action_names.delete_at(r[1]) }
      end
      opp_removed = prune.(aps.opponent)
      unless opp_removed.empty?
        if verbose
          puts "Opponent rules removed: #{opp_removed.map{|_,r| "r#{r}"}}"
        end
      end

      fill_usage_hashes(aps) unless agent_removed.empty? && opp_removed.empty?
    end

    # Removes arguments not involved in any premise or modifier
    #
    # @param aps [APS] The APS to optimize
    def remove_irrelevant_args(aps, verbose)
      if verbose
        puts "\n--> Applying irrelevant arguments optimization:"
      end
      remove = ->(set, num="") do
        args = Array.new(set.arguments)
        set.arguments.each do |arg|
          if @premises_hash["#{arg}#{num}"].nil? and @modif_hash["#{arg}#{num}"].nil?
            args.delete(arg)
          end
        end
        removed       = set.arguments - args
        set.arguments = Array.new(args)
        removed
      end

      agent_removed  = remove.(aps.agent, "1")
      opp_removed    = remove.(aps.opponent, "2")
      public_removed = remove.(aps.public_space)
      atk_removed    = Array.new
      aps.public_space.attacks.each do |atk|
        str = "#{atk.argument1}_#{atk.argument2}"
        if @premises_hash[str].nil? and @modif_hash[str].nil?
          atk_removed << atk
          public_space.attacks.delete(atk)
          public_space.backup_attacks.delete(atk)
        end
      end

      if verbose
        puts %Q{Private arguments removed for agent 1: #{agent_removed.map{|a| "h(#{a})"}}
Private arguments removed for agent 2: #{opp_removed.map{|a| "h(#{a})"}}
Public arguments removed: #{public_removed.map{|a| "a(#{a})"}}
Attacks removed: #{atk_removed.map{|atk| "e(#{atk.argument1},#{atk.argument2})"}}}
      end
      !agent_removed.empty? || !opp_removed.empty? || !public_removed.empty? || !atk_removed.empty?
    end

    # Removes attacks from the rules if they are implicitely accessible
    def remove_enthymemes(aps, verbose)
      if verbose
        puts "\n--> Applying enthymemes optimization:"
      end
      removed = []
      aps.public_space.attacks.each do |atk|
        skip = false
        next unless @premises_hash["#{atk.argument1}_#{atk.argument2}"].nil? # Too difficult if the attack is in premise
        atking = Predicate.new(:pub, atk.argument1)
        atked  = Predicate.new(:pub, atk.argument2)
        (aps.agent.rules + aps.opponent.rules).each do |rule|
          break if skip
          rule.alternatives.each do |alt|
            atk_mod = arg_mod = nil
            alt.modifiers.each do |m|
              atk_mod = m.type if m.predicate == atk
              arg_mod = m.type if m.predicate == atking
            end
            atk_but_no_arg     = (arg_mod.nil? && !atk_mod.nil?)
            atk_arg_sides_diff = (!arg_mod.nil? && !atk_mod.nil? && (atk_mod != arg_mod))
            atk_but_no_atked   = (atk_mod == :add && !rule.premises.include?(atked))
            if atk_but_no_arg || atk_arg_sides_diff || atk_but_no_atked
              skip = true
              break
            end
          end
        end
        removed << atk unless skip
      end

      removed.each do |atk|
        [aps.agent, aps.opponent].each do |ag|
          ag.rules.each do |rule|
            rule.alternatives.each do |alt|
              alt.modifiers.delete_if {|m| m.predicate == atk}
            end
          end
        end
        @modif_hash.delete("#{atk.argument1}_#{atk.argument2}")
        aps.public_space.attacks.delete atk
      end

      unless removed.empty?
        puts "Attacks removed: #{removed.map(&:to_s)}" if verbose
        aps.public_space.enthymeme = true
      end
      !removed.empty?
    end

    # Removes arguments of agent 1 that can never be defended
    def remove_dominated(aps, verbose)
      if verbose
        puts "\n--> Applying dominated arguments optimization:"
      end
      graph = AtkGraph::Graph.new
      aps.public_space.backup_attacks.each do |atk| # Creates the graph
        arg1 = atk.argument1
        arg2 = atk.argument2
        next if @user_hash[arg1].nil? || @user_hash[arg2].nil?
        v1   = AtkGraph::Vertex.new(arg1)
        v2   = AtkGraph::Vertex.new(arg2)
        graph.add_atk(v1, v2)
      end

      modified = false
      loop do
        to_rem = graph.vertices.select {|v| v.dominated? && @user_hash[v.value] == :agent} # Gets all dominated arguments of agent 1
        break if to_rem.empty?
        modified = true
        to_rem.each do |vertex|
          aps.agent.arguments.delete(vertex.value)
          puts "Argument #{vertex.value} removed" if verbose
          aps.agent.actions.reverse.each do |act|
            act.alternatives[0].modifiers.delete_if {|m| m.predicate.argument1 == vertex.value} # Removes modifiers
            act.premises.delete_if {|p| p.argument1 == vertex.value}
          end
          graph.vertices.delete(vertex)
        end
      end
      modified
    end

    def remove_incompatible_initial(aps, verbose)
      if verbose
        puts "\n--> Applying incompatible initial arguments optimization:"
      end
      checker = ->(rules, type, suffix, initial) do
        mod = false
        rules.each do |r|
          r.premises.select{|p| p.type == type}.each do |p|
            if @modif_hash["#{p.argument1}#{suffix}"].nil?
              print "#{p.argument1}#{suffix} optimizes #{r}" if verbose
              if @premises_hash["#{p.argument1}#{suffix}"] != (initial[p.argument1] ? :pos : :neg)
                r.alternatives.each{|a| a.modifiers.clear}
              else
                r.premises.reject!{|prem| p == prem}
              end
              puts " to #{r}"
              mod = true
            else # !@modif_hash["#{p.argument1}1"].nil?
              if @modif_hash["#{p.argument1}#{suffix}"] == (initial[p.argument1] ? :pos : :neg)
                print "#{p.argument1}#{suffix} optimizes #{r}" if verbose
                r.alternatives.each{|a| a.modifiers.clear}
                puts " to #{r}"
                mod = true
              end
            end
          end
        end
        mod
      end
      modified = checker.(aps.agent.rules, :priv, 1, aps.agent.initial_state)
      modified = checker.(aps.agent.rules, :pub, "", aps.public_space.initial_state) || modified
      checker.(aps.opponent.rules, :pub, "", aps.public_space.initial_state) || modified
    end
  end
end