or-tools 0.1.3 → 0.3.1

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