or-tools 0.1.5 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/NOTICE.txt +1 -0
- data/README.md +430 -195
- data/ext/or-tools/ext.cpp +23 -1
- data/ext/or-tools/extconf.rb +19 -6
- data/ext/or-tools/vendor.rb +98 -0
- data/lib/or-tools.rb +7 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +5 -1
- 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 +8 -3
- data/lib/or_tools/ext.bundle +0 -0
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)
|
@@ -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
|
-
.
|
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
|
|
data/ext/or-tools/extconf.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "mkmf-rice"
|
2
2
|
|
3
|
-
|
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,#{
|
30
|
+
$LDFLAGS << " -Wl,-rpath,#{rpath}"
|
18
31
|
$LDFLAGS << " -L#{lib}"
|
19
|
-
|
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
|
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
@@ -12,7 +12,11 @@ module ORTools
|
|
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)
|
@@ -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
|