knapsack_solver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e7d04df167b3dc41b861e541d7fba44765765280
4
+ data.tar.gz: 1832b618f0828d2409583bf97f0804cd13ac5b24
5
+ SHA512:
6
+ metadata.gz: 5615a26a781ae3cd094666aaf93c8d412083882a4e4c8c3eb800735d6513794e9f27ad81cda4d447dd6707eb8b18f4b73868ef92ef54792d243f804a8f936b1c
7
+ data.tar.gz: 697349d2dce6d2f8d9acdaf32e552594c30745aa787f46b7051ff2635908bafb8374bd27d2b3e00a43dbb05436c24a99514872786dc315e1e92fcf2b999d449c
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018, Ján Sučan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ # knapsack-solver
2
+ 0/1 knapsack problem solver.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'knapsack_solver/cli'
4
+
5
+ begin
6
+ KnapsackSolver::CLI.run(ARGV)
7
+ exit 0
8
+ rescue StandardError => e
9
+ STDERR.puts "ERROR: #{e.message}"
10
+ STDERR.puts "Try 'knapsack_solver --help' for more information."
11
+ exit 1
12
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path('lib/knapsack_solver/version', __dir__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'knapsack_solver'
5
+ s.version = KnapsackSolver::VERSION
6
+ s.homepage = 'https://github.com/sucanjan/knapsack-solver'
7
+ s.license = 'MIT'
8
+ s.author = 'Jan Sucan'
9
+ s.email = 'sucanjan@fit.cvut.cz'
10
+
11
+ s.summary = '0/1 knapsack problem solver.'
12
+ s.description = <<-EOF
13
+ This gem contains command-line utility for solving 0/1 knapsack problem using
14
+ branch-and-bound method, dynamic programming, simple heuristic (weight/price)
15
+ and fully polynomial time approximation scheme.
16
+
17
+ It can measure CPU and wall-clock time spent by solving a problem, compute
18
+ relative error of the result and generate graphs from those values.
19
+ EOF
20
+
21
+ s.files = Dir['bin/*', 'lib/**/*', '*.gemspec', 'LICENSE*', 'README*', 'test/*']
22
+ s.executables = Dir['bin/*'].map { |f| File.basename(f) }
23
+ s.has_rdoc = 'yard'
24
+
25
+ s.required_ruby_version = '>= 2.2'
26
+
27
+ s.add_runtime_dependency 'gnuplot', '~> 2.6'
28
+
29
+ s.add_development_dependency 'rake', '~> 12.0'
30
+ s.add_development_dependency 'rspec', '~> 3.6'
31
+ s.add_development_dependency 'rubocop', '~> 0.50.0'
32
+ s.add_development_dependency 'yard', '~> 0.9'
33
+ end
@@ -0,0 +1,2 @@
1
+ require 'knapsack_solver/version'
2
+ require 'knapsack_solver/solver'
@@ -0,0 +1,49 @@
1
+ require 'knapsack_solver/solver'
2
+ require 'knapsack_solver/version'
3
+ require 'knapsack_solver/cli_option_parser'
4
+ require 'knapsack_solver/output_printer'
5
+ require 'knapsack_solver/graph_printer'
6
+
7
+ module KnapsackSolver
8
+ # This class implements a command-line interface for the 0/1 knapsack
9
+ # problem solver.
10
+ class CLI
11
+ # Suffix of a text file which will containg results of dataset solving
12
+ # (price, knapsack things presence, cpu time, wall clock time,
13
+ # relative_error).
14
+ RESULTS_FILNEMAE_SUFFIX = '.results'.freeze
15
+
16
+ # Suffix of a text file which will containg statistic data (average price,
17
+ # execution times, relative error)
18
+ STATS_FILNEMAE_SUFFIX = '.stats'.freeze
19
+
20
+ # Processes command-line arguments. If no option is given, converts arabic
21
+ # number to roman number and prints it to stdout.
22
+ #
23
+ # @param args [Array] the command-line arguments
24
+ def self.run(args)
25
+ options = CliOptionParser.parse(args)
26
+ return if options.nil?
27
+ datasets = args.each_with_object([]) do |file, sets|
28
+ sets << Dataset.parse(File.new(file))
29
+ end
30
+ s = Solver.new(options, datasets)
31
+ results = s.run
32
+ print_results(results, s.stats(results), options, args)
33
+ end
34
+
35
+ # Prints output of datasets solving. Results and statistics are printed to
36
+ # stdout or to a text files. Graphs of statistic values can be created.
37
+ #
38
+ # @param results [Hash] results of dataset solvings
39
+ # @param stats [Hash] statistics from the results of dataset solvings
40
+ # @param options [Hash] Command-line line options supplied to the CLI
41
+ # @param args [Array] array of the positional command-line arguments
42
+ def self.print_results(results, stats, options, args)
43
+ OutputPrinter.new(args, RESULTS_FILNEMAE_SUFFIX, results).print(options[:output_dir])
44
+ OutputPrinter.new(args, STATS_FILNEMAE_SUFFIX, stats).print(options[:output_dir])
45
+ return unless options[:graphs_dir]
46
+ GraphPrinter.new(args, stats, options[:graphs_dir]).print
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,83 @@
1
+ require 'optparse'
2
+
3
+ module KnapsackSolver
4
+ # This class checks command line arguments provided to the knapsack_solver
5
+ # binary.
6
+ class CliOptionChecker
7
+ # Checks command-line options, their arguments and positional arguments
8
+ # provided to the CLI.
9
+ #
10
+ # @param opts [Hash] parsed command-line options
11
+ # @param args [Array<String>] command-line positional arguments
12
+ def self.check(opts, args)
13
+ if !opts[:branch_and_bound] && !opts[:dynamic_programming] &&
14
+ !opts[:fptas] && !opts[:heuristic]
15
+ raise StandardError, 'At least one method of solving must be requested'
16
+ end
17
+ check_fptas_options(opts)
18
+ check_directories(opts)
19
+ check_positional_arguments(args)
20
+ end
21
+
22
+ # Checks command-line options and arguments used by FPTAS solving method.
23
+ #
24
+ # @param opts [Hash] parsed command-line options
25
+ def self.check_fptas_options(opts)
26
+ return if !opts[:fptas] && !opts.key?(:fptas_epsilon)
27
+ check_incomplete_fptas_options(opts)
28
+ eps = opts[:fptas_epsilon].to_f
29
+ return unless eps <= 0 || eps >= 1 || eps.to_s != opts[:fptas_epsilon]
30
+ raise StandardError,
31
+ 'FPTAS epsilon must be number from range (0,1)'
32
+ end
33
+
34
+ # Checks command-line options and arguments used by FPTAS solving
35
+ # method. Recignizes cases when mandatory FPTAS epsilon constant is
36
+ # missing or when it the constant is provided and FPTAS method is not
37
+ # requested.
38
+ #
39
+ # @param opts [Hash] parsed command-line options
40
+ def self.check_incomplete_fptas_options(opts)
41
+ raise StandardError, 'Missing FPTAS epsilon constant' if opts[:fptas] && !opts.key?(:fptas_epsilon)
42
+ return unless !opts[:fptas] && opts.key?(:fptas_epsilon)
43
+ raise StandardError,
44
+ 'epsilon constant must not be provided when FPTAS is not selected'
45
+ end
46
+
47
+ # Checks directory for result and statistic output logs, and directory for
48
+ # graph files.
49
+ #
50
+ # @param opts [Hash] parsed command-line options
51
+ def self.check_directories(opts)
52
+ check_output_directory(opts[:output_dir]) if opts[:output_dir]
53
+ check_output_directory(opts[:graphs_dir]) if opts[:graphs_dir]
54
+ end
55
+
56
+ # Checks if at least one dataset input file was provided and if the input
57
+ # files are readable.
58
+ #
59
+ # @param args [Array<String>] positional arguments provided to the CLI
60
+ def self.check_positional_arguments(args)
61
+ raise StandardError, 'Missing datset file(s)' if args.empty?
62
+ args.each { |f| check_input_file(f) }
63
+ end
64
+
65
+ # Checks if an output directory exist and is writable.
66
+ #
67
+ # @param path [Path] path to output directory
68
+ def self.check_output_directory(path)
69
+ raise StandardError, "Directory '#{path}' does not exists" unless File.exist?(path)
70
+ raise StandardError, "'#{path}' is not a directory" unless File.directory?(path)
71
+ raise StandardError, "Directory '#{path}' is not writable" unless File.writable?(path)
72
+ end
73
+
74
+ # Checks if an input file exist and is readable.
75
+ #
76
+ # @param path [Path] path to input regular file
77
+ def self.check_input_file(path)
78
+ raise StandardError, "File '#{path}' does not exists" unless File.exist?(path)
79
+ raise StandardError, "'#{path}' is not a regular file" unless File.file?(path)
80
+ raise StandardError, "File '#{path}' is not readable" unless File.readable?(path)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,68 @@
1
+ require 'optparse'
2
+ require 'knapsack_solver/cli_option_checker'
3
+
4
+ module KnapsackSolver
5
+ # This class parses command line arguments provided to the knapsack_solver
6
+ # binary.
7
+ class CliOptionParser
8
+ # Message that describes how to use this CLI utility.
9
+ USAGE_MESSAGE = 'Usage: knapsack_solver OPTIONS DATASET_FILE...'.freeze
10
+
11
+ # Parses command-line arguments and removes them from the array of
12
+ # arguments.
13
+ #
14
+ # @param [Array] arguments the command-line arguments.
15
+ # @return [Hash] hash of recognized options.
16
+ #
17
+ # rubocop:disable Metrics/AbcSize, Metric/MethodLength, Metric/BlockLength
18
+ def self.parse(arguments)
19
+ options = {}
20
+ parser = OptionParser.new do |opts|
21
+ opts.banner = USAGE_MESSAGE
22
+ opts.on('-b', '--branch-and-bound', 'Use branch and boung method of solving') do
23
+ options[:branch_and_bound] = true
24
+ end
25
+ opts.on('-d', '--dynamic-programming', 'Use dynamic programming for solving') do
26
+ options[:dynamic_programming] = true
27
+ end
28
+ opts.on('-f', '--fptas', 'Use FPTAS for solving') do
29
+ options[:fptas] = true
30
+ end
31
+ opts.on('-r', '--heuristic', 'Use brute force method of solving') do
32
+ options[:heuristic] = true
33
+ end
34
+ opts.on('-e', '--fptas-epsilon EPS', 'Relative error for FPTAS from range (0,1)') do |eps|
35
+ options[:fptas_epsilon] = eps
36
+ end
37
+ opts.on('-o', '--output DIR', 'Directory for output log files') do |dir|
38
+ options[:output_dir] = dir
39
+ end
40
+ opts.on('-g', '--graphs DIR', 'Directory for graphs') do |dir|
41
+ options[:graphs_dir] = dir
42
+ end
43
+ opts.on('-v', '--version', 'Show program version') do
44
+ options[:version] = true
45
+ end
46
+ opts.on_tail('-h', '--help', 'Show this help message') do
47
+ options[:help] = true
48
+ end
49
+ end
50
+ parser.parse!(arguments)
51
+ process_help_and_version_opts(options, arguments, parser.to_s)
52
+ end
53
+ # rubocop:enable Metrics/AbcSize, Metric/MethodLength, Metric/BlockLength
54
+
55
+ def self.process_help_and_version_opts(options, arguments, usage_msg)
56
+ if !options[:help] && !options[:version]
57
+ CliOptionChecker.check(options, arguments)
58
+ return options
59
+ end
60
+ if options[:help]
61
+ puts usage_msg
62
+ elsif options[:version]
63
+ puts "knapsack_solver #{KnapsackSolver::VERSION}"
64
+ end
65
+ nil
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ require 'knapsack_solver/instance'
2
+
3
+ module KnapsackSolver
4
+ # This class represents a set of 0/1 knapsack problem instances.
5
+ class Dataset
6
+ # Initializes set of 0/1 knapsack problem instances.
7
+ #
8
+ # @param id [Integer] Dataset ID number.
9
+ # @param instances [Array<Instance>] set of the 0/1 knapsack problem instances.
10
+ def initialize(id, instances)
11
+ @id = id
12
+ @instances = instances
13
+ end
14
+
15
+ # Parses set of a 0/1 knapsack problem instances from a character stream.
16
+ #
17
+ # @param stream [#eof?,#readline,#each_line] character stream holding the dataset.
18
+ # @return [Dataset] dataset instance parsed from the stream.
19
+ def self.parse(stream)
20
+ id = parse_id(stream)
21
+ instances = stream.each_line.with_object([]) { |l, o| o << Instance.parse(l) }
22
+ raise StandardError, 'dataset: missing instances' if instances.empty?
23
+ Dataset.new(id, instances)
24
+ end
25
+
26
+ # Parses ID of a 0/1 knapsack problem dataset from a character stream.
27
+ #
28
+ # @param stream [#eof?,#readline,#each_line] character stream holding the dataset.
29
+ # @return [Integer] dataset ID number.
30
+ def self.parse_id(stream)
31
+ raise StandardError, 'dataset: missing ID' if stream.eof?
32
+ s = stream.readline.split
33
+ raise StandardError, 'dataset: first line does not contain ID' if s.size != 1
34
+ begin
35
+ raise StandardError, 'dataset: ID is negative' if Integer(s.first) < 0
36
+ rescue ArgumentError
37
+ raise StandardError, 'dataset: ID is not an integer'
38
+ end
39
+ Integer(s.first)
40
+ end
41
+
42
+ attr_reader :id, :instances
43
+ end
44
+ end
@@ -0,0 +1,115 @@
1
+ require 'gnuplot'
2
+
3
+ module KnapsackSolver
4
+ # This class provides support for making graphs from statistics of datasets
5
+ # solving results. It uses Gnuplot and also generates a Gnuplot config file
6
+ # for each generated graph.
7
+ class GraphPrinter
8
+ # Initializes printer for graph data (graphs, Gnuplot config files).
9
+ #
10
+ # @param dataset_filenames [Array<String>] dataset filenames
11
+ # @param stats [Hash] statistics of results
12
+ # @param out_dir [String] statistics of results to print
13
+ def initialize(dataset_filenames, stats, out_dir)
14
+ @dataset_basenames = file_basenames(dataset_filenames)
15
+ @stats = stats
16
+ @out_dir = out_dir
17
+ end
18
+
19
+ # Create graphs from statistics and Gnuplot configuration files.
20
+ def print
21
+ stats_to_datasets.each do |title, ds|
22
+ ofn = File.join(@out_dir, title + '.png')
23
+ plot(title, ds, ofn)
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # Create graph.
30
+ #
31
+ # @param title [String] title of the graph
32
+ # @param data [Array<Gnuplot::DataSet>] Gnuplot datasets to plot
33
+ # @param filename [String] path to the output image file
34
+ def plot(title, data, filename)
35
+ Gnuplot.open do |gp|
36
+ Gnuplot::Plot.new(gp, &plot_config(title, 'dataset', 'y', data, filename))
37
+ end
38
+ File.open(File.join(File.dirname(filename), File.basename(filename, '.png') + '.gnuplot'), 'w') do |gp|
39
+ Gnuplot::Plot.new(gp, &plot_config(title, 'dataset', 'y', data, filename))
40
+ end
41
+ end
42
+
43
+ # Creates Gnuplot datasets from statistics.
44
+ #
45
+ # @return [Array<Gnuplot::DataSet>] Gnuplot datasets created from the statistics.
46
+ def stats_to_datasets
47
+ graphs = @stats.values.first.values.first.first.keys
48
+ x_data = @stats.keys
49
+ datasets(graphs, x_data)
50
+ end
51
+
52
+ # Creates Gnuplot datasets from statistics.
53
+ #
54
+ # @param graphs [Array] array of graph titles
55
+ # @param x_data [Array] array of X axis values
56
+ def datasets(graphs, x_data)
57
+ graphs.each_with_object({}) do |g, gnuplot_datasets|
58
+ @stats.each_value do |s|
59
+ gnuplot_datasets[g.to_s] = s.each_key.with_object([]) do |method, o|
60
+ o << plot_dataset(method.to_s, x_data, @stats.map { |_, v| v[method].first[g] })
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Create dataset from provided title, X axis data and Y axis data.
67
+ #
68
+ # @param title [String] Gnuplot dataset title
69
+ # @param x_data [Array] Array of X values
70
+ # @param y_data [Array] Array of Y values
71
+ # @return [Gnuplot::DataSet] Gnuplot dataset.
72
+ def plot_dataset(title, x_data, y_data)
73
+ Gnuplot::DataSet.new([x_data, y_data]) { |ds| ds.title = escape_gnuplot_special_chars(title) }
74
+ end
75
+
76
+ # Creates Gnuplot plot configuration (configuration text lines).
77
+ #
78
+ # @param title [String] graph title
79
+ # @param xlabel [String] label of X axis
80
+ # @param ylabel [String] label of Y axis
81
+ # @param plot_datasets [Array<Gnuplot::DataSet>] Gnuplot datasets for plotting
82
+ # @param out_file [String] output file
83
+ # @return [lambda] Lambda for setting plot configuration.
84
+ def plot_config(title, xlabel, ylabel, plot_datasets, out_file)
85
+ lambda do |plot|
86
+ plot.term('png')
87
+ plot.output(out_file)
88
+ plot.title("'#{escape_gnuplot_special_chars(title)}'")
89
+ plot.ylabel("'#{escape_gnuplot_special_chars(ylabel)}'")
90
+ plot.xlabel("'#{escape_gnuplot_special_chars(xlabel)}'")
91
+ plot.key('outside')
92
+ plot.data = plot_datasets
93
+ end
94
+ end
95
+
96
+ # Escapes Gnuplot special characters.
97
+ #
98
+ # @param str [String] a string
99
+ # @return [Strnig] the string with Gnuplot special chars escaped
100
+ def escape_gnuplot_special_chars(str)
101
+ # underscore means subscript in Gnuplot
102
+ str.gsub('_', '\_')
103
+ end
104
+
105
+ # Gets basenames of supplied file paths.
106
+ #
107
+ # @param paths [Array<String>] path to files
108
+ # @return [Array<String>] basenames of the paths
109
+ def file_basenames(paths)
110
+ paths.each_with_object([]) do |path, basenames|
111
+ basenames << File.basename(path, File.extname(path))
112
+ end
113
+ end
114
+ end
115
+ end