job-shop-lib 0.3.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,156 @@
1
+ """Home of the `EarliestStartTimeObserver` class."""
2
+
3
+ import numpy as np
4
+
5
+ from job_shop_lib.dispatching import Dispatcher
6
+ from job_shop_lib.dispatching.feature_observers import (
7
+ FeatureObserver,
8
+ FeatureType,
9
+ )
10
+ from job_shop_lib.scheduled_operation import ScheduledOperation
11
+
12
+
13
+ class EarliestStartTimeObserver(FeatureObserver):
14
+ """Observer that adds a feature indicating the earliest start time of
15
+ each operation, machine, and job in the graph."""
16
+
17
+ def __init__(
18
+ self,
19
+ dispatcher: Dispatcher,
20
+ feature_types: list[FeatureType] | FeatureType | None = None,
21
+ subscribe: bool = True,
22
+ ):
23
+
24
+ # Earliest start times initialization
25
+ # -------------------------------
26
+ squared_duration_matrix = dispatcher.instance.durations_matrix_array
27
+ self.earliest_start_times = np.hstack(
28
+ (
29
+ np.zeros((squared_duration_matrix.shape[0], 1)),
30
+ np.cumsum(squared_duration_matrix[:, :-1], axis=1),
31
+ )
32
+ )
33
+ self.earliest_start_times[np.isnan(squared_duration_matrix)] = np.nan
34
+ # -------------------------------
35
+ super().__init__(
36
+ dispatcher, feature_types, feature_size=1, subscribe=subscribe
37
+ )
38
+
39
+ def update(self, scheduled_operation: ScheduledOperation):
40
+ """Recomputes the earliest start times and calls the
41
+ `initialize_features` method.
42
+
43
+ The earliest start times is computed as the cumulative sum of the
44
+ previous unscheduled operations in the job plus the maximum of the
45
+ completion time of the last scheduled operation and the next available
46
+ time of the machine(s) the operation is assigned.
47
+
48
+ After that, we substract the current time.
49
+ """
50
+ # We compute the gap that the current scheduled operation could be
51
+ # adding to each job.
52
+ job_id = scheduled_operation.job_id
53
+ next_operation_idx = self.dispatcher.job_next_operation_index[job_id]
54
+ if next_operation_idx < len(self.dispatcher.instance.jobs[job_id]):
55
+ old_start_time = self.earliest_start_times[
56
+ job_id, next_operation_idx
57
+ ]
58
+ next_operation = self.dispatcher.instance.jobs[job_id][
59
+ next_operation_idx
60
+ ]
61
+ new_start_time = max(
62
+ scheduled_operation.end_time,
63
+ old_start_time,
64
+ self.dispatcher.earliest_start_time(next_operation),
65
+ )
66
+ gap = new_start_time - old_start_time
67
+ self.earliest_start_times[job_id, next_operation_idx:] += gap
68
+
69
+ # Now, we compute the gap that could be introduced by the new
70
+ # next_available_time of the machine.
71
+ operations_by_machine = self.dispatcher.instance.operations_by_machine
72
+ for operation in operations_by_machine[scheduled_operation.machine_id]:
73
+ if self.dispatcher.is_scheduled(operation):
74
+ continue
75
+ old_start_time = self.earliest_start_times[
76
+ operation.job_id, operation.position_in_job
77
+ ]
78
+ new_start_time = max(old_start_time, scheduled_operation.end_time)
79
+ gap = new_start_time - old_start_time
80
+ self.earliest_start_times[
81
+ operation.job_id, operation.position_in_job :
82
+ ] += gap
83
+
84
+ self.initialize_features()
85
+
86
+ def initialize_features(self):
87
+ """Initializes the features based on the current state of the
88
+ dispatcher."""
89
+ mapping = {
90
+ FeatureType.OPERATIONS: self._update_operation_features,
91
+ FeatureType.MACHINES: self._update_machine_features,
92
+ FeatureType.JOBS: self._update_job_features,
93
+ }
94
+ for feature_type in self.features:
95
+ mapping[feature_type]()
96
+
97
+ def _update_operation_features(self):
98
+ """Ravels the 2D array into a 1D array"""
99
+ current_time = self.dispatcher.current_time()
100
+ next_index = 0
101
+ for job_id, operations in enumerate(self.dispatcher.instance.jobs):
102
+ self.features[FeatureType.OPERATIONS][
103
+ next_index : next_index + len(operations), 0
104
+ ] = (
105
+ self.earliest_start_times[job_id, : len(operations)]
106
+ - current_time
107
+ )
108
+ next_index += len(operations)
109
+
110
+ def _update_machine_features(self):
111
+ """Picks the minimum start time of all operations that can be scheduled
112
+ on that machine"""
113
+ current_time = self.dispatcher.current_time()
114
+ operations_by_machine = self.dispatcher.instance.operations_by_machine
115
+ for machine_id, operations in enumerate(operations_by_machine):
116
+ min_earliest_start_time = min(
117
+ (
118
+ self.earliest_start_times[
119
+ operation.job_id, operation.position_in_job
120
+ ]
121
+ for operation in operations
122
+ if not self.dispatcher.is_scheduled(operation)
123
+ ),
124
+ default=0,
125
+ )
126
+ self.features[FeatureType.MACHINES][machine_id, 0] = (
127
+ min_earliest_start_time - current_time
128
+ )
129
+
130
+ def _update_job_features(self):
131
+ """Picks the earliest start time of the next operation in the job"""
132
+ current_time = self.dispatcher.current_time()
133
+ for job_id, next_operation_idx in enumerate(
134
+ self.dispatcher.job_next_operation_index
135
+ ):
136
+ job_length = len(self.dispatcher.instance.jobs[job_id])
137
+ if next_operation_idx == job_length:
138
+ continue
139
+ self.features[FeatureType.JOBS][job_id, 0] = (
140
+ self.earliest_start_times[job_id, next_operation_idx]
141
+ - current_time
142
+ )
143
+
144
+
145
+ if __name__ == "__main__":
146
+ squared_durations_matrix = np.array([[1, 1, 7], [5, 1, 1], [1, 3, 2]])
147
+ # Add a zeros column to the left of the matrix
148
+ cumulative_durations = np.hstack(
149
+ (
150
+ np.zeros((squared_durations_matrix.shape[0], 1)),
151
+ squared_durations_matrix[:, :-1],
152
+ )
153
+ )
154
+ # Set to nan the values that are not available
155
+ cumulative_durations[np.isnan(squared_durations_matrix)] = np.nan
156
+ print(cumulative_durations)
@@ -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)