or-tools 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4377b9acdd93be666c700e05fb56cf01e59f088cf149f345ab888ef018c82d04
4
- data.tar.gz: f32d95eb857cf479db372f4c0c27b18590bb9137aa72a9b6e478789830b7ea68
3
+ metadata.gz: 18b9ab384a5aa4b8d27a3415de9ba751085e63b95cbdc431d1fee206b05e1c37
4
+ data.tar.gz: 4b8d29c3f6b5fff8181a817737a818e2c3d7cf6cd0184d82078d4606c2c94740
5
5
  SHA512:
6
- metadata.gz: ab952c566d3f84c57cce0b843c27ad6e3fa9c06f4e85b273af2c82564403cd6bb88e8f8ae1d39c21f5732e4d3415729b8d4bd87418a34f0d02c588d816ab9d49
7
- data.tar.gz: 95a03e432680341c029e21ae2148385c1628a7df127ea8707991a47d087f37cd5ebb4288518f16b1cfd7c84c03a254c0cf8c26c5c8d574b949c3ed3901ca9cfe
6
+ metadata.gz: 26e5b155bdd5c0d37aaf13fe26c674ac15ac693933999056a477e340823ced7e86b8565c00c941d4ecdfd35e0b5a4716465e2418bf1e44ca7c1586d171e77515
7
+ data.tar.gz: 1c87b6df5082d9ab61b1ab63d154e563fdd9405fa3dda5fd00df2f6aafc65c7f5f8d251938cab9cba58de04efb7f38b11893701c128833059b8290b3a3dfb0da
@@ -1,3 +1,11 @@
1
+ ## 0.3.0 (2020-07-21)
2
+
3
+ - Updated OR-Tools to 7.7
4
+ - Added `BasicScheduler` class
5
+ - Added `Seating` class
6
+ - Added `TSP` class
7
+ - Added `Sudoku` class
8
+
1
9
  ## 0.2.0 (2020-05-22)
2
10
 
3
11
  - No longer need to download the OR-Tools C++ library separately on Mac, Ubuntu 18.04, Ubuntu 16.04, Debian 10, and CentOS 8
data/README.md CHANGED
@@ -14,12 +14,253 @@ gem 'or-tools'
14
14
 
15
15
  Installation can take a few minutes as OR-Tools downloads and builds.
16
16
 
