aboutsummaryrefslogtreecommitdiff
path: root/lib/knapsack_solver/solver.rb
blob: c392d6994a4bdb00e71f6004c2a5da1452306d1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
require 'benchmark'

require 'knapsack_solver/dataset'
require 'knapsack_solver/solving_methods/heuristic_price_weight'
require 'knapsack_solver/solving_methods/branch_and_bound'
require 'knapsack_solver/solving_methods/dynamic_programming'
require 'knapsack_solver/solving_methods/fptas'

module KnapsackSolver
  # This class solves datasets of 0/1 knapsack problem instances using a
  # requested solving methods. It measures execution time of a solving and
  # computes relative error if some exact solving method is requested.
  class Solver
    # Initializes solver for use of user selected solving methods.
    #
    # @param opts [Hash] parser command-line options
    # @param datasets [Hash] parsed sets of 0/1 knapsack problem instances
    def initialize(opts, datasets)
      @opts = opts
      @datasets = datasets
      @solver_objects = {}
      { branch_and_bound: 'KnapsackSolver::BranchAndBound',
        dynamic_programming: 'KnapsackSolver::DynamicProgramming',
        heuristic: 'KnapsackSolver::HeuristicPriceToWeight',
        fptas: 'KnapsackSolver::Fptas' }.each do |symbol, class_name|
        @solver_objects[symbol] = Object.const_get(class_name) if opts[symbol]
      end
    end

    # Solve datasets using all selected method of solving, measure their
    # execution time and compute relative errors if some exact method was
    # requested.
    #
    # @return [Hash] results of dataset solving
    def run
      results = @datasets.each_with_object({}) do |dataset, res|
        res[dataset.id] = @solver_objects.each_with_object({}) do |(solver, object), r|
          r[solver] = dataset.instances.each_with_object([]) do |inst, a|
            o = object.new(inst) unless solver == :fptas
            o = object.new(inst, @opts[:fptas_epsilon]) if solver == :fptas
            a << execution_time { o.run }
          end
        end
      end
      add_relative_error(results)
    end

    # Creates statistics (average price, execution times, relative error) from
    # results of solving.
    #
    # @param results [Hash] solving results of datasets and solving methods
    # @return [Hash] statistics for datasets and solving methods
    def stats(results)
      results.each_with_object({}) do |(dataset_id, method_results), q|
        q[dataset_id] = method_results.each_with_object({}) do |(met, res), p|
          p[met] = averages(res)
        end
      end
    end

    protected

    # Computes average values from the results.
    #
    # @param res [Hash] results for one dataset and one method
    # @return [Array<Hash>] array of computed average values
    def averages(res)
      [res.first.keys.reject { |k| k == :config }.each_with_object({}) do |v, o|
         values = res.map { |i| i[v] }
         o[('avg_' + v.to_s).to_sym] = values.reduce(:+).to_f / values.size
       end]
    end

    # Adds relative error to results of solving if some exact method of
    # solving was requested.
    #
    # @param results [Hash] results of solving using requested methods
    # @return [Hash] the results with relative error added
    def add_relative_error(results)
      return results unless @opts[:branch_and_bound] || @opts[:dynamic_programming]
      exact_method = @opts[:branch_and_bound] ? :branch_and_bound : :dynamic_programming
      results.each_value do |method_results|
        method_results.each_value do |res|
          res.each_with_index do |r, i|
            r[:relative_error] = relative_error(method_results[exact_method][i][:price], r[:price])
          end
        end
      end
      results
    end

    # Measure execution time of provided block so that measured time is non-zero.
    #
    # @yieldparam block for which execution time will be measured
    # @return [Hash] solving results with cpu time and wall clock time of execution
    def execution_time
      exec_count = 1
      result = nil
      cpu_time = wall_clock_time = 0.0
      while cpu_time.zero? || wall_clock_time.zero?
        b = Benchmark.measure { exec_count.times { result = yield } }
        cpu_time += b.total
        wall_clock_time += b.real
        exec_count *= 2
      end
      result.merge(cpu_time: cpu_time, wall_clock_time: wall_clock_time)
    end

    # Computes relative error of approximate solution.
    #
    # @param opt [Numeric] Optimal price.
    # @param apx [Numeric] Approximate price.
    # @return [Float] Relative error.
    def relative_error(opt, apx)
      (opt.to_f - apx.to_f) / opt.to_f
    end
  end
end