Skip to content

Commit ab3e103

Browse files
authored
The contractor specification (#96)
Now contractor list can be specified to each work separately through the `ScheduleSpec`
1 parent 2ac7add commit ab3e103

30 files changed

+341
-139
lines changed

examples/contractor_spec.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from random import Random
2+
3+
from sampo.scheduler import GeneticScheduler, TopologicalScheduler, RandomizedTopologicalScheduler, LFTScheduler, \
4+
RandomizedLFTScheduler
5+
from sampo.pipeline.lag_optimization import LagOptimizationStrategy
6+
from sampo.generator.base import SimpleSynthetic
7+
from sampo.generator.environment.contractor_by_wg import get_contractor_by_wg, ContractorGenerationMethod
8+
from sampo.generator import SyntheticGraphType
9+
from sampo.pipeline import SchedulingPipeline
10+
from sampo.scheduler.heft.base import HEFTScheduler
11+
from sampo.schemas.schedule_spec import ScheduleSpec
12+
13+
ss = SimpleSynthetic(rand=231)
14+
rand = Random()
15+
size = 100
16+
17+
wg = ss.work_graph(bottom_border=size - 5, top_border=size)
18+
19+
contractors = [get_contractor_by_wg(wg) for _ in range(10)]
20+
spec = ScheduleSpec()
21+
22+
for node in wg.nodes:
23+
if not node.is_inseparable_son():
24+
selected_contractor_indices = rand.choices(list(range(len(contractors))),
25+
k=rand.randint(1, len(contractors)))
26+
spec.assign_contractors(node.id, {contractors[i].id for i in selected_contractor_indices})
27+
28+
29+
scheduler = GeneticScheduler(number_of_generation=10)
30+
31+
project = SchedulingPipeline.create() \
32+
.wg(wg) \
33+
.contractors(contractors) \
34+
.lag_optimize(LagOptimizationStrategy.TRUE) \
35+
.spec(spec) \
36+
.schedule(scheduler, validate=True) \
37+
.visualization('2022-01-01')[0] \
38+
.shape((14, 14)) \
39+
.show_gant_chart()

sampo/backend/default.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def _ensure_toolbox_created(self):
6262
from sampo.scheduler.genetic.utils import init_chromosomes_f, create_toolbox_using_cached_chromosomes
6363

6464
if self._init_schedules:
65-
init_chromosomes = init_chromosomes_f(self._wg, self._contractors, self._init_schedules,
66-
self._landscape)
65+
init_chromosomes = init_chromosomes_f(self._wg, self._contractors, self._spec,
66+
self._init_schedules, self._landscape)
6767
else:
6868
init_chromosomes = []
6969

sampo/backend/multiproc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ def cache_genetic_info(self,
140140
weights, init_schedules, assigned_parent_time, fitness_weights, sgs_type,
141141
only_lft_initialization, is_multiobjective)
142142
if init_schedules:
143-
self._init_chromosomes = init_chromosomes_f(self._wg, self._contractors, init_schedules,
144-
self._landscape)
143+
self._init_chromosomes = init_chromosomes_f(self._wg, self._contractors, self._spec,
144+
init_schedules, self._landscape)
145145
else:
146146
self._init_chromosomes = []
147147
self._pool = None

sampo/generator/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ def __init__(self, rand: int | Random | None = None) -> None:
2020
else:
2121
self._rand = Random(rand)
2222

23+
def get_rand(self):
24+
return self._rand
25+
2326
def small_work_graph(self, cluster_name: str | None = 'C1') -> WorkGraph:
2427
"""
2528
Creates a small graph of works consisting of 30-50 vertices;

sampo/scheduler/generic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def run_with_contractor(contractor: Contractor) -> tuple[Time, Time, list[Worker
9999
assigned_parent_time, work_estimator)
100100
return c_st, c_ft, workers
101101

102-
return run_contractor_search(contractors, run_with_contractor)
102+
return run_contractor_search(contractors, spec, run_with_contractor)
103103

104104
return optimize_resources_def
105105

@@ -126,7 +126,7 @@ def schedule_with_cache(self,
126126
)
127127

128128
if validate:
129-
validate_schedule(schedule, wg, contractors)
129+
validate_schedule(schedule, wg, contractors, spec)
130130

131131
return [(schedule, schedule_start_time, timeline, ordered_nodes)]
132132

sampo/scheduler/genetic/base.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,15 @@ def generate_first_population(wg: WorkGraph,
171171

172172
schedule, _, _, node_order = LFTScheduler(work_estimator=work_estimator).schedule_with_cache(wg, contractors,
173173
spec,
174-
landscape=landscape)[0]
174+
landscape=landscape,
175+
validate=True)[0]
175176
init_lft_schedule = (schedule, node_order, spec)
176177

177178
def init_k_schedule(scheduler_class, k) -> tuple[Schedule | None, list[GraphNode] | None, ScheduleSpec | None]:
178179
try:
179180
schedule, _, _, node_order = (scheduler_class(work_estimator=work_estimator,
180181
resource_optimizer=AverageReqResourceOptimizer(k))
181-
.schedule_with_cache(wg, contractors, spec, landscape=landscape))[0]
182+
.schedule_with_cache(wg, contractors, spec, landscape=landscape, validate=True))[0]
182183
return schedule, node_order, spec
183184
except NoSufficientContractorError:
184185
return None, None, None
@@ -187,7 +188,7 @@ def init_k_schedule(scheduler_class, k) -> tuple[Schedule | None, list[GraphNode
187188
def init_schedule(scheduler_class) -> tuple[Schedule | None, list[GraphNode] | None, ScheduleSpec | None]:
188189
try:
189190
schedule, _, _, node_order = (scheduler_class(work_estimator=work_estimator)
190-
.schedule_with_cache(wg, contractors, spec, landscape=landscape))[0]
191+
.schedule_with_cache(wg, contractors, spec, landscape=landscape, validate=True))[0]
191192
return schedule, node_order, spec
192193
except NoSufficientContractorError:
193194
return None, None, None
@@ -197,7 +198,7 @@ def init_schedule(scheduler_class) -> tuple[Schedule | None, list[GraphNode] | N
197198
try:
198199
(schedule, _, _, node_order), modified_spec = AverageBinarySearchResourceOptimizingScheduler(
199200
scheduler_class(work_estimator=work_estimator)
200-
).schedule_with_cache(wg, contractors, deadline, spec, landscape=landscape)
201+
).schedule_with_cache(wg, contractors, deadline, spec, landscape=landscape, validate=True)
201202
return schedule, node_order, modified_spec
202203
except NoSufficientContractorError:
203204
return None, None, None
@@ -309,6 +310,6 @@ def schedule_with_cache(self,
309310

310311
if validate:
311312
for schedule, *_ in schedules:
312-
validate_schedule(schedule, wg, contractors)
313+
validate_schedule(schedule, wg, contractors, spec)
313314

314315
return schedules

sampo/scheduler/genetic/converter.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from sampo.schemas.schedule_spec import ScheduleSpec
2020
from sampo.schemas.time import Time
2121
from sampo.schemas.time_estimator import WorkTimeEstimator, DefaultWorkEstimator
22+
from sampo.utilities.collections_util import reverse_dictionary
2223
from sampo.utilities.linked_list import LinkedList
2324

2425

@@ -63,16 +64,25 @@ def convert_schedule_to_chromosome(work_id2index: dict[str, int],
6364
for node in order:
6465
node_id = node.id
6566
index = work_id2index[node_id]
66-
for resource in schedule[node_id].workers:
67-
res_count = resource.count
68-
res_index = worker_name2index[resource.name]
69-
res_contractor = resource.contractor_id
70-
resource_chromosome[index, res_index] = res_count
71-
resource_chromosome[index, -1] = contractor2index[res_contractor]
67+
68+
if schedule[node_id].workers:
69+
for resource in schedule[node_id].workers:
70+
res_count = resource.count
71+
res_index = worker_name2index[resource.name]
72+
res_contractor = resource.contractor_id
73+
74+
resource_chromosome[index, res_index] = res_count
75+
resource_chromosome[index, -1] = contractor2index[res_contractor]
76+
else:
77+
contractor_list = spec[node_id].contractors
78+
if not contractor_list:
79+
contractor_list = contractor2index.keys()
80+
random_contractor = list(contractor_list)[0]
81+
resource_chromosome[index, -1] = contractor2index[random_contractor]
7282

7383
resource_border_chromosome = np.copy(contractor_borders)
7484

75-
return order_chromosome, resource_chromosome, resource_border_chromosome, spec, zone_changes_chromosome
85+
return order_chromosome, resource_chromosome, resource_border_chromosome, copy.deepcopy(spec), zone_changes_chromosome
7686

7787

7888
def convert_chromosome_to_schedule(chromosome: ChromosomeType,

sampo/scheduler/genetic/operators.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from sampo.schemas.landscape import LandscapeConfiguration
2121
from sampo.schemas.resources import Worker
2222
from sampo.schemas.schedule import Schedule
23-
from sampo.schemas.schedule_spec import ScheduleSpec
23+
from sampo.schemas.schedule_spec import ScheduleSpec, WorkSpec
2424
from sampo.schemas.time import Time
2525
from sampo.schemas.time_estimator import WorkTimeEstimator, DefaultWorkEstimator
2626
from sampo.utilities.resource_usage import resources_peaks_sum, resources_costs_sum, resources_sum
@@ -165,6 +165,7 @@ def init_toolbox(wg: WorkGraph,
165165
parents: dict[int, set[int]],
166166
children: dict[int, set[int]],
167167
resources_border: np.ndarray,
168+
contractors_available: np.ndarray,
168169
assigned_parent_time: Time = Time(0),
169170
fitness_weights: tuple[int | float, ...] = (-1,),
170171
work_estimator: WorkTimeEstimator = DefaultWorkEstimator(),
@@ -200,7 +201,8 @@ def init_toolbox(wg: WorkGraph,
200201
# combined mutation
201202
toolbox.register('mutate', mutate, order_mutpb=mut_order_pb, res_mutpb=mut_res_pb, zone_mutpb=mut_zone_pb,
202203
rand=rand, parents=parents, children=children, resources_border=resources_border,
203-
statuses_available=statuses_available, priorities=priorities)
204+
contractors_available=contractors_available, statuses_available=statuses_available,
205+
priorities=priorities)
204206
# crossover for order
205207
toolbox.register('mate_order', mate_scheduling_order, rand=rand, toolbox=toolbox, priorities=priorities)
206208
# mutation for order
@@ -210,7 +212,7 @@ def init_toolbox(wg: WorkGraph,
210212
toolbox.register('mate_resources', mate_resources, rand=rand, toolbox=toolbox)
211213
# mutation for resources
212214
toolbox.register('mutate_resources', mutate_resources, resources_border=resources_border,
213-
mutpb=mut_res_pb, rand=rand)
215+
contractors_available=contractors_available, mutpb=mut_res_pb, rand=rand)
214216
# mutation for resource borders
215217
toolbox.register('mutate_resource_borders', mutate_resource_borders, contractor_borders=contractor_borders,
216218
mutpb=mut_res_pb, rand=rand)
@@ -219,7 +221,7 @@ def init_toolbox(wg: WorkGraph,
219221
statuses_available=landscape.zone_config.statuses.statuses_available())
220222

221223
toolbox.register('validate', is_chromosome_correct, node_indices=node_indices, parents=parents,
222-
contractor_borders=contractor_borders, index2node=index2node)
224+
contractor_borders=contractor_borders, index2node=index2node, index2contractor=index2contractor_obj)
223225
toolbox.register('schedule_to_chromosome', convert_schedule_to_chromosome,
224226
work_id2index=work_id2index, worker_name2index=worker_name2index,
225227
contractor2index=contractor2index, contractor_borders=contractor_borders, spec=spec,
@@ -324,9 +326,6 @@ def randomized_init(is_topological: bool = False) -> ChromosomeType:
324326
case _:
325327
ind = init_chromosomes[generated_type][0]
326328

327-
if not toolbox.validate(ind):
328-
SAMPO.logger.warn('HELP')
329-
330329
ind = toolbox.Individual(ind)
331330
chromosomes.append(ind)
332331

@@ -390,12 +389,13 @@ def select_new_population(population: list[Individual], k: int) -> list[Individu
390389

391390

392391
def is_chromosome_correct(ind: Individual, node_indices: list[int], parents: dict[int, set[int]],
393-
contractor_borders: np.ndarray, index2node: dict[int, GraphNode]) -> bool:
392+
contractor_borders: np.ndarray, index2node: dict[int, GraphNode],
393+
index2contractor: dict[int, Contractor]) -> bool:
394394
"""
395395
Check correctness of works order and contractors borders.
396396
"""
397397
return is_chromosome_order_correct(ind, parents, index2node) and \
398-
is_chromosome_contractors_correct(ind, node_indices, contractor_borders)
398+
is_chromosome_contractors_correct(ind, node_indices, contractor_borders, index2node, index2contractor)
399399

400400

401401
def is_chromosome_order_correct(ind: Individual, parents: dict[int, set[int]], index2node: dict[int, GraphNode]) -> bool:
@@ -419,13 +419,27 @@ def is_chromosome_order_correct(ind: Individual, parents: dict[int, set[int]], i
419419

420420

421421
def is_chromosome_contractors_correct(ind: Individual, work_indices: Iterable[int],
422-
contractor_borders: np.ndarray) -> bool:
422+
contractor_borders: np.ndarray,
423+
index2node: dict[int, GraphNode],
424+
index2contractor: dict[int, Contractor]) -> bool:
423425
"""
424426
Checks that assigned contractors can supply assigned workers.
425427
"""
426428
if not work_indices:
427429
return True
430+
431+
order = ind[0]
428432
resources = ind[1][work_indices]
433+
434+
# check contractor align with the spec
435+
spec: ScheduleSpec = ind[3]
436+
contractors = resources[:, -1]
437+
for i in range(len(order)):
438+
work_index = order[i]
439+
work_spec = spec[index2node[work_index].id]
440+
if not work_spec.is_contractor_enabled(index2contractor[contractors[work_index]].id):
441+
return False
442+
429443
# sort resource part of chromosome by contractor ids
430444
resources = resources[resources[:, -1].argsort()]
431445
# get unique contractors and indexes where they start
@@ -579,8 +593,8 @@ def mutate_scheduling_order(ind: Individual, mutpb: float, rand: random.Random,
579593
"""
580594
order = ind[0]
581595

582-
priority_groups_count = len(set(priorities))
583-
mutpb_for_priority_group = mutpb #/ priority_groups_count
596+
# priority_groups_count = len(set(priorities))
597+
mutpb_for_priority_group = mutpb # / priority_groups_count
584598

585599
# priorities of tasks with same order-index should be the same (if chromosome is valid)
586600
cur_priority = priorities[order[0]]
@@ -634,15 +648,17 @@ def mate_resources(ind1: Individual, ind2: Individual, rand: random.Random,
634648

635649

636650
def mutate_resources(ind: Individual, mutpb: float, rand: random.Random,
637-
resources_border: np.ndarray) -> Individual:
651+
resources_border: np.ndarray,
652+
contractors_available: np.ndarray) -> Individual:
638653
"""
639654
Mutation function for resources.
640655
It changes selected numbers of workers in random work in a certain interval for this work.
641656
642657
:param ind: the individual to be mutated
643-
:param resources_border: low and up borders of resources amounts
644658
:param mutpb: probability of gene mutation
645659
:param rand: the rand object used for randomized operations
660+
:param resources_border: low and up borders of resources amounts
661+
:param contractors_available: mask of contractors available to do tasks
646662
647663
:return: mutated individual
648664
"""
@@ -654,9 +670,22 @@ def mutate_resources(ind: Individual, mutpb: float, rand: random.Random,
654670
mask = np.array([rand.random() < mutpb for _ in range(num_works)])
655671
if mask.any():
656672
# generate new contractors in the number of received True values of mask
657-
new_contractors = np.array([rand.randint(0, num_contractors - 1) for _ in range(mask.sum())])
673+
674+
# [rand.randint(0, num_contractors - 1) for _ in range(mask.sum())]
675+
new_contractors_list = []
676+
677+
# TODO Rewrite to numpy functions if heavy
678+
for task, task_selected in enumerate(mask):
679+
if not task_selected:
680+
continue
681+
contractors_to_select = np.where(contractors_available[task] == 1)
682+
new_contractors_list.append(rand.choices(contractors_to_select[0], k=1)[0])
683+
684+
new_contractors = np.array(new_contractors_list)
685+
658686
# obtain a new mask of correspondence
659687
# between the borders of the received contractors and the assigned resources
688+
660689
contractor_mask = (res[mask, :-1] <= ind[2][new_contractors]).all(axis=1)
661690
# update contractors by received mask
662691
new_contractors = new_contractors[contractor_mask]
@@ -720,9 +749,9 @@ def mate(ind1: Individual, ind2: Individual, optimize_resources: bool,
720749
return toolbox.Individual(child1), toolbox.Individual(child2)
721750

722751

723-
def mutate(ind: Individual, resources_border: np.ndarray, parents: dict[int, set[int]],
724-
children: dict[int, set[int]], statuses_available: int, priorities: np.ndarray,
725-
order_mutpb: float, res_mutpb: float, zone_mutpb: float,
752+
def mutate(ind: Individual, resources_border: np.ndarray, contractors_available: np.ndarray,
753+
parents: dict[int, set[int]], children: dict[int, set[int]], statuses_available: int,
754+
priorities: np.ndarray, order_mutpb: float, res_mutpb: float, zone_mutpb: float,
726755
rand: random.Random) -> Individual:
727756
"""
728757
Combined mutation function of mutation for order, mutation for resources and mutation for zones.
@@ -740,7 +769,7 @@ def mutate(ind: Individual, resources_border: np.ndarray, parents: dict[int, set
740769
:return: mutated individual
741770
"""
742771
mutant = mutate_scheduling_order(ind, order_mutpb, rand, priorities, parents, children)
743-
mutant = mutate_resources(mutant, res_mutpb, rand, resources_border)
772+
mutant = mutate_resources(mutant, res_mutpb, rand, resources_border, contractors_available)
744773
# TODO Make better mutation for zones and uncomment this
745774
# mutant = mutate_for_zones(mutant, statuses_available, zone_mutpb, rand)
746775

sampo/scheduler/genetic/schedule_builder.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def create_toolbox(wg: WorkGraph,
4141

4242
worker_pool, index2node, index2zone, work_id2index, worker_name2index, index2contractor_obj, \
4343
worker_pool_indices, contractor2index, contractor_borders, node_indices, priorities, parents, children, \
44-
resources_border = prepare_optimized_data_structures(wg, contractors, landscape)
44+
resources_border, contractors_available = prepare_optimized_data_structures(wg, contractors, landscape, spec)
4545

4646
init_chromosomes: dict[str, tuple[ChromosomeType, float, ScheduleSpec]] = \
4747
{name: (convert_schedule_to_chromosome(work_id2index, worker_name2index,
@@ -79,6 +79,7 @@ def create_toolbox(wg: WorkGraph,
7979
parents,
8080
children,
8181
resources_border,
82+
contractors_available,
8283
assigned_parent_time,
8384
fitness_weights,
8485
work_estimator,

0 commit comments

Comments
 (0)