or-tools 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: