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.
- 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
|