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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/NOTICE.txt +2 -1
- data/README.md +463 -190
- data/ext/or-tools/ext.cpp +64 -6
- data/ext/or-tools/vendor.rb +11 -8
- data/lib/or-tools.rb +7 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +12 -4
- data/lib/or_tools/routing_index_manager.rb +11 -0
- data/lib/or_tools/seating.rb +115 -0
- data/lib/or_tools/sudoku.rb +132 -0
- data/lib/or_tools/tsp.rb +60 -0
- data/lib/or_tools/version.rb +1 -1
- metadata +12 -7
data/ext/or-tools/ext.cpp
CHANGED
@@ -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
|
-
|
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
|
-
.
|
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) {
|
data/ext/or-tools/vendor.rb
CHANGED
@@ -3,26 +3,29 @@ require "fileutils"
|
|
3
3
|
require "net/http"
|
4
4
|
require "tmpdir"
|
5
5
|
|
6
|
-
version = "7.
|
6
|
+
version = "7.8.7959"
|
7
7
|
|
8
8
|
if RbConfig::CONFIG["host_os"] =~ /darwin/i
|
9
|
-
filename = "or-tools_MacOsX-10.15.
|
10
|
-
checksum = "
|
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 == "
|
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 = "
|
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 = "
|
22
|
+
checksum = "182cd4e2a1d2f29a9a81f2f90cb3e05cf1c0731c1a1e94c471d79a74dc09fff0"
|
20
23
|
elsif os == "Debian" && os_version == "10"
|
21
24
|
filename = "or-tools_debian-10_v#{version}.tar.gz "
|
22
|
-
checksum = "
|
25
|
+
checksum = "87dd294237095a7fea466c47789802b0a66c664fa1f79e6bbb2cd36323b409ca"
|
23
26
|
elsif os == "CentOS" && os_version == "8"
|
24
27
|
filename = "or-tools_centos-8_v#{version}.tar.gz"
|
25
|
-
checksum = "
|
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)
|
data/lib/or-tools.rb
CHANGED
@@ -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
|
data/lib/or_tools/cp_solver.rb
CHANGED
@@ -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
|
-
|
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,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
|