job-shop-lib 0.5.0__py3-none-any.whl → 1.0.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.
Files changed (93) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +19 -0
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/_instance_generator.py +133 -0
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generators/__init__.py +0 -7
  84. job_shop_lib/generators/basic_generator.py +0 -197
  85. job_shop_lib/graphs/constants.py +0 -21
  86. job_shop_lib/graphs/job_shop_graph.py +0 -202
  87. job_shop_lib/graphs/node.py +0 -166
  88. job_shop_lib/operation.py +0 -122
  89. job_shop_lib/visualization/agent_task_graph.py +0 -257
  90. job_shop_lib/visualization/create_gif.py +0 -209
  91. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  92. job_shop_lib-0.5.0.dist-info/RECORD +0 -48
  93. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
job_shop_lib/__init__.py CHANGED
@@ -1,13 +1,25 @@
1
1
  """Contains the main data structures and base classes.
2
+
3
+ .. autosummary::
4
+ :nosignatures:
5
+
6
+ Operation
7
+ JobShopInstance
8
+ ScheduledOperation
9
+ Schedule
10
+ Solver
11
+ BaseSolver
12
+
2
13
  """
3
14
 
4
- from job_shop_lib.exceptions import JobShopLibError, NoSolutionFoundError
5
- from job_shop_lib.operation import Operation
6
- from job_shop_lib.job_shop_instance import JobShopInstance
7
- from job_shop_lib.scheduled_operation import ScheduledOperation
8
- from job_shop_lib.schedule import Schedule
9
- from job_shop_lib.base_solver import BaseSolver, Solver
15
+ from job_shop_lib._operation import Operation
16
+ from job_shop_lib._job_shop_instance import JobShopInstance
17
+ from job_shop_lib._scheduled_operation import ScheduledOperation
18
+ from job_shop_lib._schedule import Schedule
19
+ from job_shop_lib._base_solver import BaseSolver, Solver
20
+
10
21
 
22
+ __version__ = "1.0.0"
11
23
 
