job-shop-lib 1.3.0__py3-none-any.whl → 1.5.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.5.0"
23
23
 
24
24
  __all__ = [
25
25
  "Operation",
@@ -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 = (
@@ -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][
@@ -25,9 +25,8 @@ def no_setup_time_calculator(
25
25
  ) -> int:
26
26
  """Default start time calculator that implements the standard behavior.
27
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.
28
+ The start time is the maximum of the machine's next available time, the
29
+ job's next available time, and the operation's release date.
31
30
 
32
31
  Args:
33
32
  dispatcher:
@@ -44,6 +43,7 @@ def no_setup_time_calculator(
44
43
  return max(
45
44
  dispatcher.machine_next_available_time[machine_id],
46
45
  dispatcher.job_next_available_time[operation.job_id],
46
+ operation.release_date,
47
47
  )
48
48
 
49
49
 
@@ -552,7 +552,9 @@ class Dispatcher:
552
552
  for machine_id in operation.machines
553
553
  )
554
554
  job_start_time = self._job_next_available_time[operation.job_id]
555
- return max(machine_earliest_start_time, job_start_time)
555
+ return max(
556
+ machine_earliest_start_time, job_start_time, operation.release_date
557
+ )
556
558
 
557
559
  def remaining_duration(
558
560
  self, scheduled_operation: ScheduledOperation
@@ -632,7 +634,11 @@ class Dispatcher:
632
634
  def is_ongoing(self, scheduled_operation: ScheduledOperation) -> bool:
633
635
  """Checks if the given operation is currently being processed."""
634
636
  current_time = self.current_time()
635
- return scheduled_operation.start_time <= current_time
637
+ return (
638
+ scheduled_operation.start_time
639
+ <= current_time
640
+ < scheduled_operation.end_time
641
+ )
636
642
 
637
643
  def next_operation(self, job_id: int) -> Operation:
638
644
  """Returns the next operation to be scheduled for the given job.
@@ -88,6 +88,8 @@ def filter_non_immediate_operations(
88
88
  min_start_time = dispatcher.min_start_time(operations)
89
89
  immediate_operations: list[Operation] = []
90
90
  for operation in operations:
91
+ if operation.release_date > min_start_time:
92
+ continue
91
93
  start_time = dispatcher.earliest_start_time(operation)
92
94
  if start_time == min_start_time:
93
95
  immediate_operations.append(operation)
@@ -14,6 +14,7 @@ dispatcher.
14
14
  PositionInJobObserver
15
15
  RemainingOperationsObserver
16
16
  IsCompletedObserver
17
+ DatesObserver
17
18
  FeatureObserverType
18
19
  feature_observer_factory
19
20
  FeatureObserverConfig
@@ -41,6 +42,7 @@ from ._is_scheduled_observer import IsScheduledObserver
41
42
  from ._position_in_job_observer import PositionInJobObserver
42
43
  from ._remaining_operations_observer import RemainingOperationsObserver
43
44
  from ._is_completed_observer import IsCompletedObserver
45
+ from ._dates_observer import DatesObserver
44
46
  from ._factory import (
45
47
  FeatureObserverType,
46
48
  feature_observer_factory,
@@ -60,6 +62,7 @@ __all__ = [
60
62
  "PositionInJobObserver",
61
63
  "RemainingOperationsObserver",
62
64
  "IsCompletedObserver",
65
+ "DatesObserver",
63
66
  "FeatureObserverType",
64
67
  "feature_observer_factory",
65
68
  "FeatureObserverConfig",
@@ -0,0 +1,162 @@
1
+ """Home of the `DatesObserver` class."""
2
+
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+
7
+ from job_shop_lib import ScheduledOperation, JobShopInstance
8
+ from job_shop_lib.dispatching import Dispatcher
9
+ from job_shop_lib.dispatching.feature_observers import (
10
+ FeatureObserver,
11
+ FeatureType,
12
+ )
13
+
14
+ Attribute = Literal["release_date", "deadline", "due_date"]
15
+
16
+
17
+ class DatesObserver(FeatureObserver):
18
+ """Observes time-related attributes of operations.
19
+
20
+ This observer tracks attributes like release date, deadline, and
21
+ due date for each operation in the job shop instance. The attributes to
22
+ be observed can be specified during initialization.
23
+
24
+ The values are stored in a numpy array of shape ``(num_operations,
25
+ num_attributes)``, where ``num_attributes`` is the number of attributes
26
+ being observed.
27
+
28
+ All attributes are updated based on the current time of the dispatcher so
29
+ that they reflect the relative time remaining until the event occurs:
30
+ (attribute - current_time). This means that the values will be negative if
31
+ the event is in the past.
32
+
33
+ Args:
34
+ dispatcher:
35
+ The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
36
+ subscribe:
37
+ If ``True``, the observer is subscribed to the dispatcher upon
38
+ initialization. Otherwise, the observer must be subscribed later
39
+ or manually updated.
40
+ attributes_to_observe:
41
+ A list of attributes to observe. If ``None``, all available time
42
+ attributes (release_date, deadline, due_date) will be
43
+ observed, provided they exist in the instance.
44
+ """
45
+
46
+ _supported_feature_types = [FeatureType.OPERATIONS]
47
+
48
+ __slots__ = {
49
+ "attributes_to_observe": "List of attributes to observe.",
50
+ "_attribute_map": "Maps attributes to their column index.",
51
+ }
52
+
53
+ def __init__(
54
+ self,
55
+ dispatcher: Dispatcher,
56
+ *,
57
+ subscribe: bool = True,
58
+ attributes_to_observe: list[Attribute] | None = None,
59
+ feature_types: FeatureType | list[FeatureType] | None = None,
60
+ ):
61
+ self.attributes_to_observe = self._determine_attributes_to_observe(
62
+ dispatcher.instance, attributes_to_observe
63
+ )
64
+ self._attribute_map = {
65
+ attr: i for i, attr in enumerate(self.attributes_to_observe)
66
+ }
67
+ self._previous_current_time = 0
68
+ super().__init__(
69
+ dispatcher,
70
+ subscribe=subscribe,
71
+ feature_types=feature_types,
72
+ )
73
+
74
+ @property
75
+ def feature_sizes(self) -> dict[FeatureType, int]:
76
+ return {
77
+ FeatureType.OPERATIONS: len(self.attributes_to_observe),
78
+ }
79
+
80
+ @property
81
+ def attribute_map(self) -> dict[Attribute, int]:
82
+ """Maps attributes to their column index in the features array."""
83
+ return self._attribute_map
84
+
85
+ def initialize_features(self):
86
+ """Initializes the features for the operations.
87
+
88
+ This method sets up the features for the operations based on the
89
+ attributes to observe. It creates a numpy array with the shape
90
+ (num_operations, num_attributes) and fills it with the corresponding
91
+ values from the job shop instance. Note that the matrix may contain
92
+ ``np.nan`` values for operations that do not have deadlines or
93
+ due dates.
94
+
95
+ .. seealso::
96
+ - :meth:`job_shop_lib.JobShopInstance.release_dates_matrix_array`
97
+ - :meth:`job_shop_lib.JobShopInstance.deadlines_matrix_array`
98
+ - :meth:`job_shop_lib.JobShopInstance.due_dates_matrix_array`
99
+ """
100
+ self.features = {
101
+ FeatureType.OPERATIONS: np.zeros(
102
+ (
103
+ self.dispatcher.instance.num_operations,
104
+ len(self.attributes_to_observe),
105
+ ),
106
+ dtype=np.float32,
107
+ )
108
+ }
109
+ self._previous_current_time = self.dispatcher.current_time()
110
+ release_dates_matrix = (
111
+ self.dispatcher.instance.release_dates_matrix_array
112
+ )
113
+ valid_operations_mask = ~np.isnan(release_dates_matrix.flatten())
114
+
115
+ for attr, col_idx in self._attribute_map.items():
116
+ matrix = getattr(self.dispatcher.instance, f"{attr}s_matrix_array")
117
+ values = np.array(matrix).flatten()
118
+ valid_values = values[valid_operations_mask]
119
+ self.features[FeatureType.OPERATIONS][:, col_idx] = valid_values
120
+
121
+ def update(self, scheduled_operation: ScheduledOperation):
122
+ """Updates the features based on the scheduled operation.
123
+
124
+ This method updates the features by subtracting the current time from
125
+ the initial release date, deadline, and due date attributes of the
126
+ operations.
127
+
128
+ Args:
129
+ scheduled_operation:
130
+ The scheduled operation that has just been processed. It is
131
+ not used in this observer, but is required by the
132
+ :meth:`FeatureObserver.update` method.
133
+ """
134
+ current_time = self.dispatcher.current_time()
135
+ elapsed_time = current_time - self._previous_current_time
136
+ self._previous_current_time = current_time
137
+ cols = [
138
+ self._attribute_map[attr]
139
+ for attr in self.attributes_to_observe
140
+ ]
141
+ self.features[FeatureType.OPERATIONS][:, cols] -= elapsed_time
142
+
143
+ def _determine_attributes_to_observe(
144
+ self,
145
+ instance: JobShopInstance,
146
+ attributes_to_observe: list[Attribute] | None,
147
+ ) -> list[Attribute]:
148
+ if attributes_to_observe:
149
+ return attributes_to_observe
150
+
151
+ default_attributes: list[Attribute] = []
152
+ if instance.has_release_dates:
153
+ default_attributes.append("release_date")
154
+ if instance.has_deadlines:
155
+ default_attributes.append("deadline")
156
+ if instance.has_due_dates:
157
+ default_attributes.append("due_date")
158
+ return default_attributes
159
+
160
+ def reset(self):
161
+ """Calls :meth:`initialize_features`"""
162
+ self.initialize_features()
@@ -77,12 +77,13 @@ class DurationObserver(FeatureObserver):
77
77
  self.features[FeatureType.JOBS][job_id, 0] = job_duration
78
78
 
79
79
  def _update_operation_durations(
80
- self, scheduled_operation: ScheduledOperation
80
+ self, unused_scheduled_operation: ScheduledOperation
81
81
  ):
82
- operation_id = scheduled_operation.operation.operation_id
83
- self.features[FeatureType.OPERATIONS][operation_id, 0] = (
84
- self.dispatcher.remaining_duration(scheduled_operation)
85
- )
82
+ for scheduled_operation in self.dispatcher.ongoing_operations():
83
+ operation_id = scheduled_operation.operation.operation_id
84
+ self.features[FeatureType.OPERATIONS][operation_id, 0] = (
85
+ self.dispatcher.remaining_duration(scheduled_operation)
86
+ )
86
87
 
87
88
  def _update_machine_durations(
88
89
  self, scheduled_operation: ScheduledOperation
@@ -18,6 +18,7 @@ Dispatching rules:
18
18
  :nosignatures:
19
19
 
20
20
  shortest_processing_time_rule
21
+ largest_processing_time_rule
21
22
  first_come_first_served_rule
22
23
  most_work_remaining_rule
23
24
  most_operations_remaining_rule
@@ -32,6 +33,7 @@ Dispatching rule scorers:
32
33
  :nosignatures:
33
34
 
34
35
  shortest_processing_time_score
36
+ largest_processing_time_score
35
37
  first_come_first_served_score
36
38
  MostWorkRemainingScorer
37
39
  most_operations_remaining_score
@@ -45,6 +47,8 @@ from ._dispatching_rules_functions import (
45
47
  most_work_remaining_rule,
46
48
  most_operations_remaining_rule,
47
49
  random_operation_rule,
50
+ largest_processing_time_score,
51
+ largest_processing_time_rule,
48
52
  score_based_rule,
49
53
  score_based_rule_with_tie_breaker,
50
54
  shortest_processing_time_score,
@@ -78,6 +82,8 @@ __all__ = [
78
82
  "most_work_remaining_rule",
79
83
  "most_operations_remaining_rule",
80
84
  "random_operation_rule",
85
+ "largest_processing_time_score",
86
+ "largest_processing_time_rule",
81
87
  "score_based_rule",
82
88
  "score_based_rule_with_tie_breaker",
83
89
  "observer_based_most_work_remaining_rule",
@@ -17,6 +17,7 @@ from job_shop_lib.dispatching.rules import (
17
17
  most_operations_remaining_rule,
18
18
  random_operation_rule,
19
19
  most_work_remaining_rule,
20
+ largest_processing_time_rule,
20
21
  )
21
22
 
22
23
 
@@ -24,6 +25,7 @@ class DispatchingRuleType(str, Enum):
24
25
  """Enumeration of dispatching rules for the job shop scheduling problem."""
25
26
 
26
27
  SHORTEST_PROCESSING_TIME = "shortest_processing_time"
28
+ LARGEST_PROCESSING_TIME = "largest_processing_time"
27
29
  FIRST_COME_FIRST_SERVED = "first_come_first_served"
28
30
  MOST_WORK_REMAINING = "most_work_remaining"
29
31
  MOST_OPERATIONS_REMAINING = "most_operations_remaining"
@@ -62,6 +64,9 @@ def dispatching_rule_factory(
62
64
  DispatchingRuleType.SHORTEST_PROCESSING_TIME: (
63
65
  shortest_processing_time_rule
64
66
  ),
67
+ DispatchingRuleType.LARGEST_PROCESSING_TIME: (
68
+ largest_processing_time_rule
69
+ ),
65
70
  DispatchingRuleType.FIRST_COME_FIRST_SERVED: (
66
71
  first_come_first_served_rule
67
72
  ),
@@ -26,6 +26,37 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
26
26
  )
27
27
 
28
28
 
29
+ def largest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
30
+ """Dispatches the operation with the longest duration."""
31
+ return max(
32
+ dispatcher.available_operations(),
33
+ key=lambda operation: operation.duration,
34
+ )
35
+
36
+
37
+ def largest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
38
+ """Scores each job based on the duration of the next operation.
39
+
40
+ The score is the duration of the next operation in each job.
41
+ Jobs with longer next operations will have higher scores.
42
+
43
+ Args:
44
+ dispatcher:
45
+ The :class:`~job_shop_lib.dispatching.Dispatcher` instance
46
+ containing the job shop instance and the current state of the
47
+ schedule.
48
+
49
+ Returns:
50
+ A list of scores for each job, where the score is the duration of
51
+ the next operation in that job.
52
+ """
53
+ num_jobs = dispatcher.instance.num_jobs
54
+ scores = [0] * num_jobs
55
+ for operation in dispatcher.available_operations():
56
+ scores[operation.job_id] = operation.duration
57
+ return scores
58
+
59
+
29
60
  def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
30
61
  """Dispatches the operation with the lowest position in job."""
31
62
  return min(
@@ -98,9 +98,7 @@ def generate_machine_matrix_without_recirculation(
98
98
  (num_jobs, 1),
99
99
  )
100
100
  # Shuffle the columns:
101
- machine_matrix = np.apply_along_axis(
102
- rng.permutation, 1, machine_matrix
103
- )
101
+ machine_matrix = np.apply_along_axis(rng.permutation, 1, machine_matrix)
104
102
  return machine_matrix
105
103
 
106
104
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 1.3.0
3
+ Version: 1.5.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
@@ -1,26 +1,27 @@
1
- job_shop_lib/__init__.py,sha256=FHzL2Kx7_4FCgaaLxzb_RdTRDQgF-CraofLgc2IdSqs,639
1
+ job_shop_lib/__init__.py,sha256=vJ_HFv-mEpI6ejngfGDE4aTMgYvFR3kYm8nkFyGrMS8,639
2
2
  job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
3
- job_shop_lib/_job_shop_instance.py,sha256=FkMBy9Yb8cNEGswI9vlN3Wh4mhtEX-QuDbKvSYUOXcM,18361
4
- job_shop_lib/_operation.py,sha256=lwCjgXwWlgESFuV3Yh4SCVofPGCd3hJU4vnK7peREac,4235
3
+ job_shop_lib/_job_shop_instance.py,sha256=_92orxdi70645J7cQlRE1I0PebvpHRCP6958q9j2h18,24261
4
+ job_shop_lib/_operation.py,sha256=JI5WjvRXNBeSpPOv3ZwSrUJ4jsVDJYKfMaDHYOaFYts,5945
5
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=LMpfpgiU2etrtyVTKVKyZW1PVMrOG2TenWzfGuEGf2I,12710
11
+ job_shop_lib/constraint_programming/_ortools_solver.py,sha256=trTQtqSL2F2PXxd9RPnFhxaY8blNcfFUhTdab5QP9VU,12585
12
12
  job_shop_lib/dispatching/__init__.py,sha256=gbgY1_lhergmXaDa-VYVUmxMpOKzYko0ONREVAt_QPc,2643
13
- job_shop_lib/dispatching/_dispatcher.py,sha256=KnV_Kry3Ie81WbKhdpRQtOMsuFDNCuh5Kp2ZnelM-R8,23835
13
+ job_shop_lib/dispatching/_dispatcher.py,sha256=A54Q3_hm9Qy5Vfzeudj2QFxd9SWo9lqqBN55S6z-Who,23976
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
- job_shop_lib/dispatching/_ready_operation_filters.py,sha256=brhmhoyyoZ98wAEEfneZC-CD-aw9SerZHGMB1DpK8HY,5749
18
+ job_shop_lib/dispatching/_ready_operation_filters.py,sha256=DcwPDIF1eqVR6s3DV4J3cumii0N_apC13XA-50wjlI0,5822
19
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
- job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
21
+ job_shop_lib/dispatching/feature_observers/__init__.py,sha256=Pzud4tuO_t72d9KY_nEH-stGOvKUTNjo_6GeWDuJPvc,2322
22
22
  job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=tpvqTLIcNmbYROSFT62LiUZ_tI4fHWL_qCULKK43BU4,6429
23
- job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=fbkUIVScF1iNjdVCYr1ImQm53TfahvVnGXhsRAsgdzY,4129
23
+ job_shop_lib/dispatching/feature_observers/_dates_observer.py,sha256=oCk1XAo_2mrgD0ckHQLw3dD7DSQVVg7xBKn7D_u1Dvc,6083
24
+ job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=pBsJjT-1pbSi32hoLppoqXCftBvJPSh7r7tl3m7etAQ,4225
24
25
  job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=AQIjVp7VRDnb5GuYZlLUwk-xiXSqbsxJW-Ji7NjLoAw,11452
25
26
  job_shop_lib/dispatching/feature_observers/_factory.py,sha256=NyXYK5A1hXsYEeEqngwVRNAFkevY95DglheeqyfFv8s,3217
26
27
  job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=qbgtMUicQ5FWS-Ql4Izjsj4QrevfOGlWzoJ0JlVSLH0,8668
@@ -29,17 +30,17 @@ job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=wy_pA-1w
29
30
  job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py,sha256=OcuMUB9_By6ZMtX-1_3z-xaxGbP85a5Zv0ywAv7XxWQ,1491
30
31
  job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py,sha256=WRknpQBKXs6h6cXLFJW7ZCvjtU8CPL-iXXNPw3g-mLE,1303
31
32
  job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py,sha256=5V87lCrJUabEe8AkTGXPu5yS8OGxeN8L3-xNyHmdmLs,1441
32
- job_shop_lib/dispatching/rules/__init__.py,sha256=0Nn9FBVmxVYeDeLsd7g7WkmKFBYJqOIDzArbqsC7FAI,2187
33
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=5fNpv90fAoR6rcE6NeJOWiB7ir-FVnoONIhHtKJ9H0E,2904
33
+ job_shop_lib/dispatching/rules/__init__.py,sha256=u1XCRU4tVXJ2rdxXQabPIBaRly0PbBmBu1QcEKx9Z_Y,2396
34
+ job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=iwqGHtroZAuE90fKOAe79reHlJ8gCnJzMjCkI0lucMg,3101
34
35
  job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=1_canC1lXZATrQCZaHOY3JOLmTuT6U0Z_QWzgTOLwqI,5917
35
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=Yk40aKePBHHiMO6aTFeyJd1-khsDPhqit2WCOaByCfw,10998
36
+ job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=c-T6jUXZ2wjkmENBlDfNyWUA4T3inGll1E0jqAn6wd8,12052
36
37
  job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=CJ74ujgWXgG8cuULWY6VJkD_b3arTcOjTNLZJTAf8xE,2346
37
38
  job_shop_lib/dispatching/rules/_utils.py,sha256=m5qw4qyfaIvVrkmv51nuhreizr98-cg8AJKt2VTd48w,4603
38
39
  job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
39
40
  job_shop_lib/generation/__init__.py,sha256=QaWwuBfBNnOiG0OPiP_CV_flBu9dX7r2o_HwL47tREM,822
40
41
  job_shop_lib/generation/_general_instance_generator.py,sha256=b_tnyP4H_buoN7b6lKQRLvDkeZDdys0mpqS3thB5-SQ,6544
41
42
  job_shop_lib/generation/_instance_generator.py,sha256=doN6WySyI0k7wz3aKy_e6hj6t7WV3dNzve3YmTFShas,4584
42
- job_shop_lib/generation/_utils.py,sha256=TYBGt4Zjw94l6ukIjXBVAK3lmrrZXdyzyq_r1DMlL-E,3986
43
+ job_shop_lib/generation/_utils.py,sha256=b3SVU5DY3-VHXX2yrOwM7ABDSexiSFSRbo1d5QjRfoI,3972
43
44
  job_shop_lib/graphs/__init__.py,sha256=wlYIiXTuZRE6Kx3K0RpPUoZikzoegBuN2hcdqMODtGk,2433
44
45
  job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
45
46
  job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=vIy_EkQjgQAd5YyJxKAuGf7CLTjgCfhz-fYrObF4DTU,6962
@@ -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=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
69
70
  job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
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,,
71
+ job_shop_lib-1.5.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
72
+ job_shop_lib-1.5.0.dist-info/METADATA,sha256=ggq_EHjUpRr9jX_67Sc3Fhj7sFlOqOL1tWzqRY-4xzY,19130
73
+ job_shop_lib-1.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
74
+ job_shop_lib-1.5.0.dist-info/RECORD,,