knapsack_solver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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