Skip to content

Commit

Permalink
The zone introduction (#54)
Browse files Browse the repository at this point in the history
The great update that introduces zones, zone statuses and management.

- added ZoneReq to WorkUnit as new type of req
- added Zone to represent zone with status
- added ZoneTransition to save information about statuses' changes
- added ZoneTimeline to manage zone statuses through the time
- integrated zone accounting to all timelines as an additional constraint
- added zone operators to Genetic
- added AccessCards e.g. ZoneTransitions to the Gant chart visualization

Now we are ready to do zone planning.
  • Loading branch information
StannisMod authored Oct 17, 2023
1 parent ec9a358 commit c6602b1
Show file tree
Hide file tree
Showing 27 changed files with 799 additions and 155 deletions.
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

0 comments on commit c6602b1

Please sign in to comment.