or-tools 0.1.4 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -661,6 +661,11 @@ void Init_ext()
661
661
  "_solution_integer_value",
662
662
  *[](Object self, CpSolverResponse& response, operations_research::sat::IntVar& x) {
663
663
  return SolutionIntegerValue(response, x);
664
+ })
665
+ .define_method(
666
+ "_solution_boolean_value",
667
+ *[](Object self, CpSolverResponse& response, operations_research::sat::BoolVar& x) {
668
+ return SolutionBooleanValue(response, x);
664
669
  });
665
670
 
666
671
  define_class_under<CpSolverResponse>(rb_mORTools, "CpSolverResponse")
@@ -1,6 +1,6 @@
1
1
  require "mkmf-rice"
2
2
 
3
- abort "Missing stdc++" unless have_library("stdc++")
3
+ raise "Missing stdc++" unless have_library("stdc++")
4
4
 
5
5
  $CXXFLAGS << " -std=c++11 -DUSE_CBC"
6
6
 
@@ -8,73 +8,31 @@ $CXXFLAGS << " -std=c++11 -DUSE_CBC"
8
8
  $CXXFLAGS << " -Wno-sign-compare -Wno-shorten-64-to-32 -Wno-ignored-qualifiers"
9
9
 
10
10
  inc, lib = dir_config("or-tools")
11
-
12
- inc ||= "/usr/local/include"
13
- lib ||= "/usr/local/lib"
11
+ if inc || lib
12
+ inc ||= "/usr/local/include"
13
+ lib ||= "/usr/local/lib"
14
+ rpath = lib
15
+ else
16
+ # download
17
+ require_relative "vendor"
18
+
19
+ inc = "#{$vendor_path}/include"
20
+ lib = "#{$vendor_path}/lib"
21
+
22
+ # make rpath relative
23
+ # use double dollar sign and single quotes to escape properly
24
+ rpath_prefix = RbConfig::CONFIG["host_os"] =~ /darwin/ ? "@loader_path" : "$$ORIGIN"
25
+ rpath = "'#{rpath_prefix}/../../tmp/or-tools/lib'"
26
+ end
14
27
 
15
28
  $INCFLAGS << " -I#{inc}"
16
29
 
17
- $LDFLAGS << " -Wl,-rpath,#{lib}"
30
+ $LDFLAGS << " -Wl,-rpath,#{rpath}"
18
31
  $LDFLAGS << " -L#{lib}"
19
- $LDFLAGS << " -lortools"
32
+ raise "OR-Tools not found" unless have_library("ortools")
20
33
 
21
- %w(
22
- absl_city
23
- absl_time_zone
24
- absl_spinlock_wait
25
- absl_log_severity
26
- absl_failure_signal_handler
27
- absl_bad_optional_access
28
- absl_hash
29
- absl_raw_logging_internal
30
- absl_random_internal_pool_urbg
31
- absl_base
32
- absl_bad_any_cast_impl
33
- absl_periodic_sampler
34
- absl_random_distributions
35
- absl_flags_usage_internal
36
- absl_random_seed_sequences
37
- absl_throw_delegate
38
- absl_flags_handle
39
- absl_dynamic_annotations
40
- absl_debugging_internal
41
- absl_strings
42
- absl_flags
43
- absl_malloc_internal
44
- absl_str_format_internal
45
- absl_flags_usage
46
- absl_strings_internal
47
- absl_flags_program_name
48
- absl_flags_registry
49
- absl_int128
50
- absl_scoped_set_env
51
- absl_raw_hash_set
52
- absl_random_internal_seed_material
53
- absl_symbolize
54
- absl_random_internal_randen_slow
55
- absl_graphcycles_internal
56
- absl_exponential_biased
57
- absl_random_internal_randen_hwaes_impl
58
- absl_bad_variant_access
59
- absl_stacktrace
60
- absl_random_internal_randen_hwaes
61
- absl_flags_parse
62
- absl_random_internal_randen
63
- absl_random_internal_distribution_test_util
64
- absl_time
65
- absl_flags_config
66
- absl_synchronization
67
- absl_hashtablez_sampler
68
- absl_demangle_internal
69
- absl_leak_check
70
- absl_flags_marshalling
71
- absl_leak_check_disable
72
- absl_examine_stack
73
- absl_flags_internal
74
- absl_random_seed_gen_exception
75
- absl_civil_time
76
- ).each do |lib|
77
- $LDFLAGS << " -l#{lib}"
34
+ Dir["#{lib}/libabsl_*.a"].each do |lib|
35
+ $LDFLAGS << " #{lib}"
78
36
  end
