-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathcar_sequencing_optimization_sat.py
More file actions
271 lines (215 loc) · 10.7 KB
/
car_sequencing_optimization_sat.py
File metadata and controls
271 lines (215 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#!/usr/bin/env python3
# Copyright 2010-2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Solve the car sequencing problem as an optimization problem.
Problem Description: The Car Sequencing Problem with Optimization
-----------------------------------------------------------------
See https://en.wikipedia.org/wiki/Car_sequencing_problem for more details.
We are tasked with determining the optimal production sequence for a set of cars
on an assembly line. This is a classic and challenging combinatorial
optimization problem with the following characteristics:
Fixed Production Demand: There is a specific, non-negotiable number of cars of
different types (or 'classes') that must be produced. In our case, we have 6
distinct classes of cars, and we must produce exactly 5 of each, for a total of
30 'real' cars.
Diverse Car Configurations: Each car class is defined by a unique combination of
optional features. For example, 'Class 1' might require a sunroof (Option 1) and
a special engine (Option 4), while 'Class 3' only requires air conditioning
(Option 2).
Specialized Assembly Stations: The assembly line is composed of a series of
specialized stations. Each station is responsible for installing one specific
option. For example, there is one station for sunroofs, one for special engines,
and so on.
Capacity-Limited Stations: The core challenge of the problem lies here. The
stations cannot handle an unlimited, dense flow of cars requiring their specific
option. Their capacity is defined by a 'sliding window' constraint. For example,
the sunroof station might have a constraint of 'at most 1 car with a sunroof in
any sequence of 3 consecutive cars'. This means sequences like [Sunroof, No, No,
Sunroof] are valid, but [Sunroof, No, Sunroof, No] are not.
The Need for Spacing (Optimization): The combination of high demand for certain
options and tight capacity constraints may make it impossible to produce the 30
real cars consecutively. To create a valid sequence, we may need to insert
'dummy' or 'filler' cars into the production line. These dummy cars have no
options and therefore do not consume capacity at any station. They serve purely
as spacers to break up dense sequences of option-heavy cars.
The Goal: The objective is to find a production sequence that fulfills the
demand for all 30 real cars while using the minimum number of dummy cars. This
is equivalent to finding the shortest possible total production schedule (real
cars + dummy cars).
Modeling and Solution Approach with CP-SAT
------------------------------------------
To solve this problem, we use the CP-SAT solver from Google's OR-Tools library.
This is a constraint programming approach, which works by defining variables,
constraints, and an objective function.
1. Decision Variables
The fundamental decision the solver must make is: 'Which class of car should be
placed in each production slot?'
We define a large number of boolean variables: produces[c][s]. This variable is
True if a car of class c is scheduled in slot s, and False otherwise. We create
these for all car classes (including the dummy class) and for an extended number
of slots (30 real + a buffer of 20 for dummies).
We introduce a key integer variable: makespan. This variable represents the
total length of the 'meaningful' part of our schedule. It's the slot number
where the first dummy car appears, after which all subsequent cars are also
dummies.
2. Constraints (The Rules of the Game)
We translate the problem's rules into mathematical constraints that the solver
must obey:
One Car Per Slot: For every production slot s, exactly one car class can be
assigned. We enforce this using an AddExactlyOne constraint over all
produces[c][s] variables for that slot.
Fulfill Real Car Demand: The total number of times each real car class c appears
across all slots must equal its required demand (5 in our case). This is a
simple Add(sum(...) == 5) constraint.
Station Capacity (Sliding Window): This is the most critical constraint. For
each option (e.g., 'sunroof') and its capacity rule (e.g., '1 in 3'), we create
constraints for every possible sliding window. For every subsequence of 3 slots,
we sum up the produces variables corresponding to car classes that require that
option and constrain this sum to be less than or equal to 1.
Makespan Definition: This is the clever part of the model. We link our makespan
objective variable to the placement of dummy cars using logical equivalences for
each slot s:
(makespan <= s) is equivalent to (slot s contains a dummy car)
This ensures that if the solver chooses a makespan of 32, for example, it is
forced to place dummy cars in slots 32, 33, 34, and so on. Conversely, if the
solver is forced to place a dummy car in slot 32 to satisfy a capacity
constraint, the makespan must be at most 32.
3. The Objective Function
The objective is simple and directly tied to our goal:
Minimize makespan: By instructing the solver to find a solution with the
smallest possible value for the makespan variable, we are asking it to find the
shortest possible production schedule that satisfies all the rules. This
inherently minimizes the number of dummy cars used.
By defining the problem in this way, we let the CP-SAT solver explore the vast
search space of possible sequences efficiently, using its powerful constraint
propagation and search techniques to find an optimal arrangement that meets all
our complex requirements.
"""
from collections.abc import Sequence
from absl import app
from ortools.sat.python import cp_model
def solve_car_sequencing_optimization() -> None:
"""Solves the car sequencing problem with an optimization approach."""
# --------------------
# 1. Data
# --------------------
num_real_cars: int = 30
max_dummy_cars: int = 20
num_slots = num_real_cars + max_dummy_cars
all_slots = range(num_slots)
class_options = [
# Options: 1 2 3 4 5
[0, 0, 0, 0, 0], # Class 0 (Dummy)
[1, 0, 0, 1, 0], # Class 1
[0, 1, 0, 0, 1], # Class 2
[0, 1, 0, 0, 0], # Class 3
[0, 0, 1, 1, 0], # Class 4
[0, 0, 1, 0, 0], # Class 5
[0, 0, 0, 0, 1], # Class 6
]
num_classes = len(class_options)
all_classes = range(num_classes)
real_classes = range(1, num_classes)
dummy_class = 0
demands = [5, 5, 5, 5, 5, 5]
capacity_constraints = [(1, 3), (1, 2), (1, 3), (2, 5), (1, 5)]
num_options = len(capacity_constraints)
all_options = range(num_options)
classes_with_option = [
[c for c in real_classes if class_options[c][o] == 1] for o in all_options
]
# --------------------
# 2. Model Creation
# --------------------
model = cp_model.CpModel()
# --------------------
# 3. Decision Variables
# --------------------
produces = {}
for c in all_classes:
for s in all_slots:
produces[(c, s)] = model.new_bool_var(f"produces_c{c}_s{s}")
makespan = model.new_int_var(num_real_cars, num_slots, "makespan")
# --------------------
# 4. Constraints
# --------------------
# Constraint 1: Only one car produced per slot.
for s in all_slots:
model.add_exactly_one([produces[(c, s)] for c in all_classes])
# Constraint 2: Meet the demand of real cars.
for i, c in enumerate(real_classes):
model.add(sum(produces[(c, s)] for s in all_slots) == demands[i])
# Constraint 3: Enforce the capacity constraints on options.
for o in all_options:
max_cars, subsequence_len = capacity_constraints[o]
for start in range(num_slots - subsequence_len + 1):
window = range(start, start + subsequence_len)
cars_with_option_in_window = []
for c in classes_with_option[o]:
for s in window:
cars_with_option_in_window.append(produces[(c, s)])
model.add(sum(cars_with_option_in_window) <= max_cars)
# Constraint 4 (Link objective and dummy cars at the end of the schedule)
for s in all_slots:
makespan_le_s = model.new_bool_var(f"makespan_le_{s}")
# Enforce makespan_le_s <=> (makespan <= s)
model.add(makespan <= s).only_enforce_if(makespan_le_s)
# Use ~ for negation
model.add(makespan > s).only_enforce_if(~makespan_le_s)
# Enforce makespan_le_s => produces[dummy_class, s]
model.add_implication(makespan_le_s, produces[dummy_class, s])
# --------------------
# 5. Objective
# --------------------
model.minimize(makespan)
# --------------------
# 6. Solve and Print Solution
# --------------------
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 30.0
solver.parameters.num_search_workers = 1 # The problem is easy to solve.
# solver.parameters.log_search_progress = True # uncomment to see the log.
status = solver.Solve(model)
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
final_makespan = int(solver.ObjectiveValue())
num_dummies_needed = final_makespan - num_real_cars
print(
f'\n{"Optimal" if status == cp_model.OPTIMAL else "Feasible"}'
f" solution found with a makespan of {final_makespan}."
)
print(
f"This requires the conceptual equivalent of {num_dummies_needed} dummy"
" car(s) to be used as spacers."
)
sequence = [-1] * num_slots
for s in all_slots:
for c in all_classes:
if solver.Value(produces[(c, s)]) == 1:
sequence[s] = c
break
print("\nFull Production Sequence (Class 0 is dummy):")
print("Slot: | " + " | ".join(f"{i:2}" for i in range(num_slots)) + " |")
print("-------|-" + "--|-" * num_slots)
print("Class: | " + " | ".join(f"{c:2}" for c in sequence) + " |")
elif status == cp_model.INFEASIBLE:
print("\nNo solution found.")
else:
print(f"\nSomething went wrong. Solver status: {status}")
print("\nSolver statistics:")
print(solver.response_stats())
def main(argv: Sequence[str]) -> None:
if len(argv) > 1:
raise app.UsageError("Too many command-line arguments.")
solve_car_sequencing_optimization()
if __name__ == "__main__":
app.run(main)