diff --git a/sampo/generator/pipeline/project.py b/sampo/generator/pipeline/project.py index 78985b34..c9424c8a 100644 --- a/sampo/generator/pipeline/project.py +++ b/sampo/generator/pipeline/project.py @@ -6,7 +6,7 @@ MAX_BOREHOLES_PER_BLOCK, BRANCHING_PROBABILITY from sampo.generator.pipeline.cluster import get_cluster_works, _add_addition_work from sampo.generator.pipeline.types import SyntheticGraphType, StageType -from sampo.generator.utils.graph_node_operations import count_node_ancestors +from sampo.generator.utils.graph_node_operations import count_ancestors from sampo.schemas.graph import GraphNode, WorkGraph, EdgeType from sampo.schemas.utils import uuid_str from sampo.schemas.works import WorkUnit @@ -109,18 +109,16 @@ def get_graph(mode: SyntheticGraphType | None = SyntheticGraphType.GENERAL, root_stage = get_root_stage(stages, branching_probability, rand) checkpoints, roads = _get_cluster_graph(root_stage, f'{cluster_name_prefix}{masters_clusters_ind}', - addition_cluster_probability=addition_cluster_probability, rand=rand) - tmp_finish = get_finish_stage(checkpoints) - count_works = count_node_ancestors(tmp_finish, root_stage) + addition_cluster_probability=addition_cluster_probability, rand=rand) - if 0 < top_border < (count_works + works_generated): - break + count_works = count_ancestors(checkpoints, root_stage) stages += [(c, roads) for c in checkpoints] masters_clusters_ind += 1 works_generated += count_works - if 0 < bottom_border <= works_generated or 0 < cluster_counts <= (len(stages) - 1): + if (0 < bottom_border <= works_generated or top_border < (count_works + works_generated) + or 0 < cluster_counts <= (len(stages) - 1)): break if len(stages) == 1: diff --git a/sampo/generator/utils/graph_node_operations.py b/sampo/generator/utils/graph_node_operations.py index 847503b8..0903fb2b 100644 --- a/sampo/generator/utils/graph_node_operations.py +++ b/sampo/generator/utils/graph_node_operations.py @@ -1,28 +1,25 @@ -import queue - from sampo.schemas.graph import GraphNode -def count_node_ancestors(finish: GraphNode, root: GraphNode) -> int: +def count_ancestors(first_ancestors: list[GraphNode], root: GraphNode) -> int: """ Counts the number of ancestors of the whole graph. - :param finish: The node for which ancestors are to be counted. + :param first_ancestors: First ancestors of ancestors which must be counted. :param root: The root node of the graph. :return: """ - q = queue.Queue() - count = 0 + q = list(first_ancestors) + count = len(first_ancestors) used = set() used.add(root) - q.put(finish) - while not q.empty(): - node = q.get() + while q: + node = q.pop() for parent in node.parents: if parent in used: continue used.add(parent) - q.put(parent) + q.insert(0, parent) count += 1 return count diff --git a/sampo/scheduler/genetic/base.py b/sampo/scheduler/genetic/base.py index 29490596..37d9e91f 100644 --- a/sampo/scheduler/genetic/base.py +++ b/sampo/scheduler/genetic/base.py @@ -4,6 +4,7 @@ from sampo.scheduler.base import Scheduler, SchedulerType from sampo.scheduler.genetic.operators import FitnessFunction, TimeFitness from sampo.scheduler.genetic.schedule_builder import build_schedule +from sampo.scheduler.genetic.converter import ChromosomeType from sampo.scheduler.heft.base import HEFTScheduler, HEFTBetweenScheduler from sampo.scheduler.heft.prioritization import prioritization from sampo.scheduler.resource.average_req import AverageReqResourceOptimizer @@ -38,7 +39,7 @@ def __init__(self, seed: Optional[float or None] = None, n_cpu: int = 1, weights: list[int] = None, - fitness_constructor: Callable[[Time | None], FitnessFunction] = TimeFitness, + fitness_constructor: Callable[[Callable[[list[ChromosomeType]], list[Schedule]]], FitnessFunction] = TimeFitness, scheduler_type: SchedulerType = SchedulerType.Genetic, resource_optimizer: ResourceOptimizer = IdentityResourceOptimizer(), work_estimator: WorkTimeEstimator = DefaultWorkEstimator(), @@ -214,7 +215,6 @@ def schedule_with_cache(self, 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 scheduled_works, schedule_start_time, timeline, order_nodes = build_schedule(wg, @@ -229,7 +229,7 @@ def schedule_with_cache(self, self.rand, spec, landscape, - fitness_object, + self.fitness_constructor, self.work_estimator, self._n_cpu, assigned_parent_time, diff --git a/sampo/scheduler/genetic/converter.py b/sampo/scheduler/genetic/converter.py index 917d5173..77f91d8d 100644 --- a/sampo/scheduler/genetic/converter.py +++ b/sampo/scheduler/genetic/converter.py @@ -18,8 +18,7 @@ ChromosomeType = tuple[np.ndarray, np.ndarray, np.ndarray, ScheduleSpec, np.ndarray] -def convert_schedule_to_chromosome(wg: WorkGraph, - work_id2index: dict[str, int], +def convert_schedule_to_chromosome(work_id2index: dict[str, int], worker_name2index: dict[str, int], contractor2index: dict[str, int], contractor_borders: np.ndarray, @@ -30,7 +29,6 @@ def convert_schedule_to_chromosome(wg: WorkGraph, """ Receive a result of scheduling algorithm and transform it to chromosome - :param wg: :param work_id2index: :param worker_name2index: :param contractor2index: @@ -43,7 +41,7 @@ def convert_schedule_to_chromosome(wg: WorkGraph, """ order: list[GraphNode] = order if order is not None else [work for work in schedule.works - if not wg[work.work_unit.id].is_inseparable_son()] + if work.work_unit.id in work_id2index] # order works part of chromosome order_chromosome: np.ndarray = np.array([work_id2index[work.work_unit.id] for work in order]) diff --git a/sampo/scheduler/genetic/operators.py b/sampo/scheduler/genetic/operators.py index 9ecd7a9c..540be718 100644 --- a/sampo/scheduler/genetic/operators.py +++ b/sampo/scheduler/genetic/operators.py @@ -5,7 +5,6 @@ from functools import partial from operator import attrgetter from typing import Iterable, Callable -from enum import Enum import numpy as np from deap import creator, base @@ -23,7 +22,6 @@ from sampo.schemas.time import Time from sampo.schemas.time_estimator import WorkTimeEstimator, DefaultWorkEstimator from sampo.utilities.resource_cost import schedule_cost -from sortedcontainers import SortedList # logger = mp.log_to_stderr(logging.DEBUG) @@ -34,13 +32,13 @@ class FitnessFunction(ABC): Base class for description of different fitness functions. """ - def __init__(self, deadline: Time | None): - self._deadline = deadline + def __init__(self, evaluator: Callable[[list[ChromosomeType]], list[Schedule]]): + self._evaluator = evaluator @abstractmethod - def evaluate(self, schedules: list[Schedule]) -> list[int]: + def evaluate(self, chromosomes: list[ChromosomeType]) -> list[int]: """ - Calculate the value of fitness function of the all schedules. + Calculate the value of fitness function of the all chromosomes. It is better when value is less. """ ... @@ -51,23 +49,37 @@ class TimeFitness(FitnessFunction): Fitness function that relies on finish time. """ - def __init__(self, deadline: Time | None = None): - super().__init__(deadline) + def __init__(self, evaluator: Callable[[list[ChromosomeType]], list[Schedule]]): + super().__init__(evaluator) + + def evaluate(self, chromosomes: list[ChromosomeType]) -> list[int]: + return [schedule.execution_time.value for schedule in self._evaluator(chromosomes)] + + +class ResourcesFitness(FitnessFunction): + """ + Fitness function that relies on resource peak usage. + """ + + def __init__(self, evaluator: Callable[[list[ChromosomeType]], list[Schedule]]): + super().__init__(evaluator) - def evaluate(self, schedules: list[Schedule]) -> list[int]: - return [schedule.execution_time.value for schedule in schedules] + def evaluate(self, chromosomes: list[ChromosomeType]) -> list[int]: + evaluated = self._evaluator(chromosomes) + return [get_absolute_peak_resource_usage(schedule) for schedule in evaluated] -class TimeAndResourcesFitness(FitnessFunction): +class TimeWithResourcesFitness(FitnessFunction): """ Fitness function that relies on finish time and the set of resources. """ - def __init__(self, deadline: Time | None = None): - super().__init__(deadline) + def __init__(self, evaluator: Callable[[list[ChromosomeType]], list[Schedule]]): + super().__init__(evaluator) - def evaluate(self, schedules: list[Schedule]) -> list[int]: - return [schedule.execution_time.value + get_absolute_peak_resource_usage(schedule) for schedule in schedules] + def evaluate(self, chromosomes: list[ChromosomeType]) -> list[int]: + evaluated = self._evaluator(chromosomes) + return [schedule.execution_time.value + get_absolute_peak_resource_usage(schedule) for schedule in evaluated] class DeadlineResourcesFitness(FitnessFunction): @@ -75,13 +87,22 @@ class DeadlineResourcesFitness(FitnessFunction): The fitness function is dependent on the set of resources and requires the end time to meet the deadline. """ - def __init__(self, deadline: Time): - super().__init__(deadline) + def __init__(self, deadline: Time, evaluator: Callable[[list[ChromosomeType]], list[Schedule]]): + super().__init__(evaluator) + self._deadline = deadline - def evaluate(self, schedules: list[Schedule]) -> list[int]: + @staticmethod + def prepare(deadline: Time): + """ + Returns the constructor of that fitness function prepared to use in Genetic + """ + return partial(DeadlineResourcesFitness, deadline) + + def evaluate(self, chromosomes: list[ChromosomeType]) -> list[int]: + evaluated = self._evaluator(chromosomes) return [int(get_absolute_peak_resource_usage(schedule) * max(1.0, schedule.execution_time.value / self._deadline.value)) - for schedule in schedules] + for schedule in evaluated] class DeadlineCostFitness(FitnessFunction): @@ -89,13 +110,22 @@ class DeadlineCostFitness(FitnessFunction): The fitness function is dependent on the cost of resources and requires the end time to meet the deadline. """ - def __init__(self, deadline: Time): - super().__init__(deadline) + def __init__(self, deadline: Time, evaluator: Callable[[list[ChromosomeType]], list[Schedule]]): + super().__init__(evaluator) + self._deadline = deadline + + @staticmethod + def prepare(deadline: Time): + """ + Returns the constructor of that fitness function prepared to use in Genetic + """ + return partial(DeadlineCostFitness, deadline) - def evaluate(self, schedules: list[Schedule]) -> list[int]: + def evaluate(self, chromosomes: list[ChromosomeType]) -> list[int]: + evaluated = self._evaluator(chromosomes) # TODO Integrate cost calculation to native module return [int(schedule_cost(schedule) * max(1.0, schedule.execution_time.value / self._deadline.value)) - for schedule in schedules] + for schedule in evaluated] # create class FitnessMin, the weights = -1 means that fitness - is function for minimum @@ -105,14 +135,6 @@ def evaluate(self, schedules: list[Schedule]) -> list[int]: Individual = creator.Individual -class IndividualType(Enum): - """ - Class to define a type of individual in genetic algorithm - """ - Population = 'population' - Offspring = 'offspring' - - def init_toolbox(wg: WorkGraph, contractors: list[Contractor], worker_pool: WorkerContractorPool, @@ -135,6 +157,7 @@ def init_toolbox(wg: WorkGraph, contractor_borders: np.ndarray, node_indices: list[int], parents: dict[int, set[int]], + children: dict[int, set[int]], resources_border: np.ndarray, assigned_parent_time: Time = Time(0), work_estimator: WorkTimeEstimator = DefaultWorkEstimator()) -> base.Toolbox: @@ -163,11 +186,13 @@ def init_toolbox(wg: WorkGraph, toolbox.register('mate', mate, rand=rand) # combined mutation 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) + rand=rand, parents=parents, children=children, resources_border=resources_border, + statuses_available=statuses_available) # crossover for order toolbox.register('mate_order', mate_scheduling_order, rand=rand) # mutation for order - toolbox.register('mutate_order', mutate_scheduling_order, mutpb=mut_order_pb, rand=rand, parents=parents) + toolbox.register('mutate_order', mutate_scheduling_order, mutpb=mut_order_pb, rand=rand, parents=parents, + children=children) # crossover for resources toolbox.register('mate_resources', mate_resources, rand=rand) # mutation for resources @@ -182,7 +207,7 @@ def init_toolbox(wg: WorkGraph, 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, + toolbox.register('schedule_to_chromosome', convert_schedule_to_chromosome, work_id2index=work_id2index, worker_name2index=worker_name2index, contractor2index=contractor2index, contractor_borders=contractor_borders, spec=spec, landscape=landscape) @@ -193,8 +218,7 @@ def init_toolbox(wg: WorkGraph, 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, - worker_name2index=worker_name2index, contractor2index=contractor2index) + return toolbox @@ -223,7 +247,7 @@ def generate_population(n: int, 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, + return convert_schedule_to_chromosome(work_id2index, worker_name2index, contractor2index, contractor_borders, schedule, spec, landscape) count_for_specified_types = (n // 3) // len(init_chromosomes) @@ -271,7 +295,7 @@ def randomized_init() -> ChromosomeType: schedule = RandomizedTopologicalScheduler(work_estimator, int(rand.random() * 1000000)) \ .schedule(wg, contractors, spec, landscape=landscape) - return convert_schedule_to_chromosome(wg, work_id2index, worker_name2index, + return convert_schedule_to_chromosome(work_id2index, worker_name2index, contractor2index, contractor_borders, schedule, spec, landscape) chance = rand.random() @@ -393,32 +417,44 @@ def mate_scheduling_order(ind1: ChromosomeType, ind2: ChromosomeType, rand: rand def mutate_scheduling_order(ind: ChromosomeType, mutpb: float, rand: random.Random, - parents: dict[int, set[int]]) -> ChromosomeType: + parents: dict[int, set[int]], children: dict[int, set[int]]) -> ChromosomeType: """ - Mutation operator for order. - Swap neighbors. + Mutation operator for works scheduling order. :param ind: the individual to be mutated :param mutpb: probability of gene mutation :param rand: the rand object used for randomized operations :param parents: mapping object of works and their parent-works to create valid order + :param children: mapping object of works and their children-works to create valid order :return: mutated individual """ order = ind[0] - num_possible_muts = len(order) - 3 + num_possible_muts = len(order) - 2 mask = np.array([rand.random() < mutpb for _ in range(num_possible_muts)]) if mask.any(): - indexes_to_mutate = [rand.randint(1, num_possible_muts + 1) for _ in range(mask.sum())] - for i in indexes_to_mutate: - if order[i] not in parents[order[i + 1]]: - order[i], order[i + 1] = order[i + 1], order[i] + indexes_of_works_to_mutate = np.where(mask)[0] + 1 + rand.shuffle(indexes_of_works_to_mutate) + works_to_mutate = order[indexes_of_works_to_mutate] + for work in works_to_mutate: + i, indexes_of_works_to_mutate = indexes_of_works_to_mutate[0], indexes_of_works_to_mutate[1:] + i_parent = np.max(np.where(np.isin(order[:i], list(parents[work]), assume_unique=True))[0]) + 1 + i_children = np.min(np.where(np.isin(order[i + 1:], list(children[work]), assume_unique=True))[0]) + i + if i_parent == i_children: + continue + else: + indexes_of_works_to_mutate[indexes_of_works_to_mutate > i] -= 1 + choices = np.concatenate((np.arange(i_parent, i), np.arange(i + 1, i_children + 1))) + weights = 1 / np.abs(choices - i) + new_i = rand.choices(choices, weights=weights)[0] + order[:] = np.insert(np.delete(order, i), new_i, work) + indexes_of_works_to_mutate[indexes_of_works_to_mutate >= new_i] += 1 return ind -def mate_resources(ind1: ChromosomeType, ind2: ChromosomeType, optimize_resources: bool, - rand: random.Random, copy: bool = True) -> tuple[ChromosomeType, ChromosomeType]: +def mate_resources(ind1: ChromosomeType, ind2: ChromosomeType, rand: random.Random, + optimize_resources: bool, copy: bool = True) -> tuple[ChromosomeType, ChromosomeType]: """ One-Point crossover for resources. @@ -499,7 +535,8 @@ def mutate_resources(ind: ChromosomeType, mutpb: float, rand: random.Random, def mate(ind1: ChromosomeType, ind2: ChromosomeType, optimize_resources: bool, rand: random.Random) \ -> tuple[ChromosomeType, ChromosomeType]: """ - Combined crossover function of Two-Point crossover for order and One-Point crossover for resources. + Combined crossover function of Two-Point crossover for order, One-Point crossover for resources + and One-Point crossover for zones. :param ind1: first individual :param ind2: second individual @@ -509,30 +546,34 @@ def mate(ind1: ChromosomeType, ind2: ChromosomeType, optimize_resources: bool, r :return: two mated individuals """ child1, child2 = mate_scheduling_order(ind1, ind2, rand, copy=True) - child1, child2 = mate_resources(child1, child2, optimize_resources, rand, copy=False) + child1, child2 = mate_resources(child1, child2, rand, optimize_resources, 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, zone_mutpb: float, statuses_available: int, + children: dict[int, set[int]], statuses_available: int, + order_mutpb: float, res_mutpb: float, zone_mutpb: float, rand: random.Random) -> ChromosomeType: """ - Combined mutation function of mutation for order and mutation for resources. + Combined mutation function of mutation for order, mutation for resources and mutation for zones. :param ind: the individual to be mutated :param resources_border: low and up borders of resources amounts :param parents: mapping object of works and their parent-works to create valid order + :param children: mapping object of works and their children-works to create valid order + :param statuses_available: number of statuses available :param order_mutpb: probability of order's gene mutation :param res_mutpb: probability of resources' gene mutation + :param zone_mutpb: probability of zones' gene mutation :param rand: the rand object used for randomized operations :return: mutated individual """ - mutant = mutate_scheduling_order(ind, order_mutpb, rand, parents) + mutant = mutate_scheduling_order(ind, order_mutpb, rand, parents, children) mutant = mutate_resources(mutant, res_mutpb, rand, resources_border) - mutant = mutate_for_zones(mutant, statuses_available, zone_mutpb, rand) + mutant = mutate_for_zones(mutant, zone_mutpb, rand, statuses_available) return mutant @@ -540,7 +581,7 @@ def mutate(ind: ChromosomeType, resources_border: np.ndarray, parents: dict[int, def mutate_resource_borders(ind: ChromosomeType, mutpb: float, rand: random.Random, contractor_borders: np.ndarray) -> ChromosomeType: """ - Mutation for contractors' resource borders. + Mutation function for contractors' resource borders. :param ind: the individual to be mutated :param contractor_borders: up borders of contractors capacity @@ -587,48 +628,10 @@ def mutate_values(chromosome_part: np.ndarray, row_indexes: np.ndarray, col_inde l_borders[row_mask], u_borders[row_mask]): choices = np.concatenate((np.arange(l_border, current_amount), np.arange(current_amount + 1, u_border + 1))) - weights = 1 / abs(choices - current_amount) + weights = 1 / np.abs(choices - current_amount) cur_row[col_index] = rand.choices(choices, weights=weights)[0] -def update_resource_borders_to_peak_values(ind: ChromosomeType, schedule: Schedule, worker_name2index: dict[str, int], - contractor2index: dict[str, int]): - """ - Changes the resource borders to the peak values obtained in the schedule. - - :param ind: the individual to be updated - :param schedule: schedule obtained from the individual - :param worker_name2index: mapping object of resources and their index in chromosome - :param contractor2index: mapping object of contractors and their index in chromosome - - :return: individual with updated resource borders - """ - df = schedule.full_schedule_df - contractors = set(df.contractor) - actual_borders = np.zeros_like(ind[2]) - for contractor in contractors: - contractor_df = df[df.contractor == contractor] - points = contractor_df[['start', 'finish']].to_numpy().copy() - points[:, 1] += 1 - points = SortedList(set(points.flatten())) - contractor_res_schedule = np.zeros((len(points), len(worker_name2index))) - contractor_id = '' - for _, r in contractor_df.iterrows(): - start = points.bisect_left(r['start']) - finish = points.bisect_left(r['finish'] + 1) - swork = r['scheduled_work_object'] - workers = np.array([[worker_name2index[worker.name], worker.count] for worker in swork.workers]) - if len(workers): - contractor_res_schedule[start: finish, workers[:, 0]] += workers[:, 1] - if not contractor_id: - contractor_id = swork.workers[0].contractor_id - if contractor_id: - index = contractor2index[contractor_id] - 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]: """ @@ -636,36 +639,44 @@ def mate_for_zones(ind1: ChromosomeType, ind2: ChromosomeType, :param ind1: first individual :param ind2: second individual - :param rand: the rand object used for exchange point selection - :return: first and second individual + :param rand: the rand object used for randomized operations + :param copy: if True individuals will be copied before mating so as not to change them + + :return: two mated individuals """ 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) + zones1 = child1[4] + zones2 = child2[4] + if zones1.size: + num_works = len(zones1) + border = num_works // 4 + cxpoint = rand.randint(border, num_works - border) - mate_positions = rand.sample(range(num_works), cxpoint) + mate_positions = rand.sample(range(num_works), cxpoint) + + zones1[mate_positions], zones2[mate_positions] = zones2[mate_positions], zones1[mate_positions] - 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: +def mutate_for_zones(ind: ChromosomeType, mutpb: float, rand: random.Random, statuses_available: int) -> ChromosomeType: """ Mutation function for zones. It changes selected numbers of zones in random work in a certain interval from available statuses. - :return: mutate individual + :param ind: the individual to be mutated + :param mutpb: probability of gene mutation + :param rand: the rand object used for randomized operations + :param statuses_available: number of statuses available + + :return: mutated 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) + zones = ind[4] + if zones.size: + mask = np.array([[rand.random() < mutpb for _ in range(zones.shape[1])] for _ in range(zones.shape[0])]) + new_zones = np.array([rand.randint(0, statuses_available - 1) for _ in range(mask.sum())]) + zones[mask] = new_zones return ind diff --git a/sampo/scheduler/genetic/schedule_builder.py b/sampo/scheduler/genetic/schedule_builder.py index 1eac4c26..0b2b2325 100644 --- a/sampo/scheduler/genetic/schedule_builder.py +++ b/sampo/scheduler/genetic/schedule_builder.py @@ -1,12 +1,14 @@ import random import time +from typing import Callable import numpy as np from deap import tools from deap.base import Toolbox from sampo.scheduler.genetic.converter import convert_schedule_to_chromosome -from sampo.scheduler.genetic.operators import init_toolbox, ChromosomeType, IndividualType, FitnessFunction, TimeFitness +from sampo.scheduler.genetic.operators import (init_toolbox, ChromosomeType, FitnessFunction, TimeFitness, + ResourcesFitness) from sampo.scheduler.native_wrapper import NativeWrapper from sampo.scheduler.timeline.base import Timeline from sampo.schemas.contractor import Contractor, WorkerContractorPool @@ -16,7 +18,6 @@ from sampo.schemas.schedule_spec import ScheduleSpec from sampo.schemas.time import Time from sampo.schemas.time_estimator import WorkTimeEstimator, DefaultWorkEstimator -from sampo.scheduler.utils.peaks import get_absolute_peak_resource_usage from sampo.schemas.resources import Worker @@ -27,7 +28,8 @@ def create_toolbox_and_mapping_objects(wg: WorkGraph, mutate_order: float, mutate_resources: float, mutate_zones: float, - init_schedules: dict[str, tuple[Schedule, list[GraphNode] | None, ScheduleSpec, float]], + init_schedules: dict[ + str, tuple[Schedule, list[GraphNode] | None, ScheduleSpec, float]], rand: random.Random, spec: ScheduleSpec = ScheduleSpec(), work_estimator: WorkTimeEstimator = None, @@ -58,9 +60,9 @@ def create_toolbox_and_mapping_objects(wg: WorkGraph, inseparable_parents[child] = node # here we aggregate information about relationships from the whole inseparable chain - children = {work_id2index[node.id]: [work_id2index[inseparable_parents[child].id] - for inseparable in node.get_inseparable_chain_with_self() - for child in inseparable.children] + children = {work_id2index[node.id]: set([work_id2index[inseparable_parents[child].id] + for inseparable in node.get_inseparable_chain_with_self() + for child in inseparable.children]) for node in nodes} parents = {work_id2index[node.id]: set() for node in nodes} @@ -81,7 +83,7 @@ def create_toolbox_and_mapping_objects(wg: WorkGraph, contractor_borders[ind, ind_worker] = worker.count init_chromosomes: dict[str, tuple[ChromosomeType, float, ScheduleSpec]] = \ - {name: (convert_schedule_to_chromosome(wg, work_id2index, worker_name2index, + {name: (convert_schedule_to_chromosome(work_id2index, worker_name2index, contractor2index, contractor_borders, schedule, chromosome_spec, landscape, order), importance, chromosome_spec) @@ -113,6 +115,7 @@ def create_toolbox_and_mapping_objects(wg: WorkGraph, contractor_borders, node_indices, parents, + children, resources_border, assigned_parent_time, work_estimator), worker_name2index, worker_pool_indices, parents @@ -130,7 +133,8 @@ def build_schedule(wg: WorkGraph, rand: random.Random, spec: ScheduleSpec, landscape: LandscapeConfiguration = LandscapeConfiguration(), - fitness_object: FitnessFunction = TimeFitness(), + fitness_constructor: Callable[ + [Callable[[list[ChromosomeType]], list[Schedule]]], FitnessFunction] = TimeFitness, work_estimator: WorkTimeEstimator = DefaultWorkEstimator(), n_cpu: int = 1, assigned_parent_time: Time = Time(0), @@ -183,20 +187,18 @@ def build_schedule(wg: WorkGraph, # save best individuals hof = tools.HallOfFame(1, similar=compare_individuals) + fitness_f = fitness_constructor(native.evaluate) + evaluation_start = time.time() # map to each individual fitness function pop = [ind for ind in pop if toolbox.validate(ind)] - schedules = native.evaluate(pop) - fitness = fitness_object.evaluate(schedules) + fitness = fitness_f.evaluate(pop) evaluation_time = time.time() - evaluation_start - for ind, fit, schedule in zip(pop, fitness, schedules): + for ind, fit in zip(pop, fitness): ind.fitness.values = [fit] - if optimize_resources: - toolbox.update_resource_borders_to_peak_values(ind, schedule) - ind.type = IndividualType.Population hof.update(pop) best_fitness = hof[0].fitness.values[0] @@ -233,26 +235,16 @@ def build_schedule(wg: WorkGraph, evaluation_start = time.time() - schedules = native.evaluate(offspring) - offspring_fitness = fitness_object.evaluate(schedules) + offspring_fitness = fitness_f.evaluate(offspring) - for ind, fit, schedule in zip(offspring, offspring_fitness, schedules): + for ind, fit in zip(offspring, offspring_fitness): ind.fitness.values = [fit] - if optimize_resources: - ind.schedule = schedule - ind.type = IndividualType.Offspring evaluation_time += time.time() - evaluation_start # renewing population pop += offspring pop = toolbox.select(pop) - if optimize_resources: - for ind in pop: - if ind.type is IndividualType.Offspring: - toolbox.update_resource_borders_to_peak_values(ind, ind.schedule) - del ind.schedule - ind.type = IndividualType.Population hof.update([pop[0]]) prev_best_fitness = best_fitness @@ -271,6 +263,9 @@ def build_schedule(wg: WorkGraph, # Second stage to optimize resources if deadline is assigned if have_deadline: + + fitness_resource = ResourcesFitness(native.evaluate) + if best_fitness > deadline: print(f'Deadline not reached !!! Deadline {deadline} < best time {best_fitness}') # save best individuals @@ -279,10 +274,10 @@ def build_schedule(wg: WorkGraph, evaluation_start = time.time() - fitness = [get_absolute_peak_resource_usage(schedule) for schedule in native.evaluate(pop)] - for ind, fit, schedule in zip(pop, fitness, schedules): + fitness = fitness_resource.evaluate(pop) + for ind, res_peak in zip(pop, fitness): ind.time = ind.fitness.values[0] - ind.fitness.values = [fit] + ind.fitness.values = [res_peak] evaluation_time += time.time() - evaluation_start @@ -296,13 +291,10 @@ def build_schedule(wg: WorkGraph, evaluation_start = time.time() - schedules = native.evaluate(pop) - fitness = [get_absolute_peak_resource_usage(schedule) for schedule in schedules] - for ind, fit, schedule in zip(pop, fitness, schedules): + fitness = fitness_resource.evaluate(pop) + for ind, res_peak in zip(pop, fitness): ind.time = ind.fitness.values[0] - ind.fitness.values = [fit] - toolbox.update_resource_borders_to_peak_values(ind, schedule) - ind.type = IndividualType.Population + ind.fitness.values = [res_peak] evaluation_time += time.time() - evaluation_start @@ -319,7 +311,6 @@ def build_schedule(wg: WorkGraph, for copied_ind, ind in zip(copied_individuals, individuals_to_copy): copied_ind.fitness.values = [ind.fitness.values[0]] copied_ind.time = ind.time - copied_ind.type = ind.type pop += copied_individuals while generation <= generation_number and plateau_steps < max_plateau_steps \ @@ -342,27 +333,23 @@ def build_schedule(wg: WorkGraph, evaluation_start = time.time() - schedules = [schedule for schedule in native.evaluate(offspring)] + fitness = fitness_f.evaluate(offspring) - for ind, schedule in zip(offspring, schedules): - ind.time = schedule.execution_time.value - if ind.time <= deadline: - ind.fitness.values = [get_absolute_peak_resource_usage(schedule)] - ind.type = IndividualType.Offspring - ind.schedule = schedule + for ind, t in zip(offspring, fitness): + ind.time = t offspring = [ind for ind in offspring if ind.time <= deadline] + fitness_res = fitness_resource.evaluate(offspring) + + for ind, res_peak in zip(offspring, fitness_res): + ind.fitness.values = [res_peak] + evaluation_time += time.time() - evaluation_start # renewing population pop += offspring pop = toolbox.select(pop) - for ind in pop: - if ind.type is IndividualType.Offspring: - toolbox.update_resource_borders_to_peak_values(ind, ind.schedule) - del ind.schedule - ind.type = IndividualType.Population hof.update([pop[0]]) prev_best_fitness = best_fitness diff --git a/tests/scheduler/genetic/converter_test.py b/tests/scheduler/genetic/converter_test.py index 9312289c..06310893 100644 --- a/tests/scheduler/genetic/converter_test.py +++ b/tests/scheduler/genetic/converter_test.py @@ -54,14 +54,3 @@ def test_convert_chromosome_to_schedule(setup_toolbox): # schedule = Schedule.from_scheduled_works(schedule.values(), setup_wg) # # validate_schedule(schedule, setup_wg, contractors) - - -def test_converter_with_borders_update(setup_toolbox): - tb, _, setup_wg, setup_contractors, _, setup_landscape_many_holders = setup_toolbox - chromosome = tb.generate_chromosome(landscape=setup_landscape_many_holders) - schedule, _, _, _ = tb.chromosome_to_schedule(chromosome, landscape=setup_landscape_many_holders) - schedule = Schedule.from_scheduled_works(schedule.values(), setup_wg) - updated_chromosome = tb.update_resource_borders_to_peak_values(chromosome, schedule) - updated_schedule, _, _, _ = tb.chromosome_to_schedule(updated_chromosome, landscape=setup_landscape_many_holders) - updated_schedule = Schedule.from_scheduled_works(updated_schedule.values(), setup_wg) - assert schedule.execution_time == updated_schedule.execution_time diff --git a/tests/scheduler/resources_in_time/basic_res_test.py b/tests/scheduler/resources_in_time/basic_res_test.py index 687e2dd2..f2570c7f 100644 --- a/tests/scheduler/resources_in_time/basic_res_test.py +++ b/tests/scheduler/resources_in_time/basic_res_test.py @@ -39,7 +39,7 @@ def test_genetic_deadline_planning(setup_scheduler_parameters): mutate_order=0.05, mutate_resources=0.005, size_of_population=50, - fitness_constructor=DeadlineResourcesFitness, + fitness_constructor=DeadlineResourcesFitness.prepare(deadline), optimize_resources=True, verbose=False) @@ -92,7 +92,7 @@ def test_lexicographic_genetic_deadline_planning(setup_scheduler_parameters): mutate_order=0.05, mutate_resources=0.005, size_of_population=50, - fitness_constructor=DeadlineResourcesFitness, + fitness_constructor=DeadlineResourcesFitness.prepare(deadline), optimize_resources=True, verbose=False)