or-tools 0.2.0 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
@@ -630,18 +638,24 @@ void Init_ext()
630
638
  self.Minimize(expr);
631
639
  });
632
640
 
641
+ define_class_under<SatParameters>(rb_mORTools, "SatParameters")
642
+ .define_constructor(Constructor<SatParameters>())
643
+ .define_method("max_time_in_seconds=",
644
+ *[](SatParameters& self, double value) {
645
+ self.set_max_time_in_seconds(value);
646
+ });
647
+
633
648
  define_class_under(rb_mORTools, "CpSolver")
634
649
  .define_method(
635
650
  "_solve_with_observer",
636
- *[](Object self, CpModelBuilder& model, Object callback, bool all_solutions) {
651
+ *[](Object self, CpModelBuilder& model, SatParameters& parameters, Object callback, bool all_solutions) {
637
652
  operations_research::sat::Model m;
638
653
 
639
654
  if (all_solutions) {
640
655
  // set parameters for SearchForAllSolutions
641
- SatParameters parameters;
642
656
  parameters.set_enumerate_all_solutions(true);
643
- m.Add(NewSatParameters(parameters));
644
657
  }
658
+ m.Add(NewSatParameters(parameters));
645
659
 
646
660
  m.Add(NewFeasibleSolutionObserver(
647
661
  [callback](const CpSolverResponse& r) {
@@ -654,13 +668,20 @@ void Init_ext()
654
668
  })
655
669
  .define_method(
656
670
  "_solve",
657
- *[](Object self, CpModelBuilder& model) {
658
- return Solve(model.Build());
671
+ *[](Object self, CpModelBuilder& model, SatParameters& parameters) {
672
+ operations_research::sat::Model m;
673
+ m.Add(NewSatParameters(parameters));
674
+ return SolveCpModel(model.Build(), &m);
659
675
  })
660
676
  .define_method(
661
677
  "_solution_integer_value",
662
678
  *[](Object self, CpSolverResponse& response, operations_research::sat::IntVar& x) {
663
679
  return SolutionIntegerValue(response, x);
680
+ })
681
+ .define_method(
682
+ "_solution_boolean_value",
683
+ *[](Object self, CpSolverResponse& response, operations_research::sat::BoolVar& x) {
684
+ return SolutionBooleanValue(response, x);
664
685
  });
665
686
 
666
687
  define_class_under<CpSolverResponse>(rb_mORTools, "CpSolverResponse")
@@ -688,13 +709,24 @@ void Init_ext()
688
709
  return Symbol("infeasible");
689
710
  } else if (status == CpSolverStatus::MODEL_INVALID) {
690
711
  return Symbol("model_invalid");
712
+ } else if (status == CpSolverStatus::UNKNOWN) {
713
+ return Symbol("unknown");
691
714
  } else {
692
715
  throw std::runtime_error("Unknown solver status");
693
716
  }
694
717
  });
695
718
 
696
719
  define_class_under<RoutingIndexManager>(rb_mORTools, "RoutingIndexManager")
697
- .define_constructor(Constructor<RoutingIndexManager, int, int, RoutingNodeIndex>())
720
+ .define_singleton_method(
721
+ "_new_depot",
722
+ *[](int num_nodes, int num_vehicles, RoutingNodeIndex depot) {
723
+ return RoutingIndexManager(num_nodes, num_vehicles, depot);
724
+ })
725
+ .define_singleton_method(
726
+ "_new_starts_ends",
727
+ *[](int num_nodes, int num_vehicles, Array starts, Array ends) {
728
+ return RoutingIndexManager(num_nodes, num_vehicles, nodeIndexVector(starts), nodeIndexVector(ends));
729
+ })
698
730
  .define_method("index_to_node", &RoutingIndexManager::IndexToNode)
699
731
  .define_method("node_to_index", &RoutingIndexManager::NodeToIndex);
700
732
 
@@ -784,6 +816,23 @@ void Init_ext()
784
816
  })
785
817
  .define_method("depot", &RoutingModel::GetDepot)
786
818
  .define_method("size", &RoutingModel::Size)
819
+ .define_method("status", *[](RoutingModel& self) {
820
+ auto status = self.status();
821
+
822
+ if (status == RoutingModel::ROUTING_NOT_SOLVED) {
823
+ return Symbol("not_solved");
824
+ } else if (status == RoutingModel::ROUTING_SUCCESS) {
825
+ return Symbol("success");
826
+ } else if (status == RoutingModel::ROUTING_FAIL) {
827
+ return Symbol("fail");
828
+ } else if (status == RoutingModel::ROUTING_FAIL_TIMEOUT) {
829
+ return Symbol("fail_timeout");
830
+ } else if (status == RoutingModel::ROUTING_INVALID) {
831
+ return Symbol("invalid");
832
+ } else {
833
+ throw std::runtime_error("Unknown solver status");
834
+ }
835
+ })
787
836
  .define_method("vehicle_var", &RoutingModel::VehicleVar)