12
24
  __all__ = [
13
25
  "Operation",
@@ -16,6 +28,5 @@ __all__ = [
16
28
  "Schedule",
17
29
  "Solver",
18
30
  "BaseSolver",
19
- "JobShopLibError",
20
- "NoSolutionFoundError",
31
+ "__version__",
21
32
  ]
@@ -33,5 +33,5 @@ class BaseSolver(abc.ABC):
33
33
  schedule = self.solve(instance)
34
34
  elapsed_time = time_start - time.perf_counter()
35
35
  schedule.metadata["elapsed_time"] = elapsed_time
36
- schedule.metadata["solved_by"] = f"{self.__class__.__name__}"
36
+ schedule.metadata["solved_by"] = self.__class__.__name__
37
37
  return schedule
@@ -4,9 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import functools
7
- from typing import Any
7
+ from typing import Any, List, Union, Dict
8
8
 
9
9
  import numpy as np
10
+ from numpy.typing import NDArray
10
11
 
11
12
  from job_shop_lib import Operation
12
13
 
@@ -14,49 +15,105 @@ from job_shop_lib import Operation
14
15
  class JobShopInstance:
15
16
  """Data structure to store a Job Shop Scheduling Problem instance.
16
17
 
17
- Additional attributes such as `num_jobs` or `num_machines` can be computed
18
- from the instance and are cached for performance if they require expensive
19
- computations.
18
+ Additional attributes such as ``num_machines`` or ``durations_matrix`` can
19
+ be computed from the instance and are cached for performance if they
20
+ require expensive computations.
21
+
22
+ Methods:
23
+
24
+ .. autosummary::
25
+ :nosignatures:
26
+
27
+ from_taillard_file
28
+ to_dict
29
+ from_matrices
30
+ set_operation_attributes
31
+
32
+ Properties:
33
+
34
+ .. autosummary::
35
+ :nosignatures:
36
+
37
+ num_jobs
38
+ num_machines
39
+ num_operations
40
+ is_flexible
41
+ durations_matrix
42
+ machines_matrix
43
+ durations_matrix_array
44
+ machines_matrix_array
45
+ operations_by_machine
46
+ max_duration
47
+ max_duration_per_job
48
+ max_duration_per_machine
49
+ job_durations
50
+ machine_loads
51
+ total_duration
20
52
 
21
53
  Attributes:
22
- jobs:
54
+ jobs (List[List[Operation]]):
23
55
  A list of lists of operations. Each list of operations represents
24
56
  a job, and the operations are ordered by their position in the job.
25
- The `job_id`, `position_in_job`, and `operation_id` attributes of
26
- the operations are set when the instance is created.
27
- name:
57
+ The ``job_id``, ``position_in_job``, and ``operation_id``
58
+ attributes of the operations are set when the instance is created.
59
+ name (str):
28
60
  A string with the name of the instance.
29
- metadata:
61
+ metadata (Dict[str, Any]):
30
62
  A dictionary with additional information about the instance.
63
+
64
+ Args:
65
+ jobs:
66
+ A list of lists of operations. Each list of operations
67
+ represents a job, and the operations are ordered by their
68
+ position in the job. The ``job_id``, ``position_in_job``, and
69
+ ``operation_id`` attributes of the operations are set when the
70
+ instance is created.
71
+ name:
72
+ A string with the name of the instance.
73
+ set_operation_attributes:
74
+ If True, the ``job_id``, ``position_in_job``, and ``operation_id``
75
+ attributes of the operations are set when the instance is created.
76
+ See :meth:`set_operation_attributes` for more information. Defaults
77
+ to True.
78
+ **metadata:
79
+ Additional information about the instance.
31
80
  """
32
81
 
33
82
  def __init__(
34
83
  self,
35
- jobs: list[list[Operation]],
84
+ jobs: List[List[Operation]],
36
85
  name: str = "JobShopInstance",
86
+ set_operation_attributes: bool = True,
37
87
  **metadata: Any,
38
88
  ):
39
- """Initializes the instance based on a list of lists of operations.
89
+ self.jobs: List[List[Operation]] = jobs
90
+ if set_operation_attributes:
91
+ self.set_operation_attributes()
92
+ self.name: str = name
93
+ self.metadata: Dict[str, Any] = metadata
94
+
95
+ def set_operation_attributes(self):
96
+ """Sets the ``job_id``, ``position_in_job``, and ``operation_id``
97
+ attributes for each operation in the instance.
98
+
99
+ The ``job_id`` attribute is set to the id of the job to which the
100
+ operation belongs.
101
+
102
+ The ``position_in_job`` attribute is set to the
103
+ position of the operation in the job (starts from 0).
104
+
105
+ The ``operation_id`` attribute is set to a unique identifier for the
106
+ operation (starting from 0).
107
+
108
+ The formula to compute the ``operation_id`` in a job shop instance with
109
+ a fixed number of operations per job is:
110
+
111
+ .. code-block:: python
112
+
113
+ operation_id = job_id * num_operations_per_job + position_in_job
40
114
 
41
- Args:
42
- jobs:
43
- A list of lists of operations. Each list of operations
44
- represents a job, and the operations are ordered by their
45
- position in the job. The `job_id`, `position_in_job`, and
46
- `operation_id` attributes of the operations are set when the
47
- instance is created.
48
- name:
49
- A string with the name of the instance.
50
- **metadata:
51
- Additional information about the instance.
52
115
  """
53
- self.jobs = jobs
54
- self.set_operation_attributes()
55
- self.name = name
56
- self.metadata = metadata
57
116
 
58
- def set_operation_attributes(self):
59
- """Sets the job_id and position of each operation."""
60
117
  operation_id = 0
61
118
  for job_id, job in enumerate(self.jobs):
62
119
  for position, operation in enumerate(job):
@@ -68,10 +125,10 @@ class JobShopInstance:
68
125
  @classmethod
69
126
  def from_taillard_file(
70
127
  cls,
71
- file_path: os.PathLike | str | bytes,
128
+ file_path: Union[os.PathLike, str, bytes],
72
129
  encoding: str = "utf-8",
73
130
  comment_symbol: str = "#",
74
- name: str | None = None,
131
+ name: Union[str, None] = None,
75
132
  **metadata: Any,
76
133
  ) -> JobShopInstance:
77
134
  """Creates a JobShopInstance from a file following Taillard's format.
@@ -91,8 +148,8 @@ class JobShopInstance:
91
148
  Additional information about the instance.
92
149
 
93
150
  Returns:
94
- A JobShopInstance object with the operations read from the file,
95
- and the name and metadata provided.
151
+ A :class:`JobShopInstance` object with the operations read from the
152
+ file, and the name and metadata provided.
96
153
  """
97
154
  with open(file_path, "r", encoding=encoding) as file:
98
155
  lines = file.readlines()
@@ -121,7 +178,7 @@ class JobShopInstance:
121
178
  name = name.split(".")[0]
122
179
  return cls(jobs=jobs, name=name, **metadata)
123
180
 
124
- def to_dict(self) -> dict[str, Any]:
181
+ def to_dict(self) -> Dict[str, Any]:
125
182
  """Returns a dictionary representation of the instance.
126
183
 
127
184
  This representation is useful for saving the instance to a JSON file,
@@ -129,13 +186,17 @@ class JobShopInstance:
129
186
  like Taillard's.
130
187
 
131
188
  Returns:
132
- The returned dictionary has the following structure:
133
- {
134
- "name": self.name,
135
- "duration_matrix": self.durations_matrix,
136
- "machines_matrix": self.machines_matrix,
137
- "metadata": self.metadata,
138
- }
189
+ Dict[str, Any]: The returned dictionary has the following
190
+ structure:
191
+
192
+ .. code-block:: python
193
+
194
+ {
195
+ "name": self.name,
196
+ "duration_matrix": self.durations_matrix,
197
+ "machines_matrix": self.machines_matrix,
198
+ "metadata": self.metadata,
199
+ }
139
200
  """
140
201
  return {
141
202
  "name": self.name,
@@ -147,12 +208,13 @@ class JobShopInstance:
147
208
  @classmethod
148
209
  def from_matrices(
149
210
  cls,
150
- duration_matrix: list[list[int]],
151
- machines_matrix: list[list[list[int]]] | list[list[int]],
211
+ duration_matrix: List[List[int]],
212
+ machines_matrix: List[List[List[int]]] | List[List[int]],
152
213
  name: str = "JobShopInstance",
153
- metadata: dict[str, Any] | None = None,
214
+ metadata: Dict[str, Any] | None = None,
154
215
  ) -> JobShopInstance:
155
- """Creates a JobShopInstance from duration and machines matrices.
216
+ """Creates a :class:`JobShopInstance` from duration and machines
217
+ matrices.
156
218
 
157
219
  Args:
158
220
  duration_matrix:
@@ -169,9 +231,9 @@ class JobShopInstance:
169
231
  A dictionary with additional information about the instance.
170
232
 
171
233
  Returns:
172
- A JobShopInstance object.
234
+ A :class:`JobShopInstance` object.
173
235
  """
174
- jobs: list[list[Operation]] = [[] for _ in range(len(duration_matrix))]
236
+ jobs: List[List[Operation]] = [[] for _ in range(len(duration_matrix))]
175
237
 
176
238
  num_jobs = len(duration_matrix)
177
239
  for job_id in range(num_jobs):
@@ -221,27 +283,29 @@ class JobShopInstance:
221
283
 
222
284
  @functools.cached_property
223
285
  def is_flexible(self) -> bool:
224
- """Returns True if any operation has more than one machine."""
286
+ """Returns ``True`` if any operation has more than one machine."""
225
287
  return any(
226
288
  any(len(operation.machines) > 1 for operation in job)
227
289
  for job in self.jobs
228
290
  )
229
291
 
230
292
  @functools.cached_property
231
- def durations_matrix(self) -> list[list[int]]:
293
+ def durations_matrix(self) -> List[List[int]]:
232
294
  """Returns the duration matrix of the instance.
233
295
 
234
- The duration of the operation with `job_id` i and `position_in_job` j
235
- is stored in the i-th position of the j-th list of the returned matrix:
296
+ The duration of the operation with ``job_id`` i and ``position_in_job``
297
+ j is stored in the i-th position of the j-th list of the returned
298
+ matrix:
299
+
300
+ .. code-block:: python
301
+
302
+ duration = instance.durations_matrix[i][j]
236
303
 
237
- ```python
238
- duration = instance.durations_matrix[i][j]
239
- ```
240
304
  """
241
305
  return [[operation.duration for operation in job] for job in self.jobs]
242
306
 
243
307
  @functools.cached_property
244
- def machines_matrix(self) -> list[list[list[int]]] | list[list[int]]:
308
+ def machines_matrix(self) -> Union[List[List[List[int]]], List[List[int]]]:
245
309
  """Returns the machines matrix of the instance.
246
310
 
247
311
  If the instance is flexible (i.e., if any operation has more than one
@@ -253,9 +317,9 @@ class JobShopInstance:
253
317
  To access the machines of the operation with position i in the job
254
318
  with id j, the following code must be used:
255
319
 
256
- ```python
257
- machines = instance.machines_matrix[j][i]
258
- ```
320
+ .. code-block:: python
321
+
322
+ machines = instance.machines_matrix[j][i]
259
323
 
260
324
  """
261
325
  if self.is_flexible:
@@ -267,34 +331,34 @@ class JobShopInstance:
267
331
  ]
