or-tools 0.1.3 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +732 -439
- data/ext/or-tools/ext.cpp +114 -11
- data/ext/or-tools/extconf.rb +21 -63
- data/ext/or-tools/vendor.rb +95 -0
- data/lib/or-tools.rb +7 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/bool_var.rb +9 -0
- data/lib/or_tools/cp_solver.rb +11 -2
- data/lib/or_tools/cp_solver_solution_callback.rb +4 -0
- data/lib/or_tools/linear_expr.rb +8 -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
@@ -117,7 +117,7 @@ operations_research::sat::LinearExpr from_ruby |
|
117
117
|
Object o = cvar[0];
|
118
118
|
std::string type = ((String) o.call("class").call("name")).str();
|
119
119
|
if (type == "ORTools::BoolVar") {
|
120
|
-
expr.
|
120
|
+
expr.AddTerm(from_ruby<operations_research::sat::BoolVar>(cvar[0]), from_ruby<int64>(cvar[1]));
|
121
121
|
} else if (type == "Integer") {
|
122
122
|
expr.AddConstant(from_ruby<int64>(cvar[0]));
|
123
123
|
} else {
|
@@ -125,7 +125,12 @@ operations_research::sat::LinearExpr from_ruby |
|
125
125
|
}
|
126
126
|
}
|
127
127
|
} else {
|
128
|
-
|
128
|
+
std::string type = ((String) x.call("class").call("name")).str();
|
129
|
+
if (type == "ORTools::BoolVar") {
|
130
|
+
expr = from_ruby<operations_research::sat::BoolVar>(x);
|
131
|
+
} else {
|
132
|
+
expr = from_ruby<operations_research::sat::IntVar>(x);
|
133
|
+
}
|
129
134
|
}
|
130
135
|
|
131
136
|
return expr;
|
@@ -239,6 +244,50 @@ IntVarSpan from_ruby
|
|
239
244
|
return IntVarSpan(x);
|
240
245
|
}
|
241
246
|
|
247
|
+
// need a wrapper class since absl::Span doesn't own
|
248
|
+
class IntervalVarSpan {
|
249
|
+
std::vector<operations_research::sat::IntervalVar> vec;
|
250
|
+
public:
|
251
|
+
IntervalVarSpan(Object x) {
|
252
|
+
Array a = Array(x);
|
253
|
+
for (std::size_t i = 0; i < a.size(); ++i) {
|
254
|
+
vec.push_back(from_ruby<operations_research::sat::IntervalVar>(a[i]));
|
255
|
+
}
|
256
|
+
}
|
257
|
+
operator absl::Span<const operations_research::sat::IntervalVar>() {
|
258
|
+
return absl::Span<const operations_research::sat::IntervalVar>(vec);
|
259
|
+
}
|
260
|
+
};
|
261
|
+
|
262
|
+
template<>
|
263
|
+
inline
|
264
|
+
IntervalVarSpan from_ruby<IntervalVarSpan>(Object x)
|
265
|
+
{
|
266
|
+
return IntervalVarSpan(x);
|
267
|
+
}
|
268
|
+
|
269
|
+
// need a wrapper class since absl::Span doesn't own
|
270
|
+
class BoolVarSpan {
|
271
|
+
std::vector<operations_research::sat::BoolVar> vec;
|
272
|
+
public:
|
273
|
+
BoolVarSpan(Object x) {
|
274
|
+
Array a = Array(x);
|
275
|
+
for (std::size_t i = 0; i < a.size(); ++i) {
|
276
|
+
vec.push_back(from_ruby<operations_research::sat::BoolVar>(a[i]));
|
277
|
+
}
|
278
|
+
}
|
279
|
+
operator absl::Span<const operations_research::sat::BoolVar>() {
|
280
|
+
return absl::Span<const operations_research::sat::BoolVar>(vec);
|
281
|
+
}
|
282
|
+
};
|
283
|
+
|
284
|
+
template<>
|
285
|
+
inline
|
286
|
+
BoolVarSpan from_ruby<BoolVarSpan>(Object x)
|
287
|
+
{
|
288
|
+
return BoolVarSpan(x);
|
289
|
+
}
|
290
|
+
|
242
291
|
extern "C"
|
243
292
|
void Init_ext()
|
244
293
|
{
|
@@ -338,10 +387,15 @@ void Init_ext()
|
|
338
387
|
.define_method("solution_value", &MPVariable::solution_value)
|
339
388
|
.define_method(
|
340
389
|
"+",
|
341
|
-
*[](MPVariable& self,
|
390
|
+
*[](MPVariable& self, LinearExpr& other) {
|
342
391
|
LinearExpr s(&self);
|
343
|
-
|
344
|
-
|
392
|
+
return s + other;
|
393
|
+
})
|
394
|
+
.define_method(
|
395
|
+
"-",
|
396
|
+
*[](MPVariable& self, LinearExpr& other) {
|
397
|
+
LinearExpr s(&self);
|
398
|
+
return s - other;
|
345
399
|
})
|
346
400
|
.define_method(
|
347
401
|
"*",
|
@@ -368,6 +422,17 @@ void Init_ext()
|
|
368
422
|
LinearExpr o(&other);
|
369
423
|
return self + o;
|
370
424
|
})
|
425
|
+
.define_method(
|
426
|
+
"_gte_double",
|
427
|
+
*[](LinearExpr& self, double other) {
|
428
|
+
LinearExpr o(other);
|
429
|
+
return self >= o;
|
430
|
+
})
|
431
|
+
.define_method(
|
432
|
+
"_gte_linear_expr",
|
433
|
+
*[](LinearExpr& self, LinearExpr& other) {
|
434
|
+
return self >= other;
|
435
|
+
})
|
371
436
|
.define_method(
|
372
437
|
"_lte_double",
|
373
438
|
*[](LinearExpr& self, double other) {
|
@@ -422,6 +487,11 @@ void Init_ext()
|
|
422
487
|
.define_method("iterations", &MPSolver::iterations)
|
423
488
|
.define_method("nodes", &MPSolver::nodes)
|
424
489
|
.define_method("objective", &MPSolver::MutableObjective)
|
490
|
+
.define_method(
|
491
|
+
"maximize",
|
492
|
+
*[](MPSolver& self, LinearExpr& expr) {
|
493
|
+
return self.MutableObjective()->MaximizeLinearExpr(expr);
|
494
|
+
})
|
425
495
|
.define_method(
|
426
496
|
"minimize",
|
427
497
|
*[](MPSolver& self, LinearExpr& expr) {
|
@@ -461,13 +531,18 @@ void Init_ext()
|
|
461
531
|
}
|
462
532
|
});
|
463
533
|
|
464
|
-
// not to be confused with operations_research::IntVar
|
465
534
|
define_class_under<operations_research::sat::IntVar>(rb_mORTools, "SatIntVar")
|
466
535
|
.define_method("name", &operations_research::sat::IntVar::Name);
|
467
536
|
|
537
|
+
define_class_under<operations_research::sat::IntervalVar>(rb_mORTools, "SatIntervalVar")
|
538
|
+
.define_method("name", &operations_research::sat::IntervalVar::Name);
|
539
|
+
|
540
|
+
define_class_under<operations_research::sat::Constraint>(rb_mORTools, "SatConstraint");
|
541
|
+
|
468
542
|
define_class_under<BoolVar>(rb_mORTools, "BoolVar")
|
469
543
|
.define_method("name", &BoolVar::Name)
|
470
544
|
.define_method("index", &BoolVar::index)
|
545
|
+
.define_method("not", &BoolVar::Not)
|
471
546
|
.define_method(
|
472
547
|
"inspect",
|
473
548
|
*[](BoolVar& self) {
|
@@ -488,6 +563,11 @@ void Init_ext()
|
|
488
563
|
*[](CpModelBuilder& self, std::string name) {
|
489
564
|
return self.NewBoolVar().WithName(name);
|
490
565
|
})
|
566
|
+
.define_method(
|
567
|
+
"new_interval_var",
|
568
|
+
*[](CpModelBuilder& self, operations_research::sat::IntVar start, operations_research::sat::IntVar size, operations_research::sat::IntVar end, std::string name) {
|
569
|
+
return self.NewIntervalVar(start, size, end).WithName(name);
|
570
|
+
})
|
491
571
|
.define_method(
|
492
572
|
"add_equality",
|
493
573
|
*[](CpModelBuilder& self, operations_research::sat::LinearExpr x, operations_research::sat::LinearExpr y) {
|
@@ -523,6 +603,22 @@ void Init_ext()
|
|
523
603
|
*[](CpModelBuilder& self, IntVarSpan vars) {
|
524
604
|
self.AddAllDifferent(vars);
|
525
605
|
})
|
606
|
+
.define_method(
|
607
|
+
"add_max_equality",
|
608
|
+
*[](CpModelBuilder& self, operations_research::sat::IntVar target, IntVarSpan vars) {
|
609
|
+
self.AddMaxEquality(target, vars);
|
610
|
+
})
|
611
|
+
.define_method(
|
612
|
+
"add_no_overlap",
|
613
|
+
*[](CpModelBuilder& self, IntervalVarSpan vars) {
|
614
|
+
self.AddNoOverlap(vars);
|
615
|
+
})
|
616
|
+
.define_method(
|
617
|
+
"add_bool_or",
|
618
|
+
*[](CpModelBuilder& self, BoolVarSpan literals) {
|
619
|
+
self.AddBoolOr(literals);
|
620
|
+
})
|
621
|
+
.define_method("add_implication", &CpModelBuilder::AddImplication)
|
526
622
|
.define_method(
|
527
623
|
"maximize",
|
528
624
|
*[](CpModelBuilder& self, operations_research::sat::LinearExpr expr) {
|
@@ -537,13 +633,15 @@ void Init_ext()
|
|
537
633
|
define_class_under(rb_mORTools, "CpSolver")
|
538
634
|
.define_method(
|
539
635
|
"_solve_with_observer",
|
540
|
-
*[](Object self, CpModelBuilder& model, Object callback) {
|
636
|
+
*[](Object self, CpModelBuilder& model, Object callback, bool all_solutions) {
|
541
637
|
operations_research::sat::Model m;
|
542
638
|
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
639
|
+
if (all_solutions) {
|
640
|
+
// set parameters for SearchForAllSolutions
|
641
|
+
SatParameters parameters;
|
642
|
+
parameters.set_enumerate_all_solutions(true);
|
643
|
+
m.Add(NewSatParameters(parameters));
|
644
|
+
}
|
547
645
|
|
548
646
|
m.Add(NewFeasibleSolutionObserver(
|
549
647
|
[callback](const CpSolverResponse& r) {
|
@@ -563,6 +661,11 @@ void Init_ext()
|
|
563
661
|
"_solution_integer_value",
|
564
662
|
*[](Object self, CpSolverResponse& response, operations_research::sat::IntVar& x) {
|
565
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);
|
566
669
|
});
|
567
670
|
|
568
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,95 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "fileutils"
|
3
|
+
require "net/http"
|
4
|
+
require "tmpdir"
|
5
|
+
|
6
|
+
version = "7.7.7810"
|
7
|
+
|
8
|
+
if RbConfig::CONFIG["host_os"] =~ /darwin/i
|
9
|
+
filename = "or-tools_MacOsX-10.15.5_v#{version}.tar.gz"
|
10
|
+
checksum = "764f290f6d916bc366913a37d93e6f83bd7969ad33515ccc1ca390f544d65721"
|
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 == "18.04"
|
15
|
+
filename = "or-tools_ubuntu-18.04_v#{version}.tar.gz"
|
16
|
+
checksum = "12bdac29144b077b3f9ba602f947e4b9b9ce63ed3df4e325cda1333827edbcf8"
|
17
|
+
elsif os == "Ubuntu" && os_version == "16.04"
|
18
|
+
filename = "or-tools_ubuntu-16.04_v#{version}.tar.gz"
|
19
|
+
checksum = "cc696d342b97aa6cf7c62b6ae2cae95dfc665f2483d147c4117fdba434b13a53"
|
20
|
+
elsif os == "Debian" && os_version == "10"
|
21
|
+
filename = "or-tools_debian-10_v#{version}.tar.gz "
|
22
|
+
checksum = "3dd0299e9ad8d12fe6d186bfd59e63080c8e9f3c6b0489af9900c389cf7e4224"
|
23
|
+
elsif os == "CentOS" && os_version == "8"
|
24
|
+
filename = "or-tools_centos-8_v#{version}.tar.gz"
|
25
|
+
checksum = "1f7d8bce56807c4283374e05024ffac8afd81ff99063217418d02d626cf03088"
|
26
|
+
else
|
27
|
+
# there is a binary download for Windows
|
28
|
+
# however, it's compiled with Visual Studio rather than MinGW (which RubyInstaller uses)
|
29
|
+
raise <<~MSG
|
30
|
+
Binary installation not available for this platform.
|
31
|
+
|
32
|
+
Build the OR-Tools C++ library from source, then run:
|
33
|
+
bundle config build.or-tools --with-or-tools-dir=/path/to/or-tools
|
34
|
+
|
35
|
+
MSG
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
short_version = version.split(".").first(2).join(".")
|
40
|
+
url = "https://github.com/google/or-tools/releases/download/v#{short_version}/#{filename}"
|
41
|
+
|
42
|
+
$stdout.sync = true
|
43
|
+
|
44
|
+
def download_file(url, download_path)
|
45
|
+
uri = URI(url)
|
46
|
+
location = nil
|
47
|
+
|
48
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
49
|
+
request = Net::HTTP::Get.new(uri)
|
50
|
+
http.request(request) do |response|
|
51
|
+
case response
|
52
|
+
when Net::HTTPRedirection
|
53
|
+
location = response["location"]
|
54
|
+
when Net::HTTPSuccess
|
55
|
+
i = 0
|
56
|
+
File.open(download_path, "wb") do |f|
|
57
|
+
response.read_body do |chunk|
|
58
|
+
f.write(chunk)
|
59
|
+
|
60
|
+
# print progress
|
61
|
+
putc "." if i % 50 == 0
|
62
|
+
i += 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
puts # newline
|
66
|
+
else
|
67
|
+
raise "Bad response"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# outside of Net::HTTP block to close previous connection
|
73
|
+
download_file(location, download_path) if location
|
74
|
+
end
|
75
|
+
|
76
|
+
# download
|
77
|
+
download_path = "#{Dir.tmpdir}/#{filename}"
|
78
|
+
unless File.exist?(download_path)
|
79
|
+
puts "Downloading #{url}..."
|
80
|
+
download_file(url, download_path)
|
81
|
+
end
|
82
|
+
|
83
|
+
# check integrity - do this regardless of if just downloaded
|
84
|
+
download_checksum = Digest::SHA256.file(download_path).hexdigest
|
85
|
+
raise "Bad checksum: #{download_checksum}" if download_checksum != checksum
|
86
|
+
|
87
|
+
# extract - can't use Gem::Package#extract_tar_gz from RubyGems
|
88
|
+
# since it limits filenames to 100 characters (doesn't support UStar format)
|
89
|
+
path = File.expand_path("../../tmp/or-tools", __dir__)
|
90
|
+
FileUtils.mkdir_p(path)
|
91
|
+
tar_args = Gem.win_platform? ? ["--force-local"] : []
|
92
|
+
system "tar", "zxf", download_path, "-C", path, "--strip-components=1", *tar_args
|
93
|
+
|
94
|
+
# export
|
95
|
+
$vendor_path = path
|
data/lib/or-tools.rb
CHANGED
@@ -4,6 +4,7 @@ require "or_tools/ext"
|
|
4
4
|
# modules
|
5
5
|
require "or_tools/comparison"
|
6
6
|
require "or_tools/comparison_operators"
|
7
|
+
require "or_tools/bool_var"
|
7
8
|
require "or_tools/cp_model"
|
8
9
|
require "or_tools/cp_solver"
|
9
10
|
require "or_tools/cp_solver_solution_callback"
|
@@ -16,6 +17,12 @@ require "or_tools/sat_int_var"
|
|
16
17
|
require "or_tools/solver"
|
17
18
|
require "or_tools/version"
|
18
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
|
+
|
19
26
|
module ORTools
|
20
27
|
class Error < StandardError; end
|
21
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
|