Skip to content

Commit f29232f

Browse files
committed
Implement permutation constraints in PyJobShop
1 parent beabeed commit f29232f

File tree

5 files changed

+80
-33
lines changed

5 files changed

+80
-33
lines changed

pyjobshop/Model.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self):
3838
self._modes: list[Mode] = []
3939
self._constraints = Constraints()
4040
self._objective: Objective = Objective(weight_makespan=1)
41+
self._permutation: bool = False
4142

4243
self._id2job: dict[int, int] = {}
4344
self._id2resource: dict[int, int] = {}
@@ -190,6 +191,7 @@ def data(self) -> ProblemData:
190191
modes=self.modes,
191192
constraints=self.constraints,
192193
objective=self.objective,
194+
permutation=self._permutation,
193195
)
194196

195197
def add_job(
@@ -428,6 +430,12 @@ def set_objective(
428430
)
429431
return self._objective
430432

433+
def set_permutation(self, value: bool = True):
434+
"""
435+
Sets the permutation property of the model.
436+
"""
437+
self._permutation = value
438+
431439
def solve(
432440
self,
433441
solver: str = "ortools",

pyjobshop/ProblemData.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,9 @@ class ProblemData:
670670
constraints.
671671
objective
672672
The objective function. Default is minimizing the makespan.
673+
permutation
674+
Whether the problem is a permutation problem. Default is
675+
``False``.
673676
"""
674677

675678
def __init__(
@@ -680,6 +683,7 @@ def __init__(
680683
modes: list[Mode],
681684
constraints: Optional[Constraints] = None,
682685
objective: Optional[Objective] = None,
686+
permutation: bool = False,
683687
):
684688
self._jobs = jobs
685689
self._resources = resources
@@ -693,6 +697,7 @@ def __init__(
693697
if objective is not None
694698
else Objective(weight_makespan=1)
695699
)
700+
self._permutation = permutation
696701

697702
self._validate_parameters()
698703

@@ -859,6 +864,13 @@ def objective(self) -> Objective:
859864
"""
860865
return self._objective
861866

867+
@property
868+
def permutation(self) -> bool:
869+
"""
870+
Returns whether the problem is a permutation problem.
871+
"""
872+
return self._permutation
873+
862874
@property
863875
def num_jobs(self) -> int:
864876
"""

pyjobshop/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
MAX_VALUE = 2**48
1+
MAX_VALUE = 2**32 # lowered for large-scale instances
22
"""int: Maximum allowed value, equal to 2^48.
33
"""

pyjobshop/solvers/cpoptimizer/Constraints.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,25 @@ def _consecutive_constraints(self):
183183

184184
model.add(cpo.previous(seq_var, var1, var2))
185185

186+
def _same_sequence_constraints(self):
187+
"""
188+
Adds same sequence constraints.
189+
"""
190+
model, data = self._model, self._data
191+
192+
if not data.permutation:
193+
return # not a permutation problem
194+
195+
for idx in range(data.num_resources - 1):
196+
seq_var1 = self._sequence_vars[idx]
197+
seq_var2 = self._sequence_vars[idx + 1]
198+
199+
if seq_var1 is None or seq_var2 is None:
200+
msg = "Requested resources do not have sequence variables."
201+
raise ValueError(msg)
202+
203+
model.add(cpo.same_sequence(seq_var1, seq_var2))
204+
186205
def add_constraints(self):
187206
"""
188207
Adds all the constraints to the CP model.
@@ -195,3 +214,4 @@ def add_constraints(self):
195214
self._timing_constraints()
196215
self._identical_and_different_resource_constraints()
197216
self._consecutive_constraints()
217+
self._same_sequence_constraints()

pyjobshop/solvers/ortools/Constraints.py

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22
from ortools.sat.python.cp_model import CpModel, LinearExpr
3+
from itertools import product
34

45
import pyjobshop.solvers.utils as utils
56
from pyjobshop.ProblemData import (
@@ -203,48 +204,54 @@ def _circuit_constraints(self):
203204
"""
204205
Creates the circuit constraints for each machine, if activated by
205206
sequencing constraints (consecutive and setup times).
207+
208+
IMPORTANT: This is specifically implemented for the experiments in the
209+
paper and it is not meant to be used outside the scope of those
210+
experiments because it may not be compatible with all other features.
206211
"""
207212
model, data = self._model, self._data
208213
setup_times = utils.setup_times_matrix(data)
209214

210-
for idx, resource in enumerate(data.resources):
211-
if not isinstance(resource, Machine):
212-
continue
215+
if not data.permutation:
216+
return # not a permutation problem, skip
213217

214-
seq_var = self._sequence_vars[idx]
215-
if not seq_var.is_active:
216-
# No sequencing constraints active. Skip the creation of
217-
# expensive circuit constraints.
218-
continue
218+
# Create arcs for circuit constraints.
219+
arcs = []
220+
for idx1 in range(data.num_jobs):
221+
arcs.append((0, idx1 + 1, model.new_bool_var("start")))
222+
arcs.append((idx1 + 1, 0, model.new_bool_var("end")))
219223

220-
mode_vars = seq_var.mode_vars
221-
arcs = seq_var.arcs
224+
lits = {}
225+
for idx1, idx2 in product(range(data.num_jobs), repeat=2):
226+
if idx1 != idx2:
227+
lit = model.new_bool_var(f"{idx1} -> {idx2}")
228+
lits[idx1, idx2] = lit
229+
arcs.append((idx1 + 1, idx2 + 1, lit))
222230

223-
graph = [(u, v, var) for (u, v), var in arcs.items()]
224-
model.add_circuit(graph)
231+
model.add_circuit(arcs)
225232

226-
for idx1, var1 in enumerate(mode_vars):
227-
# If the (dummy) self arc is selected, then the var must not
228-
# be present.
229-
model.add(arcs[idx1, idx1] <= ~var1.present)
230-
model.add(arcs[seq_var.DUMMY, seq_var.DUMMY] <= ~var1.present)
233+
for res_idx, resource in enumerate(data.resources):
234+
if not isinstance(resource, Machine):
235+
raise ValueError("Machines only in permutation problems.")
231236

232-
for idx1, var1 in enumerate(mode_vars):
233-
for idx2, var2 in enumerate(mode_vars):
234-
if idx1 == idx2:
235-
continue
237+
seq_var = self._sequence_vars[res_idx]
238+
assert seq_var is not None
239+
240+
for idx1, idx2 in product(range(data.num_jobs), repeat=2):
241+
if idx1 == idx2:
242+
continue
236243

237-
# If the arc is selected, then both tasks must be present.
238-
model.add(arcs[idx1, idx2] <= var1.present)
239-
model.add(arcs[idx1, idx2] <= var2.present)
240-
241-
setup = (
242-
setup_times[idx, var1.task_idx, var2.task_idx]
243-
if setup_times is not None
244-
else 0
245-
)
246-
expr = var1.end + setup <= var2.start
247-
model.add(expr).only_enforce_if(arcs[idx1, idx2])
244+
var1 = seq_var.mode_vars[idx1]
245+
var2 = seq_var.mode_vars[idx2]
246+
247+
lit = lits[idx1, idx2]
248+
setup = (
249+
setup_times[res_idx, var1.task_idx, var2.task_idx]
250+
if setup_times is not None
251+
else 0
252+
)
253+
expr = var1.end + setup <= var2.start
254+
model.add(expr).only_enforce_if(lit)
248255

249256
def add_constraints(self):
250257
"""

0 commit comments

Comments
 (0)