Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The zone introduction #54

Merged
merged 28 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bb89690
Added zone schemas
StannisMod Aug 30, 2023
60fcb44
Added zone timeline (untested)
StannisMod Aug 30, 2023
1b1b2c9
-
StannisMod Aug 30, 2023
b03423d
Pre-finished and tested ZoneTimeline
StannisMod Sep 3, 2023
8230392
Finished ZoneTimeline development
StannisMod Sep 3, 2023
f013f6b
Added TODO
StannisMod Sep 4, 2023
3c0e76e
Merge branch 'main' into feature/zones
StannisMod Sep 4, 2023
23d0776
Started zones integration into Genetic
StannisMod Sep 4, 2023
d23ca69
Integrated zones to Genetic converter
StannisMod Sep 5, 2023
dc8fe55
Made operators for zones
StannisMod Sep 5, 2023
988fabd
Fixed visualization end-to-start work connections, fixed assert in Zo…
StannisMod Sep 6, 2023
3aa9fa0
Started zone visualization implementation
StannisMod Sep 6, 2023
c048b20
Updated visualization with zones
StannisMod Sep 6, 2023
1b57bd2
Added zone and defect alignment to parent works
StannisMod Sep 11, 2023
1f533dd
Trying to fix zone timeline end-to-start interval intersection
StannisMod Sep 12, 2023
e852d80
Pre-finished full zone planning with genetic
StannisMod Sep 15, 2023
8be55ab
Merge remote-tracking branch 'origin/feature/zones' into feature/zones
StannisMod Sep 15, 2023
d809957
Fixed MomentumTimeline start-end alignment, extended zone timeline tests
StannisMod Sep 19, 2023
d700e9b
Pre-finished all fixes, need testing
StannisMod Sep 27, 2023
7d2980e
Serializability for zones
StannisMod Oct 4, 2023
c2ba960
Merge remote-tracking branch 'origin/feature/zones' into feature/zones
StannisMod Oct 4, 2023
8c9946f
Merge branch 'main' into feature/zones
StannisMod Oct 4, 2023
3b45c9f
Fixed issues
StannisMod Oct 8, 2023
42476f7
Fix
StannisMod Oct 17, 2023
dd9b394
Fix
StannisMod Oct 17, 2023
ad88fcb
Merge remote-tracking branch 'origin/feature/zones' into feature/zones
StannisMod Oct 17, 2023
e65afc3
Fix
StannisMod Oct 17, 2023
7c205ea
Added comments
StannisMod Oct 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sampo"
version = "0.1.1.203"
version = "0.1.1.220"
description = "Open-source framework for adaptive manufacturing processes scheduling"
authors = ["iAirLab <[email protected]>"]
license = "BSD-3-Clause"
Expand Down
1 change: 1 addition & 0 deletions sampo/scheduler/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def generate_schedule(scheduling_algorithm_type: SchedulerType,
scheduler = get_scheduler_ctor(scheduling_algorithm_type)(work_estimator=work_time_estimator)
start_time = time.time()
if isinstance(scheduler, GeneticScheduler):
scheduler.number_of_generation = 5
scheduler.set_use_multiprocessing(n_cpu=4)

schedule = scheduler.schedule(work_graph,
Expand Down
2 changes: 1 addition & 1 deletion sampo/scheduler/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def build_scheduler(self,
node2swork: dict[GraphNode, ScheduledWork] = {}
# list for support the queue of workers
if not isinstance(timeline, self._timeline_type):
timeline = self._timeline_type(ordered_nodes, contractors, worker_pool, landscape)
timeline = self._timeline_type(contractors, landscape)

for index, node in enumerate(reversed(ordered_nodes)): # the tasks with the highest rank will be done first
work_unit = node.work_unit
Expand Down
16 changes: 12 additions & 4 deletions sampo/scheduler/genetic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self,
number_of_generation: Optional[int] = 50,
mutate_order: Optional[float or None] = None,
mutate_resources: Optional[float or None] = None,
mutate_zones: Optional[float or None] = None,
size_of_population: Optional[float or None] = None,
rand: Optional[random.Random] = None,
seed: Optional[float or None] = None,
Expand All @@ -49,6 +50,7 @@ def __init__(self,
self.number_of_generation = number_of_generation
self.mutate_order = mutate_order
self.mutate_resources = mutate_resources
self.mutate_zones = mutate_zones
self.size_of_population = size_of_population
self.rand = rand or random.Random(seed)
self.fitness_constructor = fitness_constructor
Expand All @@ -69,7 +71,7 @@ def __str__(self) -> str:
f'mutate_resources={self.mutate_resources}' \
f']'

def get_params(self, works_count: int) -> tuple[float, float, int]:
def get_params(self, works_count: int) -> tuple[float, float, float, int]:
"""
Return base parameters for model to make new population

Expand All @@ -84,6 +86,10 @@ def get_params(self, works_count: int) -> tuple[float, float, int]:
if mutate_resources is None:
mutate_resources = 0.005

mutate_zones = self.mutate_zones
if mutate_zones is None:
mutate_zones = 0.05

size_of_population = self.size_of_population
if size_of_population is None:
if works_count < 300:
Expand All @@ -92,11 +98,12 @@ def get_params(self, works_count: int) -> tuple[float, float, int]:
size_of_population = 100
else:
size_of_population = works_count // 25
return mutate_order, mutate_resources, size_of_population
return mutate_order, mutate_resources, mutate_zones, size_of_population

def set_use_multiprocessing(self, n_cpu: int):
"""
Set the number of CPU cores
Set the number of CPU cores.
DEPRECATED, NOT WORKING

:param n_cpu:
"""
Expand Down Expand Up @@ -205,7 +212,7 @@ def schedule_with_cache(self,
init_schedules = GeneticScheduler.generate_first_population(wg, contractors, landscape, spec,
self.work_estimator, self._deadline, self._weights)

mutate_order, mutate_resources, size_of_population = self.get_params(wg.vertex_count)
mutate_order, mutate_resources, mutate_zones, size_of_population = self.get_params(wg.vertex_count)
worker_pool = get_worker_contractor_pool(contractors)
fitness_object = self.fitness_constructor(self._deadline)
deadline = None if self._optimize_resources else self._deadline
Expand All @@ -217,6 +224,7 @@ def schedule_with_cache(self,
self.number_of_generation,
mutate_order,
mutate_resources,
mutate_zones,
init_schedules,
self.rand,
spec,
Expand Down
35 changes: 26 additions & 9 deletions sampo/scheduler/genetic/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
from sampo.schemas.contractor import WorkerContractorPool, Contractor
from sampo.schemas.graph import GraphNode, WorkGraph
from sampo.schemas.landscape import LandscapeConfiguration
from sampo.schemas.requirements import ZoneReq
from sampo.schemas.resources import Worker
from sampo.schemas.schedule import ScheduledWork, Schedule
from sampo.schemas.schedule_spec import ScheduleSpec
from sampo.schemas.time import Time
from sampo.schemas.time_estimator import WorkTimeEstimator, DefaultWorkEstimator

ChromosomeType = tuple[np.ndarray, np.ndarray, np.ndarray, ScheduleSpec]
ChromosomeType = tuple[np.ndarray, np.ndarray, np.ndarray, ScheduleSpec, np.ndarray]


def convert_schedule_to_chromosome(wg: WorkGraph,
Expand All @@ -24,6 +25,7 @@ def convert_schedule_to_chromosome(wg: WorkGraph,
contractor_borders: np.ndarray,
schedule: Schedule,
spec: ScheduleSpec,
landscape: LandscapeConfiguration,
order: list[GraphNode] | None = None) -> ChromosomeType:
"""
Receive a result of scheduling algorithm and transform it to chromosome
Expand All @@ -35,6 +37,7 @@ def convert_schedule_to_chromosome(wg: WorkGraph,
:param contractor_borders:
:param schedule:
:param spec:
:param landscape:
:param order: if passed, specify the node order that should appear in the chromosome
:return:
"""
Expand All @@ -52,6 +55,9 @@ def convert_schedule_to_chromosome(wg: WorkGraph,
# +1 stores contractors line
resource_chromosome = np.zeros((len(order_chromosome), len(worker_name2index) + 1), dtype=int)

# zone status changes after node executing
zone_changes_chromosome = np.zeros((len(order_chromosome), len(landscape.zone_config.start_statuses)), dtype=int)

for node in order:
node_id = node.work_unit.id
index = work_id2index[node_id]
Expand All @@ -64,13 +70,14 @@ def convert_schedule_to_chromosome(wg: WorkGraph,

resource_border_chromosome = np.copy(contractor_borders)

return order_chromosome, resource_chromosome, resource_border_chromosome, spec
return order_chromosome, resource_chromosome, resource_border_chromosome, spec, zone_changes_chromosome


def convert_chromosome_to_schedule(chromosome: ChromosomeType,
worker_pool: WorkerContractorPool,
index2node: dict[int, GraphNode],
index2contractor: dict[int, Contractor],
index2zone: dict[int, str],
worker_pool_indices: dict[int, dict[int, Worker]],
worker_name2index: dict[str, int],
contractor2index: dict[str, int],
Expand All @@ -89,6 +96,7 @@ def convert_chromosome_to_schedule(chromosome: ChromosomeType,
works_resources = chromosome[1]
border = chromosome[2]
spec = chromosome[3]
zone_statuses = chromosome[4]
worker_pool = copy.deepcopy(worker_pool)

# use 3rd part of chromosome in schedule generator
Expand All @@ -98,15 +106,13 @@ def convert_chromosome_to_schedule(chromosome: ChromosomeType,
worker_name2index[worker_index]])

if not isinstance(timeline, JustInTimeTimeline):
timeline = JustInTimeTimeline(index2node.values(), index2contractor.values(), worker_pool, landscape)
timeline = JustInTimeTimeline(index2contractor.values(), landscape)

order_nodes = []

for order_index, work_index in enumerate(works_order):
node = index2node[work_index]
order_nodes.append(node)
# if node.id in node2swork and not node.is_inseparable_son():
# continue

work_spec = spec.get_work_spec(node.id)

Expand All @@ -121,15 +127,26 @@ def convert_chromosome_to_schedule(chromosome: ChromosomeType,
# apply worker spec
Scheduler.optimize_resources_using_spec(node.work_unit, worker_team, work_spec)

st = timeline.find_min_start_time(node, worker_team, node2swork, work_spec,
assigned_parent_time, work_estimator)
st, ft, exec_times = timeline.find_min_start_time_with_additional(node, worker_team, node2swork, work_spec,
assigned_parent_time,
work_estimator=work_estimator)

if order_index == 0: # we are scheduling the work `start of the project`
st = assigned_parent_time # this work should always have st = 0, so we just re-assign it

# finish using time spec
timeline.schedule(node, node2swork, worker_team, contractor, work_spec,
st, work_spec.assigned_time, assigned_parent_time, work_estimator)
ft = timeline.schedule(node, node2swork, worker_team, contractor, work_spec,
st, work_spec.assigned_time, assigned_parent_time, work_estimator)
# process zones
zone_reqs = [ZoneReq(index2zone[i], zone_status) for i, zone_status in enumerate(zone_statuses[work_index])]
zone_start_time = timeline.zone_timeline.find_min_start_time(zone_reqs, ft, 0)

# we should deny scheduling
# if zone status change can be scheduled only in delayed manner
if zone_start_time != ft:
node2swork[node].zones_post = timeline.zone_timeline.update_timeline(order_index,
[z.to_zone() for z in zone_reqs],
zone_start_time, 0)

schedule_start_time = min((swork.start_time for swork in node2swork.values() if
len(swork.work_unit.worker_reqs) != 0), default=assigned_parent_time)
Expand Down
78 changes: 66 additions & 12 deletions sampo/scheduler/genetic/operators.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import random
import math
import random
from abc import ABC, abstractmethod
from copy import deepcopy
from typing import Iterable
from functools import partial
from operator import attrgetter
from typing import Iterable, Callable
from enum import Enum

import numpy as np
Expand Down Expand Up @@ -32,7 +33,7 @@ class FitnessFunction(ABC):
"""
Base class for description of different fitness functions.
"""

def __init__(self, deadline: Time | None):
self._deadline = deadline

Expand All @@ -49,7 +50,7 @@ class TimeFitness(FitnessFunction):
"""
Fitness function that relies on finish time.
"""

def __init__(self, deadline: Time | None = None):
super().__init__(deadline)

Expand Down Expand Up @@ -120,9 +121,12 @@ def init_toolbox(wg: WorkGraph,
work_id2index: dict[str, int],
worker_name2index: dict[str, int],
index2contractor_obj: dict[int, Contractor],
index2zone: dict[int, str],
init_chromosomes: dict[str, tuple[ChromosomeType, float, ScheduleSpec]],
mut_order_pb: float,
mut_res_pb: float,
mut_zone_pb: float,
statuses_available: int,
population_size: int,
rand: random.Random,
spec: ScheduleSpec,
Expand Down Expand Up @@ -158,8 +162,8 @@ def init_toolbox(wg: WorkGraph,
# combined crossover
toolbox.register('mate', mate, rand=rand)
# combined mutation
toolbox.register('mutate', mutate, order_mutpb=mut_order_pb, res_mutpb=mut_res_pb, rand=rand,
parents=parents, resources_border=resources_border)
toolbox.register('mutate', mutate, order_mutpb=mut_order_pb, res_mutpb=mut_res_pb, zone_mutpb=mut_zone_pb,
rand=rand, parents=parents, resources_border=resources_border, statuses_available=statuses_available)
# crossover for order
toolbox.register('mate_order', mate_scheduling_order, rand=rand)
# mutation for order
Expand All @@ -172,17 +176,21 @@ def init_toolbox(wg: WorkGraph,
# mutation for resource borders
toolbox.register('mutate_resource_borders', mutate_resource_borders, contractor_borders=contractor_borders,
mutpb=mut_res_pb, rand=rand)
toolbox.register('mate_post_zones', mate_for_zones, rand=rand)
toolbox.register('mutate_post_zones', mutate_for_zones, rand=rand, mutpb=mut_zone_pb,
statuses_available=landscape.zone_config.statuses.statuses_available())

toolbox.register('validate', is_chromosome_correct, node_indices=node_indices, parents=parents,
contractor_borders=contractor_borders)
toolbox.register('schedule_to_chromosome', convert_schedule_to_chromosome, wg=wg,
work_id2index=work_id2index, worker_name2index=worker_name2index,
contractor2index=contractor2index, contractor_borders=contractor_borders, spec=spec)
contractor2index=contractor2index, contractor_borders=contractor_borders, spec=spec,
landscape=landscape)
toolbox.register("chromosome_to_schedule", convert_chromosome_to_schedule, worker_pool=worker_pool,
index2node=index2node, index2contractor=index2contractor_obj,
worker_pool_indices=worker_pool_indices, assigned_parent_time=assigned_parent_time,
work_estimator=work_estimator, worker_name2index=worker_name2index,
contractor2index=contractor2index,
contractor2index=contractor2index, index2zone=index2zone,
landscape=landscape)
toolbox.register('copy_individual', lambda ind: Individual(copy_chromosome(ind)))
toolbox.register('update_resource_borders_to_peak_values', update_resource_borders_to_peak_values,
Expand All @@ -191,7 +199,8 @@ def init_toolbox(wg: WorkGraph,


def copy_chromosome(chromosome: ChromosomeType) -> ChromosomeType:
return chromosome[0].copy(), chromosome[1].copy(), chromosome[2].copy(), deepcopy(chromosome[3])
return chromosome[0].copy(), chromosome[1].copy(), chromosome[2].copy(), \
deepcopy(chromosome[3]), chromosome[4].copy()


def generate_population(n: int,
Expand All @@ -215,7 +224,7 @@ def randomized_init() -> ChromosomeType:
schedule = RandomizedTopologicalScheduler(work_estimator, int(rand.random() * 1000000)) \
.schedule(wg, contractors, landscape=landscape)
return convert_schedule_to_chromosome(wg, work_id2index, worker_name2index,
contractor2index, contractor_borders, schedule, spec)
contractor2index, contractor_borders, schedule, spec, landscape)

count_for_specified_types = (n // 3) // len(init_chromosomes)
count_for_specified_types = count_for_specified_types if count_for_specified_types > 0 else 1
Expand Down Expand Up @@ -263,7 +272,7 @@ def randomized_init() -> ChromosomeType:
int(rand.random() * 1000000)) \
.schedule(wg, contractors, spec, landscape=landscape)
return convert_schedule_to_chromosome(wg, work_id2index, worker_name2index,
contractor2index, contractor_borders, schedule, spec)
contractor2index, contractor_borders, schedule, spec, landscape)

chance = rand.random()
if chance < 0.2:
Expand Down Expand Up @@ -501,12 +510,14 @@ def mate(ind1: ChromosomeType, ind2: ChromosomeType, optimize_resources: bool, r
"""
child1, child2 = mate_scheduling_order(ind1, ind2, rand, copy=True)
child1, child2 = mate_resources(child1, child2, optimize_resources, rand, copy=False)
child1, child2 = mate_for_zones(child1, child2, rand, copy=False)

return child1, child2


def mutate(ind: ChromosomeType, resources_border: np.ndarray, parents: dict[int, set[int]],
order_mutpb: float, res_mutpb: float, rand: random.Random) -> ChromosomeType:
order_mutpb: float, res_mutpb: float, zone_mutpb: float, statuses_available: int,
rand: random.Random) -> ChromosomeType:
"""
Combined mutation function of mutation for order and mutation for resources.

Expand All @@ -521,6 +532,7 @@ def mutate(ind: ChromosomeType, resources_border: np.ndarray, parents: dict[int,
"""
mutant = mutate_scheduling_order(ind, order_mutpb, rand, parents)
mutant = mutate_resources(mutant, res_mutpb, rand, resources_border)
mutant = mutate_for_zones(mutant, statuses_available, zone_mutpb, rand)

return mutant

Expand Down Expand Up @@ -615,3 +627,45 @@ def update_resource_borders_to_peak_values(ind: ChromosomeType, schedule: Schedu
actual_borders[index] = contractor_res_schedule.max(axis=0)
ind[2][:] = actual_borders
return ind


def mate_for_zones(ind1: ChromosomeType, ind2: ChromosomeType,
rand: random.Random, copy: bool = True) -> tuple[ChromosomeType, ChromosomeType]:
"""
CxOnePoint for zones

:param ind1: first individual
:param ind2: second individual
:param rand: the rand object used for exchange point selection
:return: first and second individual
"""
child1, child2 = (Individual(copy_chromosome(ind1)), Individual(copy_chromosome(ind2))) if copy else (ind1, ind2)

res1 = child1[4]
res2 = child2[4]
num_works = len(res1)
border = num_works // 4
cxpoint = rand.randint(border, num_works - border)

mate_positions = rand.sample(range(num_works), cxpoint)

res1[mate_positions], res2[mate_positions] = res2[mate_positions], res1[mate_positions]
return child1, child2


def mutate_for_zones(ind: ChromosomeType, statuses_available: int,
mutpb: float, rand: random.Random) -> ChromosomeType:
"""
Mutation function for zones.
It changes selected numbers of zones in random work in a certain interval from available statuses.

:return: mutate individual
"""
# select random number from interval from min to max from uniform distribution
res = ind[4]
for i, work_post_zones in enumerate(res):
for type_of_zone in range(len(res[0])):
if rand.random() < mutpb:
work_post_zones[type_of_zone] = rand.randint(0, statuses_available - 1)

return ind
Loading