job-shop-lib 1.5.0__py3-none-any.whl → 1.6.1__py3-none-any.whl
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.
- job_shop_lib/__init__.py +1 -1
- job_shop_lib/_base_solver.py +7 -7
- job_shop_lib/_schedule.py +146 -18
- job_shop_lib/metaheuristics/__init__.py +61 -0
- job_shop_lib/metaheuristics/_job_shop_annealer.py +229 -0
- job_shop_lib/metaheuristics/_neighbor_generators.py +182 -0
- job_shop_lib/metaheuristics/_objective_functions.py +73 -0
- job_shop_lib/metaheuristics/_simulated_annealing_solver.py +163 -0
- {job_shop_lib-1.5.0.dist-info → job_shop_lib-1.6.1.dist-info}/METADATA +22 -34
- {job_shop_lib-1.5.0.dist-info → job_shop_lib-1.6.1.dist-info}/RECORD +12 -7
- {job_shop_lib-1.5.0.dist-info → job_shop_lib-1.6.1.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.5.0.dist-info → job_shop_lib-1.6.1.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
job_shop_lib/_base_solver.py
CHANGED
@@ -15,13 +15,13 @@ Solver = Callable[[JobShopInstance], Schedule]
|
|
15
15
|
class BaseSolver(abc.ABC):
|
16
16
|
"""Base class for all solvers implemented as classes.
|
17
17
|
|
18
|
-
A
|
19
|
-
|
20
|
-
classes. This class is provided as a base class for solvers
|
21
|
-
classes. It provides a default implementation of the
|
22
|
-
measures the time taken to solve the instance
|
23
|
-
schedule's metadata under the key "elapsed_time" if
|
24
|
-
|
18
|
+
A ``Solver`` is any ``Callable`` that takes a :class:`JobShopInstance` and
|
19
|
+
returns a :class:`Schedule`. Therefore, solvers can be implemented as
|
20
|
+
functions or as classes. This class is provided as a base class for solvers
|
21
|
+
implemented as classes. It provides a default implementation of the
|
22
|
+
``__call__`` method that measures the time taken to solve the instance
|
23
|
+
and stores it in the schedule's metadata under the key "elapsed_time" if
|
24
|
+
it is not alreadypresent.
|
25
25
|
"""
|
26
26
|
|
27
27
|
@abc.abstractmethod
|
job_shop_lib/_schedule.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from typing import Any, TYPE_CHECKING
|
6
6
|
from collections import deque
|
7
7
|
|
8
|
-
from job_shop_lib import ScheduledOperation, JobShopInstance
|
8
|
+
from job_shop_lib import ScheduledOperation, JobShopInstance, Operation
|
9
9
|
from job_shop_lib.exceptions import ValidationError
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
@@ -53,6 +53,19 @@ class Schedule:
|
|
53
53
|
"schedule. It can be used to store information about the "
|
54
54
|
"algorithm that generated the schedule, for example."
|
55
55
|
),
|
56
|
+
"operation_to_scheduled_operation": (
|
57
|
+
"A dictionary that maps an :class:`Operation` to its "
|
58
|
+
":class:`ScheduledOperation` in the schedule. This is used to "
|
59
|
+
"quickly find the scheduled operation associated with a given "
|
60
|
+
"operation."
|
61
|
+
),
|
62
|
+
"num_scheduled_operations": (
|
63
|
+
"The number of operations that have been scheduled so far."
|
64
|
+
),
|
65
|
+
"operation_with_latest_end_time": (
|
66
|
+
"The :class:`ScheduledOperation` with the latest end time. "
|
67
|
+
"This is used to quickly find the last operation in the schedule."
|
68
|
+
),
|
56
69
|
}
|
57
70
|
|
58
71
|
def __init__(
|
@@ -69,6 +82,25 @@ class Schedule:
|
|
69
82
|
self.instance: JobShopInstance = instance
|
70
83
|
self._schedule = schedule
|
71
84
|
self.metadata: dict[str, Any] = metadata
|
85
|
+
self.operation_to_scheduled_operation: dict[
|
86
|
+
Operation, ScheduledOperation
|
87
|
+
] = {
|
88
|
+
scheduled_op.operation: scheduled_op
|
89
|
+
for machine_schedule in schedule
|
90
|
+
for scheduled_op in machine_schedule
|
91
|
+
}
|
92
|
+
self.num_scheduled_operations = sum(
|
93
|
+
len(machine_schedule) for machine_schedule in schedule
|
94
|
+
)
|
95
|
+
self.operation_with_latest_end_time: ScheduledOperation | None = max(
|
96
|
+
(
|
97
|
+
scheduled_op
|
98
|
+
for machine_schedule in schedule
|
99
|
+
for scheduled_op in machine_schedule
|
100
|
+
),
|
101
|
+
key=lambda op: op.end_time, # type: ignore[union-attr]
|
102
|
+
default=None,
|
103
|
+
)
|
72
104
|
|
73
105
|
def __repr__(self) -> str:
|
74
106
|
return str(self.schedule)
|
@@ -84,11 +116,6 @@ class Schedule:
|
|
84
116
|
Schedule.check_schedule(new_schedule)
|
85
117
|
self._schedule = new_schedule
|
86
118
|
|
87
|
-
@property
|
88
|
-
def num_scheduled_operations(self) -> int:
|
89
|
-
"""The number of operations that have been scheduled so far."""
|
90
|
-
return sum(len(machine_schedule) for machine_schedule in self.schedule)
|
91
|
-
|
92
119
|
def to_dict(self) -> dict:
|
93
120
|
"""Returns a dictionary representation of the schedule.
|
94
121
|
|
@@ -106,15 +133,9 @@ class Schedule:
|
|
106
133
|
- **"metadata"**: A dictionary with additional information
|
107
134
|
about the schedule.
|
108
135
|
"""
|
109
|
-
job_sequences: list[list[int]] = []
|
110
|
-
for machine_schedule in self.schedule:
|
111
|
-
job_sequences.append(
|
112
|
-
[operation.job_id for operation in machine_schedule]
|
113
|
-
)
|
114
|
-
|
115
136
|
return {
|
116
137
|
"instance": self.instance.to_dict(),
|
117
|
-
"job_sequences": job_sequences,
|
138
|
+
"job_sequences": self.job_sequences(),
|
118
139
|
"metadata": self.metadata,
|
119
140
|
}
|
120
141
|
|
@@ -211,20 +232,35 @@ class Schedule:
|
|
211
232
|
)
|
212
233
|
return dispatcher.schedule
|
213
234
|
|
235
|
+
def job_sequences(self) -> list[list[int]]:
|
236
|
+
"""Returns the sequence of jobs for each machine in the schedule.
|
237
|
+
|
238
|
+
This method returns a list of lists, where each sublist contains the
|
239
|
+
job ids of the operations scheduled on that machine.
|
240
|
+
"""
|
241
|
+
job_sequences: list[list[int]] = []
|
242
|
+
for machine_schedule in self.schedule:
|
243
|
+
job_sequences.append(
|
244
|
+
[operation.job_id for operation in machine_schedule]
|
245
|
+
)
|
246
|
+
return job_sequences
|
247
|
+
|
214
248
|
def reset(self):
|
215
249
|
"""Resets the schedule to an empty state."""
|
216
250
|
self.schedule = [[] for _ in range(self.instance.num_machines)]
|
251
|
+
self.operation_to_scheduled_operation = {}
|
252
|
+
self.num_scheduled_operations = 0
|
253
|
+
self.operation_with_latest_end_time = None
|
217
254
|
|
218
255
|
def makespan(self) -> int:
|
219
256
|
"""Returns the makespan of the schedule.
|
220
257
|
|
221
258
|
The makespan is the time at which all operations are completed.
|
222
259
|
"""
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
return max_end_time
|
260
|
+
last_operation = self.operation_with_latest_end_time
|
261
|
+
if last_operation is None:
|
262
|
+
return 0
|
263
|
+
return last_operation.end_time
|
228
264
|
|
229
265
|
def is_complete(self) -> bool:
|
230
266
|
"""Returns ``True`` if all operations have been scheduled."""
|
@@ -245,10 +281,25 @@ class Schedule:
|
|
245
281
|
constraints.
|
246
282
|
"""
|
247
283
|
self._check_start_time_of_new_operation(scheduled_operation)
|
284
|
+
|
285
|
+
# Update attributes:
|
248
286
|
self.schedule[scheduled_operation.machine_id].append(
|
249
287
|
scheduled_operation
|
250
288
|
)
|
251
289
|
|
290
|
+
self.operation_to_scheduled_operation[
|
291
|
+
scheduled_operation.operation
|
292
|
+
] = scheduled_operation
|
293
|
+
|
294
|
+
self.num_scheduled_operations += 1
|
295
|
+
|
296
|
+
if (
|
297
|
+
self.operation_with_latest_end_time is None
|
298
|
+
or scheduled_operation.end_time
|
299
|
+
> self.operation_with_latest_end_time.end_time
|
300
|
+
):
|
301
|
+
self.operation_with_latest_end_time = scheduled_operation
|
302
|
+
|
252
303
|
def _check_start_time_of_new_operation(
|
253
304
|
self,
|
254
305
|
new_operation: ScheduledOperation,
|
@@ -333,3 +384,80 @@ class Schedule:
|
|
333
384
|
[machine_schedule.copy() for machine_schedule in self.schedule],
|
334
385
|
**self.metadata,
|
335
386
|
)
|
387
|
+
|
388
|
+
def critical_path(self) -> list[ScheduledOperation]:
|
389
|
+
"""Returns the critical path of the schedule.
|
390
|
+
|
391
|
+
The critical path is the longest path of dependent operations through
|
392
|
+
the schedule, which determines the makespan. This implementation
|
393
|
+
correctly identifies the path even in non-compact schedules where
|
394
|
+
idle time may exist.
|
395
|
+
|
396
|
+
It works by starting from an operation that determines the makespan
|
397
|
+
and tracing backwards, at each step choosing the predecessor (either
|
398
|
+
from the same job or the same machine) that finished latest.
|
399
|
+
"""
|
400
|
+
# 1. Start from the operation that determines the makespan
|
401
|
+
last_scheduled_op = self.operation_with_latest_end_time
|
402
|
+
if last_scheduled_op is None:
|
403
|
+
return []
|
404
|
+
|
405
|
+
critical_path = deque([last_scheduled_op])
|
406
|
+
current_scheduled_op = last_scheduled_op
|
407
|
+
|
408
|
+
machine_op_index = {}
|
409
|
+
for machine_id, schedule_list in enumerate(self.schedule):
|
410
|
+
machine_op_index[machine_id] = {op: idx for idx, op in
|
411
|
+
enumerate(schedule_list)}
|
412
|
+
|
413
|
+
# 2. Trace backwards from the last operation
|
414
|
+
while True:
|
415
|
+
job_pred: ScheduledOperation | None = None
|
416
|
+
machine_pred: ScheduledOperation | None = None
|
417
|
+
|
418
|
+
# Find job predecessor (the previous operation in the same job)
|
419
|
+
op_idx_in_job = current_scheduled_op.operation.position_in_job
|
420
|
+
if op_idx_in_job > 0:
|
421
|
+
prev_op_in_job = self.instance.jobs[
|
422
|
+
current_scheduled_op.job_id
|
423
|
+
][op_idx_in_job - 1]
|
424
|
+
job_pred = self.operation_to_scheduled_operation[
|
425
|
+
prev_op_in_job
|
426
|
+
]
|
427
|
+
|
428
|
+
# Find machine predecessor (the previous operation on the same
|
429
|
+
# machine)
|
430
|
+
machine_schedule = self.schedule[current_scheduled_op.machine_id]
|
431
|
+
op_idx_on_machine = (
|
432
|
+
machine_op_index
|
433
|
+
[current_scheduled_op.machine_id][current_scheduled_op])
|
434
|
+
if op_idx_on_machine > 0:
|
435
|
+
machine_pred = machine_schedule[
|
436
|
+
op_idx_on_machine - 1
|
437
|
+
]
|
438
|
+
|
439
|
+
# 3. Determine the critical predecessor
|
440
|
+
# The critical predecessor is the one that finished latest, as it
|
441
|
+
# determined the start time of the current operation.
|
442
|
+
|
443
|
+
if job_pred is None and machine_pred is None:
|
444
|
+
# Reached the beginning of the schedule, no more predecessors
|
445
|
+
break
|
446
|
+
|
447
|
+
job_pred_end_time = (
|
448
|
+
job_pred.end_time if job_pred is not None else -1
|
449
|
+
)
|
450
|
+
machine_pred_end_time = (
|
451
|
+
machine_pred.end_time if machine_pred is not None else -1
|
452
|
+
)
|
453
|
+
critical_pred = (
|
454
|
+
job_pred
|
455
|
+
if job_pred_end_time >= machine_pred_end_time
|
456
|
+
else machine_pred
|
457
|
+
)
|
458
|
+
assert critical_pred is not None
|
459
|
+
# Prepend the critical predecessor to the path and continue tracing
|
460
|
+
critical_path.appendleft(critical_pred)
|
461
|
+
current_scheduled_op = critical_pred
|
462
|
+
|
463
|
+
return list(critical_path)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
"""Metaheuristic algorithms for solving job shop scheduling problems.
|
2
|
+
|
3
|
+
This module provides implementations of various metaheuristic optimization
|
4
|
+
algorithms designed to solve the job shop scheduling problem.
|
5
|
+
|
6
|
+
Metaheuristics are particularly well-suited for JSSP due to their ability to:
|
7
|
+
|
8
|
+
- Handle large solution spaces efficiently
|
9
|
+
- Escape local optima through stochastic mechanisms
|
10
|
+
- Balance exploration and exploitation of the search space
|
11
|
+
- Provide good quality solutions within reasonable computational time
|
12
|
+
|
13
|
+
Currently implemented algorithms:
|
14
|
+
|
15
|
+
- Simulated annealing: A probabilistic technique that accepts worse
|
16
|
+
solutions with decreasing probability to escape local optima
|
17
|
+
|
18
|
+
The module aims to contain implementations of other
|
19
|
+
metaheuristic algorithms such as genetic algorithms, particle swarm
|
20
|
+
optimization, tabu search, etc. Feel free to open an issue if you want to
|
21
|
+
contribute!
|
22
|
+
|
23
|
+
.. autosummary::
|
24
|
+
:nosignatures:
|
25
|
+
|
26
|
+
JobShopAnnealer
|
27
|
+
SimulatedAnnealingSolver
|
28
|
+
NeighborGenerator
|
29
|
+
swap_adjacent_operations
|
30
|
+
swap_in_critical_path
|
31
|
+
swap_random_operations
|
32
|
+
ObjectiveFunction
|
33
|
+
get_makespan_with_penalties_objective
|
34
|
+
|
35
|
+
"""
|
36
|
+
|
37
|
+
from job_shop_lib.metaheuristics._objective_functions import (
|
38
|
+
ObjectiveFunction,
|
39
|
+
get_makespan_with_penalties_objective,
|
40
|
+
)
|
41
|
+
from job_shop_lib.metaheuristics._neighbor_generators import (
|
42
|
+
NeighborGenerator,
|
43
|
+
swap_adjacent_operations,
|
44
|
+
swap_in_critical_path,
|
45
|
+
swap_random_operations,
|
46
|
+
)
|
47
|
+
from job_shop_lib.metaheuristics._job_shop_annealer import JobShopAnnealer
|
48
|
+
from job_shop_lib.metaheuristics._simulated_annealing_solver import (
|
49
|
+
SimulatedAnnealingSolver,
|
50
|
+
)
|
51
|
+
|
52
|
+
__all__ = [
|
53
|
+
"JobShopAnnealer",
|
54
|
+
"SimulatedAnnealingSolver",
|
55
|
+
"NeighborGenerator",
|
56
|
+
"swap_adjacent_operations",
|
57
|
+
"swap_in_critical_path",
|
58
|
+
"swap_random_operations",
|
59
|
+
"ObjectiveFunction",
|
60
|
+
"get_makespan_with_penalties_objective",
|
61
|
+
]
|
@@ -0,0 +1,229 @@
|
|
1
|
+
import random
|
2
|
+
import math
|
3
|
+
import time
|
4
|
+
|
5
|
+
import simanneal
|
6
|
+
|
7
|
+
from job_shop_lib import JobShopInstance, Schedule
|
8
|
+
from job_shop_lib.exceptions import ValidationError
|
9
|
+
from job_shop_lib.metaheuristics import (
|
10
|
+
NeighborGenerator,
|
11
|
+
ObjectiveFunction,
|
12
|
+
swap_in_critical_path,
|
13
|
+
get_makespan_with_penalties_objective,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class JobShopAnnealer(simanneal.Annealer):
|
18
|
+
"""Helper class for the :class:`SimulatedAnnealingSolver`.
|
19
|
+
|
20
|
+
It uses `simanneal <https://github.com/perrygeo/simanneal>`_ as the
|
21
|
+
backend.
|
22
|
+
|
23
|
+
In the context of the job shop scheduling problem, simulated annealing is
|
24
|
+
particularly useful for improving previous solutions.
|
25
|
+
|
26
|
+
The neighbor move is pluggable via a ``neighbor_generator`` function. By
|
27
|
+
default it uses :func:`swap_in_critical_path`, but any function that takes
|
28
|
+
a schedule and a random generator and returns a new schedule can be
|
29
|
+
provided to tailor the exploration of the search space.
|
30
|
+
|
31
|
+
The process involves iteratively exploring the solution space:
|
32
|
+
|
33
|
+
1. A random move is made to alter the current state. This is done by
|
34
|
+
swapping two operations in the sequence of a machine.
|
35
|
+
2. The "energy" of the new state is evaluated using an objective function.
|
36
|
+
With the default objective function, the energy is calculated as the
|
37
|
+
makespan of the schedule plus penalties for any constraint violations
|
38
|
+
(such as deadlines and due dates). See
|
39
|
+
:func:`get_makespan_with_penalties_objective` for details. You can
|
40
|
+
create custom objective functions by implementing the
|
41
|
+
:class:`ObjectiveFunction` interface, which takes a schedule and returns
|
42
|
+
a float representing the energy of that schedule.
|
43
|
+
3. The new state is accepted if it has lower energy (a better solution).
|
44
|
+
If it has higher energy, it might still be accepted with a certain
|
45
|
+
probability, which depends on the current "temperature". The
|
46
|
+
temperature decreases over time, reducing the chance of accepting
|
47
|
+
worse solutions as the algorithm progresses. This helps to avoid
|
48
|
+
getting stuck in local optima.
|
49
|
+
|
50
|
+
This is repeated until the solution converges or a maximum number of
|
51
|
+
steps is reached.
|
52
|
+
|
53
|
+
Tuning the annealer is crucial for performance. The base
|
54
|
+
``simanneal.Annealer`` class provides parameters that can be adjusted:
|
55
|
+
|
56
|
+
- ``Tmax``: Maximum (starting) temperature (default: 25000.0).
|
57
|
+
- ``Tmin``: Minimum (ending) temperature (default: 2.5).
|
58
|
+
- ``steps``: Number of iterations (default: 50000).
|
59
|
+
- ``updates``: Number of progress updates (default: 100).
|
60
|
+
|
61
|
+
A good starting point is to set ``Tmax`` to a value that accepts about 98%
|
62
|
+
of moves and ``Tmin`` to a value where the solution no longer improves.
|
63
|
+
The number of ``steps`` should be large enough to explore the search space
|
64
|
+
thoroughly.
|
65
|
+
|
66
|
+
These parameters can be set on the annealer instance. For example:
|
67
|
+
``annealer.Tmax = 12000.0``
|
68
|
+
|
69
|
+
Alternatively, this class provides an ``auto`` method to find reasonable
|
70
|
+
parameters based on a desired runtime:
|
71
|
+
``auto_schedule = annealer.auto(minutes=1)``
|
72
|
+
``annealer.set_schedule(auto_schedule)``
|
73
|
+
|
74
|
+
Attributes:
|
75
|
+
instance:
|
76
|
+
The job shop instance to solve.
|
77
|
+
random_generator:
|
78
|
+
Random generator for reproducibility.
|
79
|
+
neighbor_generator:
|
80
|
+
Function used to generate neighbors from the current schedule.
|
81
|
+
Defaults to :func:`swap_in_critical_path`.
|
82
|
+
objective_function:
|
83
|
+
Function that computes the energy of the schedule. If ``None``,
|
84
|
+
it defaults to :func:`get_makespan_with_penalties_objective`.
|
85
|
+
This function receives a schedule and returns the energy that will
|
86
|
+
be minimized by the annealer.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
instance:
|
90
|
+
The job shop instance to solve. It retrieves the jobs and
|
91
|
+
machines from the instance and uses them to create the schedule.
|
92
|
+
initial_state:
|
93
|
+
Initial state of the schedule as a list of lists, where each
|
94
|
+
sublist represents the operations of a job.
|
95
|
+
seed:
|
96
|
+
Random seed for reproducibility. If ``None``, random behavior
|
97
|
+
will be non-deterministic.
|
98
|
+
neighbor_generator:
|
99
|
+
Function that receives the current schedule and a random generator
|
100
|
+
and returns a new schedule to explore. Defaults to
|
101
|
+
:func:`swap_in_critical_path`. Use this to plug in custom
|
102
|
+
neighborhoods (e.g., adjacent swaps).
|
103
|
+
objective_function:
|
104
|
+
Function that computes the energy of the schedule. If ``None``,
|
105
|
+
it defaults to :func:`get_makespan_with_penalties_objective`.
|
106
|
+
This callable receives a :class:`~job_shop_lib.Schedule` and
|
107
|
+
returns a float that will be minimized by the annealer.
|
108
|
+
"""
|
109
|
+
|
110
|
+
copy_strategy = "method"
|
111
|
+
|
112
|
+
def __init__(
|
113
|
+
self,
|
114
|
+
instance: JobShopInstance,
|
115
|
+
initial_state: Schedule,
|
116
|
+
*,
|
117
|
+
seed: int | None = None,
|
118
|
+
neighbor_generator: NeighborGenerator = swap_in_critical_path,
|
119
|
+
objective_function: ObjectiveFunction | None = None,
|
120
|
+
):
|
121
|
+
super().__init__(initial_state)
|
122
|
+
self.instance = instance
|
123
|
+
if objective_function is None:
|
124
|
+
self.objective_function = get_makespan_with_penalties_objective()
|
125
|
+
else:
|
126
|
+
self.objective_function = objective_function
|
127
|
+
self.random_generator = random.Random(seed)
|
128
|
+
self.neighbor_generator = neighbor_generator
|
129
|
+
|
130
|
+
def _get_state(self) -> Schedule:
|
131
|
+
"""Returns the current state of the annealer.
|
132
|
+
|
133
|
+
This method facilitates type checking.
|
134
|
+
"""
|
135
|
+
return self.state
|
136
|
+
|
137
|
+
def move(self) -> None:
|
138
|
+
"""Generates a neighbor state using the configured neighbor generator.
|
139
|
+
|
140
|
+
Delegates to ``self.neighbor_generator`` with the current schedule and
|
141
|
+
the internal random generator, enabling pluggable neighborhoods.
|
142
|
+
"""
|
143
|
+
self.state = self.neighbor_generator(
|
144
|
+
self._get_state(), self.random_generator
|
145
|
+
)
|
146
|
+
|
147
|
+
def anneal(self) -> tuple[Schedule, float]:
|
148
|
+
"""Minimizes the energy of a system by simulated annealing.
|
149
|
+
|
150
|
+
Overrides the ``anneal`` method from the base class to use the
|
151
|
+
random generator defined in the constructor.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
The best state and energy found during the annealing process.
|
155
|
+
"""
|
156
|
+
step = 0
|
157
|
+
self.start = time.time()
|
158
|
+
|
159
|
+
# Precompute factor for exponential cooling from Tmax to Tmin
|
160
|
+
if self.Tmin <= 0.0:
|
161
|
+
raise ValidationError(
|
162
|
+
"Exponential cooling requires a minimum "
|
163
|
+
"temperature greater than zero."
|
164
|
+
)
|
165
|
+
t_factor = -math.log(self.Tmax / self.Tmin)
|
166
|
+
|
167
|
+
# Note initial state
|
168
|
+
t = self.Tmax
|
169
|
+
current_energy = self.energy()
|
170
|
+
prev_state = self.copy_state(self.state)
|
171
|
+
prev_energy = current_energy
|
172
|
+
self.best_state = self.copy_state(self.state)
|
173
|
+
self.best_energy = current_energy
|
174
|
+
trials, accepts, improves = 0, 0, 0
|
175
|
+
update_wave_length = 0 # not used, but avoids pylint warning
|
176
|
+
if self.updates > 0:
|
177
|
+
update_wave_length = self.steps / self.updates
|
178
|
+
self.update(step, t, current_energy, None, None)
|
179
|
+
|
180
|
+
# Attempt moves to new states
|
181
|
+
while step < self.steps and not self.user_exit:
|
182
|
+
step += 1
|
183
|
+
t = self.Tmax * math.exp(t_factor * step / self.steps)
|
184
|
+
self.move()
|
185
|
+
current_energy = self.energy()
|
186
|
+
delta_e = current_energy - prev_energy
|
187
|
+
trials += 1
|
188
|
+
if (
|
189
|
+
delta_e > 0.0
|
190
|
+
and math.exp(-delta_e / t) < self.random_generator.random()
|
191
|
+
):
|
192
|
+
# Restore previous state
|
193
|
+
self.state = self.copy_state(prev_state)
|
194
|
+
current_energy = prev_energy
|
195
|
+
else:
|
196
|
+
# Accept new state and compare to best state
|
197
|
+
accepts += 1
|
198
|
+
if delta_e < 0.0:
|
199
|
+
improves += 1
|
200
|
+
prev_state = self.copy_state(self.state)
|
201
|
+
prev_energy = current_energy
|
202
|
+
if current_energy < self.best_energy:
|
203
|
+
self.best_state = self.copy_state(self.state)
|
204
|
+
self.best_energy = current_energy
|
205
|
+
if self.updates < 1:
|
206
|
+
continue
|
207
|
+
if (step // update_wave_length) > (
|
208
|
+
(step - 1) // update_wave_length
|
209
|
+
):
|
210
|
+
self.update(
|
211
|
+
step,
|
212
|
+
t,
|
213
|
+
current_energy,
|
214
|
+
accepts / trials,
|
215
|
+
improves / trials,
|
216
|
+
)
|
217
|
+
trials, accepts, improves = 0, 0, 0
|
218
|
+
|
219
|
+
self.state = self.copy_state(self.best_state)
|
220
|
+
if self.save_state_on_exit:
|
221
|
+
self.save_state()
|
222
|
+
|
223
|
+
return self.best_state, self.best_energy
|
224
|
+
|
225
|
+
def energy(self) -> float:
|
226
|
+
"""Computes the energy of the current schedule using the objective
|
227
|
+
function provided."""
|
228
|
+
schedule = self._get_state()
|
229
|
+
return self.objective_function(schedule)
|
@@ -0,0 +1,182 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import random
|
3
|
+
|
4
|
+
from job_shop_lib import Schedule, ScheduledOperation
|
5
|
+
from job_shop_lib.exceptions import ValidationError
|
6
|
+
|
7
|
+
|
8
|
+
NeighborGenerator = Callable[[Schedule, random.Random], Schedule]
|
9
|
+
|
10
|
+
_MAX_ATTEMPTS = 1000
|
11
|
+
|
12
|
+
|
13
|
+
def _swap_with_index_picker(
|
14
|
+
schedule: Schedule,
|
15
|
+
random_generator: random.Random | None,
|
16
|
+
index_picker: Callable[[list, random.Random], tuple[int, int]],
|
17
|
+
) -> Schedule:
|
18
|
+
"""Generates a neighbor schedule by swapping two positions chosen by a
|
19
|
+
strategy.
|
20
|
+
|
21
|
+
This private helper applies a swap on a randomly selected machine whose
|
22
|
+
sequence has at least two operations. The actual indices to swap are
|
23
|
+
chosen by the provided picker function. It attempts up to a fixed number
|
24
|
+
of times to produce a valid neighbor. If all attempts fail, it returns
|
25
|
+
the original schedule unchanged.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
schedule:
|
29
|
+
Current schedule to perturb.
|
30
|
+
|
31
|
+
random_generator:
|
32
|
+
Source of randomness. If ``None``, a new generator is created.
|
33
|
+
|
34
|
+
index_picker:
|
35
|
+
Function that receives a machine sequence and a random generator
|
36
|
+
and returns two indices to swap.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
A valid neighbor schedule if a feasible swap is found, otherwise the
|
40
|
+
original schedule.
|
41
|
+
"""
|
42
|
+
if random_generator is None:
|
43
|
+
random_generator = random.Random()
|
44
|
+
job_sequences = schedule.job_sequences()
|
45
|
+
valid_machines = [i for i, seq in enumerate(job_sequences) if len(seq) > 1]
|
46
|
+
if not valid_machines:
|
47
|
+
return schedule
|
48
|
+
|
49
|
+
for _ in range(_MAX_ATTEMPTS):
|
50
|
+
machine_id = random_generator.choice(valid_machines)
|
51
|
+
sequence = job_sequences[machine_id]
|
52
|
+
idx1, idx2 = index_picker(sequence, random_generator)
|
53
|
+
sequence[idx1], sequence[idx2] = sequence[idx2], sequence[idx1]
|
54
|
+
try:
|
55
|
+
return Schedule.from_job_sequences(
|
56
|
+
schedule.instance, job_sequences
|
57
|
+
)
|
58
|
+
except ValidationError:
|
59
|
+
pass
|
60
|
+
return schedule
|
61
|
+
|
62
|
+
|
63
|
+
def swap_adjacent_operations(
|
64
|
+
schedule: Schedule, random_generator: random.Random | None = None
|
65
|
+
) -> Schedule:
|
66
|
+
"""Generates a neighbor schedule by swapping two adjacent operations.
|
67
|
+
|
68
|
+
Selects a machine at random with at least two operations and swaps a pair
|
69
|
+
of adjacent operations in its sequence. Internally tries several times to
|
70
|
+
produce a valid neighbor; if none is found, the original schedule is
|
71
|
+
returned.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
schedule:
|
75
|
+
Current schedule to perturb.
|
76
|
+
|
77
|
+
random_generator:
|
78
|
+
Source of randomness. If ``None``, a new generator is created.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
A valid neighbor schedule with one adjacent swap applied, or the
|
82
|
+
original schedule if no valid swap is found.
|
83
|
+
"""
|
84
|
+
|
85
|
+
def adjacent_picker(seq: list, rng: random.Random) -> tuple[int, int]:
|
86
|
+
idx = rng.randint(0, len(seq) - 2)
|
87
|
+
return idx, idx + 1
|
88
|
+
|
89
|
+
return _swap_with_index_picker(schedule, random_generator, adjacent_picker)
|
90
|
+
|
91
|
+
|
92
|
+
def swap_random_operations(
|
93
|
+
schedule: Schedule, random_generator: random.Random | None = None
|
94
|
+
) -> Schedule:
|
95
|
+
"""Generates a neighbor schedule by swapping two random operations.
|
96
|
+
|
97
|
+
Selects a machine at random with at least two operations and swaps two
|
98
|
+
randomly chosen positions in its sequence. Internally tries several times
|
99
|
+
to produce a valid neighbor; if none is found, the original schedule is
|
100
|
+
returned.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
schedule:
|
104
|
+
Current schedule to perturb.
|
105
|
+
|
106
|
+
random_generator:
|
107
|
+
Source of randomness. If ``None``, a new generator is created.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
A valid neighbor schedule with one random swap applied, or the
|
111
|
+
original schedule if no valid swap is found.
|
112
|
+
"""
|
113
|
+
|
114
|
+
def random_picker(seq: list, rng: random.Random) -> tuple[int, int]:
|
115
|
+
idx1, idx2 = rng.sample(range(len(seq)), 2)
|
116
|
+
return idx1, idx2
|
117
|
+
|
118
|
+
return _swap_with_index_picker(schedule, random_generator, random_picker)
|
119
|
+
|
120
|
+
|
121
|
+
def swap_in_critical_path(
|
122
|
+
schedule: Schedule, random_generator: random.Random | None = None
|
123
|
+
) -> Schedule:
|
124
|
+
"""Generates a neighbor by targeting swaps on the critical path.
|
125
|
+
|
126
|
+
This operator focuses on pairs of consecutive scheduled operations along
|
127
|
+
the current critical path that share the same machine. Swapping such
|
128
|
+
operations directly perturbs the longest-duration chain of precedence
|
129
|
+
and resource constraints that determines the makespan.
|
130
|
+
|
131
|
+
Why target the critical path:
|
132
|
+
|
133
|
+
- The makespan is the length of the critical path; operations not on it
|
134
|
+
typically have slack, so reordering them often does not improve the
|
135
|
+
objective. By contrast, modifying machine order on the critical path
|
136
|
+
can shorten the longest path or unlock constraints that reduce
|
137
|
+
blocking and idle times.
|
138
|
+
- Swapping consecutive critical operations on the same machine always
|
139
|
+
results in a feasible schedule.
|
140
|
+
|
141
|
+
Behavior:
|
142
|
+
|
143
|
+
- Identifies all consecutive pairs on the critical path that run on the
|
144
|
+
same machine and swaps one of them at random.
|
145
|
+
- If no such pairs exist, it falls back to a standard adjacent swap.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
schedule:
|
149
|
+
Current schedule to perturb.
|
150
|
+
|
151
|
+
random_generator:
|
152
|
+
Source of randomness. If ``None``, a new generator is created.
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
A valid neighbor schedule that prioritizes swaps on the critical
|
156
|
+
path, or a neighbor produced by an adjacent swap fallback when none
|
157
|
+
applies.
|
158
|
+
"""
|
159
|
+
if random_generator is None:
|
160
|
+
random_generator = random.Random()
|
161
|
+
|
162
|
+
critical_path = schedule.critical_path()
|
163
|
+
possible_swaps: list[tuple[ScheduledOperation, ScheduledOperation]] = []
|
164
|
+
for i, current_scheduled_op in enumerate(critical_path[:-1]):
|
165
|
+
next_scheduled_op = critical_path[i + 1]
|
166
|
+
if current_scheduled_op.machine_id == next_scheduled_op.machine_id:
|
167
|
+
possible_swaps.append((current_scheduled_op, next_scheduled_op))
|
168
|
+
|
169
|
+
if not possible_swaps:
|
170
|
+
return swap_adjacent_operations(schedule, random_generator)
|
171
|
+
|
172
|
+
op1, op2 = random_generator.choice(possible_swaps)
|
173
|
+
job_sequences = schedule.job_sequences()
|
174
|
+
machine_id = op1.machine_id
|
175
|
+
idx1 = job_sequences[machine_id].index(op1.operation.job_id)
|
176
|
+
idx2 = job_sequences[machine_id].index(op2.operation.job_id)
|
177
|
+
|
178
|
+
job_sequences[machine_id][idx1], job_sequences[machine_id][idx2] = (
|
179
|
+
job_sequences[machine_id][idx2],
|
180
|
+
job_sequences[machine_id][idx1],
|
181
|
+
)
|
182
|
+
return Schedule.from_job_sequences(schedule.instance, job_sequences)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from job_shop_lib import Schedule
|
3
|
+
|
4
|
+
|
5
|
+
ObjectiveFunction = Callable[[Schedule], float]
|
6
|
+
|
7
|
+
|
8
|
+
def get_makespan_with_penalties_objective(
|
9
|
+
deadline_penalty_factor: float = 1_000_000,
|
10
|
+
due_date_penalty_factor: float = 100,
|
11
|
+
) -> ObjectiveFunction:
|
12
|
+
"""Builds an objective function that returns the makespan plus penalties.
|
13
|
+
|
14
|
+
This factory returns a callable that evaluates a Schedule as the sum of
|
15
|
+
its makespan and penalties for violating operation-level deadlines and due
|
16
|
+
dates.
|
17
|
+
|
18
|
+
Penalties are applied per scheduled operation that finishes after its
|
19
|
+
corresponding attribute value:
|
20
|
+
|
21
|
+
- Deadline violation: adds ``deadline_penalty_factor`` once per violating
|
22
|
+
operation (hard constraint surrogate).
|
23
|
+
- Due date violation: adds ``due_date_penalty_factor`` once per violating
|
24
|
+
operation (soft constraint surrogate).
|
25
|
+
|
26
|
+
Args:
|
27
|
+
deadline_penalty_factor:
|
28
|
+
Cost added for each operation that
|
29
|
+
finishes after its deadline. Defaults to 1_000_000.
|
30
|
+
due_date_penalty_factor:
|
31
|
+
Cost added for each operation that
|
32
|
+
finishes after its due date. Defaults to 100.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
A function ``f(schedule) -> float`` that
|
36
|
+
computes ``schedule.makespan() + penalty``.
|
37
|
+
|
38
|
+
Notes:
|
39
|
+
- Deadlines and due dates are taken from each operation. If an
|
40
|
+
operation does not define the attribute (``None``), no penalty is
|
41
|
+
applied for that attribute.
|
42
|
+
- If the instance has neither deadlines nor due dates, the objective is
|
43
|
+
simply the makespan.
|
44
|
+
"""
|
45
|
+
|
46
|
+
def objective(schedule: Schedule) -> float:
|
47
|
+
makespan = schedule.makespan()
|
48
|
+
instance = schedule.instance
|
49
|
+
|
50
|
+
# Fast path: no constraint attributes present in the instance
|
51
|
+
if not instance.has_deadlines and not instance.has_due_dates:
|
52
|
+
return makespan
|
53
|
+
|
54
|
+
penalty = 0.0
|
55
|
+
for machine_schedule in schedule.schedule:
|
56
|
+
for scheduled_op in machine_schedule:
|
57
|
+
op = scheduled_op.operation
|
58
|
+
# Deadline (hard) penalty
|
59
|
+
if (
|
60
|
+
op.deadline is not None
|
61
|
+
and scheduled_op.end_time > op.deadline
|
62
|
+
):
|
63
|
+
penalty += deadline_penalty_factor
|
64
|
+
# Due date (soft) penalty
|
65
|
+
if (
|
66
|
+
op.due_date is not None
|
67
|
+
and scheduled_op.end_time > op.due_date
|
68
|
+
):
|
69
|
+
penalty += due_date_penalty_factor
|
70
|
+
|
71
|
+
return makespan + penalty
|
72
|
+
|
73
|
+
return objective
|
@@ -0,0 +1,163 @@
|
|
1
|
+
from job_shop_lib import BaseSolver, JobShopInstance, Schedule
|
2
|
+
from job_shop_lib.metaheuristics import JobShopAnnealer
|
3
|
+
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
4
|
+
from job_shop_lib.metaheuristics import (
|
5
|
+
NeighborGenerator,
|
6
|
+
swap_in_critical_path,
|
7
|
+
ObjectiveFunction,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class SimulatedAnnealingSolver(BaseSolver):
|
12
|
+
"""Wraps the :class:`JobShopAnnealer` to follow the
|
13
|
+
:class`~job_shop_lib.BaseSolver` interface.
|
14
|
+
|
15
|
+
.. seealso::
|
16
|
+
See the documentation of the :class:`JobShopAnnealer` class for more
|
17
|
+
details on the annealing process.
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
initial_temperature:
|
21
|
+
Initial temperature for the annealing process. It controls the
|
22
|
+
probability of accepting worse solutions. That sets the metropolis
|
23
|
+
criterion. Corresponds to the `tmax` parameter in the annealer.
|
24
|
+
ending_temperature:
|
25
|
+
Ending temperature for the annealing process. It controls when to
|
26
|
+
stop accepting worse solutions. Corresponds to the `tmin` parameter
|
27
|
+
in the annealer.
|
28
|
+
steps:
|
29
|
+
Number of steps to perform in the annealing process. This is the
|
30
|
+
number of iterations the algorithm will run.
|
31
|
+
updates:
|
32
|
+
The number of progress updates to print during the annealing
|
33
|
+
process. Set to 0 to disable updates.
|
34
|
+
seed:
|
35
|
+
Random seed for reproducibility. If ``None``, random behavior will
|
36
|
+
be non-deterministic.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
initial_temperature:
|
40
|
+
Initial temperature for the annealing process. It controls the
|
41
|
+
probability of accepting worse solutions. That sets the metropolis
|
42
|
+
criterion. Corresponds to the `tmax` parameter in the annealer.
|
43
|
+
ending_temperature:
|
44
|
+
Ending temperature for the annealing process. It controls when to
|
45
|
+
stop accepting worse solutions. Corresponds to the `tmin` parameter
|
46
|
+
in the annealer.
|
47
|
+
steps:
|
48
|
+
Number of steps to perform in the annealing process. This is the
|
49
|
+
number of iterations the algorithm will run.
|
50
|
+
updates:
|
51
|
+
The number of progress updates to print during the annealing
|
52
|
+
process. Set to 0 to disable updates.
|
53
|
+
seed:
|
54
|
+
Random seed for reproducibility. If ``None``, random behavior will
|
55
|
+
be non-deterministic.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
*,
|
61
|
+
initial_temperature: float = 25000,
|
62
|
+
ending_temperature: float = 2.5,
|
63
|
+
steps: int = 50_000,
|
64
|
+
updates: int = 100,
|
65
|
+
objective_function: ObjectiveFunction | None = None,
|
66
|
+
seed: int | None = None,
|
67
|
+
neighbor_generator: NeighborGenerator = swap_in_critical_path,
|
68
|
+
):
|
69
|
+
self.initial_temperature = initial_temperature
|
70
|
+
self.ending_temperature = ending_temperature
|
71
|
+
self.steps = steps
|
72
|
+
self.updates = updates
|
73
|
+
self.objective_function = objective_function
|
74
|
+
self.seed = seed
|
75
|
+
self.neighbor_generator = neighbor_generator
|
76
|
+
self.annealer_: JobShopAnnealer | None = None
|
77
|
+
|
78
|
+
def setup_annealer(
|
79
|
+
self, instance: JobShopInstance, initial_state: Schedule | None = None
|
80
|
+
) -> None:
|
81
|
+
"""Initializes the annealer with the given instance and initial state.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
instance:
|
85
|
+
The job shop instance to solve.
|
86
|
+
initial_state:
|
87
|
+
Initial state of the schedule as a list of lists, where each
|
88
|
+
sublist represents the operations of a job.
|
89
|
+
"""
|
90
|
+
if initial_state is None:
|
91
|
+
initial_state = self._generate_initial_state(instance)
|
92
|
+
|
93
|
+
annealer = JobShopAnnealer(
|
94
|
+
instance,
|
95
|
+
initial_state,
|
96
|
+
objective_function=self.objective_function,
|
97
|
+
seed=self.seed,
|
98
|
+
neighbor_generator=self.neighbor_generator,
|
99
|
+
)
|
100
|
+
best_hparams = {
|
101
|
+
"tmax": self.initial_temperature,
|
102
|
+
"tmin": self.ending_temperature,
|
103
|
+
"steps": self.steps,
|
104
|
+
"updates": self.updates,
|
105
|
+
}
|
106
|
+
annealer.set_schedule(best_hparams)
|
107
|
+
self.annealer_ = annealer
|
108
|
+
|
109
|
+
def solve(
|
110
|
+
self,
|
111
|
+
instance: JobShopInstance,
|
112
|
+
initial_state: Schedule | None = None,
|
113
|
+
) -> Schedule:
|
114
|
+
"""Solves the given Job Shop Scheduling problem using
|
115
|
+
simulated annealing.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
instance:
|
119
|
+
The job shop problem instance to solve.
|
120
|
+
initial_state:
|
121
|
+
Initial job sequences for each machine. A job sequence is a
|
122
|
+
list of job ids. Each list of job ids represents the order of
|
123
|
+
operations on the machine. The machine that the list
|
124
|
+
corresponds to is determined by the index of the list. If
|
125
|
+
``None``, the solver will generate an initial state using the
|
126
|
+
:class:`DispatchingRuleSolver`.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
The best schedule found.
|
130
|
+
|
131
|
+
"""
|
132
|
+
self.setup_annealer(instance, initial_state)
|
133
|
+
# For type checking purposes, we assert that the annealer is set up.
|
134
|
+
assert (
|
135
|
+
self.annealer_ is not None
|
136
|
+
), "There was a problem setting up the annealer."
|
137
|
+
try:
|
138
|
+
best_state, _ = self.annealer_.anneal()
|
139
|
+
except KeyboardInterrupt:
|
140
|
+
# If the annealing process is interrupted, we return the best state
|
141
|
+
# found so far.
|
142
|
+
best_state = self.annealer_.best_state
|
143
|
+
return best_state
|
144
|
+
|
145
|
+
@staticmethod
|
146
|
+
def _generate_initial_state(instance: JobShopInstance) -> Schedule:
|
147
|
+
"""Uses the
|
148
|
+
:class:`~job_shop_lib.dispatching.rules.DispatchingRuleSolver` to
|
149
|
+
generate an initial state for the annealer.
|
150
|
+
|
151
|
+
.. note::
|
152
|
+
The first solution might be unfeasible if the job shop instance
|
153
|
+
has deadlines.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
instance (JobShopInstance): The job shop problem instance.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
An initial schedule generated by the dispatching rule solver.
|
160
|
+
"""
|
161
|
+
solver = DispatchingRuleSolver()
|
162
|
+
schedule = solver.solve(instance)
|
163
|
+
return schedule
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.6.1
|
4
4
|
Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
|
5
5
|
License: MIT
|
6
6
|
Author: Pabloo22
|
@@ -17,10 +17,10 @@ Requires-Dist: imageio[ffmpeg] (>=2.34.1,<3.0.0)
|
|
17
17
|
Requires-Dist: matplotlib (>=3,<4)
|
18
18
|
Requires-Dist: networkx (>=3,<4)
|
19
19
|
Requires-Dist: numpy (>=1.26.4,<3.0.0)
|
20
|
-
Requires-Dist: ortools (>=9.9,<
|
21
|
-
Requires-Dist: ortools (>=9.9,<9.13) ; sys_platform == "darwin"
|
20
|
+
Requires-Dist: ortools (>=9.9,<9.13)
|
22
21
|
Requires-Dist: pyarrow (>=15,<21)
|
23
22
|
Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
|
23
|
+
Requires-Dist: simanneal (>=0.5.0,<0.6.0)
|
24
24
|
Description-Content-Type: text/markdown
|
25
25
|
|
26
26
|
<div align="center">
|
@@ -42,49 +42,34 @@ JobShopLib is a Python package for creating, solving, and visualizing job shop s
|
|
42
42
|
|
43
43
|
It follows a modular design, allowing users to easily extend the library with new solvers, dispatching rules, visualization functions, etc.
|
44
44
|
|
45
|
-
|
45
|
+
We support multiple solvers, including:
|
46
|
+
- **Constraint Programming**: Based on OR-Tools' CP-SAT solver. It supports **release dates, deadlines, and due dates.** See the ["Solving the Problem" tutorial](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/02-Solving-the-Problem.ipynb) for an example.
|
47
|
+
- **Dispatching Rules**: A set of predefined rules and the ability to create custom ones. They support arbitrary **setup times, machine breakdowns, release dates, deadlines, and due dates**. See the [following example](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/03-Dispatching-Rules.ipynb). You can also create videos or GIFs of the scheduling process. For creating GIFs or videos, see the [Save Gif example](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/04-Save-Gif.ipynb).
|
48
|
+
- **Metaheuristics**: Currently, we have a **simulated annealing** implementation that supports **release dates, deadlines, and due dates**. We also support arbitrary neighborhood search strategies, including swapping operations in the critical path as described in the paper "Job Shop Scheduling by Simulated Annealing" by van Laarhoven et al. (1992); and energy functions. See our [simulated annealing tutorial](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/03-Simulated-Annealing.ipynb).
|
49
|
+
- **Reinforcement Learning**: Two Gymnasium environments for solving the problem with **graph neural networks** (GNNs) or any other method. The environments support **setup times, release dates, deadlines, and due dates.** We're currently building a tutorial on how to use them.
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
We also provide useful utilities, data structures, and visualization functions:
|
52
|
+
- **Intuitive Data Structures**: Easily create, manage, and manipulate job shop instances and solutions with user-friendly data structures. See [Getting Started](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/00-Getting-Started.ipynb) and [How Solutions are Represented](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/01-How-Solutions-are-Represented.ipynb).
|
53
|
+
- **Benchmark Instances**: Load well-known benchmark instances directly from the library without manual downloading. See [Load Benchmark Instances](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/05-Load-Benchmark-Instances.ipynb).
|
54
|
+
- **Random Instance Generation**: Create random instances with customizable sizes and properties. See [`generation`](https://job-shop-lib.readthedocs.io/en/stable/api/job_shop_lib.generation.html#module-job_shop_lib.generation) module.
|
55
|
+
- **Gantt Charts**: Visualize final schedules and how they are created iteratively by dispatching rule solvers or sequences of scheduling decisions with GIFs or videos.
|
56
|
+
- **Graph Representations**: Represent and visualize instances as disjunctive graphs or agent-task graphs (introduced in the ScheduleNet paper). Build your own custom graphs with the `JobShopGraph` class. See the [Disjunctive Graphs](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/04-Disjunctive-Graphs.ipynb) and [Resource Task Graphs](https://job-shop-lib.readthedocs.io/en/stable/examples/07-Resource-Task-Graph.html) examples.
|
50
57
|
|
51
58
|
## Installation :package:
|
52
59
|
|
53
60
|
<!-- start installation -->
|
54
61
|
|
55
|
-
JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/). You can install the latest stable version using `pip`:
|
56
|
-
|
57
62
|
```bash
|
58
63
|
pip install job-shop-lib
|
59
64
|
```
|
60
65
|
|
61
|
-
|
62
|
-
|
63
|
-
<!-- key features -->
|
64
|
-
|
65
|
-
## Key Features :star:
|
66
|
-
|
67
|
-
- **Data Structures**: Easily create, manage, and manipulate job shop instances and solutions with user-friendly data structures. See [Getting Started](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/00-Getting-Started.ipynb) and [How Solutions are Represented](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/01-How-Solutions-are-Represented.ipynb).
|
68
|
-
|
69
|
-
- **Benchmark Instances**: Load well-known benchmark instances directly from the library without manual downloading. See [Load Benchmark Instances](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/05-Load-Benchmark-Instances.ipynb).
|
70
|
-
|
71
|
-
- **Random Instance Generation**: Create random instances with customizable sizes and properties. See [`generation`](job_shop_lib/generation) package.
|
72
|
-
|
73
|
-
- **Multiple Solvers**:
|
74
|
-
- **Constraint Programming Solver**: OR-Tools' CP-SAT solver. See [Solving the Problem](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/02-Solving-the-Problem.ipynb).
|
75
|
-
|
76
|
-
- **Dispatching Rule Solvers**: Use any of the available dispatching rules or create custom ones. See [Dispatching Rules](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/03-Dispatching-Rules.ipynb).
|
77
|
-
|
78
|
-
- **Gantt Charts**: Visualize final schedules and how are they created iteratively by dispatching rule solvers or sequences of scheduling decisions with GIFs or videos. See [Save Gif](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/06-Save-Gif.ipynb).
|
79
|
-
|
80
|
-
- **Graph Representations**:
|
81
|
-
- **Disjunctive Graphs**: Represent and visualize instances as disjunctive graphs. See [Disjunctive Graph](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/04-Disjunctive-Graph.ipynb).
|
82
|
-
- **Agent-Task Graphs**: Encode instances as agent-task graphs (introduced in [ScheduleNet paper](https://arxiv.org/abs/2106.03051)). See [Agent-Task Graph](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/07-Agent-Task-Graph.ipynb).
|
83
|
-
- Build your own custom graphs with the `JobShopGraph` class.
|
66
|
+
or
|
84
67
|
|
85
|
-
|
68
|
+
```bash
|
69
|
+
poetry add job-shop-lib
|
70
|
+
```
|
86
71
|
|
87
|
-
<!-- end
|
72
|
+
<!-- end installation -->
|
88
73
|
|
89
74
|
## Publication :scroll:
|
90
75
|
|
@@ -219,6 +204,7 @@ A dispatching rule is a heuristic guideline used to prioritize and sequence jobs
|
|
219
204
|
```python
|
220
205
|
class DispatchingRule(str, Enum):
|
221
206
|
SHORTEST_PROCESSING_TIME = "shortest_processing_time"
|
207
|
+
LARGEST_PROCESSING_TIME = "largest_processing_time"
|
222
208
|
FIRST_COME_FIRST_SERVED = "first_come_first_served"
|
223
209
|
MOST_WORK_REMAINING = "most_work_remaining"
|
224
210
|
MOST_OPERATION_REMAINING = "most_operation_remaining"
|
@@ -432,6 +418,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
432
418
|
|
433
419
|
## References :books:
|
434
420
|
|
421
|
+
- Peter J. M. van Laarhoven, Emile H. L. Aarts, Jan Karel Lenstra, (1992) Job Shop Scheduling by Simulated Annealing. Operations Research 40(1):113-125.
|
422
|
+
|
435
423
|
- J. Adams, E. Balas, and D. Zawack, "The shifting bottleneck procedure
|
436
424
|
for job shop scheduling," Management Science, vol. 34, no. 3,
|
437
425
|
pp. 391–401, 1988.
|
@@ -1,8 +1,8 @@
|
|
1
|
-
job_shop_lib/__init__.py,sha256=
|
2
|
-
job_shop_lib/_base_solver.py,sha256=
|
1
|
+
job_shop_lib/__init__.py,sha256=_kA4WyA9KQUW4ZXB4anAXrhQfvhrLj38YLLHovGvFfA,639
|
2
|
+
job_shop_lib/_base_solver.py,sha256=8CCSiA2-DegCKRXhMw7yYyI8iPauTSuLku2LQ8dU-9U,1382
|
3
3
|
job_shop_lib/_job_shop_instance.py,sha256=_92orxdi70645J7cQlRE1I0PebvpHRCP6958q9j2h18,24261
|
4
4
|
job_shop_lib/_operation.py,sha256=JI5WjvRXNBeSpPOv3ZwSrUJ4jsVDJYKfMaDHYOaFYts,5945
|
5
|
-
job_shop_lib/_schedule.py,sha256=
|
5
|
+
job_shop_lib/_schedule.py,sha256=jvidw6iIh05QGe2OeA6JkiQuCzLoOtgqny8zj95_sGA,18173
|
6
6
|
job_shop_lib/_scheduled_operation.py,sha256=czrGr87EOTlO2NPolIN5CDigeiCzvQEyra5IZPwSFZc,2801
|
7
7
|
job_shop_lib/benchmarking/__init__.py,sha256=JPnCw5mK7sADAW0HctVKHEDRw22afp9caNh2eUS36Ys,3290
|
8
8
|
job_shop_lib/benchmarking/_load_benchmark.py,sha256=-cgyx0Kn6uAc3KdGFSQb6eUVQjQggmpVKOH9qusNkXI,2930
|
@@ -52,6 +52,11 @@ job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py,sha256=-t0T8W-J
|
|
52
52
|
job_shop_lib/graphs/graph_updaters/_graph_updater.py,sha256=j1f7iWsa62GVszK2BPaMxnKBCEGWa9owm8g4VWUje8w,1967
|
53
53
|
job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py,sha256=9NG3pu7Z5h-ZTfX8rRiZbI_NfNQi80h-XUHainshjZY,6064
|
54
54
|
job_shop_lib/graphs/graph_updaters/_utils.py,sha256=sdw2Vo75P9c6Fy-YBlfgpXb9gPwHUluTB1E-9WINm_g,730
|
55
|
+
job_shop_lib/metaheuristics/__init__.py,sha256=z7bHN5vP-ctOSL0eUYK-aRyhRkU2lrDyA4kBs15EME0,1884
|
56
|
+
job_shop_lib/metaheuristics/_job_shop_annealer.py,sha256=Ty4SLPZh1NrL-XRqU76EeN8fwUdKfqbphqfYEDje1lQ,9195
|
57
|
+
job_shop_lib/metaheuristics/_neighbor_generators.py,sha256=3RePlnYvJdpdhObmf0m_3NhyUM7avfNr4vOZT0PWTRQ,6563
|
58
|
+
job_shop_lib/metaheuristics/_objective_functions.py,sha256=GG5M3LoLnNzo1zxzfpNMvo4bdYlqWuhVA8mIkXFsxxM,2607
|
59
|
+
job_shop_lib/metaheuristics/_simulated_annealing_solver.py,sha256=EMCrFl2zzJubrvCMi5upm8lnUgtBizhZbi4EvbnIsM4,6200
|
55
60
|
job_shop_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
61
|
job_shop_lib/reinforcement_learning/__init__.py,sha256=sAVgxylKfBnn2rrz0BFcab1kjvQQ1h-hgldfbkPF--E,1537
|
57
62
|
job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=6nXw67Tfmim3LqlSuQ9Cfg3mMY-VmbMHuXfyOL90jng,15740
|
@@ -68,7 +73,7 @@ job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9G
|
|
68
73
|
job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
|
69
74
|
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
|
70
75
|
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
|
71
|
-
job_shop_lib-1.
|
72
|
-
job_shop_lib-1.
|
73
|
-
job_shop_lib-1.
|
74
|
-
job_shop_lib-1.
|
76
|
+
job_shop_lib-1.6.1.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
77
|
+
job_shop_lib-1.6.1.dist-info/METADATA,sha256=4cuNTeLFoaVOMBqGvymRzwXKgTHQSt3ZgQ-iKr0Xs1s,19159
|
78
|
+
job_shop_lib-1.6.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
79
|
+
job_shop_lib-1.6.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|