job-shop-lib 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ """Contains factory functions for creating node feature encoders."""
2
+
3
+ from enum import Enum
4
+
5
+ from job_shop_lib.dispatching.feature_observers import (
6
+ IsReadyObserver,
7
+ EarliestStartTimeObserver,
8
+ FeatureObserver,
9
+ DurationObserver,
10
+ IsScheduledObserver,
11
+ PositionInJobObserver,
12
+ RemainingOperationsObserver,
13
+ IsCompletedObserver,
14
+ )
15
+
16
+
17
+ class FeatureObserverType(str, Enum):
18
+ """Enumeration of node feature creator types for the job shop scheduling
19
+ problem."""
20
+
21
+ IS_READY = "is_ready"
22
+ EARLIEST_START_TIME = "earliest_start_time"
23
+ DURATION = "duration"
24
+ IS_SCHEDULED = "is_scheduled"
25
+ POSITION_IN_JOB = "position_in_job"
26
+ REMAINING_OPERATIONS = "remaining_operations"
27
+ IS_COMPLETED = "is_completed"
28
+ COMPOSITE = "composite"
29
+
30
+
31
+ def feature_observer_factory(
32
+ node_feature_creator_type: str | FeatureObserverType,
33
+ **kwargs,
34
+ ) -> FeatureObserver:
35
+ """Creates and returns a node feature creator based on the specified
36
+ node feature creator type.
37
+
38
+ Args:
39
+ node_feature_creator_type:
40
+ The type of node feature creator to create.
41
+ **kwargs:
42
+ Additional keyword arguments to pass to the node
43
+ feature creator constructor.
44
+
45
+ Returns:
46
+ A node feature creator instance.
47
+ """
48
+ mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
49
+ FeatureObserverType.IS_READY: IsReadyObserver,
50
+ FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
51
+ FeatureObserverType.DURATION: DurationObserver,
52
+ FeatureObserverType.IS_SCHEDULED: IsScheduledObserver,
53
+ FeatureObserverType.POSITION_IN_JOB: PositionInJobObserver,
54
+ FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
55
+ FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
56
+ }
57
+ feature_creator = mapping[node_feature_creator_type] # type: ignore[index]
58
+ return feature_creator(**kwargs)
@@ -0,0 +1,113 @@
1
+ """Home of the `FeatureObserver` class and `FeatureType` enum."""
2
+
3
+ import enum
4
+
5
+ import numpy as np
6
+ from job_shop_lib import ScheduledOperation
7
+ from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
8
+
9
+
10
+ class FeatureType(str, enum.Enum):
11
+ """Types of features that can be extracted."""
12
+
13
+ OPERATIONS = "operations"
14
+ MACHINES = "machines"
15
+ JOBS = "jobs"
16
+
17
+
18
+ class FeatureObserver(DispatcherObserver):
19
+ """Base class for feature observers."""
20
+
21
+ def __init__(
22
+ self,
23
+ dispatcher: Dispatcher,
24
+ feature_types: list[FeatureType] | FeatureType | None = None,
25
+ feature_size: dict[FeatureType, int] | int = 1,
26
+ is_singleton: bool = True,
27
+ subscribe: bool = True,
28
+ ):
29
+ feature_types = self.get_feature_types_list(feature_types)
30
+ if isinstance(feature_size, int):
31
+ feature_size = {
32
+ feature_type: feature_size for feature_type in feature_types
33
+ }
34
+ super().__init__(dispatcher, is_singleton, subscribe)
35
+
36
+ number_of_entities = {
37
+ FeatureType.OPERATIONS: dispatcher.instance.num_operations,
38
+ FeatureType.MACHINES: dispatcher.instance.num_machines,
39
+ FeatureType.JOBS: dispatcher.instance.num_jobs,
40
+ }
41
+ self.feature_dimensions = {
42
+ feature_type: (
43
+ number_of_entities[feature_type],
44
+ feature_size[feature_type],
45
+ )
46
+ for feature_type in feature_types
47
+ }
48
+ self.features = {
49
+ feature_type: np.zeros(
50
+ self.feature_dimensions[feature_type],
51
+ dtype=np.float32,
52
+ )
53
+ for feature_type in feature_types
54
+ }
55
+ self.initialize_features()
56
+
57
+ def initialize_features(self):
58
+ """Initializes the features based on the current state of the
59
+ dispatcher."""
60
+
61
+ def update(self, scheduled_operation: ScheduledOperation):
62
+ """Updates the features based on the scheduled operation.
63
+
64
+ By default, this method just calls `initialize_features`.
65
+
66
+ Args:
67
+ scheduled_operation:
68
+ The operation that has been scheduled.
69
+ """
70
+ self.initialize_features()
71
+
72
+ def reset(self):
73
+ """Sets features to zero and calls to `initialize_features`."""
74
+ self.set_features_to_zero()
75
+ self.initialize_features()
76
+
77
+ def set_features_to_zero(
78
+ self, exclude: FeatureType | list[FeatureType] | None = None
79
+ ):
80
+ """Sets features to zero."""
81
+ if exclude is None:
82
+ exclude = []
83
+ if isinstance(exclude, FeatureType):
84
+ exclude = [exclude]
85
+
86
+ for feature_type in self.features:
87
+ if feature_type in exclude:
88
+ continue
89
+ self.features[feature_type][:] = 0.0
90
+
91
+ @staticmethod
92
+ def get_feature_types_list(
93
+ feature_types: list[FeatureType] | FeatureType | None,
94
+ ) -> list[FeatureType]:
95
+ """Returns a list of feature types.
96
+
97
+ Args:
98
+ feature_types:
99
+ A list of feature types or a single feature type. If `None`,
100
+ all feature types are returned.
101
+ """
102
+ if isinstance(feature_types, FeatureType):
103
+ feature_types = [feature_types]
104
+ if feature_types is None:
105
+ feature_types = list(FeatureType)
106
+ return feature_types
107
+
108
+ def __str__(self):
109
+ out = [self.__class__.__name__, ":\n"]
110
+ out.append("-" * len(out[0]))
111
+ for feature_type, feature in self.features.items():
112
+ out.append(f"\n{feature_type.value}:\n{feature}")
113
+ return "".join(out)
@@ -0,0 +1,98 @@
1
+ """Home of the `IsCompletedObserver` class."""
2
+
3
+ from typing import Iterable
4
+
5
+ import numpy as np
6
+
7
+ from job_shop_lib import ScheduledOperation
8
+ from job_shop_lib.dispatching import Dispatcher
9
+ from job_shop_lib.dispatching.feature_observers import (
10
+ FeatureObserver,
11
+ FeatureType,
12
+ RemainingOperationsObserver,
13
+ )
14
+
15
+
16
+ class IsCompletedObserver(FeatureObserver):
17
+ """Observer that adds a binary feature indicating whether each operation,
18
+ machine, or job has been completed."""
19
+
20
+ def __init__(
21
+ self,
22
+ dispatcher: Dispatcher,
23
+ feature_types: list[FeatureType] | FeatureType | None = None,
24
+ ):
25
+ feature_types = self.get_feature_types_list(feature_types)
26
+ self.remaining_ops_per_machine = np.zeros(
27
+ (dispatcher.instance.num_machines, 1), dtype=int
28
+ )
29
+ self.remaining_ops_per_job = np.zeros(
30
+ (dispatcher.instance.num_jobs, 1), dtype=int
31
+ )
32
+ super().__init__(dispatcher, feature_types, feature_size=1)
33
+
34
+ def initialize_features(self):
35
+ self._initialize_remaining_operations()
36
+
37
+ def update(self, scheduled_operation: ScheduledOperation):
38
+ if FeatureType.OPERATIONS in self.features:
39
+ # operation_id = scheduled_operation.operation.operation_id
40
+ # self.features[FeatureType.OPERATIONS][operation_id, 0] = 1
41
+ completed_operations = [
42
+ op.operation_id
43
+ for op in self.dispatcher.completed_operations()
44
+ ]
45
+ self.features[FeatureType.OPERATIONS][completed_operations, 0] = 1
46
+ if FeatureType.MACHINES in self.features:
47
+ machine_id = scheduled_operation.machine_id
48
+ self.remaining_ops_per_machine[machine_id, 0] -= 1
49
+ is_completed = self.remaining_ops_per_machine[machine_id, 0] == 0
50
+ self.features[FeatureType.MACHINES][machine_id, 0] = is_completed
51
+ if FeatureType.JOBS in self.features:
52
+ job_id = scheduled_operation.job_id
53
+ self.remaining_ops_per_job[job_id, 0] -= 1
54
+ is_completed = self.remaining_ops_per_job[job_id, 0] == 0
55
+ self.features[FeatureType.JOBS][job_id, 0] = is_completed
56
+
57
+ def _initialize_remaining_operations(self):
58
+ remaining_ops_observer = self._get_remaining_operations_observer(
59
+ self.dispatcher, self.features
60
+ )
61
+ if remaining_ops_observer is not None:
62
+ if FeatureType.JOBS in self.features:
63
+ self.remaining_ops_per_job = remaining_ops_observer.features[
64
+ FeatureType.JOBS
65
+ ].copy()
66
+ if FeatureType.MACHINES in self.features:
67
+ self.remaining_ops_per_machine = (
68
+ remaining_ops_observer.features[
69
+ FeatureType.MACHINES
70
+ ].copy()
71
+ )
72
+ return
73
+
74
+ # If there is no remaining operations observer, we need to
75
+ # compute the remaining operations ourselves.
76
+ # We iterate over all operations using scheduled_operations
77
+ # instead of uncompleted_operations, because in this case
78
+ # they will output the same operations, and the former is slightly
79
+ # more efficient.
80
+ for operation in self.dispatcher.unscheduled_operations():
81
+ if FeatureType.JOBS in self.features:
82
+ self.remaining_ops_per_job[operation.job_id, 0] += 1
83
+ if FeatureType.MACHINES in self.features:
84
+ self.remaining_ops_per_machine[operation.machine_id, 0] += 1
85
+
86
+ def _get_remaining_operations_observer(
87
+ self, dispatcher: Dispatcher, feature_types: Iterable[FeatureType]
88
+ ) -> RemainingOperationsObserver | None:
89
+ for observer in dispatcher.subscribers:
90
+ if not isinstance(observer, RemainingOperationsObserver):
91
+ continue
92
+ has_same_features = all(
93
+ feature_type in observer.features
94
+ for feature_type in feature_types
95
+ )
96
+ if has_same_features:
97
+ return observer
98
+ return None
@@ -0,0 +1,40 @@
1
+ """Home of the `IsReadyObserver` class."""
2
+
3
+ from job_shop_lib.dispatching import Dispatcher
4
+ from job_shop_lib.dispatching.feature_observers import (
5
+ FeatureObserver,
6
+ FeatureType,
7
+ )
8
+
9
+
10
+ class IsReadyObserver(FeatureObserver):
11
+ """Feature creator that adds a binary feature indicating if the operation
12
+ is ready to be dispatched."""
13
+
14
+ def __init__(
15
+ self,
16
+ dispatcher: Dispatcher,
17
+ feature_types: list[FeatureType] | FeatureType | None = None,
18
+ subscribe: bool = True,
19
+ ):
20
+ super().__init__(
21
+ dispatcher, feature_types, feature_size=1, subscribe=subscribe
22
+ )
23
+
24
+ def initialize_features(self):
25
+ self.set_features_to_zero()
26
+ for feature_type, feature in self.features.items():
27
+ node_ids = self._get_ready_nodes(feature_type)
28
+ feature[node_ids, 0] = 1.0
29
+
30
+ def _get_ready_nodes(self, feature_type: FeatureType) -> list[int]:
31
+ mapping = {
32
+ FeatureType.OPERATIONS: self._get_ready_operation_nodes,
33
+ FeatureType.MACHINES: self.dispatcher.available_machines,
34
+ FeatureType.JOBS: self.dispatcher.available_jobs,
35
+ }
36
+ return mapping[feature_type]()
37
+
38
+ def _get_ready_operation_nodes(self) -> list[int]:
39
+ available_operations = self.dispatcher.available_operations()
40
+ return [operation.operation_id for operation in available_operations]
@@ -0,0 +1,34 @@
1
+ """Home of the `IsScheduledObserver` class."""
2
+
3
+ from job_shop_lib import ScheduledOperation
4
+ from job_shop_lib.dispatching.feature_observers import (
5
+ FeatureObserver,
6
+ FeatureType,
7
+ )
8
+
9
+
10
+ class IsScheduledObserver(FeatureObserver):
11
+ """Observer that updates features based on scheduling operations.
12
+
13
+ This observer tracks which operations have been scheduled and updates
14
+ feature matrices accordingly. It updates a feature in the
15
+ `FeatureType.OPERATIONS` matrix to indicate that an operation has been
16
+ scheduled. Additionally, it counts the number of uncompleted but
17
+ scheduled operations for each machine and job, updating the respective
18
+ `FeatureType.MACHINES` and `FeatureType.JOBS` feature matrices.
19
+ """
20
+
21
+ def update(self, scheduled_operation: ScheduledOperation):
22
+ if FeatureType.OPERATIONS in self.features:
23
+ self.features[FeatureType.OPERATIONS][
24
+ scheduled_operation.operation.operation_id, 0
25
+ ] = 1.0
26
+
27
+ ongoing_operations = self.dispatcher.ongoing_operations()
28
+ self.set_features_to_zero(exclude=FeatureType.OPERATIONS)
29
+ for scheduled_op in ongoing_operations:
30
+ if FeatureType.MACHINES in self.features:
31
+ machine_id = scheduled_op.machine_id
32
+ self.features[FeatureType.MACHINES][machine_id, 0] += 1.0
33
+ if FeatureType.JOBS in self.features:
34
+ self.features[FeatureType.JOBS][scheduled_op.job_id, 0] += 1.0
@@ -0,0 +1,39 @@
1
+ """Home of the `PositionInJobObserver` class."""
2
+
3
+ from job_shop_lib.dispatching import Dispatcher
4
+ from job_shop_lib import ScheduledOperation
5
+ from job_shop_lib.dispatching.feature_observers import (
6
+ FeatureObserver,
7
+ FeatureType,
8
+ )
9
+
10
+
11
+ class PositionInJobObserver(FeatureObserver):
12
+ """Observer that adds a feature indicating the position of
13
+ operations in their respective jobs.
14
+
15
+ Positions are adjusted dynamically as operations are scheduled.
16
+ """
17
+
18
+ def __init__(self, dispatcher: Dispatcher, subscribe: bool = True):
19
+ super().__init__(
20
+ dispatcher,
21
+ feature_types=[FeatureType.OPERATIONS],
22
+ feature_size=1,
23
+ subscribe=subscribe,
24
+ )
25
+
26
+ def initialize_features(self):
27
+ for operation in self.dispatcher.unscheduled_operations():
28
+ self.features[FeatureType.OPERATIONS][
29
+ operation.operation_id, 0
30
+ ] = operation.position_in_job
31
+
32
+ def update(self, scheduled_operation: ScheduledOperation):
33
+ job = self.dispatcher.instance.jobs[scheduled_operation.job_id]
34
+ for new_position_in_job, operation in enumerate(
35
+ job[scheduled_operation.position_in_job + 1 :]
36
+ ):
37
+ self.features[FeatureType.OPERATIONS][
38
+ operation.operation_id, 0
39
+ ] = new_position_in_job
@@ -0,0 +1,54 @@
1
+ """Home of the `RemainingOperationsObserver` class."""
2
+
3
+ from job_shop_lib import ScheduledOperation
4
+ from job_shop_lib.dispatching import Dispatcher
5
+ from job_shop_lib.dispatching.feature_observers import (
6
+ FeatureObserver,
7
+ FeatureType,
8
+ )
9
+
10
+
11
+ class RemainingOperationsObserver(FeatureObserver):
12
+ """Adds a feature indicating the number of remaining operations for each
13
+ job and machine.
14
+
15
+ It does not support FeatureType.OPERATIONS.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ dispatcher: Dispatcher,
21
+ feature_types: list[FeatureType] | FeatureType | None = None,
22
+ subscribe: bool = True,
23
+ ):
24
+ if feature_types is None:
25
+ feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
26
+
27
+ if (
28
+ feature_types == FeatureType.OPERATIONS
29
+ or FeatureType.OPERATIONS in feature_types
30
+ ):
31
+ raise ValueError("FeatureType.OPERATIONS is not supported.")
32
+ super().__init__(
33
+ dispatcher,
34
+ feature_types=feature_types,
35
+ feature_size=1,
36
+ subscribe=subscribe,
37
+ )
38
+
39
+ def initialize_features(self):
40
+ for operation in self.dispatcher.unscheduled_operations():
41
+ if FeatureType.JOBS in self.features:
42
+ self.features[FeatureType.JOBS][operation.job_id, 0] += 1
43
+ if FeatureType.MACHINES in self.features:
44
+ self.features[FeatureType.MACHINES][
45
+ operation.machine_id, 0
46
+ ] += 1
47
+
48
+ def update(self, scheduled_operation: ScheduledOperation):
49
+ if FeatureType.JOBS in self.features:
50
+ job_id = scheduled_operation.job_id
51
+ self.features[FeatureType.JOBS][job_id, 0] -= 1
52
+ if FeatureType.MACHINES in self.features:
53
+ machine_id = scheduled_operation.machine_id
54
+ self.features[FeatureType.MACHINES][machine_id, 0] -= 1
@@ -23,6 +23,26 @@ from job_shop_lib.graphs import JobShopGraph, EdgeType, NodeType, Node
23
23
 
24
24
 
25
25
  def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
26
+ """Builds and returns a disjunctive graph for the given job shop instance.
27
+
28
+ This function creates a complete disjunctive graph from a JobShopInstance.
29
+ It starts by initializing a JobShopGraph object and proceeds by adding
30
+ disjunctive edges between operations using the same machine, conjunctive
31
+ edges between successive operations in the same job, and finally, special
32
+ source and sink nodes with their respective edges to and from all other
33
+ operations.
34
+
35
+ Edges have a "type" attribute indicating whether they are disjunctive or
36
+ conjunctive.
37
+
38
+ Args:
39
+ instance (JobShopInstance): The job shop instance for which to build
40
+ the graph.
41
+
42
+ Returns:
43
+ JobShopGraph: A JobShopGraph object representing the disjunctive graph
44
+ of the job shop scheduling problem.
45
+ """
26
46
  graph = JobShopGraph(instance)
27
47
  add_disjunctive_edges(graph)
28
48
  add_conjunctive_edges(graph)
@@ -6,6 +6,8 @@ import os
6
6
  import functools
7
7
  from typing import Any
8
8
 
9
+ import numpy as np
10
+
9
11
  from job_shop_lib import Operation
10
12
 
11
13
 
@@ -264,6 +266,58 @@ class JobShopInstance:
264
266
  [operation.machine_id for operation in job] for job in self.jobs
265
267
  ]
266
268
 
269
+ @functools.cached_property
270
+ def durations_matrix_array(self) -> np.ndarray:
271
+ """Returns the duration matrix of the instance as a numpy array.
272
+
273
+ The returned array has shape (num_jobs, max_num_operations_per_job).
274
+ Non-existing operations are filled with np.nan.
275
+
276
+ Example:
277
+ >>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
278
+ >>> instance = JobShopInstance(jobs)
279
+ >>> instance.durations_matrix_array
280
+ array([[ 2., 2.],
281
+ [ 4., nan]], dtype=float32)
282
+ """
283
+ duration_matrix = self.durations_matrix
284
+ return self._fill_matrix_with_nans_2d(duration_matrix)
285
+
286
+ @functools.cached_property
287
+ def machines_matrix_array(self) -> np.ndarray:
288
+ """Returns the machines matrix of the instance as a numpy array.
289
+
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.
293
+
294
+ Example:
295
+ >>> jobs = [
296
+ ... [Operation(machines=[0, 1], 2), Operation(machines=1, 3)],
297
+ ... [Operation(machines=0, 6)],
298
+ ... ]
299
+ >>> instance = JobShopInstance(jobs)
300
+ >>> instance.machines_matrix_array
301
+ array([[[ 0., 1.],
302
+ [ 1., nan]],
303
+ [[ 0., nan],
304
+ [nan, nan]]], dtype=float32)
305
+ """
306
+
307
+ machines_matrix = self.machines_matrix
308
+ if self.is_flexible:
309
+ # False positive from mypy, the type of machines_matrix is
310
+ # list[list[list[int]]] here
311
+ return self._fill_matrix_with_nans_3d(
312
+ machines_matrix # type: ignore[arg-type]
313
+ )
314
+
315
+ # False positive from mypy, the type of machines_matrix is
316
+ # list[list[int]] here
317
+ return self._fill_matrix_with_nans_2d(
318
+ machines_matrix # type: ignore[arg-type]
319
+ )
320
+
267
321
  @functools.cached_property
268
322
  def operations_by_machine(self) -> list[list[Operation]]:
269
323
  """Returns a list of lists of operations.