788
837
  .define_method("set_arc_cost_evaluator_of_all_vehicles", &RoutingModel::SetArcCostEvaluatorOfAllVehicles)
789
838
  .define_method("set_arc_cost_evaluator_of_vehicle", &RoutingModel::SetArcCostEvaluatorOfVehicle)
@@ -800,6 +849,15 @@ void Init_ext()
800
849
  }
801
850
  self.AddDimensionWithVehicleCapacity(evaluator_index, slack_max, vehicle_capacities, fix_start_cumul_to_zero, name);
802
851
  })
852
+ .define_method(
853
+ "add_dimension_with_vehicle_transits",
854
+ *[](RoutingModel& self, Array rb_indices, int64 slack_max, int64 capacity, bool fix_start_cumul_to_zero, const std::string& name) {
855
+ std::vector<int> evaluator_indices;
856
+ for (std::size_t i = 0; i < rb_indices.size(); ++i) {
857
+ evaluator_indices.push_back(from_ruby<int>(rb_indices[i]));
858
+ }
859
+ self.AddDimensionWithVehicleTransits(evaluator_indices, slack_max, capacity, fix_start_cumul_to_zero, name);
860
+ })
803
861
  .define_method(
804
862
  "add_disjunction",
805
863
  *[](RoutingModel& self, Array rb_indices, int64 penalty) {
@@ -3,26 +3,29 @@ require "fileutils"
3
3
  require "net/http"
4
4
  require "tmpdir"
5
5
 
6
- version = "7.6.7691"
6
+ version = "7.8.7959"
7
7
 
8
8
  if RbConfig::CONFIG["host_os"] =~ /darwin/i
9
- filename = "or-tools_MacOsX-10.15.4_v#{version}.tar.gz"
10
- checksum = "39e26ba27b4d3a1c194c1478e864cd016d62cf516cd9227a9f23e6143e131572"
9
+ filename = "or-tools_MacOsX-10.15.6_v#{version}.tar.gz"
10
+ checksum = "8d1d92105e962fab2fd7bde5843b711334a9f246dc22c51d41281898dbc1f69d"
11
11
  else
12
12
  os = %x[lsb_release -is].chomp rescue nil
13
13
  os_version = %x[lsb_release -rs].chomp rescue nil
14
- if os == "Ubuntu" && os_version == "18.04"
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"
15
18
  filename = "or-tools_ubuntu-18.04_v#{version}.tar.gz"
16
- checksum = "79ef61dfc63b98133ed637f02e837f714a95987424332e511a3a87edd5ce17dc"
19
+ checksum = "6e53e1a7b82b65b928617b9dce663778c5200de9366ad948948597dc763b1943"
17
20
  elsif os == "Ubuntu" && os_version == "16.04"
18
21
  filename = "or-tools_ubuntu-16.04_v#{version}.tar.gz"
19
- checksum = "a25fc94c0f0d16abf1f6da2a054040c21ef3cbf618a831a15afe21bf14f2d1fb"
22
+ checksum = "182cd4e2a1d2f29a9a81f2f90cb3e05cf1c0731c1a1e94c471d79a74dc09fff0"
20
23
  elsif os == "Debian" && os_version == "10"
21
24
  filename = "or-tools_debian-10_v#{version}.tar.gz "
22
- checksum = "158c44038aebc42b42b98e8f3733ba83bf230e8a0379803cc48aafbb2f7bdf5a"
25
+ checksum = "87dd294237095a7fea466c47789802b0a66c664fa1f79e6bbb2cd36323b409ca"
23
26
  elsif os == "CentOS" && os_version == "8"
24
27
  filename = "or-tools_centos-8_v#{version}.tar.gz"
25
- checksum = "a2b800d4e498561e5b1fe95ee1e64c867be496038883f4f7b199499bf71a0eed"
28
+ checksum = "4e61bf2994fc767da1b4fd073f554c944d2c811609df4bc1afa6a2d876c05bb4"
26
29
  else
27
30
  # there is a binary download for Windows
28
31
  # however, it's compiled with Visual Studio rather than MinGW (which RubyInstaller uses)
@@ -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
@@ -7,22 +7,30 @@ module ORTools
7
7
  def_delegators :@response, :objective_value, :num_conflicts, :num_branches, :wall_time
8
8
 
9
9
  def solve(model)
10
- @response = _solve(model)
10
+ @response = _solve(model, parameters)
11
11
  @response.status
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)
19
- @response = _solve_with_observer(model, observer, false)
23
+ @response = _solve_with_observer(model, parameters, observer, false)
20
24
  @response.status
21
25
  end
22
26
 
23
27
  def search_for_all_solutions(model, observer)
24
- @response = _solve_with_observer(model, observer, true)
28
+ @response = _solve_with_observer(model, parameters, observer, true)
25
29
  @response.status
26
30
  end
31
+
32
+ def parameters
33
+ @parameters ||= SatParameters.new
34
+ end
27
35
  end
28
36
  end
@@ -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