diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index fd20fdd..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-
-*.pyc
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/Job_Shop_Scheduling_Benchmark_Environments_and_Instances.iml b/.idea/Job_Shop_Scheduling_Benchmark_Environments_and_Instances.iml
deleted file mode 100644
index bab880c..0000000
--- a/.idea/Job_Shop_Scheduling_Benchmark_Environments_and_Instances.iml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index a8dd5f1..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,156 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 66c8820..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index c33840b..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index cf63073..d7066ad 100644
--- a/README.md
+++ b/README.md
@@ -10,14 +10,16 @@ We aim to include a wide range of solution methods capable of solving machine sc
-| Solution methods | Job Shop | Flow Show | Flexible Job Shop | SDST | Assembly | Online Arrivals |
+| Solution methods | Job Shop (JSP) | Flow Show (FSP) | Flexible Job Shop (FJSP) | FJSP SDST | FAJSP | Online (F)JSP |
| :---: | :---:| :---: | :---: | :---: | :---: | :---: |
-| Load Balancing Heuristics | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Dispatching Rules | ✓ | ✓ | ✓ | | ✓ | ✓* |
-| Genetic Algorithm | ✓ | ✓ | ✓ | ✓ | ✓ | |
+| Load Balancing Heuristics | ✓ | ✓ | ✓ | ✓ | ✓ | |
+| Genetic Algorithm | ✓ | ✓ | ✓ | ✓ | ✓ | |
+| MILP | ✓ | ✓ | ✓ | ✓ | | |
+| CP-SAT | ✓ | ✓ | ✓ | | | |
| FJSP-DRL | ✓ | ✓ | ✓ | | | |
-*Capable of online arrivals of FJSP problems
+*Capable of online arrivals of FJSP problems
🔜 We have a few DRL-based solutions in the pipeline, which will be published here upon completion.
@@ -37,4 +39,3 @@ Robbert Reijnen, Kjell van Straaten, Zaharah Bukhsh, and Yingqian Zhang (2023) [
primaryClass={cs.AI}
}
```
-🔜 Supporting paper and corresponding reference
diff --git a/configs/FJSP_DRL.toml b/configs/FJSP_DRL.toml
index b849946..06e6a46 100644
--- a/configs/FJSP_DRL.toml
+++ b/configs/FJSP_DRL.toml
@@ -1,16 +1,16 @@
[env_parameters]
-num_jobs = 10 # Number of jobs in the environment
-num_mas = 5 # Number of machine agents
-batch_size = 20 # Batch size for training
-ope_feat_dim = 6 # Dimension of operation features
-ma_feat_dim = 3 # Dimension of machine agent features
-valid_batch_size = 5 # Batch size for validation
-show_mode = "print" # Mode for displaying information (e.g., "print")
+num_jobs = 10 # Number of jobs in the environment
+num_mas = 5 # Number of machine agents
+batch_size = 20 # Batch size for training
+ope_feat_dim = 6 # Dimension of operation features
+ma_feat_dim = 3 # Dimension of machine agent features
+valid_batch_size = 5 # Batch size for validation
+device = "cpu" # Device for training ("cpu" or "cuda")
[model_parameters]
in_size_ma = 3 # Input size for machine agent
out_size_ma = 8 # Output size for machine agent
-in_size_ope = 6 # Input size for operation
+in_size_ope = 6 # Input size for operation
out_size_ope = 8 # Output size for operation
hidden_size_ope = 128 # Hidden size for operation model
num_heads = [1, 1] # Number of attention heads
@@ -20,6 +20,7 @@ n_latent_critic = 64 # Number of latent units for critic
n_hidden_actor = 3 # Number of hidden layers for actor
n_hidden_critic = 3 # Number of hidden layers for critic
action_dim = 1 # Dimension of action space
+device = "cpu" # Device for training ("cpu" or "cuda")
[train_parameters]
lr = 0.0002 # Learning rate
@@ -33,18 +34,20 @@ entropy_coeff = 0.01 # Coefficient for entropy loss
max_iterations = 1000 # Maximum number of iterations
save_timestep = 20 # Timestep for saving model checkpoints
update_timestep = 1 # Timestep for model updates
-viz = true # Visualize the training process --> when True: first launch local server with: python -m visdom.server
-viz_name = 'training_visualisation' # Name of visualization env
minibatch_size = 512 # Mini-batch size for training
-parallel_iter = 3 # Number of parallel iterations
-device = "cpu" # Device for training ("cpu" or "cuda")
+parallel_iter = 20 # Number of parallel iterations
+viz = false # Visualize the training process --> when true: first launch local server with: python -m visdom.server (and set to: online)
+viz_name = 'training_visualisation' # Name of visualization env
validation_folder = "/fjsp/song/dev/1005/" # Folder for validation data
+device = "cpu" # Device for training ("cpu" or "cuda")
# Configuration for test parameters
[test_parameters]
-problem_instance = "/fjsp/1_Brandimarte/Mk02.fjs" # Problem instance for testing
-trained_policy = "./solutions/FJSP_DRL/models/song_10_5.pt" # Load pretrained policy
-sample = false # Sampling flag for testing
-num_sample = 5 # Number of samples for testing (nr )
-device = "cpu" # Device for testing ("cpu" or "cuda")
\ No newline at end of file
+seed = 1
+problem_instance = "/fjsp/1_Brandimarte/Mk02.fjs" # Problem instance for testing
+trained_policy = "/solution_methods/FJSP_DRL/save/train_20240314_192906/song_10_5.pt" # Load pretrained policy
+sample = false # Sampling flag for testing
+num_sample = 1 # Number of samples for testing (nr )
+plotting = true # plot instance representation and ganttchart
+device = "cpu" # Device for testing ("cpu" or "cuda")
\ No newline at end of file
diff --git a/configs/milp.toml b/configs/milp.toml
index 5b5ecf8..2809508 100644
--- a/configs/milp.toml
+++ b/configs/milp.toml
@@ -1,5 +1,5 @@
[instance]
-problem_instance = "/fjsp/2_Hurink/edata/abz5.fjs" # configure problem instance to be scheduled
+problem_instance = "/jsp/adams/abz5" # configure problem instance to be scheduled
[solver]
-time_limit = 3600 # time limit for the gurobi solver, in seconds
\ No newline at end of file
+time_limit = 64800 # time limit for the gurobi solver, in seconds
\ No newline at end of file
diff --git a/configs/or_tools.toml b/configs/or_tools.toml
new file mode 100644
index 0000000..3146e39
--- /dev/null
+++ b/configs/or_tools.toml
@@ -0,0 +1,5 @@
+[instance]
+problem_instance = "/fjsp/2_Hurink/edata/abz5.fjs" # configure problem instance to be scheduled
+
+[solver]
+time_limit = 3600 # time limit for the OR-tools CP-SAT solver, in seconds
\ No newline at end of file
diff --git a/data_parsers/parser_fajsp.py b/data_parsers/parser_fajsp.py
index e582977..c9e8645 100644
--- a/data_parsers/parser_fajsp.py
+++ b/data_parsers/parser_fajsp.py
@@ -1,8 +1,8 @@
-from pathlib import Path
import re
+from pathlib import Path
-from scheduling_environment.machine import Machine
from scheduling_environment.job import Job
+from scheduling_environment.machine import Machine
from scheduling_environment.operation import Operation
diff --git a/data_parsers/parser_fjsp.py b/data_parsers/parser_fjsp.py
index 1a796f1..b0f02f7 100644
--- a/data_parsers/parser_fjsp.py
+++ b/data_parsers/parser_fjsp.py
@@ -1,8 +1,8 @@
-from pathlib import Path
import re
+from pathlib import Path
-from scheduling_environment.machine import Machine
from scheduling_environment.job import Job
+from scheduling_environment.machine import Machine
from scheduling_environment.operation import Operation
diff --git a/data_parsers/parser_fjsp_sdst.py b/data_parsers/parser_fjsp_sdst.py
index b1d1ee3..8472526 100644
--- a/data_parsers/parser_fjsp_sdst.py
+++ b/data_parsers/parser_fjsp_sdst.py
@@ -1,8 +1,8 @@
from pathlib import Path
-from scheduling_environment.operation import Operation
-from scheduling_environment.machine import Machine
from scheduling_environment.job import Job
+from scheduling_environment.machine import Machine
+from scheduling_environment.operation import Operation
def parse(JobShop, instance, from_absolute_path=False):
diff --git a/data_parsers/parser_jsp_fsp.py b/data_parsers/parser_jsp_fsp.py
index 5b208cb..361d6a9 100644
--- a/data_parsers/parser_jsp_fsp.py
+++ b/data_parsers/parser_jsp_fsp.py
@@ -1,9 +1,9 @@
-from pathlib import Path
import re
+from pathlib import Path
-from scheduling_environment.operation import Operation
-from scheduling_environment.machine import Machine
from scheduling_environment.job import Job
+from scheduling_environment.machine import Machine
+from scheduling_environment.operation import Operation
def parse(JobShop, instance, from_absolute_path=False):
diff --git a/plotting/drawer.py b/plotting/drawer.py
index ec61f4b..3063084 100644
--- a/plotting/drawer.py
+++ b/plotting/drawer.py
@@ -1,12 +1,14 @@
import copy
-import random
import os
-import networkx as nx
+import random
from statistics import mean
-from scheduling_environment.jobShop import JobShop
-import numpy as np
-import matplotlib.pyplot as plt
+
import matplotlib.colors as mcolors
+import matplotlib.pyplot as plt
+import networkx as nx
+import numpy as np
+
+from scheduling_environment.jobShop import JobShop
def create_colormap():
diff --git a/requirements.txt b/requirements.txt
index cf369a4..50adec4 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/run_FJSP_DRL.py b/run_FJSP_DRL.py
index d56cdbd..538ca96 100644
--- a/run_FJSP_DRL.py
+++ b/run_FJSP_DRL.py
@@ -1,33 +1,16 @@
-# GITHUB REPO: https://github.com/songwenas12/fjsp-drl
-
-# Code based on the paper:
-# "Flexible Job Shop Scheduling via Graph Neural Network and Deep Reinforcement Learning"
-# by Wen Song, Xinyang Chen, Qiqiang Li and Zhiguang Cao
-# Presented in IEEE Transactions on Industrial Informatics, 2023.
-# Paper URL: https://ieeexplore.ieee.org/document/9826438
-
import argparse
-import copy
import logging
+import os
+import time
-import gymnasium as gym
-import pynvml
import torch
-from solutions.FJSP_DRL import PPO_model
-from solutions.FJSP_DRL.env import FJSPEnv
-from solutions.FJSP_DRL.load_data import nums_detec
-from solutions.helper_functions import load_parameters
from plotting.drawer import draw_gantt_chart
-
-logging.basicConfig(level=logging.INFO)
+from solution_methods.FJSP_DRL.env_test import FJSPEnv_test
+from solution_methods.FJSP_DRL.PPO import HGNNScheduler
+from solution_methods.helper_functions import load_job_shop_env, load_parameters
PARAM_FILE = "configs/FJSP_DRL.toml"
-DEFAULT_RESULTS_ROOT = "./results/single_runs"
-
-import pkg_resources
-print(pkg_resources.get_distribution("gym").version)
-
def initialize_device(parameters: dict) -> torch.device:
@@ -37,112 +20,63 @@ def initialize_device(parameters: dict) -> torch.device:
return torch.device(device_str)
-def schedule(env: FJSPEnv, model: PPO_model.PPO, memories, flag_sample=False):
- """
- Schedules the provided environment using the given model and memories.
+def run_method(**parameters):
+ # Extract parameters
+ device = initialize_device(parameters)
+ model_parameters = parameters["model_parameters"]
+ test_parameters = parameters["test_parameters"]
- Parameters:
- - env: The environment to be scheduled.
- - model: The model to be used for scheduling.
- - memories: Memories to be used with the model.
- - flag_sample: A flag to determine whether sampling should be performed.
+ # Configure default device
+ torch.set_default_tensor_type('torch.cuda.FloatTensor' if device.type == 'cuda' else 'torch.FloatTensor')
+ if device.type == 'cuda':
+ torch.cuda.set_device(device)
- Returns:
- - A deep copy of the makespan batch from the environment.
- """
+ # Load trained policy
+ trained_policy = os.getcwd() + test_parameters['trained_policy']
+ if trained_policy.endswith('.pt'):
+ if device.type == 'cuda':
+ policy = torch.load(trained_policy)
+ else:
+ policy = torch.load(trained_policy, map_location='cpu')
- # Initialize the environment state and completion signals
- state = env.state
- dones = env.done_batch
- done = False
+ model_parameters["actor_in_dim"] = model_parameters["out_size_ma"] * 2 + model_parameters["out_size_ope"] * 2
+ model_parameters["critic_in_dim"] = model_parameters["out_size_ma"] + model_parameters["out_size_ope"]
- # Iterate until all tasks are complete
- while not done:
- # Predict the next action without accumulating gradients
- with torch.no_grad():
- actions = model.policy_old.act(state, memories, dones, flag_sample=flag_sample, flag_train=False)
+ hgnn_model = HGNNScheduler(model_parameters).to(device)
+ print('\nloading saved model:', trained_policy)
+ hgnn_model.load_state_dict(policy)
- # Update the environment state based on the predicted action
- state, _, dones = env.step(actions)
- done = dones.all()
+ # Configure environment and load instance
+ instance_path = test_parameters['problem_instance']
+ JSMEnv = load_job_shop_env(instance_path)
+ env_test = FJSPEnv_test(JSMEnv, test_parameters)
- # Draw gantt chart
- for ix, sim_env in enumerate(env.simulation_envs):
- draw_gantt_chart(sim_env)
+ # Get state and completion signal
+ state = env_test.state
+ done = False
+ last_time = time.time()
- # Check the validity of the produced Gantt chart (only feasible for FJSP)
- if not env.validate_gantt()[0]:
- print("Scheduling Error!")
+ # Generate schedule for instance
+ while ~done:
+ with torch.no_grad():
+ actions = hgnn_model.act(state, [], done, flag_train=False, flag_sample=test_parameters['sample'])
+ state, _, done = env_test.step(actions)
- return copy.deepcopy(env.makespan_batch)
+ print("spend_time:", time.time() - last_time)
+ print("makespan(s):", env_test.JSP_instance.makespan)
+ if test_parameters['plotting']:
+ draw_gantt_chart(env_test.JSP_instance)
-def main(param_file: str):
- # # Initialize NVML library
- pynvml.nvmlInit()
- # Load parameters
+def main(param_file=PARAM_FILE):
try:
parameters = load_parameters(param_file)
except FileNotFoundError:
logging.error(f"Parameter file {param_file} not found.")
return
- device = initialize_device(parameters)
- logging.info(f"Using device {device}")
- # Configure PyTorch's default device
- torch.set_default_tensor_type('torch.cuda.FloatTensor' if device.type == 'cuda' else 'torch.FloatTensor')
- if device.type == 'cuda':
- torch.cuda.set_device(device)
- # Extract parameters
- env_parameters = parameters["env_parameters"]
- model_parameters = parameters["model_parameters"]
- train_parameters = parameters["train_parameters"]
- test_parameters = parameters["test_parameters"]
-
- batch_size = test_parameters["num_sample"] if test_parameters["sample"] else 1
- env_parameters["batch_size"] = batch_size
-
- model_parameters["actor_in_dim"] = model_parameters["out_size_ma"] * 2 + model_parameters["out_size_ope"] * 2
- model_parameters["critic_in_dim"] = model_parameters["out_size_ma"] + model_parameters["out_size_ope"]
-
- instance_path = "./data/{0}".format(test_parameters["problem_instance"])
-
- # Assign device to parameters
- env_parameters["device"] = device
- model_parameters["device"] = device
-
- # Initialize model and environment
- model = PPO_model.PPO(model_parameters, train_parameters)
-
- # Load trained policy
- trained_policy = test_parameters['trained_policy']
- if trained_policy.endswith('.pt'):
- if device.type == 'cuda':
- policy = torch.load(trained_policy)
- else:
- policy = torch.load(trained_policy, map_location='cpu')
- print('\nloading checkpoint:', trained_policy)
- model.policy.load_state_dict(policy)
- model.policy_old.load_state_dict(policy)
-
- # Load instance (to configure DRL env)
- with open(instance_path) as file_object:
- line = file_object.readlines()
- num_jobs, num_machines, _ = nums_detec(line)
-
- env_parameters["num_jobs"] = num_jobs
- env_parameters["num_mas"] = num_machines
-
- # sampling, each env contains multiple (=num_sample) copies of one instance
- if test_parameters["sample"]:
- env = gym.make('fjsp-v0', case=[instance_path] * test_parameters["num_sample"], env_paras=env_parameters,
- data_source='file')
- else:
- # greedy, each env contains one instance
- env = gym.make('fjsp-v0', case=[instance_path], env_paras=env_parameters, data_source='file')
- makespan = schedule(env, model, PPO_model.Memory(), flag_sample=test_parameters["sample"])
- print(f"Instance: {instance_path}, Makespan: {makespan}")
+ run_method(**parameters)
if __name__ == "__main__":
diff --git a/run_basic_heuristics.py b/run_basic_heuristics.py
index aac7942..f9889df 100644
--- a/run_basic_heuristics.py
+++ b/run_basic_heuristics.py
@@ -1,10 +1,9 @@
import argparse
-import time
import logging
+import time
-from solutions.helper_functions import *
from plotting.drawer import draw_gantt_chart, draw_precedence_relations
-from solutions.heuristics_scheduler.heuristics import *
+from solution_methods.helper_functions import *
logging.basicConfig(level=logging.INFO)
PARAM_FILE = "configs/basic_heuristics.toml"
diff --git a/run_dispatching_rules.py b/run_dispatching_rules.py
index 44431f4..54aef4e 100644
--- a/run_dispatching_rules.py
+++ b/run_dispatching_rules.py
@@ -1,11 +1,11 @@
import argparse
import logging
-from scheduling_environment.simulationEnv import SimulationEnv
from data_parsers import parser_fajsp, parser_fjsp, parser_jsp_fsp
-from solutions.dispatching_rules.helper_functions import *
-from solutions.helper_functions import load_parameters
from plotting.drawer import draw_gantt_chart, draw_precedence_relations
+from scheduling_environment.simulationEnv import SimulationEnv
+from solution_methods.dispatching_rules.helper_functions import *
+from solution_methods.helper_functions import load_parameters
PARAM_FILE = "configs/dispatching_rules.toml"
diff --git a/run_genetic_algorithm.py b/run_genetic_algorithm.py
index b4fb5c6..6f9fc93 100644
--- a/run_genetic_algorithm.py
+++ b/run_genetic_algorithm.py
@@ -1,16 +1,16 @@
import argparse
import logging
import multiprocessing
-import numpy as np
-from deap import base, creator, tools
from multiprocessing.pool import Pool
+import numpy as np
+from deap import base, creator, tools
-from plotting.drawer import draw_precedence_relations, draw_gantt_chart
-from solutions.helper_functions import record_stats, load_parameters, load_job_shop_env, dict_to_excel
-from solutions.genetic_algorithm.operators import (evaluate_population, evaluate_individual, variation,
- init_individual, init_population, mutate_shortest_proc_time,
- mutate_sequence_exchange, pox_crossover, repair_precedence_constraints)
+from plotting.drawer import draw_gantt_chart, draw_precedence_relations
+from solution_methods.genetic_algorithm.operators import (
+ evaluate_individual, evaluate_population, init_individual, init_population, mutate_sequence_exchange,
+ mutate_shortest_proc_time, pox_crossover, repair_precedence_constraints, variation)
+from solution_methods.helper_functions import dict_to_excel, load_job_shop_env, load_parameters, record_stats
logging.basicConfig(level=logging.INFO)
@@ -70,7 +70,7 @@ def initialize_run(pool: Pool, **kwargs):
return initial_population, toolbox, stats, hof, jobShopEnv
-def algo_run(jobShopEnv, population, toolbox, folder, exp_name, stats=None, hof=None, **kwargs):
+def run_method(jobShopEnv, population, toolbox, folder, exp_name, stats=None, hof=None, **kwargs):
"""Executes the genetic algorithm and returns the best individual.
Args:
@@ -161,7 +161,7 @@ def main(param_file=PARAM_FILE):
exp_name = ("/rseed" + str(parameters['algorithm']["rseed"]) + "/")
population, toolbox, stats, hof, jobShopEnv = initialize_run(pool, **parameters)
- best_individual = algo_run(jobShopEnv, population, toolbox, folder, exp_name, stats, hof, **parameters)
+ best_individual = run_method(jobShopEnv, population, toolbox, folder, exp_name, stats, hof, **parameters)
return best_individual
diff --git a/run_milp.py b/run_milp.py
index b3a9827..dbd349e 100644
--- a/run_milp.py
+++ b/run_milp.py
@@ -1,17 +1,19 @@
import argparse
-from gurobipy import GRB
-from solutions.MILP import FJSPSDSTmodel, FJSPmodel
-from solutions.helper_functions import load_parameters
-import logging
import json
+import logging
import os
+from gurobipy import GRB
+
+from solution_methods.helper_functions import load_parameters
+from solution_methods.MILP import FJSPmodel, FJSPSDSTmodel, JSPmodel
+
logging.basicConfig(level=logging.INFO)
DEFAULT_RESULTS_ROOT = "./results/milp"
PARAM_FILE = "configs/milp.toml"
-def main(param_file=PARAM_FILE):
+def run_method(folder, exp_name, **kwargs):
"""
Solve the FJSP problem for the provided input file.
@@ -21,23 +23,17 @@ def main(param_file=PARAM_FILE):
Returns:
None. Prints the optimization result.
"""
- try:
- parameters = load_parameters(param_file)
- except FileNotFoundError:
- logging.error(f"Parameter file {param_file} not found.")
- return
-
- folder = DEFAULT_RESULTS_ROOT
- exp_name = "gurobi_" + str(parameters['solver']["time_limit"]) + "/" + \
- str(parameters['instance']['problem_instance'])
+ if 'fjsp_sdst' in str(kwargs['instance']['problem_instance']):
+ data = FJSPSDSTmodel.parse_file(kwargs['instance']['problem_instance'])
+ model = FJSPSDSTmodel.fjsp_sdst_milp(data, kwargs['solver']['time_limit'])
+ elif 'fjsp' in str(kwargs['instance']['problem_instance']):
+ data = FJSPmodel.parse_file(kwargs['instance']['problem_instance'])
+ model = FJSPmodel.fjsp_milp(data, kwargs['solver']['time_limit'])
+ elif 'jsp' in str(kwargs['instance']['problem_instance']):
+ data = JSPmodel.parse_file(kwargs['instance']['problem_instance'])
+ model = JSPmodel.jsp_milp(data, kwargs['solver']['time_limit'])
- if 'fjsp_sdst' in str(parameters['instance']['problem_instance']):
- data = FJSPSDSTmodel.parse_file(parameters['instance']['problem_instance'])
- model = FJSPSDSTmodel.fjsp_sdst_milp(data, parameters['solver']['time_limit'])
- elif 'fjsp' in str(parameters['instance']['problem_instance']):
- data = FJSPmodel.parse_file(parameters['instance']['problem_instance'])
- model = FJSPmodel.fjsp_milp(data, parameters['solver']['time_limit'])
model.optimize()
# Status dictionary mapping
@@ -59,7 +55,7 @@ def main(param_file=PARAM_FILE):
}
results = {
- 'time_limit': str(parameters['solver']["time_limit"]),
+ 'time_limit': str(kwargs['solver']["time_limit"]),
'status': model.status,
'statusString': status_dict.get(model.status, 'UNKNOWN'),
'objValue': model.objVal if model.status == GRB.OPTIMAL else None,
@@ -86,6 +82,21 @@ def main(param_file=PARAM_FILE):
json.dump(results, outfile, indent=4)
+def main(param_file=PARAM_FILE):
+
+ try:
+ parameters = load_parameters(param_file)
+ except FileNotFoundError:
+ logging.error(f"Parameter file {param_file} not found.")
+ return
+
+ folder = DEFAULT_RESULTS_ROOT
+
+ exp_name = "gurobi_" + str(parameters['solver']["time_limit"]) + "/" + str(parameters['instance']['problem_instance'])
+
+ run_method(folder, exp_name, **parameters)
+
+
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run MILP")
parser.add_argument("config_file",
@@ -96,4 +107,4 @@ def main(param_file=PARAM_FILE):
help="path to config file",
)
args = parser.parse_args()
- main(param_file=args.config_file)
\ No newline at end of file
+ main(param_file=args.config_file)
diff --git a/run_or_tools.py b/run_or_tools.py
new file mode 100644
index 0000000..f81ef0a
--- /dev/null
+++ b/run_or_tools.py
@@ -0,0 +1,126 @@
+import argparse
+import json
+import logging
+import os
+
+from solution_methods.helper_functions import load_parameters
+from solution_methods.or_tools.FJSPmodel import fjsp_or_tools_model, parse_file_fjsp, parse_file_jsp, solve_model
+
+logging.basicConfig(level=logging.INFO)
+DEFAULT_RESULTS_ROOT = "./results/or_tools"
+PARAM_FILE = "configs/or_tools.toml"
+
+
+def main(param_file: str = PARAM_FILE) -> None:
+ """
+ Solve the (F)JSP problem for the provided input file.
+
+ Args:
+ filename (str): Path to the file containing the (F)JSP data.
+
+ Returns:
+ None. Prints the optimization result.
+ """
+ try:
+ parameters = load_parameters(param_file)
+ except FileNotFoundError:
+ logging.error(f"Parameter file {param_file} not found.")
+ return
+
+ folder = DEFAULT_RESULTS_ROOT
+
+ exp_name = (
+ "or_tools_" + str(parameters["solver"]["time_limit"]) + "/" + str(parameters["instance"]["problem_instance"])
+ )
+
+ if "fjsp" in str(parameters["instance"]["problem_instance"]):
+ data = parse_file_fjsp(parameters["instance"]["problem_instance"])
+ model, vars = fjsp_or_tools_model(data)
+ elif any(
+ scheduling_problem in str(parameters["instance"]["problem_instance"])
+ for scheduling_problem in ["jsp", "fsp"]
+ ):
+ data = parse_file_jsp(parameters["instance"]["problem_instance"])
+ model, vars = fjsp_or_tools_model(data)
+ solver, status, solution_count = solve_model(
+ model, parameters["solver"]["time_limit"]
+ )
+
+ # Gather Final Schedule
+ all_jobs = range(data["num_jobs"])
+ jobs = data["jobs"]
+ starts = vars["starts"]
+ presences = vars["presences"]
+
+ schedule = []
+ for job_id in all_jobs:
+ job_info = {"job": job_id, "tasks": []}
+ print("Job %i:" % job_id)
+ for task_id in range(len(jobs[job_id])):
+ start_value = solver.Value(starts[(job_id, task_id)])
+ machine = -1
+ duration = -1
+ selected = -1
+ for alt_id in range(len(jobs[job_id][task_id])):
+ if solver.Value(presences[(job_id, task_id, alt_id)]):
+ duration = jobs[job_id][task_id][alt_id][0]
+ machine = jobs[job_id][task_id][alt_id][1]
+ selected = alt_id
+ print(
+ " task_%i_%i starts at %i (alt %i, machine %i, duration %i)"
+ % (job_id, task_id, start_value, selected, machine, duration)
+ )
+ task_info = {
+ "task": task_id,
+ "start": start_value,
+ "machine": machine,
+ "duration": duration,
+ }
+ job_info["tasks"].append(task_info)
+ schedule.append(job_info)
+ # Status dictionary mapping
+ results = {
+ "time_limit": str(parameters["solver"]["time_limit"]),
+ "status": status,
+ "statusString": solver.StatusName(status),
+ "objValue": solver.ObjectiveValue(),
+ "runtime": solver.WallTime(),
+ "numBranches": solver.NumBranches(),
+ "conflicts": solver.NumConflicts(),
+ "solution_methods": solution_count,
+ "Schedule": schedule,
+ }
+
+ # Ensure the directory exists; create if not
+ dir_path = os.path.join(folder, exp_name)
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+
+ # Specify the full path for the file
+ file_path = os.path.join(dir_path, "CP_results.json")
+
+ # Save results to JSON (will create or overwrite the file)
+ with open(file_path, "w") as outfile:
+ json.dump(results, outfile, indent=4)
+
+ # Print Results
+ print("Solve status: %s" % solver.StatusName(status))
+ print("Optimal objective value: %i" % solver.ObjectiveValue())
+ print("Statistics")
+ print(" - conflicts : %i" % solver.NumConflicts())
+ print(" - branches : %i" % solver.NumBranches())
+ print(" - wall time : %f s" % solver.WallTime())
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Run OR-Tools CP-SAT")
+ parser.add_argument(
+ "config_file",
+ metavar="-f",
+ type=str,
+ nargs="?",
+ default=PARAM_FILE,
+ help="path to config file",
+ )
+ args = parser.parse_args()
+ main(param_file=args.config_file)
diff --git a/scheduling_environment/job.py b/scheduling_environment/job.py
index 9028fff..0565e36 100644
--- a/scheduling_environment/job.py
+++ b/scheduling_environment/job.py
@@ -1,6 +1,7 @@
-from .operation import Operation
from typing import List
+from .operation import Operation
+
class Job:
def __init__(self, job_id: int):
@@ -26,6 +27,16 @@ def job_id(self) -> int:
"""Return the job's id."""
return self._job_id
+ @property
+ def scheduled_operations(self) -> List[Operation]:
+ """Return a list of operations that have been scheduled to a machine."""
+ return [operation for operation in self._operations if operation.scheduling_information != {}]
+
+ @property
+ def next_ope_earliest_begin_time(self):
+ """Returns the time at which all operations currently scheduled for this job have finished processing."""
+ return max([operation.scheduled_end_time for operation in self.scheduled_operations], default=0)
+
def get_operation(self, operation_id):
"""Return operation object with operation id."""
for operation in self._operations:
diff --git a/scheduling_environment/jobShop.py b/scheduling_environment/jobShop.py
index 0bd4140..0a2b827 100644
--- a/scheduling_environment/jobShop.py
+++ b/scheduling_environment/jobShop.py
@@ -1,6 +1,7 @@
-from typing import List, Dict
-from scheduling_environment.machine import Machine
+from typing import Dict, List
+
from scheduling_environment.job import Job
+from scheduling_environment.machine import Machine
from scheduling_environment.operation import Operation
@@ -171,7 +172,7 @@ def schedule_operation_with_backfilling(self, operation: Operation, machine_id,
if not machine:
raise ValueError(
f"Invalid machine ID {machine_id}")
- self.schedule_operation_on_machine_backfilling(operation, machine_id, duration)
+ machine.add_operation_to_schedule_backfilling(operation, duration, self._sequence_dependent_setup_times)
self.mark_operation_as_scheduled(operation)
def unschedule_operation(self, operation: Operation) -> None:
@@ -180,15 +181,14 @@ def unschedule_operation(self, operation: Operation) -> None:
machine.unschedule_operation(operation)
self.mark_operation_as_available(operation)
- def schedule_operation_on_machine_backfilling(self, operation: Operation, machine_id, duration) -> None:
+ def schedule_operation_on_machine(self, operation: Operation, machine_id, duration) -> None:
"""Schedule an operation on a specific machine."""
machine = self.get_machine(machine_id)
if machine is None:
raise ValueError(
f"Invalid machine ID {machine_id}")
- machine.add_operation_to_schedule_backfilling(
- operation, duration, self._sequence_dependent_setup_times)
+ machine.add_operation_to_schedule(operation, duration, self._sequence_dependent_setup_times)
def mark_operation_as_scheduled(self, operation: Operation) -> None:
"""Mark an operation as scheduled."""
diff --git a/scheduling_environment/machine.py b/scheduling_environment/machine.py
index 867e942..25b1019 100644
--- a/scheduling_environment/machine.py
+++ b/scheduling_environment/machine.py
@@ -1,4 +1,5 @@
from typing import List
+
from scheduling_environment.operation import Operation
@@ -30,6 +31,36 @@ def scheduled_operations(self) -> List[Operation]:
sorted_operations = sorted(self._processed_operations, key=lambda op: op['start_time'])
return [op['operation'] for op in sorted_operations]
+ @property
+ def next_available_time(self):
+ """Returns the time moment all currently scheduled operations have been finished on this machine."""
+ return max([operation.scheduled_end_time for operation in self.scheduled_operations], default=0)
+
+ def add_operation_to_schedule(self, operation: Operation, processing_time, sequence_dependent_setup_times):
+ """Add an operation to the scheduled operations list of the machine without backfilling."""
+
+ # find max finishing time predecessors
+ finishing_time_predecessors = operation.finishing_time_predecessors
+ finishing_time_machine = max([operation.scheduled_end_time for operation in self.scheduled_operations],default=0)
+
+ setup_time = 0
+ if len(self.scheduled_operations) != 0:
+ setup_time = \
+ sequence_dependent_setup_times[self.machine_id][self.scheduled_operations[-1].operation_id][
+ operation.operation_id]
+ start_time = max(finishing_time_predecessors, finishing_time_machine + setup_time)
+ operation.add_operation_scheduling_information(self.machine_id, start_time, setup_time, processing_time)
+
+ self._processed_operations.append({
+ 'operation': operation,
+ 'start_time': start_time,
+ 'end_time': start_time + processing_time,
+ 'processing_time': processing_time,
+ 'start_setup': start_time-setup_time,
+ 'end_setup': start_time,
+ 'setup_time': setup_time
+ })
+
def add_operation_to_schedule_at_time(self, operation, start_time, processing_time, setup_time):
"""Scheduled an operations at a certain time."""
diff --git a/scheduling_environment/operation.py b/scheduling_environment/operation.py
index c97c498..fd67f4a 100644
--- a/scheduling_environment/operation.py
+++ b/scheduling_environment/operation.py
@@ -1,5 +1,5 @@
-from typing import List, Dict
from collections import OrderedDict
+from typing import Dict, List
class Operation:
@@ -34,7 +34,7 @@ def operation_id(self) -> int:
@property
def scheduling_information(self) -> Dict:
- """Return the scheduling information of the operation"""
+ """Return the scheduling information of the operation."""
return self._scheduling_information
@property
@@ -59,7 +59,9 @@ def scheduled_end_time(self) -> int:
@property
def scheduled_duration(self) -> int:
"""Return the scheduled duration of the operation."""
- return self._scheduled_duration
+ if 'processing_time' in self._scheduling_information:
+ return self._scheduling_information['processing_time']
+ return None
@property
def scheduled_machine(self) -> None:
@@ -73,6 +75,11 @@ def predecessors(self) -> List:
"""Return the list of predecessor operations."""
return self._predecessors
+ @property
+ def optional_machines_id(self) -> List:
+ """Returns the list of machine ids that are eligible for processing this operation."""
+ return list(self._processing_times.keys())
+
@property
def finishing_time_predecessors(self) -> int:
"""Return the finishing time of the latest predecessor."""
@@ -82,10 +89,11 @@ def finishing_time_predecessors(self) -> int:
return max(end_times_predecessors)
def update_job_id(self, new_job_id: int) -> None:
- """Update the id of a job (used for assembly scheduling problems, with no pregiven job id)"""
+ """Update the id of a job (used for assembly scheduling problems, with no pre-given job id)."""
self._job_id = new_job_id
def update_job(self, job) -> None:
+ """Update job information (edge case for FAJSP)."""
self._job = job
def add_predecessors(self, predecessors: List) -> None:
@@ -93,10 +101,11 @@ def add_predecessors(self, predecessors: List) -> None:
self.predecessors.extend(predecessors)
def add_operation_option(self, machine_id, duration) -> None:
- """Add an operation option to the current operation."""
+ """Add an machine option to the current operation."""
self._processing_times[machine_id] = duration
def update_sequence_dependent_setup_times(self, start_time_setup, setup_duration):
+ """Update the sequence dependent setup times of this operation."""
self._start_time_setup = start_time_setup
self._setup_duration = setup_duration
diff --git a/scheduling_environment/simulationEnv.py b/scheduling_environment/simulationEnv.py
index cf58126..57e44f9 100644
--- a/scheduling_environment/simulationEnv.py
+++ b/scheduling_environment/simulationEnv.py
@@ -1,10 +1,11 @@
-import simpy
import random
-from typing import Optional, List, Dict
+from typing import Dict, List, Optional
+
+import simpy
+from scheduling_environment.job import Job
from scheduling_environment.jobShop import JobShop
from scheduling_environment.machine import Machine
-from scheduling_environment.job import Job
from scheduling_environment.operation import Operation
diff --git a/solutions/FJSP_DRL/PPO_model.py b/solution_methods/FJSP_DRL/PPO.py
similarity index 92%
rename from solutions/FJSP_DRL/PPO_model.py
rename to solution_methods/FJSP_DRL/PPO.py
index de149b4..081e997 100644
--- a/solutions/FJSP_DRL/PPO_model.py
+++ b/solution_methods/FJSP_DRL/PPO.py
@@ -8,13 +8,14 @@
import copy
import math
+
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Categorical
-from solutions.FJSP_DRL.hgnn import GATedge, MLPsim
-from solutions.FJSP_DRL.mlp import MLPCritic, MLPActor
+from solution_methods.FJSP_DRL.hgnn import GATedge, MLPsim
+from solution_methods.FJSP_DRL.mlp import MLPActor, MLPCritic
class Memory:
@@ -99,12 +100,19 @@ def forward(self, ope_ma_adj_batch, ope_pre_adj_batch, ope_sub_adj_batch, batch_
"""
h = (feats[1], feats[0], feats[0], feats[0])
# Identity matrix for self-loop of nodes
- self_adj = torch.eye(feats[0].size(-2),
- dtype=torch.int64).unsqueeze(0).expand_as(ope_pre_adj_batch[batch_idxes])
+ self_adj = (
+ torch.eye(feats[0].size(-2), dtype=torch.int64)
+ .unsqueeze(0)
+ .expand_as(ope_pre_adj_batch[batch_idxes])
+ )
# Calculate an return operation embedding
- adj = (ope_ma_adj_batch[batch_idxes], ope_pre_adj_batch[batch_idxes],
- ope_sub_adj_batch[batch_idxes], self_adj)
+ adj = (
+ ope_ma_adj_batch[batch_idxes],
+ ope_pre_adj_batch[batch_idxes],
+ ope_sub_adj_batch[batch_idxes],
+ self_adj,
+ )
MLP_embeddings = []
for i in range(len(adj)):
MLP_embeddings.append(self.gnn_layers[i](h[i], adj[i]))
@@ -163,11 +171,11 @@ def forward(self):
def feature_normalize(self, data):
return (data - torch.mean(data)) / ((data.std() + 1e-5))
- '''
+ """
raw_opes: shape: [len(batch_idxes), max(num_opes), in_size_ope]
raw_mas: shape: [len(batch_idxes), num_mas, in_size_ma]
proc_time: shape: [len(batch_idxes), max(num_opes), num_mas]
- '''
+ """
def get_normalized(self, raw_opes, raw_mas, proc_time, batch_idxes, nums_opes, flag_sample=False, flag_train=False):
"""
@@ -243,7 +251,7 @@ def get_action_prob(self, state, memories, flag_sample=False, flag_train=False):
if not flag_sample and not flag_train:
h_opes_pooled = []
for i in range(len(batch_idxes)):
- h_opes_pooled.append(torch.mean(h_opes[i, :nums_opes[i], :], dim=-2))
+ h_opes_pooled.append(torch.mean(h_opes[i, : nums_opes[i], :], dim=-2))
h_opes_pooled = torch.stack(h_opes_pooled) # shape: [len(batch_idxes), d]
else:
h_opes_pooled = h_opes.mean(dim=-2) # shape: [len(batch_idxes), out_size_ope]
@@ -269,8 +277,7 @@ def get_action_prob(self, state, memories, flag_sample=False, flag_train=False):
ma_eligible = ~state.mask_ma_procing_batch[batch_idxes].unsqueeze(1).expand_as(h_jobs_padding[..., 0])
# Matrix indicating whether job is eligible
# shape: [len(batch_idxes), num_jobs, num_mas]
- job_eligible = ~(state.mask_job_procing_batch[batch_idxes] +
- state.mask_job_finish_batch[batch_idxes])[:, :, None].expand_as(h_jobs_padding[..., 0])
+ job_eligible = ~(state.mask_job_procing_batch[batch_idxes] + state.mask_job_finish_batch[batch_idxes])[:, :, None].expand_as(h_jobs_padding[..., 0])
# shape: [len(batch_idxes), num_jobs, num_mas]
eligible = job_eligible & ma_eligible & (eligible_proc == 1)
if (~(eligible)).all():
@@ -285,11 +292,11 @@ def get_action_prob(self, state, memories, flag_sample=False, flag_train=False):
# Get priority index and probability of actions with masking the ineligible actions
scores = self.actor(h_actions).flatten(1)
- scores[~mask] = float('-inf')
+ scores[~mask] = float("-inf")
action_probs = F.softmax(scores, dim=1)
# Store data in memory during training
- if flag_train == True:
+ if flag_train is True:
memories.ope_ma_adj.append(copy.deepcopy(state.ope_ma_adj_batch))
memories.ope_pre_adj.append(copy.deepcopy(state.ope_pre_adj_batch))
memories.ope_sub_adj.append(copy.deepcopy(state.ope_sub_adj_batch))
@@ -321,8 +328,7 @@ def act(self, state, memories, dones, flag_sample=True, flag_train=True):
opes = ope_step_batch[state.batch_idxes, jobs]
# Store data in memory during training
- if flag_train == True:
- # memories.states.append(copy.deepcopy(state))
+ if flag_train is True:
memories.logprobs.append(dist.log_prob(action_indexes))
memories.action_indexes.append(action_indexes)
@@ -357,7 +363,7 @@ def evaluate(self, ope_ma_adj, ope_pre_adj, ope_sub_adj, raw_opes, raw_mas, proc
scores = self.actor(h_actions).flatten(1)
mask = eligible.transpose(1, 2).flatten(1)
- scores[~mask] = float('-inf')
+ scores[~mask] = float("-inf")
action_probs = F.softmax(scores, dim=1)
state_values = self.critic(h_pooled)
dist = Categorical(action_probs.squeeze())
@@ -433,24 +439,23 @@ def update(self, memory, env_paras, train_paras):
else:
start_idx = i * minibatch_size
end_idx = full_batch_size
- logprobs, state_values, dist_entropy = \
- self.policy.evaluate(old_ope_ma_adj[start_idx: end_idx, :, :],
- old_ope_pre_adj[start_idx: end_idx, :, :],
- old_ope_sub_adj[start_idx: end_idx, :, :],
- old_raw_opes[start_idx: end_idx, :, :],
- old_raw_mas[start_idx: end_idx, :, :],
- old_proc_time[start_idx: end_idx, :, :],
- old_jobs_gather[start_idx: end_idx, :, :],
- old_eligible[start_idx: end_idx, :, :],
- old_action_envs[start_idx: end_idx])
+ logprobs, state_values, dist_entropy = self.policy.evaluate(
+ old_ope_ma_adj[start_idx:end_idx, :, :],
+ old_ope_pre_adj[start_idx:end_idx, :, :],
+ old_ope_sub_adj[start_idx:end_idx, :, :],
+ old_raw_opes[start_idx:end_idx, :, :],
+ old_raw_mas[start_idx:end_idx, :, :],
+ old_proc_time[start_idx:end_idx, :, :],
+ old_jobs_gather[start_idx:end_idx, :, :],
+ old_eligible[start_idx:end_idx, :, :],
+ old_action_envs[start_idx:end_idx],
+ )
ratios = torch.exp(logprobs - old_logprobs[i * minibatch_size:(i + 1) * minibatch_size].detach())
advantages = rewards_envs[i * minibatch_size:(i + 1) * minibatch_size] - state_values.detach()
surr1 = ratios * advantages
surr2 = torch.clamp(ratios, 1 - self.eps_clip, 1 + self.eps_clip) * advantages
- loss = - self.A_coeff * torch.min(surr1, surr2) \
- + self.vf_coeff * self.MseLoss(state_values,
- rewards_envs[i * minibatch_size:(i + 1) * minibatch_size]) \
+ loss = - self.A_coeff * torch.min(surr1, surr2) + self.vf_coeff * self.MseLoss(state_values, rewards_envs[i * minibatch_size:(i + 1) * minibatch_size]) \
- self.entropy_coeff * dist_entropy
loss_epochs += loss.mean().detach()
@@ -461,5 +466,6 @@ def update(self, memory, env_paras, train_paras):
# Copy new weights into old policy:
self.policy_old.load_state_dict(self.policy.state_dict())
- return loss_epochs.item() / self.K_epochs, \
- discounted_rewards.item() / (self.num_envs * train_paras["update_timestep"])
+ return loss_epochs.item() / self.K_epochs, discounted_rewards.item() / (
+ self.num_envs * train_paras["update_timestep"]
+ )
diff --git a/solution_methods/FJSP_DRL/case_generator.py b/solution_methods/FJSP_DRL/case_generator.py
new file mode 100644
index 0000000..819af80
--- /dev/null
+++ b/solution_methods/FJSP_DRL/case_generator.py
@@ -0,0 +1,95 @@
+import random
+
+
+class CaseGenerator:
+ '''
+ FJSP instance generator
+ '''
+ def __init__(self, job_init, num_mas, opes_per_job_min, opes_per_job_max, nums_ope=None, path='../data/',
+ flag_same_opes=True, flag_doc=False):
+ if nums_ope is None:
+ nums_ope = []
+ self.flag_doc = flag_doc # Whether save the instance to a file
+ self.flag_same_opes = flag_same_opes
+ self.nums_ope = nums_ope
+ self.path = path # Instance save path (relative path)
+ self.job_init = job_init
+ self.num_mas = num_mas
+
+ self.mas_per_ope_min = 1 # The minimum number of machines that can process an operation
+ self.mas_per_ope_max = num_mas
+ self.opes_per_job_min = opes_per_job_min # The minimum number of operations for a job
+ self.opes_per_job_max = opes_per_job_max
+ self.proctime_per_ope_min = 1 # Minimum average processing time
+ self.proctime_per_ope_max = 20
+ self.proctime_dev = 0.2
+
+ def get_case(self, idx=0):
+ '''
+ Generate FJSP instance
+ :param idx: The instance number
+ '''
+ self.num_jobs = self.job_init
+ if not self.flag_same_opes:
+ self.nums_ope = [random.randint(self.opes_per_job_min, self.opes_per_job_max) for _ in range(self.num_jobs)]
+ self.num_opes = sum(self.nums_ope)
+ self.nums_option = [random.randint(self.mas_per_ope_min, self.mas_per_ope_max) for _ in range(self.num_opes)]
+ self.num_options = sum(self.nums_option)
+ self.ope_ma = []
+ for val in self.nums_option:
+ self.ope_ma = self.ope_ma + sorted(random.sample(range(1, self.num_mas + 1), val))
+ self.proc_time = []
+ self.proc_times_mean = [random.randint(self.proctime_per_ope_min, self.proctime_per_ope_max) for _ in range(self.num_opes)]
+ for i in range(len(self.nums_option)):
+ low_bound = max(self.proctime_per_ope_min, round(self.proc_times_mean[i] * (1 - self.proctime_dev)))
+ high_bound = min(self.proctime_per_ope_max, round(self.proc_times_mean[i] * (1 + self.proctime_dev)))
+ proc_time_ope = [random.randint(low_bound, high_bound) for _ in range(self.nums_option[i])]
+ self.proc_time = self.proc_time + proc_time_ope
+ self.num_ope_biass = [sum(self.nums_ope[0:i]) for i in range(self.num_jobs)]
+ self.num_ma_biass = [sum(self.nums_option[0:i]) for i in range(self.num_opes)]
+ line0 = '{0}\t{1}\t{2}\n'.format(self.num_jobs, self.num_mas, self.num_options / self.num_opes)
+ lines = []
+ lines_doc = []
+ lines.append(line0)
+ lines_doc.append('{0}\t{1}\t{2}'.format(self.num_jobs, self.num_mas, self.num_options / self.num_opes))
+ for i in range(self.num_jobs):
+ flag = 0
+ flag_time = 0
+ flag_new_ope = 1
+ idx_ope = -1
+ idx_ma = 0
+ line = []
+ option_max = sum(self.nums_option[self.num_ope_biass[i]:(self.num_ope_biass[i] + self.nums_ope[i])])
+ idx_option = 0
+ while True:
+ if flag == 0:
+ line.append(self.nums_ope[i])
+ flag += 1
+ elif flag == flag_new_ope:
+ idx_ope += 1
+ idx_ma = 0
+ flag_new_ope += self.nums_option[self.num_ope_biass[i] + idx_ope] * 2 + 1
+ line.append(self.nums_option[self.num_ope_biass[i] + idx_ope])
+ flag += 1
+ elif flag_time == 0:
+ line.append(self.ope_ma[self.num_ma_biass[self.num_ope_biass[i] + idx_ope] + idx_ma])
+ flag += 1
+ flag_time = 1
+ else:
+ line.append(self.proc_time[self.num_ma_biass[self.num_ope_biass[i] + idx_ope] + idx_ma])
+ flag += 1
+ flag_time = 0
+ idx_option += 1
+ idx_ma += 1
+ if idx_option == option_max:
+ str_line = " ".join([str(val) for val in line])
+ lines.append(str_line + '\n')
+ lines_doc.append(str_line)
+ break
+ lines.append('\n')
+ if self.flag_doc:
+ doc = open(self.path + '{0}j_{1}m_{2}.fjs'.format(self.num_jobs, self.num_mas, str.zfill(str(idx + 1), 3)), 'a')
+ for i in range(len(lines_doc)):
+ print(lines_doc[i], file=doc)
+ doc.close()
+ return lines, self.num_jobs, self.num_jobs
diff --git a/solution_methods/FJSP_DRL/env_test.py b/solution_methods/FJSP_DRL/env_test.py
new file mode 100644
index 0000000..b4c517e
--- /dev/null
+++ b/solution_methods/FJSP_DRL/env_test.py
@@ -0,0 +1,324 @@
+# GITHUB REPO: https://github.com/songwenas12/fjsp-drl
+
+# Code based on the paper:
+# "Flexible Job Shop Scheduling via Graph Neural Network and Deep Reinforcement Learning"
+# by Wen Song, Xinyang Chen, Qiqiang Li and Zhiguang Cao
+# Presented in IEEE Transactions on Industrial Informatics, 2023.
+# Paper URL: https://ieeexplore.ieee.org/document/9826438
+
+import copy
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+import torch
+
+from scheduling_environment.jobShop import JobShop
+from solution_methods.FJSP_DRL.load_data import load_feats_from_sim
+
+# Add the base path to the Python module search path
+base_path = Path(__file__).resolve().parents[2]
+sys.path.append(str(base_path))
+
+
+@dataclass
+class EnvState:
+ '''
+ Class for the state of the environment
+ '''
+ # static
+ opes_appertain_batch: torch.Tensor = None
+ ope_pre_adj_batch: torch.Tensor = None
+ ope_sub_adj_batch: torch.Tensor = None
+ end_ope_biases_batch: torch.Tensor = None
+ nums_opes_batch: torch.Tensor = None
+
+ # dynamic
+ batch_idxes: torch.Tensor = None
+ feat_opes_batch: torch.Tensor = None
+ feat_mas_batch: torch.Tensor = None
+ proc_times_batch: torch.Tensor = None
+ ope_ma_adj_batch: torch.Tensor = None
+ time_batch: torch.Tensor = None
+
+ mask_job_procing_batch: torch.Tensor = None
+ mask_job_finish_batch: torch.Tensor = None
+ mask_ma_procing_batch: torch.Tensor = None
+ ope_step_batch: torch.Tensor = None
+
+ def update(self, batch_idxes, feat_opes_batch, feat_mas_batch, proc_times_batch, ope_ma_adj_batch,
+ mask_job_procing_batch, mask_job_finish_batch, mask_ma_procing_batch, ope_step_batch, time):
+ self.batch_idxes = batch_idxes
+ self.feat_opes_batch = feat_opes_batch
+ self.feat_mas_batch = feat_mas_batch
+ self.proc_times_batch = proc_times_batch
+ self.ope_ma_adj_batch = ope_ma_adj_batch
+
+ self.mask_job_procing_batch = mask_job_procing_batch
+ self.mask_job_finish_batch = mask_job_finish_batch
+ self.mask_ma_procing_batch = mask_ma_procing_batch
+ self.ope_step_batch = ope_step_batch
+ self.time_batch = time
+
+
+def convert_feat_job_2_ope(feat_job_batch, opes_appertain_batch):
+ """
+ Convert job features into operation features (such as dimension)
+ """
+ return feat_job_batch.gather(1, opes_appertain_batch)
+
+
+class FJSPEnv_test():
+ def __init__(self, JobShop_module, test_parameters):
+ # static
+ self.batch_size = 1
+ self.num_jobs = JobShop_module.nr_of_jobs # Number of jobs
+ self.num_mas = JobShop_module.nr_of_machines # Number of machines
+ self.device = test_parameters["device"] # Computing device for PyTorch
+
+ self.JSP_instance: list[JobShop] = JobShop_module
+
+ # load instance
+ num_data = 8 # The amount of data extracted from instance
+ tensors = [[] for _ in range(num_data)]
+ self.num_opes = 0
+ self.num_opes = max(self.num_jobs, len(self.JSP_instance.operations))
+
+ # Extract features from each JobShop module
+ raw_features = load_feats_from_sim(self.JSP_instance, self.num_mas, self.num_opes)
+ # print(raw_features[0].shape)
+ for j in range(num_data):
+ tensors[j].append(raw_features[j].to(self.device))
+
+ # dynamic feats
+ # shape: (batch_size, num_opes, num_mas)
+ self.proc_times_batch = torch.stack(tensors[0], dim=0)
+ # shape: (batch_size, num_opes, num_mas)
+ self.ope_ma_adj_batch = torch.stack(tensors[1], dim=0).long()
+ # shape: (batch_size, num_opes, num_opes), for calculating the cumulative amount along the path of each job
+ self.cal_cumul_adj_batch = torch.stack(tensors[7], dim=0).float()
+
+ # static feats
+ # shape: (batch_size, num_opes, num_opes)
+ self.ope_pre_adj_batch = torch.stack(tensors[2], dim=0)
+ # shape: (batch_size, num_opes, num_opes)
+ self.ope_sub_adj_batch = torch.stack(tensors[3], dim=0)
+ # shape: (batch_size, num_opes), represents the mapping between operations and jobs
+ self.opes_appertain_batch = torch.stack(tensors[4], dim=0).long()
+ # shape: (batch_size, num_jobs), the id of the first operation of each job
+ self.num_ope_biases_batch = torch.stack(tensors[5], dim=0).long()
+ # shape: (batch_size, num_jobs), the number of operations for each job
+ self.nums_ope_batch = torch.stack(tensors[6], dim=0).long()
+ # shape: (batch_size, num_jobs), the id of the last operation of each job
+ self.end_ope_biases_batch = self.num_ope_biases_batch + self.nums_ope_batch - 1
+ # shape: (batch_size), the number of operations for each instance
+ self.nums_opes = torch.sum(self.nums_ope_batch, dim=1)
+
+ # dynamic variable
+ self.batch_idxes = torch.arange(self.batch_size) # Uncompleted instances
+ self.JSM_time = torch.zeros(self.batch_size) # Current time of the environment
+ self.N = torch.zeros(self.batch_size).int() # Count scheduled operations
+ # shape: (batch_size, num_jobs), the id of the current operation (be waiting to be processed) of each job
+ self.ope_step_batch = copy.deepcopy(self.num_ope_biases_batch)
+
+ # Generate raw feature vectors
+ ope_feat_dim = 6
+ ma_feat_dim = 3
+ num_sample = 1
+ feat_opes_batch = torch.zeros(size=(num_sample, ope_feat_dim, self.num_opes))
+ feat_mas_batch = torch.zeros(size=(num_sample, ma_feat_dim, self.num_mas))
+
+ feat_opes_batch[:, 1, :] = torch.count_nonzero(self.ope_ma_adj_batch, dim=2)
+ feat_opes_batch[:, 2, :] = torch.sum(self.proc_times_batch, dim=2).div(feat_opes_batch[:, 1, :] + 1e-9)
+ feat_opes_batch[:, 3, :] = convert_feat_job_2_ope(self.nums_ope_batch, self.opes_appertain_batch)
+ feat_opes_batch[:, 5, :] = torch.bmm(feat_opes_batch[:, 2, :].unsqueeze(1), self.cal_cumul_adj_batch).squeeze()
+ end_time_batch = (feat_opes_batch[:, 5, :] + feat_opes_batch[:, 2, :]).gather(1, self.end_ope_biases_batch)
+ feat_opes_batch[:, 4, :] = convert_feat_job_2_ope(end_time_batch, self.opes_appertain_batch)
+ feat_mas_batch[:, 0, :] = torch.count_nonzero(self.ope_ma_adj_batch, dim=1)
+ self.JSM_feat_opes_batch = feat_opes_batch
+ self.JSM_feat_mas_batch = feat_mas_batch
+
+ # Masks of current status, dynamic
+ # shape: (batch_size, num_jobs), True for jobs in process
+ self.JSM_mask_job_procing_batch = torch.full(size=(self.batch_size, self.num_jobs), dtype=torch.bool,
+ fill_value=False)
+ # shape: (batch_size, num_jobs), True for completed jobs
+ self.JSM_mask_job_finish_batch = torch.full(size=(self.batch_size, self.num_jobs), dtype=torch.bool,
+ fill_value=False)
+ # shape: (batch_size, num_mas), True for machines in process
+ self.JSM_mask_ma_procing_batch = torch.full(size=(self.batch_size, self.num_mas), dtype=torch.bool,
+ fill_value=False)
+
+ self.makespan_batch = torch.max(self.JSM_feat_opes_batch[:, 4, :], dim=1)[0] # shape: (batch_size)
+ self.done_batch = self.JSM_mask_job_finish_batch.all(dim=1) # shape: (batch_size)
+
+ self.state = EnvState(batch_idxes=self.batch_idxes,
+ feat_opes_batch=self.JSM_feat_opes_batch, feat_mas_batch=self.JSM_feat_mas_batch,
+ proc_times_batch=self.proc_times_batch, ope_ma_adj_batch=self.ope_ma_adj_batch,
+ ope_pre_adj_batch=self.ope_pre_adj_batch, ope_sub_adj_batch=self.ope_sub_adj_batch,
+ mask_job_procing_batch=self.JSM_mask_job_procing_batch,
+ mask_job_finish_batch=self.JSM_mask_job_finish_batch,
+ mask_ma_procing_batch=self.JSM_mask_ma_procing_batch,
+ opes_appertain_batch=self.opes_appertain_batch,
+ ope_step_batch=self.ope_step_batch,
+ end_ope_biases_batch=self.end_ope_biases_batch,
+ time_batch=self.JSM_time, nums_opes_batch=self.nums_opes)
+
+ # Save initial data for reset
+ self.old_proc_times_batch = copy.deepcopy(self.proc_times_batch)
+ self.old_ope_ma_adj_batch = copy.deepcopy(self.ope_ma_adj_batch)
+ self.old_cal_cumul_adj_batch = copy.deepcopy(self.cal_cumul_adj_batch)
+ self.old_feat_opes_batch = copy.deepcopy(self.JSM_feat_opes_batch)
+ self.old_feat_mas_batch = copy.deepcopy(self.JSM_feat_mas_batch)
+ self.old_state = copy.deepcopy(self.state)
+
+ def step(self, actions):
+ """
+ Environment transition function, based on JobShop module
+ """
+ opes = actions[0, :]
+ mas = actions[1, :]
+ jobs = actions[2, :]
+ self.N += 1
+
+ ope_idx = opes.item()
+ mac_idx = mas.item()
+ env = self.JSP_instance
+ operation = env.operations[ope_idx]
+ duration = operation.processing_times[mac_idx]
+ env.schedule_operation_on_machine(operation, mac_idx, duration)
+ env.get_job(operation.job_id).scheduled_operations.append(operation)
+
+ # Removed unselected O-M arcs of the scheduled operations
+ remain_ope_ma_adj = torch.zeros(size=(self.batch_size, self.num_mas), dtype=torch.int64)
+ remain_ope_ma_adj[self.batch_idxes, mas] = 1
+ self.ope_ma_adj_batch[self.batch_idxes, opes] = remain_ope_ma_adj[self.batch_idxes, :]
+ self.proc_times_batch *= self.ope_ma_adj_batch
+
+ # Update for some O-M arcs are removed, such as 'Status', 'Number of neighboring machines' and 'Processing time'
+ proc_times = self.proc_times_batch[self.batch_idxes, opes, mas]
+ self.JSM_feat_opes_batch[self.batch_idxes, :3, opes] = torch.stack(
+ (torch.ones(self.batch_idxes.size(0), dtype=torch.float),
+ torch.ones(self.batch_idxes.size(0), dtype=torch.float),
+ proc_times), dim=1)
+
+ # Update 'Number of unscheduled operations in the job' - use JobShop
+
+ job_idx = jobs.item()
+ unscheduled_opes = 0
+ for each_ope in self.JSP_instance.get_job(job_idx).operations:
+ if each_ope.scheduling_information.__len__() == 0:
+ unscheduled_opes += 1
+ start_ope_idx = self.num_ope_biases_batch[self.batch_idxes, jobs]
+ end_ope_idx = self.end_ope_biases_batch[self.batch_idxes, jobs]
+ self.JSM_feat_opes_batch[self.batch_idxes, 3, start_ope_idx:end_ope_idx + 1] = unscheduled_opes
+
+ # Update 'Start time' and 'Job completion time' - use JobShop
+ self.JSM_feat_opes_batch[self.batch_idxes, 5, opes] = self.JSM_time
+ for each_ope in self.JSP_instance.operations:
+ if each_ope.scheduling_information.__len__() == 0:
+ if each_ope.predecessors.__len__() == 0:
+ est_start_time = self.JSM_feat_opes_batch[self.batch_idxes, 5, each_ope.operation_id]
+ else:
+ if each_ope.predecessors[0].scheduling_information.__len__() == 0:
+ est_start_time = self.JSM_feat_opes_batch[self.batch_idxes, 5, each_ope.predecessors[0].operation_id] + self.JSM_feat_opes_batch[self.batch_idxes, 2, each_ope.predecessors[0].operation_id]
+ else:
+ est_start_time = each_ope.predecessors[0].scheduling_information['start_time'] + each_ope.predecessors[0].scheduling_information['processing_time']
+ else:
+ est_start_time = each_ope.scheduling_information['start_time']
+ self.JSM_feat_opes_batch[self.batch_idxes, 5, each_ope.operation_id] = est_start_time
+
+ for each_job in self.JSP_instance.jobs:
+ est_end_times = [(self.JSM_feat_opes_batch[self.batch_idxes, 5, ope_in_job.operation_id] + self.JSM_feat_opes_batch[self.batch_idxes, 2, ope_in_job.operation_id]) for ope_in_job in each_job.operations]
+ job_end_time = max(est_end_times)
+ for ope_of_job in each_job.operations:
+ self.JSM_feat_opes_batch[self.batch_idxes, 4, ope_of_job.operation_id] = job_end_time
+
+ # Check if there are still O-M pairs to be processed, otherwise the environment transits to the next time - using JobShop Module
+ this_env = self.JSP_instance
+ cur_times = []
+ for each_job in this_env.jobs:
+ if len(each_job.scheduled_operations) == len(each_job.operations):
+ continue
+ next_ope = each_job.operations[len(each_job.scheduled_operations)]
+ latest_ope_end_time = next_ope.finishing_time_predecessors
+ for each_mach_id in next_ope.optional_machines_id:
+ # if schedule the next operation of this job on an optional machine, the earlist start time
+ # operation available: predecessor operation end
+ # machine available: last assigned operation end
+ cur_times.append(max(latest_ope_end_time, this_env.get_machine(each_mach_id).next_available_time, self.JSM_time))
+ self.JSM_time = min(cur_times, default=self.JSM_time)
+
+ # Update feature vectors of machines - using JobShop module
+ self.JSM_feat_mas_batch[self.batch_idxes, 0, :] = torch.count_nonzero(self.ope_ma_adj_batch[self.batch_idxes, :, :], dim=1).float()
+ for each_mach in self.JSP_instance.machines:
+ workload = sum([ope_on_mach.scheduled_duration for ope_on_mach in each_mach.scheduled_operations])
+ cur_time = self.JSM_time
+ workload = min(cur_time, workload)
+ self.JSM_feat_mas_batch[self.batch_idxes, 2, each_mach.machine_id] = workload / (cur_time + 1e-9)
+ self.JSM_feat_mas_batch[self.batch_idxes, 1, each_mach.machine_id] = each_mach.next_available_time
+
+ # Update other variable according to actions - using JobShop module
+ self.ope_step_batch[self.batch_idxes, jobs] += 1
+ JSM_mask_jp_list = []
+ JSM_mask_jf_list = []
+ JSM_mask_mp_list = []
+ JSM_mask_jp_list.append([True if this_job.next_ope_earliest_begin_time > self.JSM_time else False
+ for this_job in self.JSP_instance.jobs])
+ JSM_mask_jf_list.append([True if this_job.operations.__len__() == this_job.scheduled_operations.__len__() else False for this_job in self.JSP_instance.jobs])
+ JSM_mask_mp_list.append([True if this_mach.next_available_time > self.JSM_time else False for this_mach in self.JSP_instance.machines])
+ self.JSM_mask_job_procing_batch = torch.tensor(JSM_mask_jp_list, dtype=torch.bool)
+ self.JSM_mask_job_finish_batch = torch.tensor(JSM_mask_jf_list, dtype=torch.bool)
+ self.JSM_mask_ma_procing_batch = torch.tensor(JSM_mask_mp_list, dtype=torch.bool)
+
+ self.done_batch = self.JSM_mask_job_finish_batch.all(dim=1)
+ self.done = self.done_batch.all()
+
+ max_makespan = torch.max(self.JSM_feat_opes_batch[:, 4, :], dim=1)[0]
+ self.reward_batch = self.makespan_batch - max_makespan
+ self.makespan_batch = max_makespan
+
+ # Update the vector for uncompleted instances
+ mask_finish = (self.N + 1) <= self.nums_opes
+ if ~(mask_finish.all()):
+ self.batch_idxes = torch.arange(self.batch_size)[mask_finish]
+
+ # Update state of the environment
+ self.state.update(self.batch_idxes, self.JSM_feat_opes_batch, self.JSM_feat_mas_batch, self.proc_times_batch,
+ self.ope_ma_adj_batch, self.JSM_mask_job_procing_batch, self.JSM_mask_job_finish_batch,
+ self.JSM_mask_ma_procing_batch, self.ope_step_batch, self.JSM_time)
+ return self.state, self.reward_batch, self.done_batch
+
+ def reset(self):
+ """
+ Reset the environment to its initial state
+ """
+ for i in range(self.batch_size):
+ self.JSP_instance[i].reset()
+
+ self.proc_times_batch = copy.deepcopy(self.old_proc_times_batch)
+ self.ope_ma_adj_batch = copy.deepcopy(self.old_ope_ma_adj_batch)
+ self.cal_cumul_adj_batch = copy.deepcopy(self.old_cal_cumul_adj_batch)
+ self.JSM_feat_opes_batch = copy.deepcopy(self.old_feat_opes_batch)
+ self.JSM_feat_mas_batch = copy.deepcopy(self.old_feat_mas_batch)
+ self.state = copy.deepcopy(self.old_state)
+
+ self.batch_idxes = torch.arange(self.batch_size)
+
+ self.JSM_time = torch.zeros(self.batch_size)
+ self.N = torch.zeros(self.batch_size)
+ self.ope_step_batch = copy.deepcopy(self.num_ope_biases_batch)
+
+ self.JSM_mask_job_procing_batch = torch.full(size=(self.batch_size, self.num_jobs), dtype=torch.bool,
+ fill_value=False)
+ self.JSM_mask_job_finish_batch = torch.full(size=(self.batch_size, self.num_jobs), dtype=torch.bool,
+ fill_value=False)
+ self.JSM_mask_ma_procing_batch = torch.full(size=(self.batch_size, self.num_mas), dtype=torch.bool,
+ fill_value=False)
+ self.machines_batch = torch.zeros(size=(self.batch_size, self.num_mas, 4))
+ self.machines_batch[:, :, 0] = torch.ones(size=(self.batch_size, self.num_mas))
+
+ self.makespan_batch = torch.max(self.JSM_feat_opes_batch[:, 4, :], dim=1)[0]
+ self.done_batch = self.JSM_mask_job_finish_batch.all(dim=1)
+
+ return self.state
diff --git a/solutions/FJSP_DRL/env.py b/solution_methods/FJSP_DRL/env_training.py
similarity index 77%
rename from solutions/FJSP_DRL/env.py
rename to solution_methods/FJSP_DRL/env_training.py
index f0c00e9..30c90df 100644
--- a/solutions/FJSP_DRL/env.py
+++ b/solution_methods/FJSP_DRL/env_training.py
@@ -1,35 +1,17 @@
-# GITHUB REPO: https://github.com/songwenas12/fjsp-drl
-
-# Code based on the paper:
-# "Flexible Job Shop Scheduling via Graph Neural Network and Deep Reinforcement Learning"
-# by Wen Song, Xinyang Chen, Qiqiang Li and Zhiguang Cao
-# Presented in IEEE Transactions on Industrial Informatics, 2023.
-# Paper URL: https://ieeexplore.ieee.org/document/9826438
-
-import random
-import sys
import copy
-
-import gymnasium as gym
-import torch
-import numpy as np
-from pathlib import Path
from dataclasses import dataclass
-from solutions.FJSP_DRL.load_data import load_fjs, nums_detec, load_fjs_case
-from scheduling_environment.jobShop import JobShop
-
+import numpy as np
+import torch
-# Add the base path to the Python module search path
-base_path = Path(__file__).resolve().parents[2]
-sys.path.append(str(base_path))
+from solution_methods.FJSP_DRL.load_data import load_feats_from_case, nums_detec
@dataclass
class EnvState:
- '''
+ """
Class for the state of the environment
- '''
+ """
# static
opes_appertain_batch: torch.Tensor = None
ope_pre_adj_batch: torch.Tensor = None
@@ -72,21 +54,20 @@ def convert_feat_job_2_ope(feat_job_batch, opes_appertain_batch):
return feat_job_batch.gather(1, opes_appertain_batch)
-class FJSPEnv(gym.Env):
+class FJSPEnv_training():
"""
FJSP environment
"""
def __init__(self, case, env_paras, data_source='case'):
- '''
+ """
:param case: The instance generator or the addresses of the instances
:param env_paras: A dictionary of parameters for the environment
:param data_source: Indicates that the instances came from a generator or files
- '''
+ """
# load paras
# static
- self.show_mode = env_paras["show_mode"] # Result display mode (deprecated in the final experiment)
self.batch_size = env_paras["batch_size"] # Number of parallel instances during training
self.num_jobs = env_paras["num_jobs"] # Number of jobs
self.num_mas = env_paras["num_mas"] # Number of machines
@@ -95,12 +76,9 @@ def __init__(self, case, env_paras, data_source='case'):
# load instance
num_data = 8 # The amount of data extracted from instance
tensors = [[] for _ in range(num_data)]
- self.simulation_envs: list[JobShop] = [] # variable for keeping track of scheduling_environment envs
self.num_opes = 0
lines = []
- self.data_source = data_source
-
- if data_source=='case': # Generate instances through generators
+ if data_source == 'case': # Generate instances through generators
for i in range(self.batch_size):
lines.append(case.get_case(i)[0]) # Generate an instance and save it
num_jobs, num_mas, num_opes = nums_detec(lines[i])
@@ -115,13 +93,9 @@ def __init__(self, case, env_paras, data_source='case'):
self.num_opes = max(self.num_opes, num_opes)
# load feats
for i in range(self.batch_size):
- if data_source == 'case':
- load_data = load_fjs_case(lines[i], self.num_mas, self.num_opes)
- else:
- load_data, env = load_fjs(case[i], self.num_mas, self.num_opes, self.num_jobs)
- self.simulation_envs.append(env)
+ load_data = load_feats_from_case(lines[i], num_mas, self.num_opes)
for j in range(num_data):
- tensors[j].append(load_data[j].to(self.device))
+ tensors[j].append(load_data[j])
# dynamic feats
# shape: (batch_size, num_opes, num_mas)
@@ -176,8 +150,7 @@ def __init__(self, case, env_paras, data_source='case'):
feat_opes_batch[:, 3, :] = convert_feat_job_2_ope(self.nums_ope_batch, self.opes_appertain_batch)
feat_opes_batch[:, 5, :] = torch.bmm(feat_opes_batch[:, 2, :].unsqueeze(1),
self.cal_cumul_adj_batch).squeeze()
- end_time_batch = (feat_opes_batch[:, 5, :] +
- feat_opes_batch[:, 2, :]).gather(1, self.end_ope_biases_batch)
+ end_time_batch = (feat_opes_batch[:, 5, :] + feat_opes_batch[:, 2, :]).gather(1, self.end_ope_biases_batch)
feat_opes_batch[:, 4, :] = convert_feat_job_2_ope(end_time_batch, self.opes_appertain_batch)
feat_mas_batch[:, 0, :] = torch.count_nonzero(self.ope_ma_adj_batch, dim=1)
self.feat_opes_batch = feat_opes_batch
@@ -242,16 +215,6 @@ def step(self, actions):
jobs = actions[2, :]
self.N += 1
- if self.data_source != 'case':
- for index in range(self.batch_size):
- operation_ix = opes[index].item()
- machine_ix = mas[index].item()
- job_ix = jobs[index].item()
- env = self.simulation_envs[index]
- operation = env.operations[operation_ix]
- duration = operation.processing_times[machine_ix]
- env.schedule_operation_on_machine_backfilling(operation, machine_ix, duration)
-
# Removed unselected O-M arcs of the scheduled operations
remain_ope_ma_adj = torch.zeros(size=(self.batch_size, self.num_mas), dtype=torch.int64)
remain_ope_ma_adj[self.batch_idxes, mas] = 1
@@ -281,11 +244,9 @@ def step(self, actions):
start_times = self.feat_opes_batch[self.batch_idxes, 5, :] * is_scheduled # real start time of scheduled opes
un_scheduled = 1 - is_scheduled # unscheduled opes
estimate_times = torch.bmm((start_times + mean_proc_time).unsqueeze(1),
- self.cal_cumul_adj_batch[self.batch_idxes, :, :]).squeeze() \
- * un_scheduled # estimate start time of unscheduled opes
+ self.cal_cumul_adj_batch[self.batch_idxes, :, :]).squeeze() * un_scheduled # estimate start time of unscheduled opes
self.feat_opes_batch[self.batch_idxes, 5, :] = start_times + estimate_times
- end_time_batch = (self.feat_opes_batch[self.batch_idxes, 5, :] +
- self.feat_opes_batch[self.batch_idxes, 2, :]).gather(1, self.end_ope_biases_batch[
+ end_time_batch = (self.feat_opes_batch[self.batch_idxes, 5, :] + self.feat_opes_batch[self.batch_idxes, 2, :]).gather(1, self.end_ope_biases_batch[
self.batch_idxes, :])
self.feat_opes_batch[self.batch_idxes, 4, :] = convert_feat_job_2_ope(end_time_batch, self.opes_appertain_batch[
self.batch_idxes, :])
@@ -340,7 +301,7 @@ def step(self, actions):
self.ope_ma_adj_batch, self.mask_job_procing_batch, self.mask_job_finish_batch,
self.mask_ma_procing_batch,
self.ope_step_batch, self.time)
- return self.state, self.reward_batch, self.done_batch
+ return self.state, self.reward_batch, self.done_batch, None
def if_no_eligible(self):
"""
@@ -501,97 +462,3 @@ def validate_gantt(self):
def close(self):
pass
-
-
-class CaseGenerator:
- """
- FJSP instance generator
- """
- def __init__(self, job_init, num_mas, opes_per_job_min, opes_per_job_max, nums_ope=None, path='../data/',
- flag_same_opes=True, flag_doc=False):
- if nums_ope is None:
- nums_ope = []
- self.flag_doc = flag_doc # Whether save the instance to a file
- self.flag_same_opes = flag_same_opes
- self.nums_ope = nums_ope
- self.path = path # Instance save path (relative path)
- self.job_init = job_init
- self.num_mas = num_mas
-
- self.mas_per_ope_min = 1 # The minimum number of machines that can process an operation
- self.mas_per_ope_max = num_mas
- self.opes_per_job_min = opes_per_job_min # The minimum number of operations for a job
- self.opes_per_job_max = opes_per_job_max
- self.proctime_per_ope_min = 1 # Minimum average processing time
- self.proctime_per_ope_max = 20
- self.proctime_dev = 0.2
-
- def get_case(self, idx=0):
- """
- Generate FJSP instance
- :param idx: The instance number
- """
- self.num_jobs = self.job_init
- if not self.flag_same_opes:
- self.nums_ope = [random.randint(self.opes_per_job_min, self.opes_per_job_max) for _ in range(self.num_jobs)]
- self.num_opes = sum(self.nums_ope)
- self.nums_option = [random.randint(self.mas_per_ope_min, self.mas_per_ope_max) for _ in range(self.num_opes)]
- self.num_options = sum(self.nums_option)
- self.ope_ma = []
- for val in self.nums_option:
- self.ope_ma = self.ope_ma + sorted(random.sample(range(1, self.num_mas+1), val))
- self.proc_time = []
- self.proc_times_mean = [random.randint(self.proctime_per_ope_min, self.proctime_per_ope_max) for _ in range(self.num_opes)]
- for i in range(len(self.nums_option)):
- low_bound = max(self.proctime_per_ope_min,round(self.proc_times_mean[i]*(1-self.proctime_dev)))
- high_bound = min(self.proctime_per_ope_max,round(self.proc_times_mean[i]*(1+self.proctime_dev)))
- proc_time_ope = [random.randint(low_bound, high_bound) for _ in range(self.nums_option[i])]
- self.proc_time = self.proc_time + proc_time_ope
- self.num_ope_biass = [sum(self.nums_ope[0:i]) for i in range(self.num_jobs)]
- self.num_ma_biass = [sum(self.nums_option[0:i]) for i in range(self.num_opes)]
- line0 = '{0}\t{1}\t{2}\n'.format(self.num_jobs, self.num_mas, self.num_options / self.num_opes)
- lines = []
- lines_doc = []
- lines.append(line0)
- lines_doc.append('{0}\t{1}\t{2}'.format(self.num_jobs, self.num_mas, self.num_options / self.num_opes))
- for i in range(self.num_jobs):
- flag = 0
- flag_time = 0
- flag_new_ope = 1
- idx_ope = -1
- idx_ma = 0
- line = []
- option_max = sum(self.nums_option[self.num_ope_biass[i]:(self.num_ope_biass[i]+self.nums_ope[i])])
- idx_option = 0
- while True:
- if flag == 0:
- line.append(self.nums_ope[i])
- flag += 1
- elif flag == flag_new_ope:
- idx_ope += 1
- idx_ma = 0
- flag_new_ope += self.nums_option[self.num_ope_biass[i]+idx_ope] * 2 + 1
- line.append(self.nums_option[self.num_ope_biass[i]+idx_ope])
- flag += 1
- elif flag_time == 0:
- line.append(self.ope_ma[self.num_ma_biass[self.num_ope_biass[i]+idx_ope] + idx_ma])
- flag += 1
- flag_time = 1
- else:
- line.append(self.proc_time[self.num_ma_biass[self.num_ope_biass[i]+idx_ope] + idx_ma])
- flag += 1
- flag_time = 0
- idx_option += 1
- idx_ma += 1
- if idx_option == option_max:
- str_line = " ".join([str(val) for val in line])
- lines.append(str_line + '\n')
- lines_doc.append(str_line)
- break
- lines.append('\n')
- if self.flag_doc:
- doc = open(self.path + '{0}j_{1}m_{2}.fjs'.format(self.num_jobs, self.num_mas, str.zfill(str(idx+1),3)),'a')
- for i in range(len(lines_doc)):
- print(lines_doc[i], file=doc)
- doc.close()
- return lines, self.num_jobs, self.num_jobs
diff --git a/solutions/FJSP_DRL/hgnn.py b/solution_methods/FJSP_DRL/hgnn.py
similarity index 100%
rename from solutions/FJSP_DRL/hgnn.py
rename to solution_methods/FJSP_DRL/hgnn.py
index 5ac6086..5a5cff4 100644
--- a/solutions/FJSP_DRL/hgnn.py
+++ b/solution_methods/FJSP_DRL/hgnn.py
@@ -7,9 +7,9 @@
# Paper URL: https://ieeexplore.ieee.org/document/9826438
import torch
+import torch.nn.functional as F
from torch import nn
from torch.nn import Identity
-import torch.nn.functional as F
class GATedge(nn.Module):
diff --git a/solutions/FJSP_DRL/load_data.py b/solution_methods/FJSP_DRL/load_data.py
similarity index 88%
rename from solutions/FJSP_DRL/load_data.py
rename to solution_methods/FJSP_DRL/load_data.py
index 98b8a34..1df475f 100644
--- a/solutions/FJSP_DRL/load_data.py
+++ b/solution_methods/FJSP_DRL/load_data.py
@@ -7,21 +7,21 @@
# Paper URL: https://ieeexplore.ieee.org/document/9826438
import sys
+from pathlib import Path
-import torch
import numpy as np
-from pathlib import Path
+import torch
from scheduling_environment.jobShop import JobShop
-from solutions.helper_functions import load_job_shop_env
from scheduling_environment.operation import Operation
+from solution_methods.helper_functions import load_job_shop_env
# Add the base path to the Python module search path
base_path = Path(__file__).resolve().parents[2]
sys.path.append(str(base_path))
-def load_fjs_case(lines, num_mas, num_opes):
+def load_feats_from_case(lines, num_mas, num_opes):
"""
Load the local FJSP instance.
"""
@@ -38,7 +38,7 @@ def load_fjs_case(lines, num_mas, num_opes):
if flag == 0:
flag += 1
# last line
- elif line is "\n":
+ elif line == "\n":
break
# other
else:
@@ -58,19 +58,17 @@ def load_fjs_case(lines, num_mas, num_opes):
torch.tensor(nums_ope).int(), matrix_cal_cumul
-def load_fjs(path, num_mas, num_opes, num_jobs) -> tuple[tuple[
- torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor], JobShop]:
+def load_fjs(path, num_mas, num_opes, num_jobs):
"""
Load the local FJSP instance.
"""
jobShopEnv = load_job_shop_env(path, from_absolute_path=True)
- drl_tensors = load_fjs_from_sim(jobShopEnv, num_mas, num_opes)
+ drl_tensors = load_feats_from_sim(jobShopEnv, num_mas, num_opes)
return drl_tensors, jobShopEnv
-def load_fjs_from_sim(jobShopEnv: JobShop, num_mas, num_opes) -> tuple[
- torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
+def load_feats_from_sim(jobShopEnv: JobShop, num_mas, num_opes):
"""convert scheduling_environment environment to DRL environment"""
matrix_proc_time = torch.zeros(size=(num_opes, num_mas))
matrix_ope_ma_adj = torch.zeros(size=(num_opes, num_mas)).int()
@@ -98,8 +96,8 @@ def load_fjs_from_sim(jobShopEnv: JobShop, num_mas, num_opes) -> tuple[
predecessor: Operation
matrix_pre_proc[predecessor.operation_id][operation.operation_id] = True
- return matrix_proc_time, matrix_ope_ma_adj, matrix_pre_proc, matrix_pre_proc.t(), \
- opes_appertain, num_ope_biases, nums_ope, matrix_cal_cumul
+ return matrix_proc_time, matrix_ope_ma_adj, matrix_pre_proc, matrix_pre_proc.t(), opes_appertain, num_ope_biases, \
+ nums_ope, matrix_cal_cumul
def nums_detec(lines):
@@ -155,4 +153,4 @@ def edge_detec(line, num_ope_bias, matrix_proc_time, matrix_pre_proc, matrix_cal
matrix_proc_time[idx_ope + num_ope_bias][mac] = x
flag += 1
flag_time = 0
- return num_ope
\ No newline at end of file
+ return num_ope
diff --git a/solutions/FJSP_DRL/mlp.py b/solution_methods/FJSP_DRL/mlp.py
similarity index 78%
rename from solutions/FJSP_DRL/mlp.py
rename to solution_methods/FJSP_DRL/mlp.py
index 863abb1..b94eb8f 100644
--- a/solutions/FJSP_DRL/mlp.py
+++ b/solution_methods/FJSP_DRL/mlp.py
@@ -14,11 +14,11 @@
class MLP(nn.Module):
def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
"""
- num_layers: number of layers in the neural networks (EXCLUDING the input layer). If num_layers=1, this reduces to linear model.
- input_dim: dimensionality of input features
- hidden_dim: dimensionality of hidden units at ALL layers
- output_dim: number of classes for prediction
- device: which device to use
+ num_layers: number of layers in the neural networks (EXCLUDING the input layer). If num_layers=1, this reduces to linear model.
+ input_dim: dimensionality of input features
+ hidden_dim: dimensionality of hidden units at ALL layers
+ output_dim: number of classes for prediction
+ device: which device to use
"""
super(MLP, self).__init__()
@@ -60,11 +60,11 @@ def forward(self, x):
class MLPActor(nn.Module):
def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
"""
- num_layers: number of layers in the neural networks (EXCLUDING the input layer). If num_layers=1, this reduces to linear model.
- input_dim: dimensionality of input features
- hidden_dim: dimensionality of hidden units at ALL layers
- output_dim: number of classes for prediction
- device: which device to use
+ num_layers: number of layers in the neural networks (EXCLUDING the input layer). If num_layers=1, this reduces to linear model.
+ input_dim: dimensionality of input features
+ hidden_dim: dimensionality of hidden units at ALL layers
+ output_dim: number of classes for prediction
+ device: which device to use
"""
super(MLPActor, self).__init__()
@@ -81,18 +81,18 @@ def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
# Multi-layer model
self.linear_or_not = False
self.linears = torch.nn.ModuleList()
- '''
+ """
self.batch_norms = torch.nn.ModuleList()
- '''
+ """
self.linears.append(nn.Linear(input_dim, hidden_dim))
for layer in range(num_layers - 2):
self.linears.append(nn.Linear(hidden_dim, hidden_dim))
self.linears.append(nn.Linear(hidden_dim, output_dim))
- '''
+ """
for layer in range(num_layers - 1):
self.batch_norms.append(nn.BatchNorm1d((hidden_dim)))
- '''
+ """
def forward(self, x):
if self.linear_or_not:
@@ -102,22 +102,21 @@ def forward(self, x):
# If MLP
h = x
for layer in range(self.num_layers - 1):
- '''
+ """
h = F.relu(self.batch_norms[layer](self.linears[layer](h)))
- '''
+ """
h = torch.tanh(self.linears[layer](h))
- # h = F.relu(self.linears[layer](h))
return self.linears[self.num_layers - 1](h)
class MLPCritic(nn.Module):
def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
"""
- num_layers: number of layers in the neural networks (EXCLUDING the input layer). If num_layers=1, this reduces to linear model.
- input_dim: dimensionality of input features
- hidden_dim: dimensionality of hidden units at ALL layers
- output_dim: number of classes for prediction
- device: which device to use
+ num_layers: number of layers in the neural networks (EXCLUDING the input layer). If num_layers=1, this reduces to linear model.
+ input_dim: dimensionality of input features
+ hidden_dim: dimensionality of hidden units at ALL layers
+ output_dim: number of classes for prediction
+ device: which device to use
"""
super(MLPCritic, self).__init__()
@@ -134,18 +133,18 @@ def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
# Multi-layer model
self.linear_or_not = False
self.linears = torch.nn.ModuleList()
- '''
+ """
self.batch_norms = torch.nn.ModuleList()
- '''
+ """
self.linears.append(nn.Linear(input_dim, hidden_dim))
for layer in range(num_layers - 2):
self.linears.append(nn.Linear(hidden_dim, hidden_dim))
self.linears.append(nn.Linear(hidden_dim, output_dim))
- '''
+ """
for layer in range(num_layers - 1):
self.batch_norms.append(nn.BatchNorm1d((hidden_dim)))
- '''
+ """
def forward(self, x):
if self.linear_or_not:
@@ -155,9 +154,9 @@ def forward(self, x):
# If MLP
h = x
for layer in range(self.num_layers - 1):
- '''
+ """
h = F.relu(self.batch_norms[layer](self.linears[layer](h)))
- '''
+ """
h = torch.tanh(self.linears[layer](h))
# h = F.relu(self.linears[layer](h))
return self.linears[self.num_layers - 1](h)
diff --git a/solutions/FJSP_DRL/models/song_10_5.pt b/solution_methods/FJSP_DRL/save/train_20240314_192906/song_10_5.pt
similarity index 100%
rename from solutions/FJSP_DRL/models/song_10_5.pt
rename to solution_methods/FJSP_DRL/save/train_20240314_192906/song_10_5.pt
diff --git a/solution_methods/FJSP_DRL/training.py b/solution_methods/FJSP_DRL/training.py
new file mode 100644
index 0000000..63c79a3
--- /dev/null
+++ b/solution_methods/FJSP_DRL/training.py
@@ -0,0 +1,173 @@
+import argparse
+import copy
+import logging
+import os
+import random
+import sys
+import time
+from collections import deque
+from pathlib import Path
+
+import numpy as np
+import torch
+from visdom import Visdom
+
+import solution_methods.FJSP_DRL.PPO as PPO_model
+from solution_methods.FJSP_DRL.case_generator import CaseGenerator
+from solution_methods.FJSP_DRL.env_training import FJSPEnv_training
+from solution_methods.FJSP_DRL.validate import get_validate_env, validate
+from solution_methods.helper_functions import load_parameters
+
+# Add the base path to the Python module search path
+base_path = Path(__file__).resolve().parents[2]
+sys.path.append(str(base_path))
+
+# import FJSP parameters
+PARAM_FILE = str(base_path) + "/configs/FJSP_DRL.toml"
+
+
+def initialize_device(parameters: dict) -> torch.device:
+ device_str = "cpu"
+ if parameters["test_parameters"]["device"] == "cuda":
+ device_str = "cuda:0" if torch.cuda.is_available() else "cpu"
+ return torch.device(device_str)
+
+
+def train_FJSP_DRL(param_file: str = PARAM_FILE):
+ try:
+ parameters = load_parameters(param_file)
+ except FileNotFoundError:
+ logging.error(f"Parameter file {param_file} not found.")
+ return
+
+ device = initialize_device(parameters)
+
+ # Configure PyTorch's default device
+ torch.set_default_tensor_type('torch.cuda.FloatTensor' if device.type == 'cuda' else 'torch.FloatTensor')
+ if device.type == 'cuda':
+ torch.cuda.set_device(device)
+ seed = 1
+ torch.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+ random.seed(seed)
+
+ # Extract parameters
+ env_parameters = parameters["env_parameters"]
+ model_parameters = parameters["model_parameters"]
+ train_parameters = parameters["train_parameters"]
+
+ env_validation_parameters = copy.deepcopy(env_parameters)
+ env_validation_parameters["batch_size"] = env_parameters["valid_batch_size"]
+
+ model_parameters["actor_in_dim"] = model_parameters["out_size_ma"] * 2 + model_parameters["out_size_ope"] * 2
+ model_parameters["critic_in_dim"] = model_parameters["out_size_ma"] + model_parameters["out_size_ope"]
+
+ num_jobs = env_parameters["num_jobs"]
+ num_machines = env_parameters["num_mas"]
+ opes_per_job_min = int(num_machines * 0.8)
+ opes_per_job_max = int(num_machines * 1.2)
+ print(num_jobs, num_machines)
+
+ memories = PPO_model.Memory()
+ model = PPO_model.PPO(model_parameters, train_parameters, num_envs=env_parameters["batch_size"])
+
+ env_valid = get_validate_env(env_validation_parameters, train_parameters) # Create an environment for validation
+ maxlen = 1 # Save the best model
+ best_models = deque()
+ makespan_best = float("inf")
+
+ # Use visdom to visualize the training process
+ is_viz = train_parameters["viz"]
+ if is_viz:
+ viz = Visdom(env=train_parameters["viz_name"])
+
+ # Generate data files and fill in the header
+ str_time = time.strftime("%Y%m%d_%H%M%S", time.localtime(time.time()))
+ save_path = "./save/train_{0}".format(str_time)
+ os.makedirs(save_path)
+
+ valid_results = []
+ valid_results_100 = []
+
+ # Training part
+ env_training = None
+ for i in range(1, train_parameters["max_iterations"] + 1):
+ if (i - 1) % train_parameters["parallel_iter"] == 0:
+ nums_ope = [random.randint(opes_per_job_min, opes_per_job_max) for _ in range(num_jobs)]
+ case = CaseGenerator(num_jobs, num_machines, opes_per_job_min, opes_per_job_max, nums_ope=nums_ope)
+ env_training = FJSPEnv_training(case=case, env_paras=env_parameters)
+
+ # Get state and completion signal
+ env_training.reset()
+ state = env_training.state
+ done = False
+ dones = env_training.done_batch
+ last_time = time.time()
+
+ # Schedule in parallel
+ while ~done:
+ with torch.no_grad():
+ actions = model.policy_old.act(state, memories, dones)
+ state, rewards, dones, _ = env_training.step(actions)
+ done = dones.all()
+ memories.rewards.append(rewards)
+ memories.is_terminals.append(dones)
+ # gpu_tracker.track() # Used to monitor memory (of gpu)
+ print("spend_time: ", time.time() - last_time)
+
+ # Verify the solution
+ gantt_result = env_training.validate_gantt()[0]
+ if not gantt_result:
+ print("Scheduling Error!!!!!!")
+ # print("Scheduling Finish")
+ env_training.reset()
+
+ # if iter mod x = 0 then update the policy (x = 1 in paper)
+ if i % train_parameters["update_timestep"] == 0:
+ loss, reward = model.update(memories, env_parameters, train_parameters)
+ print("reward: ", "%.3f" % reward, "; loss: ", "%.3f" % loss)
+ memories.clear_memory()
+
+ if is_viz:
+ viz.line(X=np.array([i]), Y=np.array([reward]),
+ win='window{}'.format(0), update='append', opts=dict(title='reward of envs'))
+ viz.line(X=np.array([i]), Y=np.array([loss]),
+ win='window{}'.format(1), update='append', opts=dict(title='loss of envs'))
+
+ # if iter mod x = 0 then validate the policy (x = 10 in paper)
+ if i % train_parameters["save_timestep"] == 0:
+ print("\nStart validating")
+ # Record the average results and the results on each instance
+ vali_result, vali_result_100 = validate(env_validation_parameters, env_valid, model.policy_old)
+ valid_results.append(vali_result.item())
+ valid_results_100.append(vali_result_100)
+
+ # Save the best model
+ if vali_result < makespan_best:
+ makespan_best = vali_result
+ if len(best_models) == maxlen:
+ delete_file = best_models.popleft()
+ os.remove(delete_file)
+ save_file = "{0}/save_best_{1}_{2}_{3}.pt".format(save_path, num_jobs, num_machines, i)
+ best_models.append(save_file)
+ torch.save(model.policy.state_dict(), save_file)
+
+ if is_viz:
+ viz.line(
+ X=np.array([i]), Y=np.array([vali_result.item()]),
+ win="window{}".format(2), update="append", opts=dict(title="makespan of valid"))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="train FJSP_DRL")
+ parser.add_argument(
+ "config_file",
+ metavar="-f",
+ type=str,
+ nargs="?",
+ default=PARAM_FILE,
+ help="path to config file",
+ )
+
+ args = parser.parse_args()
+ train_FJSP_DRL(param_file=args.config_file)
diff --git a/solutions/FJSP_DRL/validate.py b/solution_methods/FJSP_DRL/validate.py
similarity index 54%
rename from solutions/FJSP_DRL/validate.py
rename to solution_methods/FJSP_DRL/validate.py
index a60a7d9..93ccff1 100644
--- a/solutions/FJSP_DRL/validate.py
+++ b/solution_methods/FJSP_DRL/validate.py
@@ -6,15 +6,15 @@
# Presented in IEEE Transactions on Industrial Informatics, 2023.
# Paper URL: https://ieeexplore.ieee.org/document/9826438
-import time
import copy
import os
+import time
+from pathlib import Path
import torch
-from pathlib import Path
-import gymnasium as gym
-import PPO_model
+import solution_methods.FJSP_DRL.PPO as PPO_model
+from solution_methods.FJSP_DRL.env_training import FJSPEnv_training
base_path = Path(__file__).resolve().parents[2]
@@ -23,35 +23,40 @@ def get_validate_env(env_paras, train_paras):
"""
Generate and return the validation environment from the validation set ()
"""
- file_path = str(base_path) + '/data/' + train_paras["validation_folder"]
+ file_path = str(base_path) + "/data/" + train_paras["validation_folder"]
valid_data_files = os.listdir(file_path)
for i in range(len(valid_data_files)):
valid_data_files[i] = file_path + valid_data_files[i]
- env = gym.make('fjsp-v0', case=valid_data_files, env_paras=env_paras, data_source='file')
+ env = FJSPEnv_training(case=valid_data_files, env_paras=env_paras, data_source="file")
return env
-def validate(env_paras, env, model_policy):
+def validate(env_paras, env_validate, model_policy):
"""
Validate the policy during training, and the process is similar to test
"""
start = time.time()
batch_size = env_paras["batch_size"]
memory = PPO_model.Memory()
- print('There are {0} dev instances.'.format(batch_size)) # validation set is also called development set
- state = env.state
+ print(
+ "There are {0} dev instances.".format(batch_size)
+ ) # validation set is also called development set
+ env_validate.reset()
+ state = env_validate.state
done = False
- dones = env.done_batch
+ dones = env_validate.done_batch
while ~done:
with torch.no_grad():
- actions = model_policy.act(state, memory, dones, flag_sample=False, flag_train=False)
- state, rewards, dones = env.step(actions)
+ actions = model_policy.act(
+ state, memory, dones, flag_sample=False, flag_train=False
+ )
+ state, rewards, dones, _ = env_validate.step(actions)
done = dones.all()
- gantt_result = env.validate_gantt()[0]
+ gantt_result = env_validate.validate_gantt()[0]
if not gantt_result:
print("Scheduling Error!")
- makespan = copy.deepcopy(env.makespan_batch.mean())
- makespan_batch = copy.deepcopy(env.makespan_batch)
- env.reset()
- print('validating time: ', time.time() - start, '\n')
+ makespan = copy.deepcopy(env_validate.makespan_batch.mean())
+ makespan_batch = copy.deepcopy(env_validate.makespan_batch)
+ env_validate.reset()
+ print("validating time: ", time.time() - start, "\n")
return makespan, makespan_batch
diff --git a/solutions/MILP/FJSPSDSTmodel.py b/solution_methods/MILP/FJSPSDSTmodel.py
similarity index 99%
rename from solutions/MILP/FJSPSDSTmodel.py
rename to solution_methods/MILP/FJSPSDSTmodel.py
index f5da63f..304bb55 100644
--- a/solutions/MILP/FJSPSDSTmodel.py
+++ b/solution_methods/MILP/FJSPSDSTmodel.py
@@ -4,7 +4,7 @@
# Presented in European Journal of Operational Research, 2018.
# Paper URL: https://www.sciencedirect.com/science/article/pii/S037722171730752X
-from gurobipy import Model, GRB, quicksum
+from gurobipy import GRB, Model, quicksum
def parse_file(filename):
diff --git a/solutions/MILP/FJSPmodel.py b/solution_methods/MILP/FJSPmodel.py
similarity index 98%
rename from solutions/MILP/FJSPmodel.py
rename to solution_methods/MILP/FJSPmodel.py
index dd0c36d..6e495bf 100644
--- a/solutions/MILP/FJSPmodel.py
+++ b/solution_methods/MILP/FJSPmodel.py
@@ -4,8 +4,7 @@
# Presented in International Journal of Production Research, 2013.
# Paper URL: https://www.tandfonline.com/doi/full/10.1080/00207543.2013.827806
-
-from gurobipy import Model, GRB, quicksum
+from gurobipy import GRB, Model, quicksum
def parse_file(filename):
@@ -64,7 +63,7 @@ def parse_file(filename):
'operations_per_job': operations_per_job,
'machine_allocations': machine_allocations,
'operations_times': operations_times,
- 'largeM': largeM, #
+ 'largeM': largeM,
"sdsts": sdsts
}
diff --git a/solution_methods/MILP/JSPmodel.py b/solution_methods/MILP/JSPmodel.py
new file mode 100644
index 0000000..31d4f62
--- /dev/null
+++ b/solution_methods/MILP/JSPmodel.py
@@ -0,0 +1,116 @@
+# Code based on the paper: "On the job-shop scheduling problem"
+# by A. S. Manne, Presented in Operations Research, 1960.
+# Paper URL: https://www.jstor.org/stable/167204
+
+from gurobipy import GRB, Model
+
+
+def parse_file(filename):
+ # Initialize variables
+ machine_allocations = {}
+ operations_times = {}
+ total_op_nr = 0
+
+ with open("./data/" + filename, 'r') as f:
+ # Extract header data
+ number_operations, number_machines = map(float, f.readline().split())
+ number_jobs = int(number_operations)
+ number_machines = int(number_machines)
+ operations_per_job = {j: [] for j in range(1, number_jobs + 1)}
+
+ # Process job operations data
+ for i in range(number_jobs):
+ operation_data = list(map(int, f.readline().split()))
+ index, operation_id = 0, 0
+ while index < len(operation_data):
+ total_op_nr += 1
+
+ job_machines = operation_data[index]
+ job_processingtime = operation_data[index + 1]
+ machine_allocations[(i + 1, operation_id + 1)] = job_machines
+ operations_times[(i + 1, operation_id + 1, job_machines)] = job_processingtime
+ operations_per_job[i+1].append(operation_id + 1)
+
+ operation_id += 1
+ index += 2
+
+ # Calculate derived values
+ jobs = list(range(1, number_jobs + 1))
+ machines = list(range(0, number_machines))
+ # calculate upper bound
+ largeM = sum([processing_time for operation,processing_time in operations_times.items()])
+
+ # Return parsed data
+ return {
+ 'number_jobs': number_jobs,
+ 'number_machines': number_machines,
+ 'jobs': jobs,
+ 'machines': machines,
+ 'operations_per_job': operations_per_job,
+ 'machine_allocations': machine_allocations,
+ 'operations_times': operations_times,
+ 'largeM': largeM,
+ }
+
+
+def jsp_milp(instance_data, time_limit):
+ # Extracting the data
+ jobs = instance_data['jobs']
+ operations_per_job = instance_data['operations_per_job']
+ machine_allocations = instance_data['machine_allocations']
+ operations_times = instance_data['operations_times']
+ largeM = instance_data['largeM']
+ model = Model("JSP_MILP")
+
+ # Decision variables
+ x = {}
+ z = {}
+
+ for j in jobs:
+ for l in operations_per_job[j]:
+ i = machine_allocations[(j, l)]
+ x[(j, l, i)] = model.addVar(vtype=GRB.INTEGER, name=f"x_{j}_{l}_{i}", lb=0)
+ for h in jobs:
+ if h > j:
+ for k in operations_per_job[h]:
+ if machine_allocations[(h, k)] == i:
+ z[(j, l, i, h, k)] = model.addVar(vtype=GRB.BINARY, name=f"z_{j}_{l}_{i}_{h}_{k}")
+
+ # Objective function (1)
+ cmax = model.addVar(vtype=GRB.CONTINUOUS, name="Cmax")
+ model.setObjective(cmax, GRB.MINIMIZE)
+
+ # Constraints
+
+ # Operation start time constraints (2)
+ for j in jobs:
+ for l in operations_per_job[j]:
+ model.addConstr(x[(j, l, machine_allocations[(j, l)])] >= 0)
+
+ # Precedence constraints (3)
+ for j in jobs:
+ for l in operations_per_job[j][:-1]:
+ model.addConstr(x[(j, l+1, machine_allocations[(j, l+1)])] >=
+ x[(j, l, machine_allocations[(j, l)])] + operations_times[(j, l, machine_allocations[(j, l)])])
+
+ # Disjunctive constraints (no two jobs can be scheduled on the same machine at the same time) (4 & 5)
+ for j in jobs:
+ for l in operations_per_job[j]:
+ for h in jobs:
+ if h > j:
+ for k in operations_per_job[h]:
+ if machine_allocations[(h, k)] == machine_allocations[(j, l)]:
+ model.addConstr(x[(h, k, machine_allocations[(h, k)])] + operations_times[(h, k, machine_allocations[(h, k)])] <=
+ x[(j, l, machine_allocations[(j, l)])] + largeM * z[(j, l, machine_allocations[(j, l)], h, k)])
+ model.addConstr(x[(j, l, machine_allocations[(j, l)])] + operations_times[(j, l, machine_allocations[(j, l)])] <=
+ x[(h, k, machine_allocations[(h, k)])] + largeM * (1 - z[(j, l, machine_allocations[(j, l)], h, k)]))
+
+ # Capture objective function (6)
+ for j in jobs:
+ model.addConstr(
+ cmax >= x[j, max(operations_per_job[j]), machine_allocations[j, max(operations_per_job[j])]] +
+ operations_times[j, max(operations_per_job[j]), machine_allocations[j, max(operations_per_job[j])]])
+
+ model.params.TimeLimit = time_limit
+
+ return model
\ No newline at end of file
diff --git a/solutions/__init__.py b/solution_methods/__init__.py
similarity index 100%
rename from solutions/__init__.py
rename to solution_methods/__init__.py
diff --git a/solutions/dispatching_rules/helper_functions.py b/solution_methods/dispatching_rules/helper_functions.py
similarity index 100%
rename from solutions/dispatching_rules/helper_functions.py
rename to solution_methods/dispatching_rules/helper_functions.py
diff --git a/solutions/genetic_algorithm/operators.py b/solution_methods/genetic_algorithm/operators.py
similarity index 96%
rename from solutions/genetic_algorithm/operators.py
rename to solution_methods/genetic_algorithm/operators.py
index 9475e5c..205643b 100644
--- a/solutions/genetic_algorithm/operators.py
+++ b/solution_methods/genetic_algorithm/operators.py
@@ -1,12 +1,13 @@
-import time
import random
+import time
import numpy as np
from scheduling_environment.jobShop import JobShop
from scheduling_environment.operation import Operation
-from solutions.heuristics_scheduler.heuristics import global_selection_scheduler, local_selection_scheduler, random_scheduler
-from solutions.helper_functions import update_operations_available_for_scheduling
+from solution_methods.helper_functions import update_operations_available_for_scheduling
+from solution_methods.heuristics_scheduler.heuristics import (
+ global_selection_scheduler, local_selection_scheduler, random_scheduler)
def select_next_operation_from_job(jobShopEnv: JobShop, job_id) -> Operation:
diff --git a/solutions/helper_functions.py b/solution_methods/helper_functions.py
similarity index 97%
rename from solutions/helper_functions.py
rename to solution_methods/helper_functions.py
index 15396dc..ff3d1b0 100644
--- a/solutions/helper_functions.py
+++ b/solution_methods/helper_functions.py
@@ -1,10 +1,10 @@
import os
-import tomli
import pandas as pd
+import tomli
+from data_parsers import parser_fajsp, parser_fjsp, parser_fjsp_sdst, parser_jsp_fsp
from scheduling_environment.jobShop import JobShop
-from data_parsers import parser_fajsp, parser_fjsp, parser_jsp_fsp, parser_fjsp_sdst
def load_parameters(config_toml):
diff --git a/solutions/heuristics_scheduler/heuristics.py b/solution_methods/heuristics_scheduler/heuristics.py
similarity index 98%
rename from solutions/heuristics_scheduler/heuristics.py
rename to solution_methods/heuristics_scheduler/heuristics.py
index c837541..12937de 100644
--- a/solutions/heuristics_scheduler/heuristics.py
+++ b/solution_methods/heuristics_scheduler/heuristics.py
@@ -2,8 +2,8 @@
import numpy as np
-from solutions.helper_functions import update_operations_available_for_scheduling
from scheduling_environment.jobShop import JobShop
+from solution_methods.helper_functions import update_operations_available_for_scheduling
def random_scheduler(jobShop: JobShop) -> JobShop:
diff --git a/solution_methods/or_tools/FJSPmodel.py b/solution_methods/or_tools/FJSPmodel.py
new file mode 100644
index 0000000..004858e
--- /dev/null
+++ b/solution_methods/or_tools/FJSPmodel.py
@@ -0,0 +1,254 @@
+"""
+This file contains the OR-Tools model for the Flexible Job Shop Problem (FJSP).
+This code has been adapted from the OR-Tools example for the FJSP, which can be found at:
+https://github.com/google/or-tools/blob/stable/examples/python/flexible_job_shop_sat.py
+"""
+
+import collections
+
+from ortools.sat.python import cp_model
+
+
+def parse_file_jsp(filename: str) -> dict:
+ """
+ Parses a file containing job shop scheduling data and returns a dictionary
+ with the number of jobs, number of machines, and the job operations.
+
+ Args:
+ filename (str): The name of the file to parse.
+
+ Returns:
+ dict: A dictionary containing the parsed data with the following keys:
+ - "num_jobs": The number of jobs.
+ - "num_machines": The number of machines.
+ - "jobs": A list of job operations, where each job operation is a list
+ with a single tuple representing the machine and processing time.
+
+ """
+ with open("./data/" + filename, "r") as f:
+ num_jobs, num_machines = tuple(map(int, f.readline().strip().split()))
+ jobs = []
+ for _ in range(num_jobs):
+ job_operations = []
+ data_line = list(map(int, f.readline().split()))
+ job_operations = [
+ [(data_line[i + 1], data_line[i])] for i in range(0, len(data_line), 2)
+ ]
+ jobs.append(job_operations)
+
+ return {"num_jobs": num_jobs, "num_machines": num_machines, "jobs": jobs}
+
+
+def parse_file_fjsp(filename: str) -> dict:
+ """
+ Parses a file containing flexible job shop scheduling data and returns a
+ dictionary with the number of jobs, number of machines, and the job operations.
+
+ Args:
+ filename (str): The name of the file to parse.
+
+ Returns:
+ dict: A dictionary containing the following keys:
+ - "num_jobs" (int): The number of jobs.
+ - "num_machines" (int): The number of machines.
+ - "jobs" (list): A list of job operations. Each job operation is a list
+ of tuples, where each tuple contains the processing time and machine
+ index for an operation.
+ """
+ with open("./data/" + filename, "r") as f:
+ num_jobs, num_machines = tuple(map(int, f.readline().strip().split()[:2]))
+ jobs = []
+ for _ in range(num_jobs):
+ job_operations = []
+ operation_data = list(map(int, f.readline().split()))
+ index = 1
+ while index < len(operation_data):
+ # Extract machine and processing time data for each operation
+ machines_for_task = operation_data[index]
+ # below x - 1 to go from 1 as lowest index to 0 as lowest index
+ job_machines = list(
+ map(
+ lambda x: x - 1,
+ operation_data[
+ index + 1 : index + 1 + machines_for_task * 2 : 2
+ ],
+ )
+ )
+ job_processingtimes = operation_data[
+ index + 2 : index + 2 + machines_for_task * 2 : 2
+ ]
+ operation_info = list(zip(job_processingtimes, job_machines))
+ index += machines_for_task * 2 + 1
+ job_operations.append(operation_info)
+ jobs.append(job_operations)
+ return {"num_jobs": num_jobs, "num_machines": num_machines, "jobs": jobs}
+
+
+class SolutionPrinter(cp_model.CpSolverSolutionCallback):
+ """Print intermediate solution_methods."""
+
+ def __init__(self):
+ cp_model.CpSolverSolutionCallback.__init__(self)
+ self.__solution_count = 0
+
+ def on_solution_callback(self):
+ """Called at each new solution."""
+ print(
+ "Solution %i, time = %f s, objective = %i"
+ % (self.__solution_count, self.WallTime(), self.ObjectiveValue())
+ )
+ self.__solution_count += 1
+
+ def solution_count(self):
+ return self.__solution_count
+
+
+def fjsp_or_tools_model(data: dict) -> tuple[cp_model.CpModel, dict]:
+ """
+ Creates a flexible job shop scheduling model using the OR-Tools library.
+
+ Args:
+ data (dict): A dictionary containing the input data for the flexible job shop scheduling problem.
+ The dictionary should have the following keys:
+ - "num_jobs" (int): The number of jobs in the problem.
+ - "num_machines" (int): The number of machines in the problem.
+ - "jobs" (list): A list of jobs, where each job is represented as a list of tasks.
+ Each task is represented as a list of alternatives, where each alternative is a tuple
+ containing the duration of the task and the machine on which it can be executed.
+
+ Returns:
+ tuple[cp_model.CpModel, dict]: A tuple containing the flexible job shop scheduling model and a dictionary
+ with the variables and intervals created during the model construction. The dictionary has the
+ following keys:
+ - "starts" (dict): A dictionary mapping (job_id, task_id) tuples to the corresponding start variables.
+ - "presences" (dict): A dictionary mapping (job_id, task_id, alt_id) tuples to the corresponding
+ presence variables.
+
+ """
+ num_jobs = data["num_jobs"]
+ num_machines = data["num_machines"]
+ jobs = data["jobs"]
+ all_jobs = range(num_jobs)
+ all_machines = range(num_machines)
+
+ model = cp_model.CpModel()
+
+ horizon = 0
+ for job in jobs:
+ for task in job:
+ max_task_duration = 0
+ for alternative in task:
+ max_task_duration = max(max_task_duration, alternative[0])
+ horizon += max_task_duration
+
+ print(f"Horizon = {horizon}")
+
+ # Global storage of variables.
+ intervals_per_resources = collections.defaultdict(list)
+ starts = {} # indexed by (job_id, task_id).
+ presences = {} # indexed by (job_id, task_id, alt_id).
+ job_ends = []
+
+ # Scan the jobs and create the relevant variables and intervals.
+ for job_id in all_jobs:
+ job = jobs[job_id]
+ num_tasks = len(job)
+ previous_end = None
+ for task_id in range(num_tasks):
+ task = job[task_id]
+
+ min_duration = task[0][0]
+ max_duration = task[0][0]
+
+ num_alternatives = len(task)
+ all_alternatives = range(num_alternatives)
+
+ for alt_id in range(1, num_alternatives):
+ alt_duration = task[alt_id][0]
+ min_duration = min(min_duration, alt_duration)
+ max_duration = max(max_duration, alt_duration)
+
+ # Create main interval for the task.
+ suffix_name = "_j%i_t%i" % (job_id, task_id)
+ start = model.NewIntVar(0, horizon, "start" + suffix_name)
+ duration = model.NewIntVar(
+ min_duration, max_duration, "duration" + suffix_name
+ )
+ end = model.NewIntVar(0, horizon, "end" + suffix_name)
+ interval = model.NewIntervalVar(
+ start, duration, end, "interval" + suffix_name
+ )
+
+ # Store the start for the solution.
+ starts[(job_id, task_id)] = start
+
+ # Add precedence with previous task in the same job.
+ if previous_end is not None:
+ model.Add(start >= previous_end)
+ previous_end = end
+
+ # Create alternative intervals.
+ if num_alternatives > 1:
+ l_presences = []
+ for alt_id in all_alternatives:
+ alt_suffix = "_j%i_t%i_a%i" % (job_id, task_id, alt_id)
+ l_presence = model.NewBoolVar("presence" + alt_suffix)
+ l_start = model.NewIntVar(0, horizon, "start" + alt_suffix)
+ l_duration = task[alt_id][0]
+ l_end = model.NewIntVar(0, horizon, "end" + alt_suffix)
+ l_interval = model.NewOptionalIntervalVar(
+ l_start, l_duration, l_end, l_presence, "interval" + alt_suffix
+ )
+ l_presences.append(l_presence)
+
+ # Link the primary/global variables with the local ones.
+ model.Add(start == l_start).OnlyEnforceIf(l_presence)
+ model.Add(duration == l_duration).OnlyEnforceIf(l_presence)
+ model.Add(end == l_end).OnlyEnforceIf(l_presence)
+
+ # Add the local interval to the right machine.
+ intervals_per_resources[task[alt_id][1]].append(l_interval)
+
+ # Store the presences for the solution.
+ presences[(job_id, task_id, alt_id)] = l_presence
+
+ # Select exactly one presence variable.
+ model.AddExactlyOne(l_presences)
+ else:
+ intervals_per_resources[task[0][1]].append(interval)
+ presences[(job_id, task_id, 0)] = model.NewConstant(1)
+
+ job_ends.append(previous_end)
+
+ # Create machines constraints.
+ for machine_id in all_machines:
+ intervals = intervals_per_resources[machine_id]
+ if len(intervals) > 1:
+ model.AddNoOverlap(intervals)
+
+ # Makespan objective
+ makespan = model.NewIntVar(0, horizon, "makespan")
+ model.AddMaxEquality(makespan, job_ends)
+ model.Minimize(makespan)
+ return model, {"starts": starts, "presences": presences}
+
+
+def solve_model(
+ model: cp_model.CpModel, time_limit: float | int
+) -> tuple[cp_model.CpSolver, int, int]:
+ """
+ Solves the given constraint programming model within the specified time limit.
+
+ Args:
+ model: The constraint programming model to solve.
+ time_limit: The maximum time limit in seconds for solving the model.
+
+ Returns:
+ A tuple containing the solver object, the status of the solver, and the number of solution_methods found.
+ """
+ solver = cp_model.CpSolver()
+ solver.parameters.max_time_in_seconds = time_limit
+ solution_printer = SolutionPrinter()
+ status = solver.Solve(model, solution_printer)
+ solution_count = solution_printer.solution_count()
+ return solver, status, solution_count
diff --git a/solutions/FJSP_DRL/__init__.py b/solutions/FJSP_DRL/__init__.py
deleted file mode 100644
index 034a5ee..0000000
--- a/solutions/FJSP_DRL/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from gym.envs.registration import register
-
-# Registrar for the gym environment
-# https://www.gymlibrary.ml/content/environment_creation/ for reference
-register(
- id='fjsp-v0', # Environment name (including version number)
- entry_point='solutions.FJSP_DRL.env:FJSPEnv', # The location of the environment class, like 'foldername.filename:classname'
-)
\ No newline at end of file
diff --git a/solutions/FJSP_DRL/train.py b/solutions/FJSP_DRL/train.py
index 3a27d02..d1808d7 100644
--- a/solutions/FJSP_DRL/train.py
+++ b/solutions/FJSP_DRL/train.py
@@ -14,7 +14,7 @@
import logging
from collections import deque
-import gymnasium as gym
+import gym
import pandas as pd
from pathlib import Path
import torch