@@ -353,3 +407,50 @@ class JobShopInstance:
353
407
  def total_duration(self) -> int:
354
408
  """Returns the sum of the durations of all operations in all jobs."""
355
409
  return sum(self.job_durations)
410
+
411
+ @staticmethod
412
+ def _fill_matrix_with_nans_2d(matrix: list[list[int]]) -> np.ndarray:
413
+ """Fills a matrix with np.nan values.
414
+
415
+ Args:
416
+ matrix:
417
+ A list of lists of integers.
418
+
419
+ Returns:
420
+ A numpy array with the same shape as the input matrix, filled with
421
+ np.nan values.
422
+ """
423
+ max_length = max(len(row) for row in matrix)
424
+ squared_matrix = np.full(
425
+ (len(matrix), max_length), np.nan, dtype=np.float32
426
+ )
427
+ for i, row in enumerate(matrix):
428
+ squared_matrix[i, : len(row)] = row
429
+ return squared_matrix
430
+
431
+ @staticmethod
432
+ def _fill_matrix_with_nans_3d(matrix: list[list[list[int]]]) -> np.ndarray:
433
+ """Fills a 3D matrix with np.nan values.
434
+
435
+ Args:
436
+ matrix:
437
+ A list of lists of lists of integers.
438
+
439
+ Returns:
440
+ A numpy array with the same shape as the input matrix, filled with
441
+ np.nan values.
442
+ """
443
+ max_length = max(len(row) for row in matrix)
444
+ max_inner_length = len(matrix[0][0])
445
+ for row in matrix:
446
+ for inner_row in row:
447
+ max_inner_length = max(max_inner_length, len(inner_row))
448
+ squared_matrix = np.full(
449
+ (len(matrix), max_length, max_inner_length),
450
+ np.nan,
451
+ dtype=np.float32,
452
+ )
453
+ for i, row in enumerate(matrix):
454
+ for j, inner_row in enumerate(row):
455
+ squared_matrix[i, j, : len(inner_row)] = inner_row
456
+ return squared_matrix