job-shop-lib 1.0.3__py3-none-any.whl → 1.1.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 CHANGED
@@ -19,7 +19,7 @@ from job_shop_lib._schedule import Schedule
19
19
  from job_shop_lib._base_solver import BaseSolver, Solver
20
20
 
21
21
 
22
- __version__ = "1.0.3"
22
+ __version__ = "1.1.0"
23
23
 
24
24
  __all__ = [
25
25
  "Operation",
job_shop_lib/_schedule.py CHANGED
@@ -121,7 +121,23 @@ class Schedule:
121
121
  job_sequences: list[list[int]],
122
122
  metadata: dict[str, Any] | None = None,
123
123
  ) -> Schedule:
124
- """Creates a schedule from a dictionary representation."""
124
+ """Creates a schedule from a dictionary representation.
125
+
126
+ Args:
127
+ instance:
128
+ The instance to create the schedule for. Can be a dictionary
129
+ representation of a :class:`JobShopInstance` or a
130
+ :class:`JobShopInstance` object.
131
+ job_sequences:
132
+ A list of lists of job ids. Each list of job ids represents the
133
+ order of operations on the machine. The machine that the list
134
+ corresponds to is determined by the index of the list.
135
+ metadata:
136
+ A dictionary with additional information about the schedule.
137
+
138
+ Returns:
139
+ A :class:`Schedule` object with the given job sequences.
140
+ """
125
141
  if isinstance(instance, dict):
126
142
  instance = JobShopInstance.from_matrices(**instance)
127
143
  schedule = Schedule.from_job_sequences(instance, job_sequences)
@@ -157,7 +173,7 @@ class Schedule:
157
173
  dispatcher.reset()
158
174
  raw_solution_deques = [deque(job_ids) for job_ids in job_sequences]
159
175
 
160
- while not dispatcher.schedule.is_complete():
176
+ while any(job_seq for job_seq in raw_solution_deques):
161
177
  at_least_one_operation_scheduled = False
162
178
  for machine_id, job_ids in enumerate(raw_solution_deques):
163
179
  if not job_ids:
@@ -18,13 +18,24 @@ Problem step-by-step.
18
18
  filter_non_idle_machines
19
19
  filter_non_immediate_operations
20
20
  create_composite_operation_filter
21
+ StartTimeCalculator
22
+ no_setup_time_calculator
23
+ get_machine_dependent_setup_time_calculator
24
+ get_matrix_setup_time_calculator
25
+ get_breakdown_calculator
26
+ get_job_dependent_setup_calculator
21
27
 
22
28
  Dispatching refers to the decision-making process of selecting which job
23
29
  should be processed next on a particular machine when that machine becomes
24
30
  available.
