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
@@ -1,197 +0,0 @@
1
- """Home of the `BasicGenerator` class."""
2
-
3
- import random
4
- from typing import Iterator
5
-
6
- from job_shop_lib import JobShopInstance, Operation
7
-
8
-
9
- class BasicGenerator: # pylint: disable=too-many-instance-attributes
10
- """Generates instances for job shop problems.
11
-
12
- This class is designed to be versatile, enabling the creation of various
13
- job shop instances without the need for multiple dedicated classes.
14
-
15
- It supports customization of the number of jobs, machines, operation
16
- durations, and more.
17
-
18
- The class supports both single instance generation and iteration over
19
- multiple instances, controlled by the `iteration_limit` parameter. It
20
- implements the iterator protocol, allowing it to be used in a `for` loop.
21
-
22
- Note:
23
- When used as an iterator, the generator will produce instances until it
24
- reaches the specified `iteration_limit`. If `iteration_limit` is None,
25
- it will continue indefinitely.
26
-
27
- Attributes:
28
- num_jobs_range:
29
- The range of the number of jobs to generate. If a single
30
- int is provided, it is used as both the minimum and maximum.
31
- duration_range:
32
- The range of durations for each operation.
33
- num_machines_range:
34
- The range of the number of machines available. If a
35
- single int is provided, it is used as both the minimum and maximum.
36
- machines_per_operation:
37
- Specifies how many machines each operation
38
- can be assigned to. If a single int is provided, it is used for
39
- all operations.
40
- allow_less_jobs_than_machines:
41
- If True, allows generating instances where the number of jobs is
42
- less than the number of machines.
43
- allow_recirculation:
44
- If True, a job can visit the same machine more than once.
45
- name_suffix:
46
- A suffix to append to each instance's name for identification.
47
- seed:
48
- Seed for the random number generator to ensure reproducibility.
49
- """
50
-
51
- def __init__( # pylint: disable=too-many-arguments
52
- self,
53
- num_jobs: int | tuple[int, int] = (10, 20),
54
- num_machines: int | tuple[int, int] = (5, 10),
55
- duration_range: tuple[int, int] = (1, 99),
56
- allow_less_jobs_than_machines: bool = True,
57
- allow_recirculation: bool = False,
58
- machines_per_operation: int | tuple[int, int] = 1,
59
- name_suffix: str = "classic_generated_instance",
60
- seed: int | None = None,
61
- iteration_limit: int | None = None,
62
- ):
63
- """Initializes the instance generator with the given parameters.
64
-
65
- Args:
66
- num_jobs:
67
- The range of the number of jobs to generate.
68
- num_machines:
69
- The range of the number of machines available.
70
- duration_range:
71
- The range of durations for each operation.
72
- allow_less_jobs_than_machines:
73
- Allows instances with fewer jobs than machines.
74
- allow_recirculation:
75
- Allows jobs to visit the same machine multiple times.
76
- machines_per_operation:
77
- Specifies how many machines each operation can be assigned to.
78
- If a single int is provided, it is used for all operations.
79
- name_suffix:
80
- Suffix for instance names.
81
- seed:
82
- Seed for the random number generator.
83
- iteration_limit:
84
- Maximum number of instances to generate in iteration mode.
85
- """
86
- if isinstance(num_jobs, int):
87
- num_jobs = (num_jobs, num_jobs)
88
-
89
- if isinstance(num_machines, int):
90
- num_machines = (num_machines, num_machines)
91
-
92
- if isinstance(machines_per_operation, int):
93
- machines_per_operation = (
94
- machines_per_operation,
95
- machines_per_operation,
96
- )
97
-
98
- self.num_jobs_range = num_jobs
99
- self.duration_range = duration_range
100
- self.num_machines_range = num_machines
101
- self.machines_per_operation = machines_per_operation
102
-
103
- self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
104
- self.allow_recirculation = allow_recirculation
105
- self.name_suffix = name_suffix
106
-
107
- self._counter = 0
108
- self._current_iteration = 0
109
- self._iteration_limit = iteration_limit
110
-
111
- if seed is not None:
112
- random.seed(seed)
113
-
114
- def generate(self) -> JobShopInstance:
115
- """Generates a single job shop instance"""
116
- num_jobs = random.randint(*self.num_jobs_range)
117
-
118
- min_num_machines, max_num_machines = self.num_machines_range
119
- if not self.allow_less_jobs_than_machines:
120
- min_num_machines = min(num_jobs, max_num_machines)
121
- num_machines = random.randint(min_num_machines, max_num_machines)
122
-
123
- jobs = []
124
- available_machines = list(range(num_machines))
125
- for _ in range(num_jobs):
126
- job = []
127
- for _ in range(num_machines):
128
- operation = self.create_random_operation(available_machines)
129
- job.append(operation)
130
- jobs.append(job)
131
- available_machines = list(range(num_machines))
132
-
133
- return JobShopInstance(jobs=jobs, name=self._get_name())
134
-
135
- def __iter__(self) -> Iterator[JobShopInstance]:
136
- self._current_iteration = 0
137
- return self
138
-
139
- def __next__(self) -> JobShopInstance:
140
- if (
141
- self._iteration_limit is not None
142
- and self._current_iteration >= self._iteration_limit
143
- ):
144
- raise StopIteration
145
- self._current_iteration += 1
146
- return self.generate()
147
-
148
- def __len__(self) -> int:
149
- if self._iteration_limit is None:
150
- raise ValueError("Iteration limit is not set.")
151
- return self._iteration_limit
152
-
153
- def create_random_operation(
154
- self, available_machines: list[int] | None = None
155
- ) -> Operation:
156
- """Creates a random operation with the given available machines.
157
-
158
- Args:
159
- available_machines:
160
- A list of available machine_ids to choose from.
161
- If None, all machines are available.
162
- """
163
- duration = random.randint(*self.duration_range)
164
-
165
- if self.machines_per_operation[1] > 1:
166
- machines = self._choose_multiple_machines()
167
- return Operation(machines=machines, duration=duration)
168
-
169
- machine_id = self._choose_one_machine(available_machines)
170
- return Operation(machines=machine_id, duration=duration)
171
-
172
- def _choose_multiple_machines(self) -> list[int]:
173
- num_machines = random.randint(*self.machines_per_operation)
174
- available_machines = list(range(num_machines))
175
- machines = []
176
- for _ in range(num_machines):
177
- machine = random.choice(available_machines)
178
- machines.append(machine)
179
- available_machines.remove(machine)
180
- return machines
181
-
182
- def _choose_one_machine(
183
- self, available_machines: list[int] | None = None
184
- ) -> int:
185
- if available_machines is None:
186
- _, max_num_machines = self.num_machines_range
187
- available_machines = list(range(max_num_machines))
188
-
189
- machine_id = random.choice(available_machines)
190
- if not self.allow_recirculation:
191
- available_machines.remove(machine_id)
192
-
193
- return machine_id
194
-
195
- def _get_name(self) -> str:
196
- self._counter += 1
197
- return f"{self.name_suffix}_{self._counter}"
@@ -1,21 +0,0 @@
1
- """Constants for the graph module."""
2
-
3
- import enum
4
-
5
-
6
- class EdgeType(enum.Enum):
7
- """Enumeration of edge types."""
8
-
9
- CONJUNCTIVE = 0
10
- DISJUNCTIVE = 1
11
-
12
-
13
- class NodeType(enum.Enum):
14
- """Enumeration of node types."""
15
-
16
- OPERATION = enum.auto()
17
- MACHINE = enum.auto()
18
- JOB = enum.auto()
19
- GLOBAL = enum.auto()
20
- SOURCE = enum.auto()
21
- SINK = enum.auto()
@@ -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})"