or-tools 0.1.5 → 0.3.3

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.
@@ -101,6 +101,14 @@ Object to_ruby<RoutingNodeIndex>(RoutingNodeIndex const &x)
101
101
  return to_ruby<int>(x.value());
102
102
  }
103
103
 
104
+ std::vector<RoutingNodeIndex> nodeIndexVector(Array x) {
105
+ std::vector<RoutingNodeIndex> res;
106
+ for (auto const& v : x) {
107
+ res.push_back(from_ruby<RoutingNodeIndex>(v));
108
+ }
109
+ return res;
110
+ }
111
+
104
112
  template<>
105
113
  inline
106
114
  operations_research::sat::LinearExpr from_ruby<operations_research::sat::LinearExpr>(Object x)
@@ -661,6 +669,11 @@ void Init_ext()
661
669
  "_solution_integer_value",
662
670
  *[](Object self, CpSolverResponse& response, operations_research::sat::IntVar& x) {
663
671
  return SolutionIntegerValue(response, x);
672
+ })
673
+ .define_method(
674
+ "_solution_boolean_value",
675
+ *[](Object self, CpSolverResponse& response, operations_research::sat::BoolVar& x) {
676
+ return SolutionBooleanValue(response, x);
664
677
  });
665
678
 
666
679
  define_class_under<CpSolverResponse>(rb_mORTools, "CpSolverResponse")
@@ -694,7 +707,16 @@ void Init_ext()
694
707
  });
695
708
 
696
709
  define_class_under<RoutingIndexManager>(rb_mORTools, "RoutingIndexManager")
697
- .define_constructor(Constructor<RoutingIndexManager, int, int, RoutingNodeIndex>())
710
+ .define_singleton_method(
711
+ "_new_depot",
712
+ *[](int num_nodes, int num_vehicles, RoutingNodeIndex depot) {
713
+ return RoutingIndexManager(num_nodes, num_vehicles, depot);
714
+ })
715
+ .define_singleton_method(
716
+ "_new_starts_ends",
717
+ *[](int num_nodes, int num_vehicles, Array starts, Array ends) {
718
+ return RoutingIndexManager(num_nodes, num_vehicles, nodeIndexVector(starts), nodeIndexVector(ends));
719
+ })
698
720
  .define_method("index_to_node", &RoutingIndexManager::IndexToNode)
699
721
  .define_method("node_to_index", &RoutingIndexManager::NodeToIndex);
700
722
 
@@ -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,15 +8,28 @@ $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
34
  Dir["#{lib}/libabsl_*.a"].each do |lib|
22
35
  $LDFLAGS << " #{lib}"
@@ -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
@@ -11,12 +11,19 @@ require "or_tools/cp_solver_solution_callback"
11
11
  require "or_tools/int_var"
12
12
  require "or_tools/knapsack_solver"
13
13
  require "or_tools/linear_expr"
14
+ require "or_tools/routing_index_manager"
14
15
  require "or_tools/routing_model"
15
16
  require "or_tools/sat_linear_expr"
16
17
  require "or_tools/sat_int_var"
17
18
  require "or_tools/solver"
18
19
  require "or_tools/version"
19
20
 
21
+ # higher level interfaces
22
+ require "or_tools/basic_scheduler"
23
+ require "or_tools/seating"
24
+ require "or_tools/sudoku"
25
+ require "or_tools/tsp"
26
+
20
27
  module ORTools
21
28
  class Error < StandardError; end
22
29
  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,11 @@
1
+ module ORTools
2
+ class RoutingIndexManager
3
+ def self.new(num_nodes, num_vehicles, starts, ends = nil)
4
+ if ends
5
+ _new_starts_ends(num_nodes, num_vehicles, starts, ends)
6
+ else
7
+ _new_depot(num_nodes, num_vehicles, starts)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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