25
31
  """
26
32
 
27
- from ._dispatcher import Dispatcher, DispatcherObserver
33
+ from ._dispatcher import (
34
+ Dispatcher,
35
+ DispatcherObserver,
36
+ StartTimeCalculator,
37
+ no_setup_time_calculator,
38
+ )
28
39
  from ._history_observer import (
29
40
  HistoryObserver,
30
41
  )
@@ -37,6 +48,12 @@ from ._ready_operation_filters import (
37
48
  filter_non_idle_machines,
38
49
  filter_non_immediate_operations,
39
50
  )
51
+ from ._start_time_calculators import (
52
+ get_machine_dependent_setup_time_calculator,
53
+ get_breakdown_calculator,
54
+ get_job_dependent_setup_calculator,
55
+ get_matrix_setup_time_calculator,
56
+ )
40
57
  from ._dispatcher_observer_config import DispatcherObserverConfig
41
58
  from ._factories import (
42
59
  ReadyOperationsFilterType,
@@ -48,6 +65,7 @@ from ._factories import (
48
65
  __all__ = [
49
66
  "Dispatcher",
50
67
  "DispatcherObserver",
68
+ "StartTimeCalculator",
51
69
  "HistoryObserver",
52
70
  "UnscheduledOperationsObserver",
53
71
  "OptimalOperationsObserver",
@@ -60,4 +78,10 @@ __all__ = [
60
78
  "filter_non_idle_machines",
61
79
  "filter_non_immediate_operations",
62
80
  "create_composite_operation_filter",
81
+ "no_setup_time_calculator",
82
+ "get_machine_dependent_setup_time_calculator",
83
+ "StartTimeCalculator",
84
+ "get_matrix_setup_time_calculator",
85
+ "get_breakdown_calculator",
86
+ "get_job_dependent_setup_calculator",
63
87
  ]
@@ -16,6 +16,34 @@ from job_shop_lib import (
16
16
  from job_shop_lib.exceptions import ValidationError
17
17
 
18
18
 
19
+ # Type alias for start time calculator callable
20
+ StartTimeCalculator = Callable[["Dispatcher", Operation, int], int]
21
+
22
+
23
+ def no_setup_time_calculator(
24
+ dispatcher: Dispatcher, operation: Operation, machine_id: int
25
+ ) -> int:
26
+ """Default start time calculator that implements the standard behavior.
27
+
28
+ The start time is the maximum of the next available time for the
29
+ machine and the next available time for the job to which the
30
+ operation belongs.
31
+
32
+ Args:
33
+ dispatcher: The dispatcher instance.
34
+ operation: The operation to be scheduled.
35
+ machine_id: The id of the machine on which the operation is to be
36
+ scheduled.
37
+
38
+ Returns:
39
+ The start time for the operation on the given machine.
40
+ """
41
+ return max(
42
+ dispatcher.machine_next_available_time[machine_id],
43
+ dispatcher.job_next_available_time[operation.job_id],
44
+ )
45
+
46
+
19
47
  # Added here to avoid circular imports
20
48
  class DispatcherObserver(abc.ABC):
21
49
  """Abstract class that allows objects to observe and respond to changes
@@ -192,6 +220,14 @@ class Dispatcher:
192
220
  list of operations as input and return a list of operations
193
221
  that are ready to be scheduled. If ``None``, no filtering is
194
222
  done.
223
+ start_time_calculator:
224
+ A function that calculates the start time for a given operation
225
+ on a given machine. The function should take a dispatcher, an
226
+ operation and a machine id as input and return the start time for
227
+ the operation. Defaults to the standard calculation which is the
228
+ maximum of the machine's next available time and the job's next
229
+ available time. This allows for implementing context-dependent
230
+ setup times, downtime, and machine breakdowns.
195
231
  """
196
232
 
197
233
  __slots__ = {
@@ -204,6 +240,10 @@ class Dispatcher:
204
240
  "A function that filters out operations that are not ready to be "
205
241
  "scheduled."
206
242
  ),
243
+ "start_time_calculator": (
244
+ "A function that calculates the start time for a given operation "
245
+ "on a given machine."
246
+ ),
207
247
  "subscribers": "A list of observers subscribed to the dispatcher.",
208
248
  "_cache": "A dictionary to cache the results of the cached methods.",
209
249
  }
@@ -214,11 +254,15 @@ class Dispatcher:
214
254
  ready_operations_filter: (
215
255
  Callable[[Dispatcher, list[Operation]], list[Operation]] | None
216
256
  ) = None,
257
+ start_time_calculator: StartTimeCalculator = (
258
+ no_setup_time_calculator
259
+ ),
217
260
  ) -> None:
218
261
 
219
262
  self.instance = instance
220
263
  self.schedule = Schedule(self.instance)
221
264
  self.ready_operations_filter = ready_operations_filter
265
+ self.start_time_calculator = start_time_calculator
222
266
  self.subscribers: list[DispatcherObserver] = []
223
267
 
224
268
  self._machine_next_available_time = [0] * self.instance.num_machines
@@ -327,7 +371,8 @@ class Dispatcher:
327
371
  """Computes the start time for the given operation on the given
328
372
  machine.
329
373
 
330
- The start time is the maximum of the next available time for the
374
+ Uses the configured start time calculator to compute the start time.
375
+ By default, this is the maximum of the next available time for the
331
376
  machine and the next available time for the job to which the
332
377
  operation belongs.
333
378
 
@@ -338,10 +383,7 @@ class Dispatcher:
338
383
  The id of the machine on which the operation is to be
339
384
  scheduled.