79
37
 
80
38
  create_makefile("or_tools/ext")
@@ -0,0 +1,98 @@
1
+ require "digest"
2
+ require "fileutils"
3
+ require "net/http"
4
+ require "tmpdir"
5
+
6
+ version = "7.8.7959"
7
+
8
+ if RbConfig::CONFIG["host_os"] =~ /darwin/i
9
+ filename = "or-tools_MacOsX-10.15.6_v#{version}.tar.gz"
10
+ checksum = "8d1d92105e962fab2fd7bde5843b711334a9f246dc22c51d41281898dbc1f69d"
11
+ else
12
+ os = %x[lsb_release -is].chomp rescue nil
13
+ os_version = %x[lsb_release -rs].chomp rescue nil
14
+ if os == "Ubuntu" && os_version == "20.04"
15
+ filename = "or-tools_ubuntu-20.04_v#{version}.tar.gz"
16
+ checksum = "40018cd573305cec76e12ff87e84d4ec18ce0823f265e4d75625bf3aefdea7c9"
17
+ elsif os == "Ubuntu" && os_version == "18.04"
18
+ filename = "or-tools_ubuntu-18.04_v#{version}.tar.gz"
19
+ checksum = "6e53e1a7b82b65b928617b9dce663778c5200de9366ad948948597dc763b1943"
20
+ elsif os == "Ubuntu" && os_version == "16.04"
21
+ filename = "or-tools_ubuntu-16.04_v#{version}.tar.gz"
22
+ checksum = "182cd4e2a1d2f29a9a81f2f90cb3e05cf1c0731c1a1e94c471d79a74dc09fff0"
23
+ elsif os == "Debian" && os_version == "10"
24
+ filename = "or-tools_debian-10_v#{version}.tar.gz "
25
+ checksum = "87dd294237095a7fea466c47789802b0a66c664fa1f79e6bbb2cd36323b409ca"
26
+ elsif os == "CentOS" && os_version == "8"
27
+ filename = "or-tools_centos-8_v#{version}.tar.gz"
28
+ checksum = "4e61bf2994fc767da1b4fd073f554c944d2c811609df4bc1afa6a2d876c05bb4"
29
+ else
30
+ # there is a binary download for Windows
31
+ # however, it's compiled with Visual Studio rather than MinGW (which RubyInstaller uses)
32
+ raise <<~MSG
33
+ Binary installation not available for this platform.
34
+
35
+ Build the OR-Tools C++ library from source, then run:
36
+ bundle config build.or-tools --with-or-tools-dir=/path/to/or-tools
37
+
38
+ MSG
39
+ end
40
+ end
41
+
42
+ short_version = version.split(".").first(2).join(".")
43
+ url = "https://github.com/google/or-tools/releases/download/v#{short_version}/#{filename}"
44
+
45
+ $stdout.sync = true
46
+
47
+ def download_file(url, download_path)
48
+ uri = URI(url)
49
+ location = nil
50
+
51
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
52
+ request = Net::HTTP::Get.new(uri)
53
+ http.request(request) do |response|
54
+ case response
55
+ when Net::HTTPRedirection
56
+ location = response["location"]
57
+ when Net::HTTPSuccess
58
+ i = 0
59
+ File.open(download_path, "wb") do |f|
60
+ response.read_body do |chunk|
61
+ f.write(chunk)
62
+
63
+ # print progress
64
+ putc "." if i % 50 == 0
65
+ i += 1
66
+ end
67
+ end
68
+ puts # newline
69
+ else
70
+ raise "Bad response"
71
+ end
72
+ end
73
+ end
74
+
75
+ # outside of Net::HTTP block to close previous connection
76
+ download_file(location, download_path) if location
77
+ end
78
+
79
+ # download
80
+ download_path = "#{Dir.tmpdir}/#{filename}"
81
+ unless File.exist?(download_path)
82
+ puts "Downloading #{url}..."
83
+ download_file(url, download_path)
84
+ end
85
+
86
+ # check integrity - do this regardless of if just downloaded
87
+ download_checksum = Digest::SHA256.file(download_path).hexdigest
88
+ raise "Bad checksum: #{download_checksum}" if download_checksum != checksum
89
+
90
+ # extract - can't use Gem::Package#extract_tar_gz from RubyGems
91
+ # since it limits filenames to 100 characters (doesn't support UStar format)
92
+ path = File.expand_path("../../tmp/or-tools", __dir__)
93
+ FileUtils.mkdir_p(path)
94
+ tar_args = Gem.win_platform? ? ["--force-local"] : []
95
+ system "tar", "zxf", download_path, "-C", path, "--strip-components=1", *tar_args
96
+
97
+ # export
98
+ $vendor_path = path
@@ -17,6 +17,12 @@ require "or_tools/sat_int_var"
17
17
  require "or_tools/solver"
