or-tools 0.2.0 → 0.3.4

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)
@@ -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