340
385
  """
341
- return max(
342
- self._machine_next_available_time[machine_id],
343
- self._job_next_available_time[operation.job_id],
344
- )
386
+ return self.start_time_calculator(self, operation, machine_id)
345
387
 
346
388
  def _update_tracking_attributes(
347
389
  self, scheduled_operation: ScheduledOperation
@@ -0,0 +1,195 @@
1
+ """Predefined start time calculator functions for the Dispatcher class.
2
+
3
+ This module contains commonly used start time calculators that can be used
4
+ with the Dispatcher class to implement different scheduling behaviors.
5
+ """
6
+
7
+ from collections.abc import Sequence
8
+ import numpy as np
9
+ from numpy.typing import NDArray
10
+
11
+ from job_shop_lib import Operation
12
+ from job_shop_lib.dispatching import (
13
+ Dispatcher,
14
+ no_setup_time_calculator,
15
+ StartTimeCalculator,
16
+ )
17
+
18
+
19
+ def get_matrix_setup_time_calculator(
20
+ setup_times: Sequence[Sequence[int]] | NDArray[np.integer],
21
+ ) -> StartTimeCalculator:
22
+ """Returns a start time calculator that adds setup times based on a
23
+ matrix of setup times.
24
+
25
+ Args:
26
+ setup_times:
27
+ A 2D matrix where setup_times[i][j] is the setuptime
28
+ for switching from operation ``i`` to operation ``j``. Here,
29
+ ``i`` and ``j`` are operation's IDs.
30
+ Returns:
31
+ A start time calculator function that adds setup times based on the
32
+ matrix.
33
+
34
+ Example:
35
+ >>> setup_calc = get_matrix_setup_time_calculator([[0, 2], [1, 0]])
36
+ >>> dispatcher = Dispatcher(instance, start_time_calculator=setup_calc)
37
+ """
38
+
39
+ def calculator(
40
+ dispatcher: Dispatcher, operation: Operation, machine_id: int
41
+ ) -> int:
42
+ default_start = no_setup_time_calculator(
43
+ dispatcher, operation, machine_id
44
+ )
45
+ machine_schedule = dispatcher.schedule.schedule[machine_id]
46
+ if not machine_schedule:
47
+ return default_start
48
+
49
+ last_operation = machine_schedule[-1].operation
50
+ setup_time = setup_times[last_operation.operation_id][
51
+ operation.operation_id
52
+ ]
53
+
54
+ return default_start + setup_time
55
+
56
+ return calculator
57
+
58
+
59
+ def get_machine_dependent_setup_time_calculator(
60
+ setup_times: dict[int, int], default: int = 0
61
+ ):
62
+ """Returns a start time calculator that adds setup times based on
63
+ machine IDs.
64
+
65
+ Args:
66
+ setup_times:
67
+ Dictionary mapping machine_id to setup time in time units.
68
+ default:
69
+ Default setup time to use if a machine_id is not found
70
+ in the setup_times dictionary.
71
+
72
+ Returns:
73
+ A start time calculator function that adds setup times.
74
+
75
+ Example:
76
+ >>> setup_calc = get_machine_dependent_setup_time_calculator(
77
+ ... {0: 2, 1: 1, 2: 3}
78
+ ... )
79
+ >>> dispatcher = Dispatcher(instance, start_time_calculator=setup_calc)
80
+ """
81
+
82
+ def calculator(
83
+ dispatcher: Dispatcher, operation: Operation, machine_id: int
84
+ ) -> int:
85
+ default_start = no_setup_time_calculator(
86
+ dispatcher, operation, machine_id
87
+ )
88
+ setup_time = setup_times.get(machine_id, default)
89
+ return default_start + setup_time
90
+
91
+ return calculator
92
+
93
+
94
+ def get_breakdown_calculator(breakdowns: dict[int, list[tuple[int, int]]]):
95
+ """Returns a start time calculator that accounts for machine breakdowns.
96
+
97
+ This calculator adjusts the start time of an operation based on
98
+ when the machine is expected to be down due to breakdowns, maintenance,
99
+ holidays, etc.
100
+
101
+ Args:
102
+ breakdowns: Dictionary mapping machine_id to list of
103
+ (start_time, duration) tuples representing when
104
+ the machine breaks down.
105
+
106
+ Returns:
107
+ A start time calculator function that accounts for breakdowns.
108
+
109
+ Example:
110
+ >>> breakdown_calc = breakdown_calculator({0: [(5, 3)], 1: [(8, 2)]})
111
+ >>> dispatcher = Dispatcher(instance,
112
+ ... start_time_calculator=breakdown_calc)
113
+ """
114
+
115
+ def calculator(
116
+ dispatcher: Dispatcher, operation: Operation, machine_id: int
117
+ ) -> int:
118
+ default_start = no_setup_time_calculator(
119
+ dispatcher, operation, machine_id
120
+ )
121
+ if machine_id not in breakdowns:
122
+ return default_start
123
+
124
+ start_time = default_start
125
+ for breakdown_start, breakdown_duration in breakdowns[machine_id]:
126
+ breakdown_end = breakdown_start + breakdown_duration
127
+
128
+ start_during_breakdown = (
129
+ breakdown_start <= start_time < breakdown_end
130
+ )
131
+ completion_time = start_time + operation.duration
132
+ run_into_breakdown = start_time < breakdown_start < completion_time
133
+ if start_during_breakdown or run_into_breakdown:
134
+ start_time = breakdown_end
135
+
136
+ return start_time
137
+
138
+ return calculator
139
+
140
+
141
+ def get_job_dependent_setup_calculator(
142
+ same_job_setup: int = 0,
143
+ different_job_setup: int = 4,
144
+ initial_setup: int = 0,
145
+ ):
146
+ """Factory for a calculator with sequence-dependent setup times.
147
+
148
+ This calculator determines setup time based on whether the current
149
+ operation's job matches the job of the previously processed operation
150
+ on the same machine.
151
+
152
+ Args:
153
+ same_job_setup:
154
+ Setup time when the previous operation on the
155
+ machine was from the same job.
156
+ different_job_setup:
157
+ Setup time when the previous operation
158
+ on the machine was from a different job.
159
+ initial_setup:
160
+ Setup time for the first operation on a machine
161
+ or if the machine is currently idle.
162
+
163
+ Returns:
164
+ A start time calculator function that applies
165
+ sequence-dependent setup times.
166
+
167
+ Example:
168
+ >>> seq_dep_calc = sequence_dependent_setup_calculator(
169
+ ... same_job_setup=1, different_job_setup=4, initial_setup=1
170
+ ... )
171
+ >>> # Assuming 'instance' is a JobShopInstance
172
+ >>> dispatcher = Dispatcher(
173
+ ... instance, start_time_calculator=seq_dep_calc
174
+ ... )
175
+ """
176
+
177
+ def calculator(
178
+ dispatcher: Dispatcher, operation: Operation, machine_id: int
179
+ ) -> int:
180
+ default_start = no_setup_time_calculator(
181
+ dispatcher, operation, machine_id
182
+ )
183
+ machine_schedule = dispatcher.schedule.schedule[machine_id]
184
+ if not machine_schedule:
185
+ setup_time = initial_setup
186
+ else:
187
+ last_operation_on_machine = machine_schedule[-1].operation
188
+ if last_operation_on_machine.job_id == operation.job_id:
189
+ setup_time = same_job_setup
190
+ else:
191
+ setup_time = different_job_setup
192
+
193
+ return default_start + setup_time
194
+
195
+ return calculator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 1.0.3
3
+ Version: 1.1.0
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
@@ -50,9 +50,7 @@ See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcoww
50
50
 
51
51
  <!-- start installation -->
52
52
 
53
- JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/) and it supports Python 3.10+.
54
-
55
- You can install the latest stable version using `pip`:
53
+ JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/). You can install the latest stable version using `pip`:
56
54
 
57
55
  ```bash
58
56
  pip install job-shop-lib
@@ -68,7 +66,7 @@ pip install job-shop-lib
68
66
 
69
67
  - **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
68
 
71
- - **Random Instance Generation**: Create random instances with customizable sizes and properties or augment existing ones. See [`generation`](job_shop_lib/generation) package.
69
+ - **Random Instance Generation**: Create random instances with customizable sizes and properties. See [`generation`](job_shop_lib/generation) package.
72
70
 
73
71
  - **Multiple Solvers**:
74
72
  - **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).
@@ -134,7 +132,7 @@ The contributions to this benchmark dataset are as follows:
134
132
 
135
133
  - `orb01-10`: by Applegate and Cook (1991).
136
134
 
137
- - `swb01-20`: by Storer et al. (1992).
135
+ - `swv01-20`: by Storer et al. (1992).
138
136
 
139
137
  - `yn1-4`: by Yamada and Nakano (1992).
140
138
 
@@ -1,21 +1,22 @@
1
- job_shop_lib/__init__.py,sha256=LO5qbdFVxGXYyTatsF_9qx-gZ53kyr2x5MUwaMqaLNw,639
1
+ job_shop_lib/__init__.py,sha256=GsvMMVRVHzH1Qy7eKfNKp87NNRO8TBE-asEgZF7TIaA,639
2
2
  job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
3
3
  job_shop_lib/_job_shop_instance.py,sha256=RWibh5_lVTHj0DEkMw94O4jYZhwRReYYpVVIbT0cYtQ,18353
4
4
  job_shop_lib/_operation.py,sha256=lwCjgXwWlgESFuV3Yh4SCVofPGCd3hJU4vnK7peREac,4235
5
- job_shop_lib/_schedule.py,sha256=QQ7orbpd00pTjJvsh8bNuRSEFJLst8B8GaCSB8JPXTY,11251
5
+ job_shop_lib/_schedule.py,sha256=pY02c-VF7cHxR29dFAktrn32mFi0cmRIcDAGsoekK8g,11941
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
11
  job_shop_lib/constraint_programming/_ortools_solver.py,sha256=oMPeA2VHoYX1ZvmygQ8kYew40ITLAQATmM4OhgVFuXM,10482
12
- job_shop_lib/dispatching/__init__.py,sha256=dGctOuTVkj0eLfmRM1Us5x7ZtnUvtJC_M7lnZpZrxo0,1835
13
- job_shop_lib/dispatching/_dispatcher.py,sha256=6LmobUuE8-tPSAmfsoSiOjarPk5AIcaJY5it0Jxr2Eo,22006
12
+ job_shop_lib/dispatching/__init__.py,sha256=eyiCpCWIsx3LDoSOtPImjYAkI6R8t93kz56hM03WODE,2558
13
+ job_shop_lib/dispatching/_dispatcher.py,sha256=5zCTBIl96dcqAmdwp_1I_4V9ox2X4zMz5-ZHwpMvWGU,23765
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=Tdclcvt_BK28eYxTzgF7Kiz1IBZAFpXRay0U62-VDHA,4211
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=sEtInDnFW9gsKDUEDUGQBaIjDWgCkFYvBca46j8XYfE,6408
19
20
  job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=0he-j4OlvqtXAJZD5x1nuBnUKqZUfftVx9NT3CVxPyg,2708
20
21
  job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
21
22
  job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=FPVnrX9ySS9Bg7OpIP9FPdUeJ25Jtti3q3JvpkkHvqk,7915
@@ -67,7 +68,7 @@ job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9G
67
68
  job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
68
69
  job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=YyNEKgg1ol34JYu8Ej1kKonsuFu7zDav4uuUzU_FS4Q,15905
69
70
  job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=Sp1aokyM_8QZb39QCLrjQECcbDHkdcYw7wMZILWxN9o,13191
70
- job_shop_lib-1.0.3.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
71
- job_shop_lib-1.0.3.dist-info/METADATA,sha256=nPyfAr6Aa-IxK-5XR3c_MtA9DLmv8Bsqp94Zb5ZnPIg,16460
72
- job_shop_lib-1.0.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
73
- job_shop_lib-1.0.3.dist-info/RECORD,,
71
+ job_shop_lib-1.1.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
72
+ job_shop_lib-1.1.0.dist-info/METADATA,sha256=VOiEg8x4xozLpf04qjhX0NBEsnDrbzEUPtUi89GqE10,16405
73
+ job_shop_lib-1.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
74
+ job_shop_lib-1.1.0.dist-info/RECORD,,