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.
@@ -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.AddVar(from_ruby<operations_research::sat::BoolVar>(cvar[0]));
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
- expr = from_ruby<operations_research::sat::IntVar>(x);
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(Object x)
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, MPVariable& other) {
390
+ *[](MPVariable& self, LinearExpr& other) {
342
391
  LinearExpr s(&self);
343
- LinearExpr o(&other);
344
- return s + o;
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
- // set parameters for SearchForAllSolutions
544
- SatParameters parameters;
545
- parameters.set_enumerate_all_solutions(true);
546
- m.Add(NewSatParameters(parameters));
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")
@@ -1,6 +1,6 @@
1
1
  require "mkmf-rice"
2
2
 
3
- abort "Missing stdc++" unless have_library("stdc++")
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,#{lib}"
30
+ $LDFLAGS << " -Wl,-rpath,#{rpath}"
18
31
  $LDFLAGS << " -L#{lib}"
19
- $LDFLAGS << " -lortools"
32
+ raise "OR-Tools not found" unless have_library("ortools")
20
33
 
21
- %w(
22
- absl_city
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
@@ -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