268
332
 
269
333
  @functools.cached_property
270
- def durations_matrix_array(self) -> np.ndarray:
334
+ def durations_matrix_array(self) -> NDArray[np.float32]:
271
335
  """Returns the duration matrix of the instance as a numpy array.
272
336
 
273
- The returned array has shape (num_jobs, max_num_operations_per_job).
274
- Non-existing operations are filled with np.nan.
337
+ The returned array has shape (``num_jobs``,
338
+ ``max_num_operations_per_job``).
339
+ Non-existing operations are filled with ``np.nan``.
275
340
 
276
341
  Example:
277
342
  >>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
278
343
  >>> instance = JobShopInstance(jobs)
279
344
  >>> instance.durations_matrix_array
280
- array([[ 2., 2.],
345
+ array([[ 2., 3.],
281
346
  [ 4., nan]], dtype=float32)
282
347
  """
283
348
  duration_matrix = self.durations_matrix
284
349
  return self._fill_matrix_with_nans_2d(duration_matrix)
285
350
 
286
351
  @functools.cached_property
287
- def machines_matrix_array(self) -> np.ndarray:
352
+ def machines_matrix_array(self) -> NDArray[np.float32]:
288
353
  """Returns the machines matrix of the instance as a numpy array.
289
354
 
290
- The returned array has shape (num_jobs, max_num_operations_per_job,
291
- max_num_machines_per_operation). Non-existing machines are filled with
292
- np.nan.
355
+ The returned array has shape (``num_jobs``,
356
+ ``max_num_operations_per_job``, ``max_num_machines_per_operation``).
357
+ Non-existing machines are filled with ``np.nan``.
293
358
 
294
359
  Example:
295
360
  >>> jobs = [
296
- ... [Operation(machines=[0, 1], 2), Operation(machines=1, 3)],
297
- ... [Operation(machines=0, 6)],
361
+ ... [Operation([0, 1], 2), Operation(1, 3)], [Operation(0, 6)]
298
362
  ... ]
299
363
  >>> instance = JobShopInstance(jobs)
300
364
  >>> instance.machines_matrix_array
@@ -307,25 +371,25 @@ class JobShopInstance:
307
371
  machines_matrix = self.machines_matrix
308
372
  if self.is_flexible:
309
373
  # False positive from mypy, the type of machines_matrix is
310
- # list[list[list[int]]] here
374
+ # List[List[List[int]]] here
311
375
  return self._fill_matrix_with_nans_3d(
312
376
  machines_matrix # type: ignore[arg-type]
313
377
  )
314
378
 
315
379
  # False positive from mypy, the type of machines_matrix is
316
- # list[list[int]] here
380
+ # List[List[int]] here
317
381
  return self._fill_matrix_with_nans_2d(
318
382
  machines_matrix # type: ignore[arg-type]
319
383
  )
320
384
 
321
385
  @functools.cached_property
322
- def operations_by_machine(self) -> list[list[Operation]]:
386
+ def operations_by_machine(self) -> List[List[Operation]]:
323
387
  """Returns a list of lists of operations.