18
18
  require "or_tools/version"
19
19
 
20
+ # higher level interfaces
21
+ require "or_tools/basic_scheduler"
22
+ require "or_tools/seating"
23
+ require "or_tools/sudoku"
24
+ require "or_tools/tsp"
25
+
20
26
  module ORTools
21
27
  class Error < StandardError; end
22
28
  end
@@ -0,0 +1,86 @@
1
+ module ORTools
2
+ class BasicScheduler
3
+ attr_reader :assignments, :assigned_hours
4
+
5
+ # for time blocks (shifts and availability)
6
+ # could also use time range (starts_at..ends_at) or starts_at + duration
7
+ # keep current format for now for flexibility
8
+ def initialize(people:, shifts:)
9
+ @shifts = shifts
10
+
11
+ model = ORTools::CpModel.new
12
+
13
+ # create variables
14
+ # a person must be available for the entire shift to be considered for it
15
+ vars = []
16
+ shifts.each_with_index do |shift, i|
17
+ people.each_with_index do |person, j|
18
+ if person[:availability].any? { |a| a[:starts_at] <= shift[:starts_at] && a[:ends_at] >= shift[:ends_at] }
19
+ vars << {shift: i, person: j, var: model.new_bool_var("{shift: #{i}, person: #{j}}")}
20
+ end
21
+ end
22
+ end
23
+
24
+ vars_by_shift = vars.group_by { |v| v[:shift] }
25
+ vars_by_person = vars.group_by { |v| v[:person] }
26
+
27
+ # one person per shift
28
+ vars_by_shift.each do |j, vs|
29
+ model.add(model.sum(vs.map { |v| v[:var] }) <= 1)
30
+ end
31
+
32
+ # one shift per day per person
33
+ # in future, may also want to add option to ensure assigned shifts are N hours apart
34
+ vars_by_person.each do |j, vs|
35
+ vs.group_by { |v| shift_dates[v[:shift]] }.each do |_, vs2|
36
+ model.add(model.sum(vs2.map { |v| v[:var] }) <= 1)
37
+ end
38
+ end
39
+
40
+ # max hours per person
41
+ # use seconds since model needs integers
42
+ vars_by_person.each do |j, vs|
43
+ max_hours = people[j][:max_hours]
44
+ if max_hours
45
+ model.add(model.sum(vs.map { |v| v[:var] * shift_duration[v[:shift]] }) <= max_hours * 3600)
46
+ end
47
+ end
48
+
49
+ # maximize hours assigned
50
+ # could also include distance from max hours
51
+ model.maximize(model.sum(vars.map { |v| v[:var] * shift_duration[v[:shift]] }))
52
+
53
+ # solve
54
+ solver = ORTools::CpSolver.new
55
+ status = solver.solve(model)
56
+ raise Error, "No solution found" unless [:feasible, :optimal].include?(status)
57
+
58
+ # read solution
59
+ @assignments = []
60
+ vars.each do |v|
61
+ if solver.value(v[:var])
62
+ @assignments << {
63
+ person: v[:person],
64
+ shift: v[:shift]
65
+ }
66
+ end
67
+ end
68
+ # can calculate manually if objective changes
69
+ @assigned_hours = solver.objective_value / 3600.0
70
+ end
71
+
72
+ def total_hours
73
+ @total_hours ||= shift_duration.sum / 3600.0
74
+ end
75
+
76
+ private
77
+
78
+ def shift_duration
79
+ @shift_duration ||= @shifts.map { |s| (s[:ends_at] - s[:starts_at]).round }
80
+ end
81
+
82
+ def shift_dates
83
+ @shift_dates ||= @shifts.map { |s| s[:starts_at].to_date }
84
+ end
85
+ end
86
+ end
@@ -12,7 +12,11 @@ module ORTools
12
12
  end
13
13
 
14
14
  def value(var)
