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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {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}"
|
job_shop_lib/graphs/constants.py
DELETED
@@ -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]
|
job_shop_lib/graphs/node.py
DELETED
@@ -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})"
|