324
388
 
325
389
  The i-th list contains the operations that can be processed in the
326
390
  machine with id i.
327
391
  """
328
- operations_by_machine: list[list[Operation]] = [
392
+ operations_by_machine: List[List[Operation]] = [
329
393
  [] for _ in range(self.num_machines)
330
394
  ]
331
395
  for job in self.jobs:
@@ -345,7 +409,7 @@ class JobShopInstance:
345
409
  )
346
410
 
347
411
  @functools.cached_property
348
- def max_duration_per_job(self) -> list[float]:
412
+ def max_duration_per_job(self) -> List[float]:
349
413
  """Returns the maximum duration of each job in the instance.
350
414
 
351
415
  The maximum duration of the job with id i is stored in the i-th
@@ -356,7 +420,7 @@ class JobShopInstance:
356
420
  return [max(op.duration for op in job) for job in self.jobs]
357
421
 
358
422
  @functools.cached_property
359
- def max_duration_per_machine(self) -> list[int]:
423
+ def max_duration_per_machine(self) -> List[int]:
360
424
  """Returns the maximum duration of each machine in the instance.
361
425
 
362
426
  The maximum duration of the machine with id i is stored in the i-th
@@ -375,7 +439,7 @@ class JobShopInstance:
375
439
  return max_duration_per_machine
376
440
 
377
441
  @functools.cached_property
378
- def job_durations(self) -> list[int]:
442
+ def job_durations(self) -> List[int]:
379
443
  """Returns a list with the duration of each job in the instance.