17
- ## Getting Started
17
+ ## Higher Level Interfaces
18
+
19
+ - [Scheduling](#scheduling)
20
+ - [Seating](#seating)
21
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
22
+ - [Sudoku](#sudoku)
23
+
24
+ ### Scheduling
25
+
26
+ Specify people and their availabililty
27
+
28
+ ```ruby
29
+ people = [
30
+ {
31
+ availability: [
32
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
33
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")}
34
+ ],
35
+ max_hours: 40 # optional, applies to entire scheduling period
36
+ },
37
+ {
38
+ availability: [
39
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
40
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
41
+ ],
42
+ max_hours: 20
43
+ }
44
+ ]
45
+ ```
46
+
47
+ Specify shifts
48
+
49
+ ```ruby
50
+ shifts = [
51
+ {starts_at: Time.parse("2020-01-01 08:00:00"), ends_at: Time.parse("2020-01-01 16:00:00")},
52
+ {starts_at: Time.parse("2020-01-02 08:00:00"), ends_at: Time.parse("2020-01-02 16:00:00")},
53
+ {starts_at: Time.parse("2020-01-03 08:00:00"), ends_at: Time.parse("2020-01-03 16:00:00")}
54
+ ]
55
+ ```
56
+
57
+ Run the scheduler
58
+
59
+ ```ruby
60
+ scheduler = ORTools::BasicScheduler.new(people: people, shifts: shifts)
61
+ ```
62
+
63
+ The scheduler maximizes the number of assigned hours. A person must be available for the entire shift to be considered for it.
64
+
65
+ Get assignments (returns indexes of people and shifts)
66
+
67
+ ```ruby
68
+ scheduler.assignments
69
+ # [
70
+ # {person: 2, shift: 0},
71
+ # {person: 0, shift: 1},
72
+ # {person: 1, shift: 2}
73
+ # ]
74
+ ```
75
+
76
+ Get assigned hours and total hours
77
+
78
+ ```ruby
79
+ scheduler.assigned_hours
80
+ scheduler.total_hours
81
+ ```
82
+
83
+ Feel free to create an issue if you have a scheduling use case that’s not covered.
84
+
85
+ ### Seating
86
+
87
+ Create a seating chart based on personal connections. Uses [this approach](https://www.improbable.com/news/2012/Optimal-seating-chart.pdf).
88
+
89
+ Specify connections
90
+
91
+ ```ruby
92
+ connections = [
93
+ {people: ["A", "B", "C"], weight: 2},
94
+ {people: ["C", "D", "E", "F"], weight: 1}
95
+ ]
96
+ ```
97
+
98
+ Use different weights to prioritize seating. For a wedding, it may look like:
99
+
100
+ ```ruby
101
+ connections = [
102
+ {people: knows_partner1, weight: 1},
103
+ {people: knows_partner2, weight: 1},
104
+ {people: relationship1, weight: 100},
105
+ {people: relationship2, weight: 100},
106
+ {people: relationship3, weight: 100},
107
+ {people: friend_group1, weight: 10},
108
+ {people: friend_group2, weight: 10},
109
+ # ...
110
+ ]
111
+ ```
112
+
113
+ If two people have multiple connections, weights are added.
114
+
115
+ Specify tables and their capacity
116
+
117
+ ```ruby
118
+ tables = [3, 3]
119
+ ```
120
+
121
+ Assign seats
122
+
123
+ ```ruby
124
+ seating = ORTools::Seating.new(connections: connections, tables: tables)
125
+ ```
126
+
127
+ Each person will have a connection with at least one other person at their table.
128
+
129
+ Get tables
130
+
131
+ ```ruby
132
+ seating.assigned_tables
133
+ ```
134
+
135
+ Get assignments by person
136
+
137
+ ```ruby
138
+ seating.assignments
139
+ ```
140
+
141
+ Get all connections for a person
142
+
143
+ ```ruby
144
+ seating.connections_for(person)
145
+ ```
146
+
147
+ Get connections for a person at their table
148
+
149
+ ```ruby
150
+ seating.connections_for(person, same_table: true)
151
+ ```
152
+
153
+ ### Traveling Salesperson Problem (TSP)
154
+
155
+ Create locations - the first location will be the starting and ending point
156
+
157
+ ```ruby
158
+ locations = [
159
+ {name: "Tokyo", latitude: 35.6762, longitude: 139.6503},
160
+ {name: "Delhi", latitude: 28.7041, longitude: 77.1025},
161
+ {name: "Shanghai", latitude: 31.2304, longitude: 121.4737},
162
+ {name: "São Paulo", latitude: -23.5505, longitude: -46.6333},
163
+ {name: "Mexico City", latitude: 19.4326, longitude: -99.1332},
164
+ {name: "Cairo", latitude: 30.0444, longitude: 31.2357},
165
+ {name: "Mumbai", latitude: 19.0760, longitude: 72.8777},
166
+ {name: "Beijing", latitude: 39.9042, longitude: 116.4074},
167
+ {name: "Dhaka", latitude: 23.8103, longitude: 90.4125},
168
+ {name: "Osaka", latitude: 34.6937, longitude: 135.5023},
169
+ {name: "New York City", latitude: 40.7128, longitude: -74.0060},
170
+ {name: "Karachi", latitude: 24.8607, longitude: 67.0011},
171
+ {name: "Buenos Aires", latitude: -34.6037, longitude: -58.3816}
172
+ ]
173
+ ```
174
+
175
+ Locations can have any fields - only `latitude` and `longitude` are required
176
+
177
+ Get route
178
+
179
+ ```ruby
180
+ tsp = ORTools::TSP.new(locations)
181
+ tsp.route # [{name: "Tokyo", ...}, {name: "Osaka", ...}, ...]
182
+ ```
183
+
184
+ Get distances between locations on route
185
+
186
+ ```ruby
187
+ tsp.distances # [392.441, 1362.926, 1067.31, ...]
188
+ ```
189
+
190
+ Distances are in kilometers - multiply by `0.6214` for miles
191
+
192
+ Get total distance
193
+
194
+ ```ruby
195
+ tsp.total_distance
196
+ ```
197
+
198
+ ### Sudoku
199
+
200
+ Create a puzzle with zeros in empty cells
201
+
202
+ ```ruby
203
+ grid = [
204
+ [0, 6, 0, 0, 5, 0, 0, 2, 0],
205
+ [0, 0, 0, 3, 0, 0, 0, 9, 0],
206
+ [7, 0, 0, 6, 0, 0, 0, 1, 0],
207
+ [0, 0, 6, 0, 3, 0, 4, 0, 0],
208
+ [0, 0, 4, 0, 7, 0, 1, 0, 0],
209
+ [0, 0, 5, 0, 9, 0, 8, 0, 0],
210
+ [0, 4, 0, 0, 0, 1, 0, 0, 6],
211
+ [0, 3, 0, 0, 0, 8, 0, 0, 0],
212
+ [0, 2, 0, 0, 4, 0, 0, 5, 0]
213
+ ]
214
+ sudoku = ORTools::Sudoku.new(grid)
215
+ sudoku.solution
216
+ ```
217
+
218
+ It can also solve more advanced puzzles like [The Miracle](https://www.youtube.com/watch?v=yKf9aUIxdb4)
219
+
220
+ ```ruby
221
+ grid = [
222
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
223
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
224
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
225
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
226
+ [0, 0, 1, 0, 0, 0, 0, 0, 0],
227
+ [0, 0, 0, 0, 0, 0, 2, 0, 0],
228
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
229
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
230
+ [0, 0, 0, 0, 0, 0, 0, 0, 0]
231
+ ]
232
+ sudoku = ORTools::Sudoku.new(grid, anti_knight: true, anti_king: true, non_consecutive: true)
233
+ sudoku.solution
234
+ ```
235
+
236
+ And [this 4-digit puzzle](https://www.youtube.com/watch?v=hAyZ9K2EBF0)
237
+
238
+ ```ruby
239
+ grid = [
240
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
241
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
242
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
243
+ [3, 8, 4, 0, 0, 0, 0, 0, 0],
244
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
245
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
246
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
247
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
248
+ [0, 0, 0, 0, 0, 0, 0, 0, 2]
249
+ ]
250
+ sudoku = ORTools::Sudoku.new(grid, x: true, anti_knight: true, magic_square: true)
251
+ sudoku.solution
252
+ ```
253
+
254
+ ## Guides
18
255
 
19
256
  Linear Optimization
20
257
 
21
258
  - [The Glop Linear Solver](#the-glop-linear-solver)
22
259
 
260
+ Integer Optimization
261
+
262
+ - [Mixed-Integer Programming](#mixed-integer-programming)
263
+
23
264
  Constraint Optimization
24
265
 
25
266
  - [CP-SAT Solver](#cp-sat-solver)
@@ -27,13 +268,14 @@ Constraint Optimization
27
268
  - [Cryptarithmetic](#cryptarithmetic)
28
269
  - [The N-queens Problem](#the-n-queens-problem)
29
270
 
30
- Integer Optimization
271
+ Assignment
31
272
 
32
- - [Mixed-Integer Programming](#mixed-integer-programming)
273
+ - [Assignment](#assignment)
274
+ - [Assignment with Teams](#assignment-with-teams)
33
275
 
34
276
  Routing
35
277
 
36
- - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp)
278
+ - [Traveling Salesperson Problem (TSP)](#traveling-salesperson-problem-tsp-1)
37
279
  - [Vehicle Routing Problem (VRP)](#vehicle-routing-problem-vrp)
38
280
  - [Capacity Constraints](#capacity-constraints)
39
281
  - [Pickups and Deliveries](#pickups-and-deliveries)
@@ -52,12 +294,7 @@ Network Flows
52
294
 
53
295
  - [Maximum Flows](#maximum-flows)
54
296
  - [Minimum Cost Flows](#minimum-cost-flows)
55
-
56
- Assignment
57
-
58
- - [Assignment](#assignment)
59
- - [Assignment as a Min Cost Problem](#assignment-as-a-min-cost-problem)
60
- - [Assignment as a MIP Problem](#assignment-as-a-mip-problem)
297
+ - [Assignment as a Min Cost Flow Problem](#assignment-as-a-min-cost-flow-problem)
61
298
 
62
299
  Scheduling
63
300
 
@@ -66,7 +303,7 @@ Scheduling
66
303
 
67
304
  Other Examples
68
305
 
69
- - [Sudoku](#sudoku)
306
+ - [Sudoku](#sudoku-1)
70
307
  - [Wedding Seating Chart](#wedding-seating-chart)
71
308
  - [Set Partitioning](#set-partitioning)
72
309
 
@@ -114,6 +351,52 @@ puts "y = #{y.solution_value}"
114
351
  puts "Optimal objective value = #{opt_solution}"
115
352
  ```
116
353
 
354
+ ### Mixed-Integer Programming
355
+
356
+ [Guide](https://developers.google.com/optimization/mip/integer_opt)
357
+
358
+ ```ruby
359
+ # declare the MIP solver
360
+ solver = ORTools::Solver.new("simple_mip_program", :cbc)
361
+
362
+ # define the variables
363
+ infinity = solver.infinity
364
+ x = solver.int_var(0.0, infinity, "x")
365
+ y = solver.int_var(0.0, infinity, "y")
366
+
367
+ puts "Number of variables = #{solver.num_variables}"
368
+
369
+ # define the constraints
370
+ c0 = solver.constraint(-infinity, 17.5)
371
+ c0.set_coefficient(x, 1)
372
+ c0.set_coefficient(y, 7)
373
+
374
+ c1 = solver.constraint(-infinity, 3.5)
375
+ c1.set_coefficient(x, 1);
376
+ c1.set_coefficient(y, 0);
377
+
378
+ puts "Number of constraints = #{solver.num_constraints}"
379
+
380
+ # define the objective
381
+ objective = solver.objective
382
+ objective.set_coefficient(x, 1)
383
+ objective.set_coefficient(y, 10)
384
+ objective.set_maximization
385
+
386
+ # call the solver
387
+ status = solver.solve
388
+
389
+ # display the solution
390
+ if status == :optimal
391
+ puts "Solution:"
392
+ puts "Objective value = #{solver.objective.value}"
393
+ puts "x = #{x.solution_value}"
394
+ puts "y = #{y.solution_value}"
395
+ else
396
+ puts "The problem does not have an optimal solution."
397
+ end
398
+ ```
399
+
117
400
  ### CP-SAT Solver
118
401
 
119
402
  [Guide](https://developers.google.com/optimization/cp/cp_solver)
@@ -136,7 +419,7 @@ solver = ORTools::CpSolver.new
136
419
  status = solver.solve(model)
137
420
 
138
421
  # display the first solution
139
- if status == :feasible
422
+ if status == :optimal
140
423
  puts "x = #{solver.value(x)}"
141
424
  puts "y = #{solver.value(y)}"
142
425
  puts "z = #{solver.value(z)}"
@@ -295,50 +578,119 @@ puts
295
578
  puts "Solutions found : %i" % solution_printer.solution_count
296
579
  ```
297
580
 
298
- ### Mixed-Integer Programming
299
581
 
300
- [Guide](https://developers.google.com/optimization/mip/integer_opt)
582
+ ### Assignment
583
+
584
+ [Guide](https://developers.google.com/optimization/assignment/assignment_example)
301
585
 
302
586
  ```ruby
303
- # declare the MIP solver
304
- solver = ORTools::Solver.new("simple_mip_program", :cbc)
587
+ # create the data
588
+ cost = [[ 90, 76, 75, 70],
589
+ [ 35, 85, 55, 65],
590
+ [125, 95, 90, 105],
591
+ [ 45, 110, 95, 115]]
305
592
 
306
- # define the variables
307
- infinity = solver.infinity
308
- x = solver.int_var(0.0, infinity, "x")
309
- y = solver.int_var(0.0, infinity, "y")
593
+ rows = cost.length
594
+ cols = cost[0].length
310
595
 
311
- puts "Number of variables = #{solver.num_variables}"
596
+ # create the solver
597
+ assignment = ORTools::LinearSumAssignment.new
312
598
 
313
- # define the constraints
314
- c0 = solver.constraint(-infinity, 17.5)
315
- c0.set_coefficient(x, 1)
316
- c0.set_coefficient(y, 7)
599
+ # add the costs to the solver
600
+ rows.times do |worker|
601
+ cols.times do |task|
602
+ if cost[worker][task]
603
+ assignment.add_arc_with_cost(worker, task, cost[worker][task])
604
+ end
605
+ end
606
+ end
317
607
 
318
- c1 = solver.constraint(-infinity, 3.5)
319
- c1.set_coefficient(x, 1);
320
- c1.set_coefficient(y, 0);
608
+ # invoke the solver
609
+ solve_status = assignment.solve
610
+ if solve_status == :optimal
611
+ puts "Total cost = #{assignment.optimal_cost}"
612
+ puts
613
+ assignment.num_nodes.times do |i|
614
+ puts "Worker %d assigned to task %d. Cost = %d" % [
615
+ i,
616
+ assignment.right_mate(i),
617
+ assignment.assignment_cost(i)
618
+ ]
619
+ end
620
+ elsif solve_status == :infeasible
621
+ puts "No assignment is possible."
622
+ elsif solve_status == :possible_overflow
623
+ puts "Some input costs are too large and may cause an integer overflow."
624
+ end
625
+ ```
321
626
 
322
- puts "Number of constraints = #{solver.num_constraints}"
627
+ ### Assignment with Teams
323
628
 
324
- # define the objective
325
- objective = solver.objective
326
- objective.set_coefficient(x, 1)
327
- objective.set_coefficient(y, 10)
328
- objective.set_maximization
629
+ [Guide](https://developers.google.com/optimization/assignment/assignment_teams)
329
630
 
330
- # call the solver
331
- status = solver.solve
631
+ ```ruby
632
+ # create the solver
633
+ solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
332
634
 
333
- # display the solution
334
- if status == :optimal
335
- puts "Solution:"
336
- puts "Objective value = #{solver.objective.value}"
337
- puts "x = #{x.solution_value}"
338
- puts "y = #{y.solution_value}"
339
- else
340
- puts "The problem does not have an optimal solution."
635
+ # create the data
636
+ cost = [[90, 76, 75, 70],
637
+ [35, 85, 55, 65],
638
+ [125, 95, 90, 105],
639
+ [45, 110, 95, 115],
640
+ [60, 105, 80, 75],
641
+ [45, 65, 110, 95]]
642
+
643
+ team1 = [0, 2, 4]
644
+ team2 = [1, 3, 5]
645
+ team_max = 2
646
+
647
+ # create the variables
648
+ num_workers = cost.length
649
+ num_tasks = cost[1].length
650
+ x = {}
651
+
652
+ num_workers.times do |i|
653
+ num_tasks.times do |j|
654
+ x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
655
+ end
341
656
  end
657
+
658
+ # create the objective function
659
+ solver.minimize(solver.sum(
660
+ num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
661
+ ))
662
+
663
+ # create the constraints
664
+ num_workers.times do |i|
665
+ solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
666
+ end
667
+
668
+ num_tasks.times do |j|
669
+ solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
670
+ end
671
+
672
+ solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
673
+ solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
674
+
675
+ # invoke the solver
676
+ sol = solver.solve
677
+
678
+ puts "Total cost = #{solver.objective.value}"
679
+ puts
680
+ num_workers.times do |i|
681
+ num_tasks.times do |j|
682
+ if x[[i, j]].solution_value > 0
683
+ puts "Worker %d assigned to task %d. Cost = %d" % [
684
+ i,
685
+ j,
686
+ cost[i][j]
687
+ ]
688
+ end
689
+ end
690
+ end
691
+
692
+ puts
693
+ puts "Time = #{solver.wall_time} milliseconds"
342
694
  ```
343
695
 
344
696
  ### Traveling Salesperson Problem (TSP)
@@ -380,7 +732,7 @@ transit_callback_index = routing.register_transit_callback(distance_callback)
380
732
  routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
381
733
 
382
734
  # run the solver
383
- assignment = routing.solve(first_solution_strategy: :path_cheaper_arc)
735
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
384
736
 
385
737
  # print the solution
386
738
  puts "Objective: #{assignment.objective_value} miles"
@@ -1276,52 +1628,7 @@ else
1276
1628
  end
1277
1629
  ```
1278
1630
 
1279
- ### Assignment
1280
-
1281
- [Guide](https://developers.google.com/optimization/assignment/simple_assignment)
1282
-
1283
- ```ruby
1284
- # create the data
1285
- cost = [[ 90, 76, 75, 70],
1286
- [ 35, 85, 55, 65],
1287
- [125, 95, 90, 105],
1288
- [ 45, 110, 95, 115]]
1289
-
1290
- rows = cost.length
1291
- cols = cost[0].length
1292
-
1293
- # create the solver
1294
- assignment = ORTools::LinearSumAssignment.new
1295
-
1296
- # add the costs to the solver
1297
- rows.times do |worker|
1298
- cols.times do |task|
1299
- if cost[worker][task]
1300
- assignment.add_arc_with_cost(worker, task, cost[worker][task])
1301
- end
1302
- end
1303
- end
1304
-
1305
- # invoke the solver
1306
- solve_status = assignment.solve
1307
- if solve_status == :optimal
1308
- puts "Total cost = #{assignment.optimal_cost}"
1309
- puts
1310
- assignment.num_nodes.times do |i|
1311
- puts "Worker %d assigned to task %d. Cost = %d" % [
1312
- i,
1313
- assignment.right_mate(i),
1314
- assignment.assignment_cost(i)
1315
- ]
1316
- end
1317
- elsif solve_status == :infeasible
1318
- puts "No assignment is possible."
1319
- elsif solve_status == :possible_overflow
1320
- puts "Some input costs are too large and may cause an integer overflow."
1321
- end
1322
- ```
1323
-
1324
- ### Assignment as a Min Cost Problem
1631
+ ### Assignment as a Min Cost Flow Problem
1325
1632
 
1326
1633
  [Guide](https://developers.google.com/optimization/assignment/assignment_min_cost_flow)
1327
1634
 
@@ -1370,75 +1677,6 @@ else
1370
1677
  end
1371
1678
  ```
1372
1679
 
1373
- ### Assignment as a MIP Problem
1374
-
1375
- [Guide](https://developers.google.com/optimization/assignment/assignment_mip)
1376
-
1377
- ```ruby
1378
- # create the solver
1379
- solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
1380
-
1381
- # create the data
1382
- cost = [[90, 76, 75, 70],
1383
- [35, 85, 55, 65],
1384
- [125, 95, 90, 105],
1385
- [45, 110, 95, 115],
1386
- [60, 105, 80, 75],
1387
- [45, 65, 110, 95]]
1388
-
1389
- team1 = [0, 2, 4]
1390
- team2 = [1, 3, 5]
1391
- team_max = 2
1392
-
1393
- # create the variables
1394
- num_workers = cost.length
1395
- num_tasks = cost[1].length
1396
- x = {}
1397
-
1398
- num_workers.times do |i|
1399
- num_tasks.times do |j|
1400
- x[[i, j]] = solver.bool_var("x[#{i},#{j}]")
1401
- end
1402
- end
1403
-
1404
- # create the objective function
1405
- solver.minimize(solver.sum(
1406
- num_workers.times.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] * cost[i][j] } }
1407
- ))
1408
-
1409
- # create the constraints
1410
- num_workers.times do |i|
1411
- solver.add(solver.sum(num_tasks.times.map { |j| x[[i, j]] }) <= 1)
1412
- end
1413
-
1414
- num_tasks.times do |j|
1415
- solver.add(solver.sum(num_workers.times.map { |i| x[[i, j]] }) == 1)
1416
- end
1417
-
1418
- solver.add(solver.sum(team1.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1419
- solver.add(solver.sum(team2.flat_map { |i| num_tasks.times.map { |j| x[[i, j]] } }) <= team_max)
1420
-
1421
- # invoke the solver
1422
- sol = solver.solve
1423
-
1424
- puts "Total cost = #{solver.objective.value}"
1425
- puts
1426
- num_workers.times do |i|
1427
- num_tasks.times do |j|
1428
- if x[[i, j]].solution_value > 0
1429
- puts "Worker %d assigned to task %d. Cost = %d" % [
1430
- i,
1431
- j,
1432
- cost[i][j]
1433
- ]
1434
- end
1435
- end
1436
- end
1437
-
1438
- puts
1439
- puts "Time = #{solver.wall_time} milliseconds"
1440
- ```
1441
-
1442
1680
  ### Employee Scheduling
1443
1681
 
1444
1682
  [Guide](https://developers.google.com/optimization/scheduling/employee_scheduling)
@@ -1710,7 +1948,7 @@ end
1710
1948
  # solve and print solution
1711
1949
  solver = ORTools::CpSolver.new
1712
1950
  status = solver.solve(model)
1713
- if status == :feasible
1951
+ if status == :optimal
1714
1952
  line.each do |i|
1715
1953
  p line.map { |j| solver.value(grid[[i, j]]) }
1716
1954
  end
@@ -1725,8 +1963,8 @@ end
1725
1963
  # From
1726
1964
  # Meghan L. Bellows and J. D. Luc Peterson
1727
1965
  # "Finding an optimal seating chart for a wedding"
1728
- # http://www.improbable.com/news/2012/Optimal-seating-chart.pdf
1729
- # http://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
1966
+ # https://www.improbable.com/news/2012/Optimal-seating-chart.pdf
1967
+ # https://www.improbable.com/2012/02/12/finding-an-optimal-seating-chart-for-a-wedding
1730
1968
  #
1731
1969
  # Every year, millions of brides (not to mention their mothers, future
1732
1970
  # mothers-in-law, and occasionally grooms) struggle with one of the
@@ -1799,19 +2037,17 @@ all_tables.each do |t|
1799
2037
  end
1800
2038
  end
1801
2039
 
2040
+ pairs = all_guests.combination(2)
2041
+
1802
2042
  colocated = {}
1803
- (num_guests - 1).times do |g1|
1804
- (g1 + 1).upto(num_guests - 1) do |g2|
1805
- colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
1806
- end
2043
+ pairs.each do |g1, g2|
2044
+ colocated[[g1, g2]] = model.new_bool_var("guest %i seats with guest %i" % [g1, g2])
1807
2045
  end
1808
2046
 
1809
2047
  same_table = {}
1810
- (num_guests - 1).times do |g1|
1811
- (g1 + 1).upto(num_guests - 1) do |g2|
1812
- all_tables.each do |t|
1813
- same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
1814
- end
2048
+ pairs.each do |g1, g2|
2049
+ all_tables.each do |t|
2050
+ same_table[[g1, g2, t]] = model.new_bool_var("guest %i seats with guest %i on table %i" % [g1, g2, t])
1815
2051
  end
1816
2052
  end
1817
2053
 
@@ -1833,29 +2069,32 @@ all_tables.each do |t|
1833
2069
  end
1834
2070
 
1835
2071
  # Link colocated with seats
1836
- (num_guests - 1).times do |g1|
1837
- (g1 + 1).upto(num_guests - 1) do |g2|
1838
- all_tables.each do |t|
1839
- # Link same_table and seats.
1840
- model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
1841
- model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
1842
- model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
1843
- end
1844
-
1845
- # Link colocated and same_table.
1846
- model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
2072
+ pairs.each do |g1, g2|
2073
+ all_tables.each do |t|
2074
+ # Link same_table and seats.
2075
+ model.add_bool_or([seats[[t, g1]].not, seats[[t, g2]].not, same_table[[g1, g2, t]]])
2076
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g1]])
2077
+ model.add_implication(same_table[[g1, g2, t]], seats[[t, g2]])
1847
2078
  end
2079
+
2080
+ # Link colocated and same_table.
2081
+ model.add(model.sum(all_tables.map { |t| same_table[[g1, g2, t]] }) == colocated[[g1, g2]])
1848
2082
  end
1849
2083
 
1850
2084
  # Min known neighbors rule.
1851
- all_tables.each do |t|
2085
+ all_guests.each do |g|
1852
2086
  model.add(
1853
2087
  model.sum(
1854
- (num_guests - 1).times.flat_map { |g1|
1855
- (g1 + 1).upto(num_guests - 1).select { |g2| c[g1][g2] > 0 }.flat_map { |g2|
1856
- all_tables.map { |t2| same_table[[g1, g2, t2]] }
1857
- }
1858
- }
2088
+ (g + 1).upto(num_guests - 1).
2089
+ select { |g2| c[g][g2] > 0 }.
2090
+ product(all_tables).
2091
+ map { |g2, t| same_table[[g, g2, t]] }
2092
+ ) +
2093
+ model.sum(
2094
+ g.times.
2095
+ select { |g1| c[g1][g] > 0 }.
2096
+ product(all_tables).
2097
+ map { |g1, t| same_table[[g1, g, t]] }
1859
2098
  ) >= min_known_neighbors
1860
2099
  )
1861
2100
  end
@@ -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")
@@ -3,26 +3,26 @@ require "fileutils"
3
3
  require "net/http"
4
4
  require "tmpdir"
5
5
 
6
- version = "7.6.7691"
6
+ version = "7.7.7810"
7
7
 
8
8
  if RbConfig::CONFIG["host_os"] =~ /darwin/i
9
- filename = "or-tools_MacOsX-10.15.4_v#{version}.tar.gz"
10
- checksum = "39e26ba27b4d3a1c194c1478e864cd016d62cf516cd9227a9f23e6143e131572"
9
+ filename = "or-tools_MacOsX-10.15.5_v#{version}.tar.gz"
10
+ checksum = "764f290f6d916bc366913a37d93e6f83bd7969ad33515ccc1ca390f544d65721"
11
11
  else
12
12
  os = %x[lsb_release -is].chomp rescue nil
13
13
  os_version = %x[lsb_release -rs].chomp rescue nil
14
14
  if os == "Ubuntu" && os_version == "18.04"
15
15
  filename = "or-tools_ubuntu-18.04_v#{version}.tar.gz"
16
- checksum = "79ef61dfc63b98133ed637f02e837f714a95987424332e511a3a87edd5ce17dc"
16
+ checksum = "12bdac29144b077b3f9ba602f947e4b9b9ce63ed3df4e325cda1333827edbcf8"
17
17
  elsif os == "Ubuntu" && os_version == "16.04"
18
18
  filename = "or-tools_ubuntu-16.04_v#{version}.tar.gz"
19
- checksum = "a25fc94c0f0d16abf1f6da2a054040c21ef3cbf618a831a15afe21bf14f2d1fb"
19
+ checksum = "cc696d342b97aa6cf7c62b6ae2cae95dfc665f2483d147c4117fdba434b13a53"
20
20
  elsif os == "Debian" && os_version == "10"
21
21
  filename = "or-tools_debian-10_v#{version}.tar.gz "
22
- checksum = "158c44038aebc42b42b98e8f3733ba83bf230e8a0379803cc48aafbb2f7bdf5a"
22
+ checksum = "3dd0299e9ad8d12fe6d186bfd59e63080c8e9f3c6b0489af9900c389cf7e4224"
23
23
  elsif os == "CentOS" && os_version == "8"
24
24
  filename = "or-tools_centos-8_v#{version}.tar.gz"
25
- checksum = "a2b800d4e498561e5b1fe95ee1e64c867be496038883f4f7b199499bf71a0eed"
25
+ checksum = "1f7d8bce56807c4283374e05024ffac8afd81ff99063217418d02d626cf03088"
26
26
  else
27
27
  # there is a binary download for Windows
28
28
  # however, it's compiled with Visual Studio rather than MinGW (which RubyInstaller uses)
@@ -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
@@ -12,7 +12,11 @@ module ORTools
12
12
  end
13
13
 
14
14
  def value(var)
15
- _solution_integer_value(@response, var)
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)
Binary file
@@ -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
@@ -0,0 +1,60 @@
1
+ module ORTools
2
+ class TSP
3
+ attr_reader :route, :route_indexes, :distances, :total_distance
4
+
5
+ DISTANCE_SCALE = 1000
6
+ DEGREES_TO_RADIANS = Math::PI / 180
7
+
8
+ def initialize(locations)
9
+ raise ArgumentError, "Locations must have latitude and longitude" unless locations.all? { |l| l[:latitude] && l[:longitude] }
10
+ raise ArgumentError, "Latitude must be between -90 and 90" unless locations.all? { |l| l[:latitude] >= -90 && l[:latitude] <= 90 }
11
+ raise ArgumentError, "Longitude must be between -180 and 180" unless locations.all? { |l| l[:longitude] >= -180 && l[:longitude] <= 180 }
12
+ raise ArgumentError, "Must be at least two locations" unless locations.size >= 2
13
+
14
+ distance_matrix =
15
+ locations.map do |from|
16
+ locations.map do |to|
17
+ # must be integers
18
+ (distance(from, to) * DISTANCE_SCALE).to_i
19
+ end
20
+ end
21
+
22
+ manager = ORTools::RoutingIndexManager.new(locations.size, 1, 0)
23
+ routing = ORTools::RoutingModel.new(manager)
24
+
25
+ distance_callback = lambda do |from_index, to_index|
26
+ from_node = manager.index_to_node(from_index)
27
+ to_node = manager.index_to_node(to_index)
28
+ distance_matrix[from_node][to_node]
29
+ end
30
+
31
+ transit_callback_index = routing.register_transit_callback(distance_callback)
32
+ routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
33
+ assignment = routing.solve(first_solution_strategy: :path_cheapest_arc)
34
+
35
+ @route_indexes = []
36
+ @distances = []
37
+
38
+ index = routing.start(0)
39
+ while !routing.end?(index)
40
+ @route_indexes << manager.index_to_node(index)
41
+ previous_index = index
42
+ index = assignment.value(routing.next_var(index))
43
+ @distances << routing.arc_cost_for_vehicle(previous_index, index, 0) / DISTANCE_SCALE.to_f
44
+ end
45
+ @route_indexes << manager.index_to_node(index)
46
+ @route = locations.values_at(*@route_indexes)
47
+ @total_distance = @distances.sum
48
+ end
49
+
50
+ private
51
+
52
+ def distance(from, to)
53
+ from_lat = from[:latitude] * DEGREES_TO_RADIANS
54
+ from_lng = from[:longitude] * DEGREES_TO_RADIANS
55
+ to_lat = to[:latitude] * DEGREES_TO_RADIANS
56
+ to_lng = to[:longitude] * DEGREES_TO_RADIANS
57
+ 2 * 6371 * Math.asin(Math.sqrt(Math.sin((to_lat - from_lat) / 2.0)**2 + Math.cos(from_lat) * Math.cos(to_lat) * Math.sin((from_lng - to_lng) / 2.0)**2))
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,3 @@
1
1
  module ORTools
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: or-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-23 00:00:00.000000000 Z
11
+ date: 2020-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rice
@@ -95,19 +95,24 @@ files:
95
95
  - ext/or-tools/extconf.rb
96
96
  - ext/or-tools/vendor.rb
97
97
  - lib/or-tools.rb
98
+ - lib/or_tools/basic_scheduler.rb
98
99
  - lib/or_tools/bool_var.rb
99
100
  - lib/or_tools/comparison.rb
100
101
  - lib/or_tools/comparison_operators.rb
101
102
  - lib/or_tools/cp_model.rb
102
103
  - lib/or_tools/cp_solver.rb
103
104
  - lib/or_tools/cp_solver_solution_callback.rb
105
+ - lib/or_tools/ext.bundle
104
106
  - lib/or_tools/int_var.rb
105
107
  - lib/or_tools/knapsack_solver.rb
106
108
  - lib/or_tools/linear_expr.rb
107
109
  - lib/or_tools/routing_model.rb
108
110
  - lib/or_tools/sat_int_var.rb
109
111
  - lib/or_tools/sat_linear_expr.rb
112
+ - lib/or_tools/seating.rb
110
113
  - lib/or_tools/solver.rb
114
+ - lib/or_tools/sudoku.rb
115
+ - lib/or_tools/tsp.rb
111
116
  - lib/or_tools/version.rb
112
117
  homepage: https://github.com/ankane/or-tools
113
118
  licenses: