job-shop-lib 1.1.3__py3-none-any.whl → 1.3.0__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/_schedule.py +28 -2
- job_shop_lib/constraint_programming/_ortools_solver.py +71 -12
- job_shop_lib/dispatching/__init__.py +3 -0
- job_shop_lib/dispatching/_start_time_calculators.py +49 -0
- job_shop_lib/generation/_instance_generator.py +3 -3
- {job_shop_lib-1.1.3.dist-info → job_shop_lib-1.3.0.dist-info}/METADATA +1 -1
- {job_shop_lib-1.1.3.dist-info → job_shop_lib-1.3.0.dist-info}/RECORD +10 -10
- {job_shop_lib-1.1.3.dist-info → job_shop_lib-1.3.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.1.3.dist-info → job_shop_lib-1.3.0.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
job_shop_lib/_schedule.py
CHANGED
@@ -2,12 +2,15 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import Any
|
5
|
+
from typing import Any, TYPE_CHECKING
|
6
6
|
from collections import deque
|
7
7
|
|
8
8
|
from job_shop_lib import ScheduledOperation, JobShopInstance
|
9
9
|
from job_shop_lib.exceptions import ValidationError
|
10
10
|
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from job_shop_lib.dispatching import Dispatcher
|
13
|
+
|
11
14
|
|
12
15
|
class Schedule:
|
13
16
|
r"""Data structure to store a complete or partial solution for a particular
|
@@ -148,6 +151,7 @@ class Schedule:
|
|
148
151
|
def from_job_sequences(
|
149
152
|
instance: JobShopInstance,
|
150
153
|
job_sequences: list[list[int]],
|
154
|
+
dispatcher: Dispatcher | None = None,
|
151
155
|
) -> Schedule:
|
152
156
|
"""Creates an active schedule from a list of job sequences.
|
153
157
|
|
@@ -163,13 +167,27 @@ class Schedule:
|
|
163
167
|
A list of lists of job ids. Each list of job ids represents the
|
164
168
|
order of operations on the machine. The machine that the list
|
165
169
|
corresponds to is determined by the index of the list.
|
170
|
+
dispatcher:
|
171
|
+
A :class:`~job_shop_lib.dispatching.Dispatcher` to use for
|
172
|
+
scheduling. If not provided, a new dispatcher will be
|
173
|
+
created.
|
174
|
+
|
175
|
+
.. note::
|
176
|
+
You will need to provide a dispatcher if you want to
|
177
|
+
take into account start time calculators different from
|
178
|
+
the default one.
|
166
179
|
|
167
180
|
Returns:
|
168
181
|
A :class:`Schedule` object with the given job sequences.
|
182
|
+
|
183
|
+
.. seealso::
|
184
|
+
See :mod:`job_shop_lib.dispatching` for more information
|
185
|
+
about dispatchers and the start time calculators available.
|
169
186
|
"""
|
170
187
|
from job_shop_lib.dispatching import Dispatcher
|
171
188
|
|
172
|
-
dispatcher
|
189
|
+
if dispatcher is None:
|
190
|
+
dispatcher = Dispatcher(instance)
|
173
191
|
dispatcher.reset()
|
174
192
|
raw_solution_deques = [deque(job_ids) for job_ids in job_sequences]
|
175
193
|
|
@@ -307,3 +325,11 @@ class Schedule:
|
|
307
325
|
return False
|
308
326
|
|
309
327
|
return self.schedule == value.schedule
|
328
|
+
|
329
|
+
def copy(self) -> Schedule:
|
330
|
+
"""Returns a copy of the schedule."""
|
331
|
+
return Schedule(
|
332
|
+
self.instance,
|
333
|
+
[machine_schedule.copy() for machine_schedule in self.schedule],
|
334
|
+
**self.metadata,
|
335
|
+
)
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from typing import Any
|
5
|
+
from typing import Any, Sequence
|
6
6
|
import time
|
7
7
|
|
8
8
|
from ortools.sat.python import cp_model
|
@@ -64,14 +64,28 @@ class ORToolsSolver(BaseSolver):
|
|
64
64
|
self.solver = cp_model.CpSolver()
|
65
65
|
self._operations_start: dict[Operation, tuple[IntVar, IntVar]] = {}
|
66
66
|
|
67
|
-
def __call__(
|
67
|
+
def __call__(
|
68
|
+
self,
|
69
|
+
instance: JobShopInstance,
|
70
|
+
arrival_times: Sequence[Sequence[int]] | None = None,
|
71
|
+
deadlines: Sequence[Sequence[int]] | None = None,
|
72
|
+
) -> Schedule:
|
68
73
|
"""Equivalent to calling the :meth:`~ORToolsSolver.solve` method.
|
69
74
|
|
70
75
|
This method is necessary because, in JobShopLib, solvers are defined
|
71
76
|
as callables that receive an instance and return a schedule.
|
72
77
|
|
73
78
|
Args:
|
74
|
-
instance:
|
79
|
+
instance:
|
80
|
+
The job shop instance to be solved.
|
81
|
+
arrival_times:
|
82
|
+
Optional arrival times for each operation.
|
83
|
+
If provided, the solver will ensure that operations do not
|
84
|
+
start before their respective arrival times.
|
85
|
+
deadlines:
|
86
|
+
Optional deadlines for each operation.
|
87
|
+
If provided, the solver will ensure that operations are
|
88
|
+
completed before their respective deadlines.
|
75
89
|
|
76
90
|
Returns:
|
77
91
|
The best schedule found by the solver.
|
@@ -84,14 +98,30 @@ class ORToolsSolver(BaseSolver):
|
|
84
98
|
"""
|
85
99
|
# Re-defined here since we already add metadata to the schedule in
|
86
100
|
# the solve method.
|
87
|
-
return self.solve(
|
101
|
+
return self.solve(
|
102
|
+
instance, arrival_times=arrival_times, deadlines=deadlines
|
103
|
+
)
|
88
104
|
|
89
|
-
def solve(
|
105
|
+
def solve(
|
106
|
+
self,
|
107
|
+
instance: JobShopInstance,
|
108
|
+
arrival_times: Sequence[Sequence[int]] | None = None,
|
109
|
+
deadlines: Sequence[Sequence[int]] | None = None,
|
110
|
+
) -> Schedule:
|
90
111
|
"""Creates the variables, constraints and objective, and solves the
|
91
112
|
problem.
|
92
113
|
|
93
114
|
Args:
|
94
|
-
instance:
|
115
|
+
instance:
|
116
|
+
The job shop instance to be solved.
|
117
|
+
arrival_times:
|
118
|
+
Optional arrival times for each operation.
|
119
|
+
If provided, the solver will ensure that operations do not
|
120
|
+
start before their respective arrival times.
|
121
|
+
deadlines:
|
122
|
+
Optional deadlines for each operation.
|
123
|
+
If provided, the solver will ensure that operations are
|
124
|
+
completed before their respective deadlines.
|
95
125
|
|
96
126
|
Returns:
|
97
127
|
The best schedule found by the solver.
|
@@ -107,7 +137,9 @@ class ORToolsSolver(BaseSolver):
|
|
107
137
|
If no solution could be found for the given problem within the
|
108
138
|
time limit.
|
109
139
|
"""
|
110
|
-
self._initialize_model(
|
140
|
+
self._initialize_model(
|
141
|
+
instance, arrival_times=arrival_times, deadlines=deadlines
|
142
|
+
)
|
111
143
|
|
112
144
|
start_time = time.perf_counter()
|
113
145
|
status = self.solver.Solve(self.model)
|
@@ -128,7 +160,12 @@ class ORToolsSolver(BaseSolver):
|
|
128
160
|
}
|
129
161
|
return self._create_schedule(instance, metadata)
|
130
162
|
|
131
|
-
def _initialize_model(
|
163
|
+
def _initialize_model(
|
164
|
+
self,
|
165
|
+
instance: JobShopInstance,
|
166
|
+
arrival_times: Sequence[Sequence[int]] | None = None,
|
167
|
+
deadlines: Sequence[Sequence[int]] | None = None,
|
168
|
+
):
|
132
169
|
"""Initializes the model with variables, constraints and objective.
|
133
170
|
|
134
171
|
The model is initialized with two variables for each operation: start
|
@@ -147,7 +184,9 @@ class ORToolsSolver(BaseSolver):
|
|
147
184
|
self.solver.parameters.max_time_in_seconds = (
|
148
185
|
self.max_time_in_seconds
|
149
186
|
)
|
150
|
-
self._create_variables(
|
187
|
+
self._create_variables(
|
188
|
+
instance, arrival_times=arrival_times, deadlines=deadlines
|
189
|
+
)
|
151
190
|
self._add_constraints(instance)
|
152
191
|
self._set_objective(instance)
|
153
192
|
|
@@ -177,15 +216,35 @@ class ORToolsSolver(BaseSolver):
|
|
177
216
|
instance=instance, schedule=sorted_schedule, **metadata
|
178
217
|
)
|
179
218
|
|
180
|
-
def _create_variables(
|
219
|
+
def _create_variables(
|
220
|
+
self,
|
221
|
+
instance: JobShopInstance,
|
222
|
+
arrival_times: Sequence[Sequence[int]] | None = None,
|
223
|
+
deadlines: Sequence[Sequence[int]] | None = None,
|
224
|
+
):
|
181
225
|
"""Creates two variables for each operation: start and end time."""
|
182
226
|
for job in instance.jobs:
|
183
227
|
for operation in job:
|
228
|
+
# Initial Naive Bounds
|
229
|
+
lower_bound = 0
|
230
|
+
upper_bound = instance.total_duration
|
231
|
+
|
232
|
+
if arrival_times is not None:
|
233
|
+
lower_bound = arrival_times[operation.job_id][
|
234
|
+
operation.position_in_job
|
235
|
+
]
|
236
|
+
|
237
|
+
if deadlines is not None:
|
238
|
+
op_deadline = deadlines[operation.job_id][
|
239
|
+
operation.position_in_job
|
240
|
+
]
|
241
|
+
upper_bound = op_deadline
|
242
|
+
|
184
243
|
start_var = self.model.NewIntVar(
|
185
|
-
|
244
|
+
lower_bound, upper_bound, f"start_{operation}"
|
186
245
|
)
|
187
246
|
end_var = self.model.NewIntVar(
|
188
|
-
|
247
|
+
lower_bound, upper_bound, f"end_{operation}"
|
189
248
|
)
|
190
249
|
self._operations_start[operation] = (start_var, end_var)
|
191
250
|
self.model.Add(end_var == start_var + operation.duration)
|
@@ -24,6 +24,7 @@ Problem step-by-step.
|
|
24
24
|
get_matrix_setup_time_calculator
|
25
25
|
get_breakdown_calculator
|
26
26
|
get_job_dependent_setup_calculator
|
27
|
+
get_arrival_calculator
|
27
28
|
|
28
29
|
Dispatching refers to the decision-making process of selecting which job
|
29
30
|
should be processed next on a particular machine when that machine becomes
|
@@ -53,6 +54,7 @@ from ._start_time_calculators import (
|
|
53
54
|
get_breakdown_calculator,
|
54
55
|
get_job_dependent_setup_calculator,
|
55
56
|
get_matrix_setup_time_calculator,
|
57
|
+
get_arrival_calculator,
|
56
58
|
)
|
57
59
|
from ._dispatcher_observer_config import DispatcherObserverConfig
|
58
60
|
from ._factories import (
|
@@ -84,4 +86,5 @@ __all__ = [
|
|
84
86
|
"get_matrix_setup_time_calculator",
|
85
87
|
"get_breakdown_calculator",
|
86
88
|
"get_job_dependent_setup_calculator",
|
89
|
+
"get_arrival_calculator",
|
87
90
|
]
|
@@ -193,3 +193,52 @@ def get_job_dependent_setup_calculator(
|
|
193
193
|
return default_start + setup_time
|
194
194
|
|
195
195
|
return calculator
|
196
|
+
|
197
|
+
|
198
|
+
def get_arrival_calculator(
|
199
|
+
arrival_times: Sequence[Sequence[int]] | NDArray[np.integer] | None = None,
|
200
|
+
) -> StartTimeCalculator:
|
201
|
+
"""Returns a start time calculator that respects operation arrival times.
|
202
|
+
|
203
|
+
This calculator uses a predefined matrix of arrival times to
|
204
|
+
ensure that no operation begins before its specified arrival
|
205
|
+
time. If the ``arrival_times`` matrix isn't provided directly,
|
206
|
+
the calculator attempts to retrieve it from the dispatcher's
|
207
|
+
instance metadata using the key ``"arrival_times_matrix"``.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
arrival_times:
|
211
|
+
A 2D matrix where ``arrival_times[i][j]`` is the
|
212
|
+
arrival time for the operation at index ``j`` of
|
213
|
+
job ``i``. If ``None``, the calculator will
|
214
|
+
attempt to retrieve it from the dispatcher metadata.
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
A start time calculator function that uses the arrival times.
|
218
|
+
|
219
|
+
Example:
|
220
|
+
>>> arrival_calc = get_arrival_calculator([[0, 2], [1, 0]])
|
221
|
+
>>> dispatcher = Dispatcher(
|
222
|
+
... instance, start_time_calculator=arrival_calc
|
223
|
+
... )
|
224
|
+
"""
|
225
|
+
|
226
|
+
def calculator(
|
227
|
+
dispatcher: Dispatcher, operation: Operation, machine_id: int
|
228
|
+
) -> int:
|
229
|
+
default_start_time = no_setup_time_calculator(
|
230
|
+
dispatcher, operation, machine_id
|
231
|
+
)
|
232
|
+
arrival_matrix = arrival_times
|
233
|
+
if arrival_matrix is None:
|
234
|
+
arrival_matrix = dispatcher.instance.metadata.get(
|
235
|
+
"arrival_times_matrix"
|
236
|
+
)
|
237
|
+
if arrival_matrix is None:
|
238
|
+
return default_start_time
|
239
|
+
operation_arrival_time = arrival_matrix[operation.job_id][
|
240
|
+
operation.position_in_job
|
241
|
+
]
|
242
|
+
return max(default_start_time, operation_arrival_time)
|
243
|
+
|
244
|
+
return calculator
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Home of the `InstanceGenerator` class."""
|
2
2
|
|
3
|
-
import
|
3
|
+
from abc import ABC, abstractmethod
|
4
4
|
|
5
5
|
import random
|
6
6
|
from typing import Iterator
|
@@ -9,7 +9,7 @@ from job_shop_lib import JobShopInstance
|
|
9
9
|
from job_shop_lib.exceptions import UninitializedAttributeError
|
10
10
|
|
11
11
|
|
12
|
-
class InstanceGenerator(
|
12
|
+
class InstanceGenerator(ABC):
|
13
13
|
"""Common interface for all generators.
|
14
14
|
|
15
15
|
The class supports both single instance generation and iteration over
|
@@ -75,7 +75,7 @@ class InstanceGenerator(abc.ABC):
|
|
75
75
|
self._current_iteration = 0
|
76
76
|
self._iteration_limit = iteration_limit
|
77
77
|
|
78
|
-
@
|
78
|
+
@abstractmethod
|
79
79
|
def generate(
|
80
80
|
self,
|
81
81
|
num_jobs: int | None = None,
|
@@ -1,22 +1,22 @@
|
|
1
|
-
job_shop_lib/__init__.py,sha256=
|
1
|
+
job_shop_lib/__init__.py,sha256=FHzL2Kx7_4FCgaaLxzb_RdTRDQgF-CraofLgc2IdSqs,639
|
2
2
|
job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
|
3
3
|
job_shop_lib/_job_shop_instance.py,sha256=FkMBy9Yb8cNEGswI9vlN3Wh4mhtEX-QuDbKvSYUOXcM,18361
|
4
4
|
job_shop_lib/_operation.py,sha256=lwCjgXwWlgESFuV3Yh4SCVofPGCd3hJU4vnK7peREac,4235
|
5
|
-
job_shop_lib/_schedule.py,sha256=
|
5
|
+
job_shop_lib/_schedule.py,sha256=3PgDZ-DZmlESh5TASNHTqW_8Z7XPVSF64knvXEGRIbM,12927
|
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
|
9
9
|
job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMhsKstmyUmroyWduM7a00KQ,464841
|
10
10
|
job_shop_lib/constraint_programming/__init__.py,sha256=kKQRUxxS_nVFUdXGnf4bQOD9mqrXxZZWElS753A4YiA,454
|
11
|
-
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=
|
12
|
-
job_shop_lib/dispatching/__init__.py,sha256=
|
11
|
+
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=LMpfpgiU2etrtyVTKVKyZW1PVMrOG2TenWzfGuEGf2I,12710
|
12
|
+
job_shop_lib/dispatching/__init__.py,sha256=gbgY1_lhergmXaDa-VYVUmxMpOKzYko0ONREVAt_QPc,2643
|
13
13
|
job_shop_lib/dispatching/_dispatcher.py,sha256=KnV_Kry3Ie81WbKhdpRQtOMsuFDNCuh5Kp2ZnelM-R8,23835
|
14
14
|
job_shop_lib/dispatching/_dispatcher_observer_config.py,sha256=QF2d3rJWwmvutQBAkKxzQ1toJs6eMelT404LGS2z9HQ,2467
|
15
15
|
job_shop_lib/dispatching/_factories.py,sha256=j3MhIwVXiq-B8JMit72ObvXSa2sdgWNhUD86gghL6Gg,4689
|
16
16
|
job_shop_lib/dispatching/_history_observer.py,sha256=Vl8rQaxekUeEB-AyNxyC3c76zQakeh-rdri2iDnZvXw,610
|
17
17
|
job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=2EYxevjpeGMP3do-m0ZmtmjIjmNcxrWOSKzN_bW37gQ,4247
|
18
18
|
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=brhmhoyyoZ98wAEEfneZC-CD-aw9SerZHGMB1DpK8HY,5749
|
19
|
-
job_shop_lib/dispatching/_start_time_calculators.py,sha256=
|
19
|
+
job_shop_lib/dispatching/_start_time_calculators.py,sha256=N4kz3c4TmXbyFsY6ctxruYK2ucnjSVXWNMhvsUWFuDg,8192
|
20
20
|
job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=0he-j4OlvqtXAJZD5x1nuBnUKqZUfftVx9NT3CVxPyg,2708
|
21
21
|
job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
|
22
22
|
job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=tpvqTLIcNmbYROSFT62LiUZ_tI4fHWL_qCULKK43BU4,6429
|
@@ -38,7 +38,7 @@ job_shop_lib/dispatching/rules/_utils.py,sha256=m5qw4qyfaIvVrkmv51nuhreizr98-cg8
|
|
38
38
|
job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
|
39
39
|
job_shop_lib/generation/__init__.py,sha256=QaWwuBfBNnOiG0OPiP_CV_flBu9dX7r2o_HwL47tREM,822
|
40
40
|
job_shop_lib/generation/_general_instance_generator.py,sha256=b_tnyP4H_buoN7b6lKQRLvDkeZDdys0mpqS3thB5-SQ,6544
|
41
|
-
job_shop_lib/generation/_instance_generator.py,sha256=
|
41
|
+
job_shop_lib/generation/_instance_generator.py,sha256=doN6WySyI0k7wz3aKy_e6hj6t7WV3dNzve3YmTFShas,4584
|
42
42
|
job_shop_lib/generation/_utils.py,sha256=TYBGt4Zjw94l6ukIjXBVAK3lmrrZXdyzyq_r1DMlL-E,3986
|
43
43
|
job_shop_lib/graphs/__init__.py,sha256=wlYIiXTuZRE6Kx3K0RpPUoZikzoegBuN2hcdqMODtGk,2433
|
44
44
|
job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
|
@@ -67,7 +67,7 @@ job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9G
|
|
67
67
|
job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
|
68
68
|
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
|
69
69
|
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
|
70
|
-
job_shop_lib-1.
|
71
|
-
job_shop_lib-1.
|
72
|
-
job_shop_lib-1.
|
73
|
-
job_shop_lib-1.
|
70
|
+
job_shop_lib-1.3.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
71
|
+
job_shop_lib-1.3.0.dist-info/METADATA,sha256=oVgHOo9l8-crgD3w_uiR5zU6B0m9gxRAI0IPu7pSnpU,19130
|
72
|
+
job_shop_lib-1.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
73
|
+
job_shop_lib-1.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|