380
444
 
381
445
  The duration of a job is the sum of the durations of its operations.
@@ -386,7 +450,7 @@ class JobShopInstance:
386
450
  return [sum(op.duration for op in job) for job in self.jobs]
387
451
 
388
452
  @functools.cached_property
389
- def machine_loads(self) -> list[int]:
453
+ def machine_loads(self) -> List[int]:
390
454
  """Returns the total machine load of each machine in the instance.
391
455
 
392
456
  The total machine load of a machine is the sum of the durations of the
@@ -409,8 +473,10 @@ class JobShopInstance:
409
473
  return sum(self.job_durations)
410
474
 
411
475
  @staticmethod
412
- def _fill_matrix_with_nans_2d(matrix: list[list[int]]) -> np.ndarray:
413
- """Fills a matrix with np.nan values.
476
+ def _fill_matrix_with_nans_2d(
477
+ matrix: List[List[int]],
478
+ ) -> NDArray[np.float32]:
479
+ """Fills a matrix with ``np.nan`` values.
414
480
 
415
481
  Args:
416
482
  matrix:
@@ -418,7 +484,7 @@ class JobShopInstance:
418
484
 
419
485
  Returns:
420
486
  A numpy array with the same shape as the input matrix, filled with
421
- np.nan values.
487
+ ``np.nan`` values.
422
488
  """
423
489
  max_length = max(len(row) for row in matrix)
