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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +428 -189
- data/ext/or-tools/ext.cpp +5 -0
- data/ext/or-tools/vendor.rb +7 -7
- data/lib/or-tools.rb +6 -0
- data/lib/or_tools/basic_scheduler.rb +86 -0
- data/lib/or_tools/cp_solver.rb +5 -1
- data/lib/or_tools/ext.bundle +0 -0
- data/lib/or_tools/seating.rb +115 -0
- data/lib/or_tools/sudoku.rb +132 -0
- data/lib/or_tools/tsp.rb +60 -0
- data/lib/or_tools/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18b9ab384a5aa4b8d27a3415de9ba751085e63b95cbdc431d1fee206b05e1c37
|
4
|
+
data.tar.gz: 4b8d29c3f6b5fff8181a817737a818e2c3d7cf6cd0184d82078d4606c2c94740
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26e5b155bdd5c0d37aaf13fe26c674ac15ac693933999056a477e340823ced7e86b8565c00c941d4ecdfd35e0b5a4716465e2418bf1e44ca7c1586d171e77515
|
7
|
+
data.tar.gz: 1c87b6df5082d9ab61b1ab63d154e563fdd9405fa3dda5fd00df2f6aafc65c7f5f8d251938cab9cba58de04efb7f38b11893701c128833059b8290b3a3dfb0da
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
##
|
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
|
-
|
271
|
+
Assignment
|
31
272
|
|
32
|
-
- [
|
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 == :
|
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
|
-
|
582
|
+
### Assignment
|
583
|
+
|
584
|
+
[Guide](https://developers.google.com/optimization/assignment/assignment_example)
|
301
585
|
|
302
586
|
```ruby
|
303
|
-
#
|
304
|
-
|
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
|
-
|
307
|
-
|
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
|
-
|
596
|
+
# create the solver
|
597
|
+
assignment = ORTools::LinearSumAssignment.new
|
312
598
|
|
313
|
-
#
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
627
|
+
### Assignment with Teams
|
323
628
|
|
324
|
-
|
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
|
-
|
331
|
-
|
631
|
+
```ruby
|
632
|
+
# create the solver
|
633
|
+
solver = ORTools::Solver.new("SolveAssignmentProblemMIP", :cbc)
|
332
634
|
|
333
|
-
#
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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: :
|
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 == :
|
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
|
-
#
|
1729
|
-
#
|
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
|
-
|
1804
|
-
|
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
|
-
|
1811
|
-
|
1812
|
-
|
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
|
-
|
1837
|
-
|
1838
|
-
|
1839
|
-
|
1840
|
-
|
1841
|
-
|
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
|
-
|
2085
|
+
all_guests.each do |g|
|
1852
2086
|
model.add(
|
1853
2087
|
model.sum(
|
1854
|
-
(num_guests - 1).
|
1855
|
-
|
1856
|
-
|
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
|
data/ext/or-tools/ext.cpp
CHANGED
@@ -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")
|
data/ext/or-tools/vendor.rb
CHANGED
@@ -3,26 +3,26 @@ require "fileutils"
|
|
3
3
|
require "net/http"
|
4
4
|
require "tmpdir"
|
5
5
|
|
6
|
-
version = "7.
|
6
|
+
version = "7.7.7810"
|
7
7
|
|
8
8
|
if RbConfig::CONFIG["host_os"] =~ /darwin/i
|
9
|
-
filename = "or-tools_MacOsX-10.15.
|
10
|
-
checksum = "
|
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 = "
|
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 = "
|
19
|
+
checksum = "cc696d342b97aa6cf7c62b6ae2cae95dfc665f2483d147c4117fdba434b13a53"
|
20
20
|
elsif os == "Debian" && os_version == "10"
|
21
21
|
filename = "or-tools_debian-10_v#{version}.tar.gz "
|
22
|
-
checksum = "
|
22
|
+
checksum = "3dd0299e9ad8d12fe6d186bfd59e63080c8e9f3c6b0489af9900c389cf7e4224"
|
23
23
|
elsif os == "CentOS" && os_version == "8"
|
24
24
|
filename = "or-tools_centos-8_v#{version}.tar.gz"
|
25
|
-
checksum = "
|
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)
|
data/lib/or-tools.rb
CHANGED
@@ -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
|
data/lib/or_tools/cp_solver.rb
CHANGED
@@ -12,7 +12,11 @@ module ORTools
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def value(var)
|
15
|
-
|
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
|
data/lib/or_tools/tsp.rb
ADDED
@@ -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
|
data/lib/or_tools/version.rb
CHANGED
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.
|
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-
|
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:
|