job-shop-lib 0.5.1__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 (95) 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 +10 -2
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
  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.1.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.1.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/generation/general_instance_generator.py +0 -169
  84. job_shop_lib/generation/transformations.py +0 -164
  85. job_shop_lib/generators/__init__.py +0 -8
  86. job_shop_lib/generators/basic_generator.py +0 -200
  87. job_shop_lib/graphs/constants.py +0 -21
  88. job_shop_lib/graphs/job_shop_graph.py +0 -202
  89. job_shop_lib/graphs/node.py +0 -166
  90. job_shop_lib/operation.py +0 -122
  91. job_shop_lib/visualization/agent_task_graph.py +0 -257
  92. job_shop_lib/visualization/create_gif.py +0 -209
  93. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  94. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  95. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -1,202 +0,0 @@
1
- """Home of the `JobShopGraph` class."""
2
-
3
- import collections
4
- import networkx as nx
5
-
6
- from job_shop_lib import JobShopInstance, JobShopLibError
7
- from job_shop_lib.graphs import Node, NodeType
8
-
9
-
10
- NODE_ATTR = "node"
11
-
12
-
13
- # pylint: disable=too-many-instance-attributes
14
- class JobShopGraph:
15
- """Data structure to represent a `JobShopInstance` as a graph.
16
-
17
- Provides a comprehensive graph-based representation of a job shop
18
- scheduling problem, utilizing the `networkx` library to model the complex
19
- relationships between jobs, operations, and machines. This class transforms
20
- the abstract scheduling problem into a directed graph, where various
21
- entities (jobs, machines, and operations) are nodes, and the dependencies
22
- (such as operation order within a job or machine assignment) are edges.
23
-
24
- This transformation allows for the application of graph algorithms
25
- to analyze and solve scheduling problems.
26
-
27
- Attributes:
28
- instance:
29
- The job shop instance encapsulated by this graph.
30
- graph:
31
- The directed graph representing the job shop, where nodes are
32
- operations, machines, jobs, or abstract concepts like global,
33
- source, and sink, with edges indicating dependencies.
34
- """
35
-
36
- def __init__(self, instance: JobShopInstance):
37
- """Initializes the graph with the given instance.
38
-
39
- Nodes of type `OPERATION` are added to the graph based on the
40
- operations of the instance.
41
-
42
- Args:
43
- instance:
44
- The job shop instance that the graph represents.
45
- """
46
- self.graph = nx.DiGraph()
47
- self.instance = instance
48
-
49
- self._nodes: list[Node] = []
50
- self._nodes_by_type: dict[NodeType, list[Node]] = (
51
- collections.defaultdict(list)
52
- )
53
- self._nodes_by_machine: list[list[Node]] = [
54
- [] for _ in range(instance.num_machines)
55
- ]
56
- self._nodes_by_job: list[list[Node]] = [
57
- [] for _ in range(instance.num_jobs)
58
- ]
59
- self._next_node_id = 0
60
- self.removed_nodes: list[bool] = []
61
- self._add_operation_nodes()
62
-
63
- @property
64
- def nodes(self) -> list[Node]:
65
- """List of all nodes added to the graph.
66
-
67
- It may contain nodes that have been removed from the graph.
68
- """
69
- return self._nodes
70
-
71
- @property
72
- def nodes_by_type(self) -> dict[NodeType, list[Node]]:
73
- """Dictionary mapping node types to lists of nodes.
74
-
75
- It may contain nodes that have been removed from the graph.
76
- """
77
- return self._nodes_by_type
78
-
79
- @property
80
- def nodes_by_machine(self) -> list[list[Node]]:
81
- """List of lists mapping machine ids to operation nodes.
82
-
83
- It may contain nodes that have been removed from the graph.
84
- """
85
- return self._nodes_by_machine
86
-
87
- @property
88
- def nodes_by_job(self) -> list[list[Node]]:
89
- """List of lists mapping job ids to operation nodes.
90
-
91
- It may contain nodes that have been removed from the graph.
92
- """
93
- return self._nodes_by_job
94
-
95
- @property
96
- def num_edges(self) -> int:
97
- """Number of edges in the graph."""
98
- return self.graph.number_of_edges()
99
-
100
- @property
101
- def num_job_nodes(self) -> int:
102
- """Number of job nodes in the graph."""
103
- return len(self._nodes_by_type[NodeType.JOB])
104
-
105
- def _add_operation_nodes(self) -> None:
106
- """Adds operation nodes to the graph."""
107
- for job in self.instance.jobs:
108
- for operation in job:
109
- node = Node(node_type=NodeType.OPERATION, operation=operation)
110
- self.add_node(node)
111
-
112
- def add_node(self, node_for_adding: Node) -> None:
113
- """Adds a node to the graph and updates relevant class attributes.
114
-
115
- This method assigns a unique identifier to the node, adds it to the
116
- graph, and updates the nodes list and the nodes_by_type dictionary. If
117
- the node is of type `OPERATION`, it also updates `nodes_by_job` and
118
- `nodes_by_machine` based on the operation's job_id and machine_ids.
119
-
120
- Args:
121
- node_for_adding (Node): The node to be added to the graph.
122
-
123
- Raises:
124
- ValueError: If the node type is unsupported or if required
125
- attributes for the node type are missing.
126
-
127
- Note:
128
- This method directly modifies the graph attribute as well as
129
- several other class attributes. Thus, adding nodes to the graph
130
- should be done exclusively through this method to avoid
131
- inconsistencies.
132
- """
133
- node_for_adding.node_id = self._next_node_id
134
- self.graph.add_node(
135
- node_for_adding.node_id, **{NODE_ATTR: node_for_adding}
136
- )
137
- self._nodes_by_type[node_for_adding.node_type].append(node_for_adding)
138
- self._nodes.append(node_for_adding)
139
- self._next_node_id += 1
140
- self.removed_nodes.append(False)
141
-
142
- if node_for_adding.node_type == NodeType.OPERATION:
143
- operation = node_for_adding.operation
144
- self._nodes_by_job[operation.job_id].append(node_for_adding)
145
- for machine_id in operation.machines:
146
- self._nodes_by_machine[machine_id].append(node_for_adding)
147
-
148
- def add_edge(
149
- self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
150
- ) -> None:
151
- """Adds an edge to the graph.
152
-
153
- Args:
154
- u_of_edge: The source node of the edge. If it is a `Node`, its
155
- `node_id` is used as the source. Otherwise, it is assumed to be
156
- the node_id of the source.
157
- v_of_edge: The destination node of the edge. If it is a `Node`, its
158
- `node_id` is used as the destination. Otherwise, it is assumed
159
- to be the node_id of the destination.
160
- **attr: Additional attributes to be added to the edge.
161
-
162
- Raises:
163
- JobShopLibError: If `u_of_edge` or `v_of_edge` are not in the
164
- graph.
165
- """
166
- if isinstance(u_of_edge, Node):
167
- u_of_edge = u_of_edge.node_id
168
- if isinstance(v_of_edge, Node):
169
- v_of_edge = v_of_edge.node_id
170
- if u_of_edge not in self.graph or v_of_edge not in self.graph:
171
- raise JobShopLibError(
172
- "`u_of_edge` and `v_of_edge` must be in the graph."
173
- )
174
- self.graph.add_edge(u_of_edge, v_of_edge, **attr)
175
-
176
- def remove_node(self, node_id: int) -> None:
177
- """Removes a node from the graph and the isolated nodes that result
178
- from the removal.
179
-
180
- Args:
181
- node_id: The id of the node to remove.
182
- """
183
- self.graph.remove_node(node_id)
184
- self.removed_nodes[node_id] = True
185
-
186
- isolated_nodes = list(nx.isolates(self.graph))
187
- for isolated_node in isolated_nodes:
188
- self.removed_nodes[isolated_node] = True
189
-
190
- self.graph.remove_nodes_from(isolated_nodes)
191
-
192
- def is_removed(self, node: int | Node) -> bool:
193
- """Returns whether the node is removed from the graph.
194
-
195
- Args:
196
- node: The node to check. If it is a `Node`, its `node_id` is used
197
- as the node to check. Otherwise, it is assumed to be the
198
- `node_id` of the node to check.
199
- """
200
- if isinstance(node, Node):
201
- node = node.node_id
202
- return self.removed_nodes[node]
@@ -1,166 +0,0 @@
1
- """Home of the `Node` class."""
2
-
3
- from job_shop_lib import Operation, JobShopLibError
4
- from job_shop_lib.graphs.constants import NodeType
5
-
6
-
7
- class Node:
8
- """Data structure to represent a node in the `JobShopGraph`.
9
-
10
- A node is hashable by its id. The id is assigned when the node is added to
11
- the graph. The id must be unique for each node in the graph, and should be
12
- used to identify the node in the networkx graph.
13
-
14
- Depending on the type of the node, it can have different attributes. The
15
- following table shows the attributes of each type of node:
16
-
17
- Node Type | Required Attribute
18
- ----------------|---------------------
19
- OPERATION | `operation`
20
- MACHINE | `machine_id`
21
- JOB | `job_id`
22
-
23
- In terms of equality, two nodes are equal if they have the same id.
24
- Additionally, one node is equal to an integer if the integer is equal to
25
- its id. It is also hashable by its id.
26
-
27
- This allows for using the node as a key in a dictionary, at the same time
28
- we can use its id to index that dictionary. Example:
29
-
30
- ```python
31
- node = Node(NodeType.SOURCE)
32
- node.node_id = 1
33
- graph = {node: "some value"}
34
- print(graph[node]) # "some value"
35
- print(graph[1]) # "some value"
36
- ```
37
-
38
- Attributes:
39
- node_type:
40
- The type of the node. It can be one of the following:
41
- - NodeType.OPERATION
42
- - NodeType.MACHINE
43
- - NodeType.JOB
44
- - NodeType.GLOBAL
45
- ...
46
- """
47
-
48
- __slots__ = "node_type", "_node_id", "_operation", "_machine_id", "_job_id"
49
-
50
- def __init__(
51
- self,
52
- node_type: NodeType,
53
- operation: Operation | None = None,
54
- machine_id: int | None = None,
55
- job_id: int | None = None,
56
- ):
57
- """Initializes the node with the given attributes.
58
-
59
- Args:
60
- node_type:
61
- The type of the node. It can be one of the following:
62
- - NodeType.OPERATION
63
- - NodeType.MACHINE
64
- - NodeType.JOB
65
- - NodeType.GLOBAL
66
- ...
67
- operation:
68
- The operation of the node. It should be provided if the
69
- `node_type` is NodeType.OPERATION.
70
- machine_id:
71
- The id of the machine of the node. It should be provided if the
72
- node_type is NodeType.MACHINE.
73
- job_id:
74
- The id of the job of the node. It should be provided if the
75
- node_type is NodeType.JOB.
76
-
77
- Raises:
78
- JobShopLibError:
79
- If the node_type is OPERATION and operation is None.
80
- JobShopLibError:
81
- If the node_type is MACHINE and machine_id is None.
82
- JobShopLibError:
83
- If the node_type is JOB and job_id is None.
84
- """
85
- if node_type == NodeType.OPERATION and operation is None:
86
- raise JobShopLibError("Operation node must have an operation.")
87
-
88
- if node_type == NodeType.MACHINE and machine_id is None:
89
- raise JobShopLibError("Machine node must have a machine_id.")
90
-
91
- if node_type == NodeType.JOB and job_id is None:
92
- raise JobShopLibError("Job node must have a job_id.")
93
-
94
- self.node_type = node_type
95
- self._node_id: int | None = None
96
-
97
- self._operation = operation
98
- self._machine_id = machine_id
99
- self._job_id = job_id
100
-
101
- @property
102
- def node_id(self) -> int:
103
- """Returns a unique identifier for the node."""
104
- if self._node_id is None:
105
- raise JobShopLibError("Node has not been assigned an id.")
106
- return self._node_id
107
-
108
- @node_id.setter
109
- def node_id(self, value: int) -> None:
110
- self._node_id = value
111
-
112
- @property
113
- def operation(self) -> Operation:
114
- """Returns the operation of the node.
115
-
116
- This property is mandatory for nodes of type `OPERATION`.
117
- """
118
- if self._operation is None:
119
- raise JobShopLibError("Node has no operation.")
120
- return self._operation
121
-
122
- @property
123
- def machine_id(self) -> int:
124
- """Returns the `machine_id` of the node.
125
-
126
- This property is mandatory for nodes of type `MACHINE`.
127
- """
128
- if self._machine_id is None:
129
- raise JobShopLibError("Node has no `machine_id`.")
130
- return self._machine_id
131
-
132
- @property
133
- def job_id(self) -> int:
134
- """Returns the `job_id` of the node.
135
-
136
- This property is mandatory for nodes of type `JOB`.
137
- """
138
- if self._job_id is None:
139
- raise JobShopLibError("Node has no `job_id`.")
140
- return self._job_id
141
-
142
- def __hash__(self) -> int:
143
- return self.node_id
144
-
145
- def __eq__(self, __value: object) -> bool:
146
- if isinstance(__value, Node):
147
- __value = __value.node_id
148
- return self.node_id == __value
149
-
150
- def __repr__(self) -> str:
151
- if self.node_type == NodeType.OPERATION:
152
- return (
153
- f"Node(node_type={self.node_type.name}, id={self._node_id}, "
154
- f"operation={self.operation})"
155
- )
156
- if self.node_type == NodeType.MACHINE:
157
- return (
158
- f"Node(node_type={self.node_type.name}, id={self._node_id}, "
159
- f"machine_id={self._machine_id})"
160
- )
161
- if self.node_type == NodeType.JOB:
162
- return (
163
- f"Node(node_type={self.node_type.name}, id={self._node_id}, "
164
- f"job_id={self._job_id})"
165
- )
166
- return f"Node(node_type={self.node_type.name}, id={self._node_id})"
job_shop_lib/operation.py DELETED
@@ -1,122 +0,0 @@
1
- """Home of the `Operation` class."""
2
-
3
- from __future__ import annotations
4
-
5
- from job_shop_lib import JobShopLibError
6
-
7
-
8
- class Operation:
9
- """Stores machine and duration information for a job operation.
10
-
11
- Note:
12
- To increase performance, some solvers such as the CP-SAT solver use
13
- only integers to represent the operation's attributes. Should a
14
- problem involve operations with non-integer durations, it would be
15
- necessary to multiply all durations by a sufficiently large integer so
16
- that every duration is an integer.
17
-
18
- Attributes:
19
- machines: A list of machine ids that can perform the operation.
20
- duration: The time it takes to perform the operation.
21
- """
22
-
23
- __slots__ = (
24
- "machines",
25
- "duration",
26
- "_job_id",
27
- "_position_in_job",
28
- "_operation_id",
29
- )
30
-
31
- def __init__(self, machines: int | list[int], duration: int):
32
- """Initializes the object with the given machines and duration.
33
-
34
- Args:
35
- machines: A list of machine ids that can perform the operation. If
36
- only one machine can perform the operation, it can be passed as
37
- an integer.
38
- duration: The time it takes to perform the operation.
39
- """
40
- self.machines = [machines] if isinstance(machines, int) else machines
41
- self.duration = duration
42
-
43
- # Defined outside the class by the JobShopInstance class:
44
- self._job_id: int | None = None
45
- self._position_in_job: int | None = None
46
- self._operation_id: int | None = None
47
-
48
- @property
49
- def machine_id(self) -> int:
50
- """Returns the id of the machine associated with the operation.
51
-
52
- Raises:
53
- ValueError: If the operation has multiple machines in its list.
54
- """
55
- if len(self.machines) > 1:
56
- raise JobShopLibError("Operation has multiple machines.")
57
- return self.machines[0]
58
-
59
- @property
60
- def job_id(self) -> int:
61
- """Returns the id of the job that the operation belongs to."""
62
- if self._job_id is None:
63
- raise JobShopLibError("Operation has no job_id.")
64
- return self._job_id
65
-
66
- @job_id.setter
67
- def job_id(self, value: int) -> None:
68
- self._job_id = value
69
-
70
- @property
71
- def position_in_job(self) -> int:
72
- """Returns the position (starting at zero) of the operation in the
73
- job.
74
-
75
- Raises:
76
- ValueError: If the operation has no position_in_job.
77
- """
78
- if self._position_in_job is None:
79
- raise JobShopLibError("Operation has no position_in_job.")
80
- return self._position_in_job
81
-
82
- @position_in_job.setter
83
- def position_in_job(self, value: int) -> None:
84
- self._position_in_job = value
85
-
86
- @property
87
- def operation_id(self) -> int:
88
- """Returns the id of the operation.
89
-
90
- The operation id is unique within a job shop instance and should
91
- be set by the JobShopInstance class.
92
-
93
- It starts at 0 and is incremented by 1 for each operation in the
94
- instance.
95
-
96
- Raises:
97
- ValueError: If the operation has no id.
98
- """
99
- if self._operation_id is None:
100
- raise JobShopLibError("Operation has no id.")
101
- return self._operation_id
102
-
103
- @operation_id.setter
104
- def operation_id(self, value: int) -> None:
105
- self._operation_id = value
106
-
107
- def __hash__(self) -> int:
108
- return hash(self.operation_id)
109
-
110
- def __eq__(self, value: object) -> bool:
111
- if not isinstance(value, Operation):
112
- return False
113
- return self.__slots__ == value.__slots__
114
-
115
- def __repr__(self) -> str:
116
- machines = (
117
- self.machines[0] if len(self.machines) == 1 else self.machines
118
- )
119
- return (
120
- f"O(m={machines}, d={self.duration}, "
121
- f"j={self.job_id}, p={self.position_in_job})"
122
- )