424
490
  squared_matrix = np.full(
@@ -429,8 +495,10 @@ class JobShopInstance:
429
495
  return squared_matrix
430
496
 
431
497
  @staticmethod
432
- def _fill_matrix_with_nans_3d(matrix: list[list[list[int]]]) -> np.ndarray:
433
- """Fills a 3D matrix with np.nan values.
498
+ def _fill_matrix_with_nans_3d(
499
+ matrix: List[List[List[int]]],
500
+ ) -> NDArray[np.float32]:
501
+ """Fills a 3D matrix with ``np.nan`` values.
434
502
 
435
503
  Args:
436
504
  matrix:
@@ -438,7 +506,7 @@ class JobShopInstance:
438
506
 
439
507
  Returns:
440
508
  A numpy array with the same shape as the input matrix, filled with
441
- np.nan values.
509
+ ``np.nan`` values.
442
510
  """
443
511
  max_length = max(len(row) for row in matrix)
444
512
  max_inner_length = len(matrix[0][0])
@@ -454,3 +522,9 @@ class JobShopInstance:
454
522
  for j, inner_row in enumerate(row):
455
523
  squared_matrix[i, j, : len(inner_row)] = inner_row
456
524
  return squared_matrix
525
+
526
+
527
+ if __name__ == "__main__":
528
+ import doctest
529
+
530
+ doctest.testmod()
@@ -0,0 +1,118 @@
1
+ """Home of the `Operation` class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Union, List
6
+
7
+ from job_shop_lib.exceptions import ValidationError
8
+
9
+
10
+ class Operation:
11
+ """Stores machine and duration information for a job operation.
12
+
13
+ An operation is a task that must be performed on a machine. It is part of a
14
+ job and has a duration that represents the time it takes to complete the
15
+ task.
16
+
17
+ Tip:
18
+ To use custom attributes, such as due dates or priorities, subclass
19
+ this class and add the desired attributes.
20
+
21
+ Note:
22
+ To increase performance, some solvers such as the CP-SAT solver use
23
+ only integers to represent the operation's attributes. Should a
24
+ problem involve operations with non-integer durations, it would be
25
+ necessary to multiply all durations by a sufficiently large integer so
26
+ that every duration is an integer.
27
+
28
+ Args:
29
+ machines:
30
+ A list of machine ids that can perform the operation. If
31
+ only one machine can perform the operation, it can be passed as
32
+ an integer.
33
+ duration:
34
+ The time it takes to perform the operation.
35
+ """
36
+
37
+ __slots__ = {
38
+ "machines": (
39
+ "A list of machine ids that can perform the operation. If "
40
+ "only one machine can perform the operation, it can be passed as "
41
+ "an integer."
42
+ ),
43
+ "duration": (
44
+ "The time it takes to perform the operation. Often referred"
45
+ " to as the processing time."
46
+ ),
47
+ "job_id": (
48
+ "The id of the job the operation belongs to. Defaults to -1. "
49
+ "It is usually set by the :class:`JobShopInstance` class after "
50
+ "initialization."
51
+ ),
52
+ "position_in_job": (
53
+ "The index of the operation in the job. Defaults to -1. "
54
+ "It is usually set by the :class:`JobShopInstance` class after "
55
+ "initialization."
56
+ ),
57
+ "operation_id": (
58
+ "The id of the operation. This is unique within a "
59
+ ":class:`JobShopInstance`. Defaults to -1. It is usually set by "
60
+ "the :class:`JobShopInstance` class after initialization."
61
+ ),
62
+ }
63
+
64
+ def __init__(self, machines: Union[int, List[int]], duration: int):
65
+ self.machines: List[int] = (
66
+ [machines] if isinstance(machines, int) else machines
67
+ )
68
+ self.duration: int = duration
69
+
70
+ # Defined outside the class by the JobShopInstance class:
71
+ self.job_id: int = -1
72
+ self.position_in_job: int = -1
73
+ self.operation_id: int = -1
74
+
75
+ @property
76
+ def machine_id(self) -> int:
77
+ """Returns the id of the machine associated with the operation.
78
+
79
+ Raises:
80
+ UninitializedAttributeError:
81
+ If the operation has multiple machines in its list.
82
+ """
83
+ if len(self.machines) > 1:
84
+ raise ValidationError(
85
+ "Operation has multiple machines. The `machine_id` property "
86
+ "should only be used when working with a classic JSSP "
87
+ "instance. This error prevents silent bugs. To handle "
88
+ "operations with more machines you have to use the machines "
89
+ "attribute. If you get this error using `job_shop_lib` "
90
+ "objects, it means that that object does not support "
91
+ "operations with multiple machines yet."
92
+ )
93
+ return self.machines[0]
94
+
95
+ def is_initialized(self) -> bool:
96
+ """Returns whether the operation has been initialized."""
97
+ return (
98
+ self.job_id == -1
99
+ or self.position_in_job == -1
100
+ or self.operation_id == -1
101
+ )
102
+
103
+ def __hash__(self) -> int:
104
+ return hash(self.operation_id)
105
+
106
+ def __eq__(self, value: object) -> bool:
107
+ if not isinstance(value, Operation):
108
+ return False
109
+ return self.__slots__ == value.__slots__
110
+
111
+ def __repr__(self) -> str:
112
+ machines = (
113
+ self.machines[0] if len(self.machines) == 1 else self.machines
114
+ )
115
+ return (
116
+ f"O(m={machines}, d={self.duration}, "
117
+ f"j={self.job_id}, p={self.position_in_job})"
118
+ )