or-tools 0.1.4 → 0.3.2
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 +25 -0
- data/README.md +430 -195
- data/ext/or-tools/ext.cpp +5 -0
- data/ext/or-tools/extconf.rb +21 -63
- data/ext/or-tools/vendor.rb +98 -0
- data/lib/or-tools.rb +6 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +5 -1
- 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 +7 -3
- data/lib/or_tools/ext.bundle +0 -0
data/ext/or-tools/ext.cpp
CHANGED
@@ -661,6 +661,11 @@ void Init_ext()
|
|
661
661
|
"_solution_integer_value",
|
662
662
|
*[](Object self, CpSolverResponse& response, operations_research::sat::IntVar& x) {
|
663
663
|
return SolutionIntegerValue(response, x);
|
664
|
+
})
|
665
|
+
.define_method(
|
666
|
+
"_solution_boolean_value",
|
667
|
+
*[](Object self, CpSolverResponse& response, operations_research::sat::BoolVar& x) {
|
668
|
+
return SolutionBooleanValue(response, x);
|
664
669
|
});
|
665
670
|
|
666
671
|
define_class_under<CpSolverResponse>(rb_mORTools, "CpSolverResponse")
|
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,73 +8,31 @@ $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
|
-
|
22
|
-
|
23
|
-
absl_time_zone
|
24
|
-
absl_spinlock_wait
|
25
|
-
absl_log_severity
|
26
|
-
absl_failure_signal_handler
|
27
|
-
absl_bad_optional_access
|
28
|
-
absl_hash
|
29
|
-
absl_raw_logging_internal
|
30
|
-
absl_random_internal_pool_urbg
|
31
|
-
absl_base
|
32
|
-
absl_bad_any_cast_impl
|
33
|
-
absl_periodic_sampler
|
34
|
-
absl_random_distributions
|
35
|
-
absl_flags_usage_internal
|
36
|
-
absl_random_seed_sequences
|
37
|
-
absl_throw_delegate
|
38
|
-
absl_flags_handle
|
39
|
-
absl_dynamic_annotations
|
40
|
-
absl_debugging_internal
|
41
|
-
absl_strings
|
42
|
-
absl_flags
|
43
|
-
absl_malloc_internal
|
44
|
-
absl_str_format_internal
|
45
|
-
absl_flags_usage
|
46
|
-
absl_strings_internal
|
47
|
-
absl_flags_program_name
|
48
|
-
absl_flags_registry
|
49
|
-
absl_int128
|
50
|
-
absl_scoped_set_env
|
51
|
-
absl_raw_hash_set
|
52
|
-
absl_random_internal_seed_material
|
53
|
-
absl_symbolize
|
54
|
-
absl_random_internal_randen_slow
|
55
|
-
absl_graphcycles_internal
|
56
|
-
absl_exponential_biased
|
57
|
-
absl_random_internal_randen_hwaes_impl
|
58
|
-
absl_bad_variant_access
|
59
|
-
absl_stacktrace
|
60
|
-
absl_random_internal_randen_hwaes
|
61
|
-
absl_flags_parse
|
62
|
-
absl_random_internal_randen
|
63
|
-
absl_random_internal_distribution_test_util
|
64
|
-
absl_time
|
65
|
-
absl_flags_config
|
66
|
-
absl_synchronization
|
67
|
-
absl_hashtablez_sampler
|
68
|
-
absl_demangle_internal
|
69
|
-
absl_leak_check
|
70
|
-
absl_flags_marshalling
|
71
|
-
absl_leak_check_disable
|
72
|
-
absl_examine_stack
|
73
|
-
absl_flags_internal
|
74
|
-
absl_random_seed_gen_exception
|
75
|
-
absl_civil_time
|
76
|
-
).each do |lib|
|
77
|
-
$LDFLAGS << " -l#{lib}"
|
34
|
+
Dir["#{lib}/libabsl_*.a"].each do |lib|
|
35
|
+
$LDFLAGS << " #{lib}"
|
78
36
|
end
|
79
37
|
|
80
38
|
create_makefile("or_tools/ext")
|
@@ -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
@@ -17,6 +17,12 @@ require "or_tools/sat_int_var"
|
|
17
17
|
require "or_tools/solver"
|
18
18
|
require "or_tools/version"
|
19
19
|
|
20
|
+
# higher level interfaces
|
21
|
+
require "or_tools/basic_scheduler"
|
22
|
+
require "or_tools/seating"
|
23
|
+
require "or_tools/sudoku"
|
24
|
+
require "or_tools/tsp"
|
25
|
+
|
20
26
|
module ORTools
|
21
27
|
class Error < StandardError; end
|
22
28
|
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
|