job-shop-lib 1.3.0__py3-none-any.whl → 1.6.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.3.0"
22
+ __version__ = "1.6.0"
23
23
 
24
24
  __all__ = [
25
25
  "Operation",
@@ -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 `Solver` is any `Callable` that takes a `JobShopInstance` and returns a
19
- `Schedule`. Therefore, solvers can be implemented as functions or as
20
- classes. This class is provided as a base class for solvers implemented as
21
- classes. It provides a default implementation of the `__call__` method that
22
- measures the time taken to solve the instance and stores it in the
23
- schedule's metadata under the key "elapsed_time" if it is not already
24
- present.
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
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from collections.abc import Sequence
6
7
  import functools
7
8
  from typing import Any
9
+ import warnings
8
10
 
9
11
  import numpy as np
10
12
  from numpy.typing import NDArray
@@ -40,6 +42,9 @@ class JobShopInstance:
40
42
  is_flexible
41
43
  durations_matrix
42
44
  machines_matrix
45
+ release_dates_matrix
46
+ deadlines_matrix
47
+ due_dates_matrix
43
48
  durations_matrix_array
44
49
  machines_matrix_array
45
50
  operations_by_machine
@@ -92,6 +97,24 @@ class JobShopInstance:
92
97
  self.name: str = name
93
98
  self.metadata: dict[str, Any] = metadata
94
99
 
100
+ deprecated_keys = {
101
+ "release_dates_matrix",
102
+ "deadlines_matrix",
103
+ "due_dates_matrix",
104
+ }
105
+ if any(key in self.metadata for key in deprecated_keys):
106
+ warnings.warn(
107
+ "The use of 'release_dates_matrix', 'deadlines_matrix', or "
108
+ "'due_dates_matrix' in metadata is deprecated."
109
+ "Please add these attributes "
110
+ "directly to the Operation class. Not doing so may cause bugs "
111
+ "when using the dispatching module.You can use the "
112
+ "`JobShopInstance.from_matrices` method to create an "
113
+ "instance from 2D sequences. ",
114
+ DeprecationWarning,
115
+ stacklevel=2,
116
+ )
117
+
95
118
  def set_operation_attributes(self):
96
119
  """Sets the ``job_id``, ``position_in_job``, and ``operation_id``
97
120
  attributes for each operation in the instance.
@@ -196,14 +219,25 @@ class JobShopInstance:
196
219
  "duration_matrix": self.durations_matrix,
197
220
  "machines_matrix": self.machines_matrix,
198
221
  "metadata": self.metadata,
222
+ # Optionally (if the instance has them):
223
+ "release_dates_matrix": self.release_dates_matrix,
224
+ "deadlines_matrix": self.deadlines_matrix,
225
+ "due_dates_matrix": self.due_dates_matrix,
199
226
  }
200
227
  """
201
- return {
228
+ data = {
202
229
  "name": self.name,
203
230
  "duration_matrix": self.durations_matrix,
204
231
  "machines_matrix": self.machines_matrix,
205
232
  "metadata": self.metadata,
206
233
  }
234
+ if self.has_release_dates:
235
+ data["release_dates_matrix"] = self.release_dates_matrix
236
+ if self.has_deadlines:
237
+ data["deadlines_matrix"] = self.deadlines_matrix
238
+ if self.has_due_dates:
239
+ data["due_dates_matrix"] = self.due_dates_matrix
240
+ return data
207
241
 
208
242
  @classmethod
209
243
  def from_matrices(
@@ -212,6 +246,9 @@ class JobShopInstance:
212
246
  machines_matrix: list[list[list[int]]] | list[list[int]],
213
247
  name: str = "JobShopInstance",
214
248
  metadata: dict[str, Any] | None = None,
249
+ release_dates_matrix: list[list[int]] | None = None,
250
+ deadlines_matrix: list[list[int | None]] | None = None,
251
+ due_dates_matrix: list[list[int | None]] | None = None,
215
252
  ) -> JobShopInstance:
216
253
  """Creates a :class:`JobShopInstance` from duration and machines
217
254
  matrices.
@@ -219,16 +256,26 @@ class JobShopInstance:
219
256
  Args:
220
257
  duration_matrix:
221
258
  A list of lists of integers. The i-th list contains the
222
- durations of the operations of the job with id i.
259
+ durations of the operations of the job with id ``i``.
223
260
  machines_matrix:
224
261
  A list of lists of lists of integers if the
225
262
  instance is flexible, or a list of lists of integers if the
226
263
  instance is not flexible. The i-th list contains the machines
227
- in which the operations of the job with id i can be processed.
264
+ in which the operations of the job with id ``i`` can be
265
+ processed.
228
266
  name:
229
267
  A string with the name of the instance.
230
268
  metadata:
231
269
  A dictionary with additional information about the instance.
270
+ release_dates_matrix:
271
+ A list of lists of integers. The i-th list contains the
272
+ release dates of the operations of the job with id ``i``.
273
+ deadlines_matrix:
274
+ A list of lists of optional integers. The i-th list contains
275
+ the deadlines of the operations of the job with id ``i``.
276
+ due_dates_matrix:
277
+ A list of lists of optional integers. The i-th list contains
278
+ the due dates of the operations of the job with id ``i``.
232
279
 
233
280
  Returns:
234
281
  A :class:`JobShopInstance` object.
@@ -241,8 +288,29 @@ class JobShopInstance:
241
288
  for position_in_job in range(num_operations):
242
289
  duration = duration_matrix[job_id][position_in_job]
243
290
  machines = machines_matrix[job_id][position_in_job]
291
+ release_date = (
292
+ release_dates_matrix[job_id][position_in_job]
293
+ if release_dates_matrix
294
+ else 0
295
+ )
296
+ deadline = (
297
+ deadlines_matrix[job_id][position_in_job]
298
+ if deadlines_matrix
299
+ else None
300
+ )
301
+ due_date = (
302
+ due_dates_matrix[job_id][position_in_job]
303
+ if due_dates_matrix
304
+ else None
305
+ )
244
306
  jobs[job_id].append(
245
- Operation(duration=duration, machines=machines)
307
+ Operation(
308
+ duration=duration,
309
+ machines=machines,
310
+ release_date=release_date,
311
+ deadline=deadline,
312
+ due_date=due_date,
313
+ )
246
314
  )
247
315
 
248
316
  metadata = {} if metadata is None else metadata
@@ -289,6 +357,21 @@ class JobShopInstance:
289
357
  for job in self.jobs
290
358
  )
291
359
 
360
+ @functools.cached_property
361
+ def has_release_dates(self) -> bool:
362
+ """Returns ``True`` if any operation has a release date > 0."""
363
+ return any(op.release_date > 0 for job in self.jobs for op in job)
364
+
365
+ @functools.cached_property
366
+ def has_deadlines(self) -> bool:
367
+ """Returns ``True`` if any operation has a deadline."""
368
+ return any(op.deadline is not None for job in self.jobs for op in job)
369
+
370
+ @functools.cached_property
371
+ def has_due_dates(self) -> bool:
372
+ """Returns ``True`` if any operation has a due date."""
373
+ return any(op.due_date is not None for job in self.jobs for op in job)
374
+
292
375
  @functools.cached_property
293
376
  def durations_matrix(self) -> list[list[int]]:
294
377
  """Returns the duration matrix of the instance.
@@ -330,23 +413,75 @@ class JobShopInstance:
330
413
  [operation.machine_id for operation in job] for job in self.jobs
331
414
  ]
332
415
 
416
+ @functools.cached_property
417
+ def release_dates_matrix(self) -> list[list[int]]:
418
+ """Returns the release dates matrix of the instance.
419
+
420
+ The release date of the operation with ``job_id`` i and
421
+ ``position_in_job`` j is stored in the i-th position of the j-th list
422
+ of the returned matrix.
423
+ """
424
+ return [
425
+ [operation.release_date for operation in job] for job in self.jobs
426
+ ]
427
+
428
+ @functools.cached_property
429
+ def deadlines_matrix(self) -> list[list[int | None]]:
430
+ """Returns the deadlines matrix of the instance.
431
+
432
+ The deadline of the operation with ``job_id`` i and
433
+ ``position_in_job`` j is stored in the i-th position of the j-th list
434
+ of the returned matrix.
435
+ """
436
+ return [[operation.deadline for operation in job] for job in self.jobs]
437
+
438
+ @functools.cached_property
439
+ def due_dates_matrix(self) -> list[list[int | None]]:
440
+ """Returns the due dates matrix of the instance.
441
+
442
+ The due date of the operation with ``job_id`` i and
443
+ ``position_in_job`` j is stored in the i-th position of the j-th list
444
+ of the returned matrix.
445
+ """
446
+ return [[operation.due_date for operation in job] for job in self.jobs]
447
+
333
448
  @functools.cached_property
334
449
  def durations_matrix_array(self) -> NDArray[np.float32]:
335
450
  """Returns the duration matrix of the instance as a numpy array.
336
451
 
337
- The returned array has shape (``num_jobs``,
338
- ``max_num_operations_per_job``).
339
- Non-existing operations are filled with ``np.nan``.
452
+ If the jobs have different number of operations, the matrix is
453
+ padded with ``np.nan`` to make it rectangular.
454
+ """
455
+ return self._fill_matrix_with_nans_2d(self.durations_matrix)
340
456
 
341
- Example:
342
- >>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
343
- >>> instance = JobShopInstance(jobs)
344
- >>> instance.durations_matrix_array
345
- array([[ 2., 3.],
346
- [ 4., nan]], dtype=float32)
457
+ @functools.cached_property
458
+ def release_dates_matrix_array(self) -> NDArray[np.float32]:
459
+ """Returns the release dates matrix of the instance as a numpy array.
460
+
461
+ If the jobs have different number of operations, the matrix is
462
+ padded with ``np.nan`` to make it rectangular.
347
463
  """
348
- duration_matrix = self.durations_matrix
349
- return self._fill_matrix_with_nans_2d(duration_matrix)
464
+ return self._fill_matrix_with_nans_2d(self.release_dates_matrix)
465
+
466
+ @functools.cached_property
467
+ def deadlines_matrix_array(self) -> NDArray[np.float32]:
468
+ """Returns the deadlines matrix of the instance as a numpy array.
469
+
470
+ If the jobs have different number of operations, the matrix is
471
+ padded with ``np.nan`` to make it rectangular. None values are also
472
+ converted to ``np.nan``.
473
+ """
474
+ return self._fill_matrix_with_nans_2d(self.deadlines_matrix)
475
+
476
+ @functools.cached_property
477
+ def due_dates_matrix_array(self) -> NDArray[np.float32]:
478
+ """Returns the due dates matrix of the instance as a numpy array.
479
+
480
+ If the jobs have different number of operations, the matrix is
481
+ padded with ``np.nan`` to make it rectangular. None values are also
482
+ converted to ``np.nan``.
483
+ """
484
+ return self._fill_matrix_with_nans_2d(self.due_dates_matrix)
350
485
 
351
486
  @functools.cached_property
352
487
  def machines_matrix_array(self) -> NDArray[np.float32]:
@@ -474,13 +609,13 @@ class JobShopInstance:
474
609
 
475
610
  @staticmethod
476
611
  def _fill_matrix_with_nans_2d(
477
- matrix: list[list[int]],
612
+ matrix: Sequence[Sequence[int | None]],
478
613
  ) -> NDArray[np.float32]:
479
- """Fills a matrix with ``np.nan`` values.
614
+ """Creates a 2D numpy array padded with ``np.nan`` values.
480
615
 
481
616
  Args:
482
617
  matrix:
483
- A list of lists of integers.
618
+ A list of lists of integers or Nones.
484
619
 
485
620
  Returns:
486
621
  A numpy array with the same shape as the input matrix, filled with
@@ -491,14 +626,17 @@ class JobShopInstance:
491
626
  (len(matrix), max_length), np.nan, dtype=np.float32
492
627
  )
