From b6dbc6f70e3b33f9980f198db99603901713e805 Mon Sep 17 00:00:00 2001 From: LongxingTan Date: Mon, 21 Aug 2023 15:18:17 +0800 Subject: [PATCH] Update backward and forward (#31) * Update operation * Update backward * Update forward and backward * Update forward and backward --------- Co-authored-by: yuetan1988 <65592503+yuetan1988@users.noreply.github.com> --- .github/workflows/test.yml | 42 +++++++++++ CHANGELOG.md | 5 +- docs/requirements_docs.txt | 3 +- docs/source/application.rst | 2 +- docs/source/rules.rst | 1 + examples/rule_example.py | 6 +- lekin/dashboard/gantt.py | 3 +- lekin/lekin_struct/job.py | 13 +--- lekin/lekin_struct/resource.py | 6 +- lekin/objective/__init__.py | 2 + lekin/objective/makespan.py | 14 ++-- lekin/scheduler.py | 4 +- .../construction_heuristics/backward.py | 17 ++++- .../solver/construction_heuristics/forward.py | 9 ++- lekin/solver/meta_heuristics/genetic.py | 73 ++++++++++--------- tests/test_dashboard/test_gantt.py | 0 tests/test_lekin_struct/test_job.py | 0 tests/test_lekin_struct/test_operation.py | 0 tests/test_lekin_struct/test_resource.py | 0 tests/test_lekin_struct/test_route.py | 0 tests/test_lekin_struct/test_timeslot.py | 0 .../test_meta_heuristics/test_genetic.py | 0 .../test_meta_heuristics/test_nsga3.py | 0 .../test_meta_heuristics/test_tabu_search.py | 0 .../test_reinforcement_learning/__init__.py | 0 .../test_reinforcement_learning/test_dqn.py | 0 26 files changed, 132 insertions(+), 68 deletions(-) create mode 100644 tests/test_dashboard/test_gantt.py create mode 100644 tests/test_lekin_struct/test_job.py create mode 100644 tests/test_lekin_struct/test_operation.py create mode 100644 tests/test_lekin_struct/test_resource.py create mode 100644 tests/test_lekin_struct/test_route.py create mode 100644 tests/test_lekin_struct/test_timeslot.py create mode 100644 tests/test_solver/test_meta_heuristics/test_genetic.py create mode 100644 tests/test_solver/test_meta_heuristics/test_nsga3.py create mode 100644 tests/test_solver/test_meta_heuristics/test_tabu_search.py create mode 100644 tests/test_solver/test_reinforcement_learning/__init__.py create mode 100644 tests/test_solver/test_reinforcement_learning/test_dqn.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d7e1b8..b78dac0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,3 +81,45 @@ jobs: flags: cpu, unittest name: CPU-coverage fail_ci_if_error: false + + docs: + name: Test docs build + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements_docs.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + sudo apt-get update && sudo apt-get install -y pandoc + python -m pip install --upgrade pip + pip install -r docs/requirements_docs.txt + shell: bash + + - name: Build sphinx documentation + run: | + cd docs + make clean + make html --debug --jobs 2 SPHINXOPTS="-W" + + - name: Upload built docs + uses: actions/upload-artifact@v2 + with: + name: docs-results-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.requires }} + path: docs/build/html/ + # Use always() to always run this step to publish test results when there are test failures + if: success() diff --git a/CHANGELOG.md b/CHANGELOG.md index 811faaa..4eb3502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,10 @@ - spt - fifo - edd - - heuristics + - forward scheduling + - backward scheduling + - meta heuristics - local search - - shifting_bottle_neck - genetic ### Contributor diff --git a/docs/requirements_docs.txt b/docs/requirements_docs.txt index 26cba64..b1acf8e 100644 --- a/docs/requirements_docs.txt +++ b/docs/requirements_docs.txt @@ -9,4 +9,5 @@ pydata_sphinx_theme==0.8.0 docutils sphinx-autobuild -tensorflow==2.10.0 +pandas +numpy diff --git a/docs/source/application.rst b/docs/source/application.rst index b42790e..b342da8 100644 --- a/docs/source/application.rst +++ b/docs/source/application.rst @@ -10,7 +10,7 @@ Application ---------------- MRP: Material Requirements Planning -- 每个零件的库存 + BOM: Bill Of Materials diff --git a/docs/source/rules.rst b/docs/source/rules.rst index e3a1825..2acb1be 100644 --- a/docs/source/rules.rst +++ b/docs/source/rules.rst @@ -47,6 +47,7 @@ SPT—EDD规则 顺排和倒排,和其他规则启发式算法一样,一个工序集一个工序集的排。每排一个工序,工序job完成后,更新机器、job状态、后续job状态。 +在顺排中,排的比较紧密的资源往往就是瓶颈资源。 倒排 --------------- diff --git a/examples/rule_example.py b/examples/rule_example.py index af16a18..46d0cb3 100644 --- a/examples/rule_example.py +++ b/examples/rule_example.py @@ -35,7 +35,6 @@ def prepare_data(file_path="./data/k1.json"): re_name = re["machineName"] re_id = int(re_name.replace("M", "")) resource = Resource(resource_id=re_id, resource_name=re_name) - resource.available_hours = list(range(1, 150)) resource_collector.add_resource_dict({re_id: resource}) # print([i.resource_id for i in resource_collector.get_all_resources()]) # print(resource_collector.get_all_resources()[0].available_hours) @@ -86,8 +85,9 @@ def prepare_data(file_path="./data/k1.json"): def run_scheduling(job_collector, resource_collector, route_collector): - scheduler = ForwardScheduler(job_collector, resource_collector, route_collector) - # scheduler = BackwardScheduler(job_collector, resource_collector, route_collector) + # scheduler = ForwardScheduler(job_collector, resource_collector, route_collector) + scheduler = BackwardScheduler(job_collector, resource_collector, route_collector) + scheduler.run() return diff --git a/lekin/dashboard/gantt.py b/lekin/dashboard/gantt.py index 5511f99..5ecb133 100644 --- a/lekin/dashboard/gantt.py +++ b/lekin/dashboard/gantt.py @@ -23,12 +23,13 @@ def get_scheduling_res_from_all_jobs(job_collector): [ op.operation_id, op.parent_job_id, + op.quantity, op.assigned_resource.resource_id, min(op.assigned_hours), max(op.assigned_hours), ] ) - scheduling_res = pd.DataFrame(scheduling_res, columns=["Operation", "Job", "Resource", "Start", "End"]) + scheduling_res = pd.DataFrame(scheduling_res, columns=["Operation", "Job", "Quantity", "Resource", "Start", "End"]) scheduling_res["Duration"] = scheduling_res["End"] - scheduling_res["Start"] # + 1 return scheduling_res diff --git a/lekin/lekin_struct/job.py b/lekin/lekin_struct/job.py index 4c850d7..5344491 100644 --- a/lekin/lekin_struct/job.py +++ b/lekin/lekin_struct/job.py @@ -2,17 +2,6 @@ Struct Job/订单作业 - a job could finish one product while finished - job/mo/operation/activity - -property - - 已完成活动 - - 待完成活动 - - processing time - - due date - - weight - - slack time remaining - - critical ratio - - priority - - 属于哪个订单 """ from datetime import datetime @@ -46,6 +35,8 @@ def __init__( self.assigned_route_id = assigned_route_id # Route object assigned to this job self.assigned_bom_id = assigned_bom_id self._operations_sequence = [] # List of Operation objects for this job + self.makespan = None # finish of the job + self.tardiness = None # delay of the job for key, value in kwargs.items(): setattr(self, key, value) diff --git a/lekin/lekin_struct/resource.py b/lekin/lekin_struct/resource.py index 085cfb6..d05e87f 100644 --- a/lekin/lekin_struct/resource.py +++ b/lekin/lekin_struct/resource.py @@ -4,6 +4,7 @@ import math +import numpy as np import pandas as pd from lekin.lekin_struct.timeslot import TimeSlot @@ -21,6 +22,8 @@ def __init__(self, resource_id, resource_name=None, max_tasks=1, **kwargs): self.assigned_operations = [] self.assigned_time_slots = [] self.assigned_hours = [] + self.changeover_number = None # number of times + self.changeover_time = None # total time costs for key, value in kwargs.items(): setattr(self, key, value) @@ -67,7 +70,8 @@ def get_earliest_available_time(self, duration=None, start=None): def get_latest_available_time(self, duration=None, end=None): self.update_continuous_empty_hours() - return max([i for i in self.continuous_empty_hours[:end] if i >= duration]) + return max([i + 1 for (i, v) in enumerate(self.continuous_empty_hours[:end]) if v >= duration]) + def update_continuous_empty_hours(self): if len(self.available_hours) != len(self._available_timeslots): diff --git a/lekin/objective/__init__.py b/lekin/objective/__init__.py index e69de29..428b4b1 100644 --- a/lekin/objective/__init__.py +++ b/lekin/objective/__init__.py @@ -0,0 +1,2 @@ +from lekin.objective.makespan import calculate_makespan +from lekin.objective.tardiness import calculate_tardiness diff --git a/lekin/objective/makespan.py b/lekin/objective/makespan.py index f4a15c2..c354ece 100644 --- a/lekin/objective/makespan.py +++ b/lekin/objective/makespan.py @@ -1,11 +1,11 @@ -def calculate_makespan(schedule_result): - end_times = [end_time for (_, end_time) in schedule_result.values()] - return max(end_times) +def calculate_makespan(job_collector): + for job in job_collector.job_list: + op = job.operations + job.makespan = op.assigned_hours[-1] - -def calculate_flow_time(schedule_result, job): - start_time, end_time = schedule_result[job.route.operations[-1]] - return end_time - job.release_date + if job.demand_date is not None: + job.tardiness = job.makespan - job.demand_date + return def calculate_changeover_time(schedule_result, job_collector): diff --git a/lekin/scheduler.py b/lekin/scheduler.py index 98beea9..4662266 100644 --- a/lekin/scheduler.py +++ b/lekin/scheduler.py @@ -10,12 +10,12 @@ class Scheduler(object): - def __init__(self, objective, solver, max_operations): + def __init__(self, objective, solver, max_operations, **kwargs): self.objective = objective self.solver = solver self.max_operations = max_operations - def solve(self, jobs, machines): + def run(self, jobs, machines): self.solver.solve(jobs, machines) def evaluate(self): diff --git a/lekin/solver/construction_heuristics/backward.py b/lekin/solver/construction_heuristics/backward.py index bf07264..25b8444 100644 --- a/lekin/solver/construction_heuristics/backward.py +++ b/lekin/solver/construction_heuristics/backward.py @@ -3,12 +3,18 @@ import logging import math -from lekin.lekin_struct.timeslot import TimeSlot +from lekin.lekin_struct import JobCollector, ResourceCollector, RouteCollector, TimeSlot from lekin.solver.construction_heuristics.base import BaseScheduler class BackwardScheduler(object): - def __init__(self, job_collector, resource_collector, route_collector=None, **kwargs): + def __init__( + self, + job_collector: JobCollector, + resource_collector: ResourceCollector, + route_collector: RouteCollector = None, + **kwargs, + ): self.job_collector = job_collector self.resource_collector = resource_collector self.route_collector = route_collector @@ -39,7 +45,8 @@ def scheduling_job(self, job, resource_collector, route_collector): op_earliest_start = 0 # forward constraint op_latest_end = 150 # backward constraint - for operation in job.operations[::-1]: # inverse + + for operation in job.operations[::-1]: # inverse for backward logging.info(f"\tAssign Operation {operation.operation_id} of Job {job.job_id}") chosen_resource, chosen_timeslot_hour = self.find_best_resource_and_timeslot_for_operation( operation, op_latest_end, op_earliest_start @@ -76,9 +83,11 @@ def find_best_resource_and_timeslot_for_operation( latest_index = i resource_latest_time = resource_time + # print(operation.operation_id, operation.processing_time, op_latest_end, resource_latest_time) chosen_resource = available_resource[latest_index] latest_time = int(min(op_latest_end, resource_latest_time)) - chosen_hours = list(range(latest_time - math.ceil(operation.processing_time), latest_time + 0)) + chosen_hours = list(range(latest_time - math.ceil(operation.processing_time), latest_time + 1)) + return chosen_resource, chosen_hours def assign_operation(self, operation, start_time, end_time, resources): diff --git a/lekin/solver/construction_heuristics/forward.py b/lekin/solver/construction_heuristics/forward.py index eca7f98..aa66e53 100644 --- a/lekin/solver/construction_heuristics/forward.py +++ b/lekin/solver/construction_heuristics/forward.py @@ -3,11 +3,18 @@ import logging import math +from lekin.lekin_struct import JobCollector, ResourceCollector, RouteCollector from lekin.solver.construction_heuristics.base import BaseScheduler class ForwardScheduler(BaseScheduler): - def __init__(self, job_collector, resource_collector, route_collector=None, **kwargs): + def __init__( + self, + job_collector: JobCollector, + resource_collector: ResourceCollector, + route_collector: RouteCollector = None, + **kwargs, + ): super().__init__(job_collector, resource_collector, **kwargs) self.job_collector = job_collector self.resource_collector = resource_collector diff --git a/lekin/solver/meta_heuristics/genetic.py b/lekin/solver/meta_heuristics/genetic.py index 92e26b9..0dba9ba 100644 --- a/lekin/solver/meta_heuristics/genetic.py +++ b/lekin/solver/meta_heuristics/genetic.py @@ -3,24 +3,61 @@ import copy import random +from lekin.lekin_struct import JobCollector, ResourceCollector, RouteCollector + class GeneticScheduler: def __init__( self, - job_collector=None, + job_collector: JobCollector, + resource_collector: ResourceCollector, + route_collector: RouteCollector = None, initial_schedule=None, population_size=50, generations=1000, crossover_rate=0.8, mutation_rate=0.2, + **kwargs, ): self.job_collector = job_collector - self.initial_schedule = initial_schedule # 倒排顺排后的初始结果 + self.initial_schedule = initial_schedule self.population_size = population_size self.generations = generations self.crossover_rate = crossover_rate self.mutation_rate = mutation_rate + def run(self): + population = self.initialize_population() + + for generation in range(self.generations): + selected_individuals = self.selection(population) + new_population = [] + + while len(new_population) < self.population_size: + parent1 = random.choice(selected_individuals) + parent2 = random.choice(selected_individuals) + + if random.random() < self.crossover_rate: + offspring1, offspring2 = self.crossover(parent1, parent2) + else: + offspring1, offspring2 = parent1, parent2 + + if random.random() < self.mutation_rate: + offspring1 = self.mutation(offspring1) + if random.random() < self.mutation_rate: + offspring2 = self.mutation(offspring2) + + new_population.append(offspring1) + new_population.append(offspring2) + + population = new_population + + # Find the best solution in the final population + best_solution = min(population, key=lambda chromosome: self.fitness(chromosome)[0]) + + # Return the best schedule + return self.job_collector.create_schedule_from_operations(best_solution) + def initialize_population(self): population = [] for _ in range(self.population_size): @@ -59,35 +96,3 @@ def mutation(self, chromosome): # Return the mutated chromosome mutated_chromosome = 0 return mutated_chromosome - - def evolve(self): - population = self.initialize_population() - - for generation in range(self.generations): - selected_individuals = self.selection(population) - new_population = [] - - while len(new_population) < self.population_size: - parent1 = random.choice(selected_individuals) - parent2 = random.choice(selected_individuals) - - if random.random() < self.crossover_rate: - offspring1, offspring2 = self.crossover(parent1, parent2) - else: - offspring1, offspring2 = parent1, parent2 - - if random.random() < self.mutation_rate: - offspring1 = self.mutation(offspring1) - if random.random() < self.mutation_rate: - offspring2 = self.mutation(offspring2) - - new_population.append(offspring1) - new_population.append(offspring2) - - population = new_population - - # Find the best solution in the final population - best_solution = min(population, key=lambda chromosome: self.fitness(chromosome)[0]) - - # Return the best schedule - return self.job_collector.create_schedule_from_operations(best_solution) diff --git a/tests/test_dashboard/test_gantt.py b/tests/test_dashboard/test_gantt.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_lekin_struct/test_job.py b/tests/test_lekin_struct/test_job.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_lekin_struct/test_operation.py b/tests/test_lekin_struct/test_operation.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_lekin_struct/test_resource.py b/tests/test_lekin_struct/test_resource.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_lekin_struct/test_route.py b/tests/test_lekin_struct/test_route.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_lekin_struct/test_timeslot.py b/tests/test_lekin_struct/test_timeslot.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_solver/test_meta_heuristics/test_genetic.py b/tests/test_solver/test_meta_heuristics/test_genetic.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_solver/test_meta_heuristics/test_nsga3.py b/tests/test_solver/test_meta_heuristics/test_nsga3.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_solver/test_meta_heuristics/test_tabu_search.py b/tests/test_solver/test_meta_heuristics/test_tabu_search.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_solver/test_reinforcement_learning/__init__.py b/tests/test_solver/test_reinforcement_learning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_solver/test_reinforcement_learning/test_dqn.py b/tests/test_solver/test_reinforcement_learning/test_dqn.py new file mode 100644 index 0000000..e69de29