15
- _solution_integer_value(@response, var)
15
+ if var.is_a?(BoolVar)
16
+ _solution_boolean_value(@response, var)
17
+ else
18
+ _solution_integer_value(@response, var)
19
+ end
16
20
  end
17
21
 
18
22
  def solve_with_solution_callback(model, observer)
@@ -0,0 +1,115 @@
1
+ module ORTools
2
+ class Seating
3
+ attr_reader :assignments, :people, :total_weight
4
+
5
+ def initialize(connections:, tables:, min_connections: 1)
6
+ @people = connections.flat_map { |c| c[:people] }.uniq
7
+
8
+ @connections_for = {}
9
+ @people.each do |person|
10
+ @connections_for[person] = {}
11
+ end
12
+ connections.each do |c|
13
+ c[:people].each_with_index do |person, i|
14
+ others = c[:people].dup
15
+ others.delete_at(i)
16
+ others.each do |other|
17
+ @connections_for[person][other] ||= 0
18
+ @connections_for[person][other] += c[:weight]
19
+ end
20
+ end
21
+ end
22
+
23
+ model = ORTools::CpModel.new
24
+ all_tables = tables.size.times.to_a
25
+
26
+ # decision variables
27
+ seats = {}
28
+ all_tables.each do |t|
29
+ people.each do |g|
30
+ seats[[t, g]] = model.new_bool_var("guest %s seats on table %i" % [g, t])
31
+ end
32
+ end
33
+
34
+ pairs = people.combination(2)
35
+
36
+ colocated = {}
37
+ pairs.each do |g1, g2|
38
+ colocated[[g1, g2]] = model.new_bool_var("guest %s seats with guest %s" % [g1, g2])
39
+ end
40
+
41
+ same_table = {}
42
+ pairs.each do |g1, g2|
43
+ all_tables.each do |t|
44
+ same_table[[g1, g2, t]] = model.new_bool_var("guest %s seats with guest %s on table %i" % [g1, g2, t])
45
+ end
46
+ end
47
+
48
+ # objective
49
+ objective = []
50
+ pairs.each do |g1, g2|
51
+ weight = @connections_for[g1][g2]
52
+ objective << colocated[[g1, g2]] * weight if weight
53
+ end
54
+ model.maximize(model.sum(objective))
55
+
56
+ # everybody seats at one table
57
+ people.each do |g|
58
+ model.add(model.sum(all_tables.map { |t| seats[[t, g]] }) == 1)
59
+ end
60
+
61
+ # tables have a max capacity
62
+ all_tables.each do |t|
63
+ model.add(model.sum(@people.map { |g| seats[[t, g]] }) <= tables[t])
64
+ end
65
+
66
+ # link colocated with seats
67
+ pairs.each do |g1, g2|
68
+ all_tables.each do |t|
69
+ # link same_table and seats
70
+ model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
71
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
72
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
73
+ end
74
+
75
+ # link colocated and same_table
76
+ model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
77
+ end
78
+
79
+ # min known neighbors rule
80
+ same_table_by_person = Hash.new { |hash, key| hash[key] = [] }
81
+ same_table.each do |(g1, g2, t), v|
82
+ next unless @connections_for[g1][g2]
83
+ same_table_by_person[g1] << v
84
+ same_table_by_person[g2] << v
85
+ end
86
+ same_table_by_person.each do |_, vars|
87
+ model.add(model.sum(vars) >= min_connections)
88
+ end
89
+
90
+ # solve
91
+ solver = ORTools::CpSolver.new
92
+ status = solver.solve(model)
93
+ raise Error, "No solution found" unless [:feasible, :optimal].include?(status)
94
+
95
+ # read solution
96
+ @assignments = {}
97
+ seats.each do |k, v|
98
+ if solver.value(v)
99
+ @assignments[k[1]] = k[0]
100
+ end
101
+ end
102
+ @total_weight = solver.objective_value
103
+ end
104
+
105
+ def assigned_tables
106
+ assignments.group_by { |_, v| v }.map { |k, v| [k, v.map(&:first)] }.sort_by(&:first).map(&:last)
107
+ end
108
+
109
+ def connections_for(person, same_table: false)
110
+ result = @connections_for[person]
111
+ result = result.select { |k, _| @assignments[k] == @assignments[person] } if same_table
112
+ result
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,132 @@
1
+ module ORTools
2
+ class Sudoku
3
+ attr_reader :solution
4
+
5
+ def initialize(initial_grid, x: false, magic_square: false, anti_knight: false, anti_king: false, non_consecutive: false)
6
+ raise ArgumentError, "Grid must be 9x9" unless initial_grid.size == 9 && initial_grid.all? { |r| r.size == 9 }
7
+ raise ArgumentError, "Grid must contain values between 0 and 9" unless initial_grid.flatten(1).all? { |v| (0..9).include?(v) }
8
+
9
+ model = ORTools::CpModel.new
10
+
11
+ cell_size = 3
12
+ line_size = cell_size**2
13
+ line = (0...line_size).to_a
14
+ cell = (0...cell_size).to_a
15
+
16
+ grid = {}
17
+ line.each do |i|
18
+ line.each do |j|
19
+ grid[[i, j]] = model.new_int_var(1, line_size, "grid %i %i" % [i, j])
20
+ end
21
+ end
22
+
23
+ line.each do |i|
24
+ model.add_all_different(line.map { |j| grid[[i, j]] })
25
+ end
26
+
27
+ line.each do |j|
28
+ model.add_all_different(line.map { |i| grid[[i, j]] })
29
+ end
30
+
31
+ cell.each do |i|
32
+ cell.each do |j|
33
+ one_cell = []
34
+ cell.each do |di|
35
+ cell.each do |dj|
36
+ one_cell << grid[[i * cell_size + di, j * cell_size + dj]]
37
+ end
38
+ end
39
+ model.add_all_different(one_cell)
40
+ end
41
+ end
42
+
43
+ line.each do |i|
44
+ line.each do |j|
45
+ if initial_grid[i][j] != 0
46
+ model.add(grid[[i, j]] == initial_grid[i][j])
47
+ end
48
+ end
49
+ end
50
+
51
+ if x
52
+ model.add_all_different(9.times.map { |i| grid[[i, i]] })
53
+ model.add_all_different(9.times.map { |i| grid[[i, 8 - i]] })
54
+ end
55
+
56
+ if magic_square
57
+ magic_sums = []
58
+ 3.times do |i|
59
+ magic_sums << model.sum(3.times.map { |j| grid[[3 + i, 3 + j]] })
60
+ magic_sums << model.sum(3.times.map { |j| grid[[3 + j, 3 + i]] })
61
+ end
62
+
63
+ magic_sums << model.sum(3.times.map { |i| grid[[3 + i, 3 + i]] })
64
+ magic_sums << model.sum(3.times.map { |i| grid[[3 + i, 5 - i]] })
65
+
66
+ first_sum = magic_sums.shift
67
+ magic_sums.each do |magic_sum|
68
+ model.add(magic_sum == first_sum)
69
+ end
70
+ end
71
+
72
+ if anti_knight
73
+ # add anti-knights rule
74
+ # for each square, add squares that cannot be feasible
75
+ moves = [[1, 2], [2, 1], [2, -1], [1, -2], [-1, -2], [-2, -1], [-2, 1], [-1, 2]]
76
+ 9.times do |i|
77
+ 9.times do |j|
78
+ moves.each do |mi, mj|
79
+ square = grid[[i + mi, j + mj]]
80
+ if square
81
+ model.add(grid[[i, j]] != square)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ if anti_king
89
+ # add anti-king rule
90
+ # for each square, add squares that cannot be feasible
91
+ moves = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]
92
+ 9.times do |i|
93
+ 9.times do |j|
94
+ moves.each do |mi, mj|
95
+ square = grid[[i + mi, j + mj]]
96
+ if square
97
+ model.add(grid[[i, j]] != square)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ if non_consecutive
105
+ # add non-consecutive rule
106
+ # for each square, add squares that cannot be feasible
107
+ moves = [[1, 0], [0, 1], [-1, 0], [0, -1]]
108
+ 9.times do |i|
109
+ 9.times do |j|
110
+ moves.each do |mi, mj|
111
+ square = grid[[i + mi, j + mj]]
112
+ if square
113
+ model.add(grid[[i, j]] + 1 != square)
114
+ model.add(grid[[i, j]] - 1 != square)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ solver = ORTools::CpSolver.new
122
+ status = solver.solve(model)
123
+ raise Error, "No solution found" unless [:feasible, :optimal].include?(status)
124
+
125
+ solution = []
126
+ line.each do |i|
127
+ solution << line.map { |j| solver.value(grid[[i, j]]) }
128
+ end
129
+ @solution = solution
130
+ end
131
+ end
132
+ end