493
628
  for i, row in enumerate(matrix):
494
- squared_matrix[i, : len(row)] = row
629
+ processed_row = [
630
+ item if item is not None else np.nan for item in row
631
+ ]
632
+ squared_matrix[i, : len(processed_row)] = processed_row
495
633
  return squared_matrix
496
634
 
497
635
  @staticmethod
498
636
  def _fill_matrix_with_nans_3d(
499
637
  matrix: list[list[list[int]]],
500
638
  ) -> NDArray[np.float32]:
501
- """Fills a 3D matrix with ``np.nan`` values.
639
+ """Creates a 3D numpy array padded with ``np.nan`` values.
502
640
 
503
641
  Args:
504
642
  matrix:
@@ -522,9 +660,3 @@ class JobShopInstance:
522
660
  for j, inner_row in enumerate(row):
523
661
  squared_matrix[i, j, : len(inner_row)] = inner_row
524
662
  return squared_matrix
525
-
526
-
527
- if __name__ == "__main__":
528
- import doctest
529
-
530
- doctest.testmod()
@@ -30,6 +30,16 @@ class Operation:
30
30
  an integer.
31
31
  duration:
32
32
  The time it takes to perform the operation.
33
+ release_date:
34
+ The earliest moment this operation can be scheduled to start.
35
+ Defaults to ``0``.
36
+ deadline:
37
+ A hard cutoff time by which the job must be finished. A schedule
38
+ is invalid if the job completes after this time. Defaults to
39
+ ``None``.
40
+ due_date:
41
+ The target completion time for the job. Finishing late is allowed
42
+ but incurs a penalty (e.g., tardiness). Defaults to ``None``.
33
43
  """
34
44
 
35
45
  __slots__ = {
@@ -42,6 +52,20 @@ class Operation:
42
52
  "The time it takes to perform the operation. Often referred"
43
53
  " to as the processing time."
44
54
  ),
55
+ "release_date": (
56
+ "The earliest moment this operation can be scheduled to start. "
57
+ "Defaults to ``0``."
58
+ ),
59
+ "deadline": (
60
+ "A hard cutoff time by which the job must be finished. A schedule "
61
+ "is invalid if the job completes after this time. Defaults to "
62
+ "``None``."
63
+ ),
64
+ "due_date": (
65
+ "The target completion time for the job. Finishing late is "
66
+ "allowed but incurs a penalty (e.g., tardiness). Defaults to "
67
+ "``None``."
68
+ ),
45
69
  "job_id": (
46
70
  "The id of the job the operation belongs to. Defaults to -1. "
47
71
  "It is usually set by the :class:`JobShopInstance` class after "
@@ -59,11 +83,21 @@ class Operation:
59
83
  ),
60
84
  }
61
85
 
62
- def __init__(self, machines: int | list[int], duration: int):
86
+ def __init__(
87
+ self,
88
+ machines: int | list[int],
89
+ duration: int,
90
+ release_date: int = 0,
91
+ deadline: int | None = None,
92
+ due_date: int | None = None,
93
+ ):
63
94
  self.machines: list[int] = (
64
95
  [machines] if isinstance(machines, int) else machines
65
96
  )
66
97
  self.duration: int = duration
98
+ self.release_date: int = release_date
99
+ self.deadline: int | None = deadline
100
+ self.due_date: int | None = due_date
67
101
 
68
102
  # Defined outside the class by the JobShopInstance class:
69
103
  self.job_id: int = -1
@@ -93,9 +127,9 @@ class Operation:
93
127
  def is_initialized(self) -> bool:
94
128
  """Returns whether the operation has been initialized."""
95
129
  return (
96
- self.job_id == -1
97
- or self.position_in_job == -1
98
- or self.operation_id == -1
130
+ self.job_id != -1
131
+ and self.position_in_job != -1
132
+ and self.operation_id != -1
99
133
  )
100
134
 
101
135
  def __hash__(self) -> int:
@@ -104,7 +138,16 @@ class Operation:
104
138
  def __eq__(self, value: object) -> bool:
105
139
  if not isinstance(value, Operation):
106
140
  return False
107
- return self.__slots__ == value.__slots__
141
+ return (
142
+ self.machines == value.machines
143
+ and self.duration == value.duration
144
+ and self.release_date == value.release_date
145
+ and self.deadline == value.deadline
146
+ and self.due_date == value.due_date
147
+ and self.job_id == value.job_id
148
+ and self.position_in_job == value.position_in_job
149
+ and self.operation_id == value.operation_id
150
+ )
108
151
 
109
152
  def __repr__(self) -> str:
110
153
  machines = (
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
- max_end_time = 0
224
- for machine_schedule in self.schedule:
225
- if machine_schedule:
226
- max_end_time = max(max_end_time, machine_schedule[-1].end_time)
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,71 @@ 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
+ # 2. Trace backwards from the last operation
409
+ while True:
410
+ job_pred = None
411
+ machine_pred = None
412
+
413
+ # Find job predecessor (the previous operation in the same job)
414
+ op_idx_in_job = current_scheduled_op.operation.position_in_job
415
+ if op_idx_in_job > 0:
416
+ prev_op_in_job = self.instance.jobs[
417
+ current_scheduled_op.job_id
418
+ ][op_idx_in_job - 1]
419
+ job_pred = self.operation_to_scheduled_operation[
420
+ prev_op_in_job
421
+ ]
422
+
423
+ # Find machine predecessor (the previous operation on the same
424
+ # machine)
425
+ machine_schedule = self.schedule[current_scheduled_op.machine_id]
426
+ op_idx_on_machine = machine_schedule.index(current_scheduled_op)
427
+ if op_idx_on_machine > 0:
428
+ machine_pred = machine_schedule[op_idx_on_machine - 1]
429
+
430
+ # 3. Determine the critical predecessor
431
+ # The critical predecessor is the one that finished latest, as it
432
+ # determined the start time of the current operation.
433
+
434
+ if job_pred is None and machine_pred is None:
435
+ # Reached the beginning of the schedule, no more predecessors
436
+ break
437
+
438
+ job_pred_end_time = (
439
+ job_pred.end_time if job_pred is not None else -1
440
+ )
441
+ machine_pred_end_time = (
442
+ machine_pred.end_time if machine_pred is not None else -1
443
+ )
444
+ critical_pred = (
445
+ job_pred
446
+ if job_pred_end_time >= machine_pred_end_time
447
+ else machine_pred
448
+ )
449
+ assert critical_pred is not None
450
+ # Prepend the critical predecessor to the path and continue tracing
451
+ critical_path.appendleft(critical_pred)
452
+ current_scheduled_op = critical_pred
453
+
454
+ return list(critical_path)
@@ -112,16 +112,12 @@ class ORToolsSolver(BaseSolver):
112
112
  problem.
113
113
 
114
114
  Args:
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.
115
+ instance: The job shop instance to be solved.
116
+
117
+ .. note::
118
+ If provided, `arrival_times` and `deadlines` will override the
119
+ `release_date` and `deadline` attributes of the operations,
120
+ respectively.
125
121
 
126
122
  Returns:
127
123
  The best schedule found by the solver.
@@ -225,9 +221,12 @@ class ORToolsSolver(BaseSolver):
225
221
  """Creates two variables for each operation: start and end time."""
226
222
  for job in instance.jobs:
227
223
  for operation in job:
228
- # Initial Naive Bounds
229
- lower_bound = 0
230
- upper_bound = instance.total_duration
224
+ lower_bound = operation.release_date
225
+ upper_bound = (
226
+ instance.total_duration
227
+ if operation.deadline is None
228
+ else operation.deadline
229
+ )
231
230
 
232
231
  if arrival_times is not None:
233
232
  lower_bound = arrival_times[operation.job_id][