Skip to content

Commit 2ac7add

Browse files
authored
Critical path integration (#94)
* added critical path calculation * removed NONE type of lag optimization * added contractor auto-generation to pipeline
1 parent cc8400b commit 2ac7add

File tree

16 files changed

+300
-91
lines changed

16 files changed

+300
-91
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Install dependencies
2525
run: |
2626
python -m pip install --upgrade pip
27-
pip install pytest pytest-rerunfailures
27+
pip install pytest==7.2.0 pytest-rerunfailures==12.0
2828
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
2929
# - name: Lint with flake8
3030
# run: |

experiments/critical_path.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from sampo.scheduler import GeneticScheduler
2+
from sampo.generator.base import SimpleSynthetic
3+
from sampo.generator.environment.contractor_by_wg import get_contractor_by_wg, ContractorGenerationMethod
4+
from sampo.generator import SyntheticGraphType
5+
from sampo.pipeline import SchedulingPipeline
6+
from sampo.scheduler.heft.base import HEFTScheduler
7+
8+
scheduler = HEFTScheduler()
9+
10+
r_seed = 231
11+
ss = SimpleSynthetic(r_seed)
12+
13+
simple_wg = ss.work_graph(mode=SyntheticGraphType.GENERAL,
14+
cluster_counts=10,
15+
bottom_border=100,
16+
top_border=200)
17+
18+
contractors = [get_contractor_by_wg(simple_wg, 10, ContractorGenerationMethod.MAX)]
19+
20+
project = SchedulingPipeline.create() \
21+
.wg('9-1-ukpg-full-with-priority.csv', sep=';', all_connections=True) \
22+
.contractors((ContractorGenerationMethod.MAX, 1, 1000)) \
23+
.schedule(scheduler) \
24+
.visualization('2022-01-01')[0] \
25+
.shape((14, 14)) \
26+
.color_type('critical_path') \
27+
.show_gant_chart()
28+
29+
schedule = project.schedule
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from sampo.generator.base import SimpleSynthetic
2+
from sampo.generator.environment.contractor_by_wg import get_contractor_by_wg, ContractorGenerationMethod
3+
from sampo.generator import SyntheticGraphType
4+
from sampo.pipeline import SchedulingPipeline
5+
from sampo.scheduler.heft.base import HEFTScheduler
6+
from sampo.scheduler.utils.critical_path import critical_path_graph
7+
from sampo.schemas.time_estimator import DefaultWorkEstimator
8+
9+
scheduler = HEFTScheduler()
10+
11+
r_seed = 231
12+
ss = SimpleSynthetic(r_seed)
13+
14+
simple_wg = ss.work_graph(mode=SyntheticGraphType.GENERAL,
15+
cluster_counts=10,
16+
bottom_border=100,
17+
top_border=200)
18+
19+
contractors = [get_contractor_by_wg(simple_wg, 10, ContractorGenerationMethod.MAX)]
20+
21+
project = SchedulingPipeline.create() \
22+
.wg('9-1-ukpg-full-with-priority.csv', sep=';', all_connections=True) \
23+
.contractors((ContractorGenerationMethod.MAX, 1, 1000)) \
24+
.schedule(scheduler) \
25+
.finish()[0]
26+
27+
schedule = project.schedule
28+
29+
graph_cp = critical_path_graph(project.wg.nodes, work_estimator=DefaultWorkEstimator())
30+
31+
schedule_cp = project.critical_path()

sampo/pipeline/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pandas as pd
44

5+
from sampo.generator.environment import ContractorGenerationMethod
56
from sampo.pipeline.lag_optimization import LagOptimizationStrategy
67
from sampo.scheduler.base import Scheduler
78
from sampo.scheduler.utils.local_optimization import OrderLocalOptimizer, ScheduleLocalOptimizer
@@ -28,7 +29,7 @@ def wg(self, wg: WorkGraph | pd.DataFrame | str,
2829
...
2930

3031
@abstractmethod
31-
def contractors(self, contractors: list[Contractor] | pd.DataFrame | str) -> 'InputPipeline':
32+
def contractors(self, contractors: list[Contractor] | pd.DataFrame | str | tuple[ContractorGenerationMethod, int, int]) -> 'InputPipeline':
3233
...
3334

3435
@abstractmethod

sampo/pipeline/default.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pandas as pd
22

3-
from sampo.generator.environment import ContractorGenerationMethod
3+
from sampo.generator.environment import ContractorGenerationMethod, get_contractor_by_wg
44
from sampo.pipeline.base import InputPipeline, SchedulePipeline
55
from sampo.pipeline.delegating import DelegatingScheduler
66
from sampo.pipeline.lag_optimization import LagOptimizationStrategy
@@ -55,11 +55,11 @@ class DefaultInputPipeline(InputPipeline):
5555

5656
def __init__(self):
5757
self._wg: WorkGraph | pd.DataFrame | str | None = None
58-
self._contractors: list[Contractor] | pd.DataFrame | str | tuple[ContractorGenerationMethod, int] | None \
59-
= ContractorGenerationMethod.AVG, 1
58+
self._contractors: list[Contractor] | pd.DataFrame | str | tuple[ContractorGenerationMethod, int, int] | None \
59+
= ContractorGenerationMethod.AVG, 1, 1
6060
self._work_estimator: WorkTimeEstimator = DefaultWorkEstimator()
6161
self._node_orders: list[list[GraphNode]] | None = None
62-
self._lag_optimize: LagOptimizationStrategy = LagOptimizationStrategy.NONE
62+
self._lag_optimize: LagOptimizationStrategy = LagOptimizationStrategy.FALSE
6363
self._spec: ScheduleSpec | None = ScheduleSpec()
6464
self._assigned_parent_time: Time | None = Time(0)
6565
self._local_optimize_stack: ApplyQueue = ApplyQueue()
@@ -98,7 +98,7 @@ def wg(self,
9898
self.sep_wg = sep
9999
return self
100100

101-
def contractors(self, contractors: list[Contractor] | pd.DataFrame | str | tuple[ContractorGenerationMethod, int]) \
101+
def contractors(self, contractors: list[Contractor] | pd.DataFrame | str | tuple[ContractorGenerationMethod, int, float]) \
102102
-> 'InputPipeline':
103103
"""
104104
Mandatory argument.
@@ -206,6 +206,15 @@ def schedule(self, scheduler: Scheduler, validate: bool = False) -> 'SchedulePip
206206

207207
check_and_correct_priorities(self._wg)
208208

209+
if not isinstance(self._contractors, list):
210+
generation_method, contractors_number, scaler = self._contractors
211+
self._contractors = [get_contractor_by_wg(self._wg,
212+
method=generation_method,
213+
contractor_id=str(i),
214+
contractor_name='Contractor' + ' ' + str(i + 1),
215+
scaler=scaler)
216+
for i in range(contractors_number)]
217+
209218
if not contractors_can_perform_work_graph(self._contractors, self._wg):
210219
raise NoSufficientContractorError('Contractors are not able to perform the graph of works')
211220

@@ -236,17 +245,6 @@ def prioritization(head_nodes: list[GraphNode],
236245
print('Trying to apply local optimizations to non-generic scheduler, ignoring it')
237246

238247
match self._lag_optimize:
239-
case LagOptimizationStrategy.NONE:
240-
wg = self._wg
241-
schedules = scheduler.schedule_with_cache(wg, self._contractors,
242-
self._spec,
243-
landscape=self._landscape_config,
244-
assigned_parent_time=self._assigned_parent_time,
245-
validate=validate)
246-
node_orders = [node_order for _, _, _, node_order in schedules]
247-
schedules = [schedule for schedule, _, _, _ in schedules]
248-
self._node_orders = node_orders
249-
250248
case LagOptimizationStrategy.AUTO:
251249
# Searching the best
252250
wg1 = graph_restructuring(self._wg, False)
@@ -278,7 +276,7 @@ def prioritization(head_nodes: list[GraphNode],
278276
wg = wg2
279277
schedules = schedules2
280278

281-
case _:
279+
case LagOptimizationStrategy.TRUE, LagOptimizationStrategy.FALSE:
282280
wg = graph_restructuring(self._wg, self._lag_optimize.value)
283281
schedules = scheduler.schedule_with_cache(wg, self._contractors,
284282
self._spec,
@@ -288,6 +286,16 @@ def prioritization(head_nodes: list[GraphNode],
288286
node_orders = [node_order for _, _, _, node_order in schedules]
289287
schedules = [schedule for schedule, _, _, _ in schedules]
290288
self._node_orders = node_orders
289+
case _:
290+
wg = self._wg
291+
schedules = scheduler.schedule_with_cache(wg, self._contractors,
292+
self._spec,
293+
landscape=self._landscape_config,
294+
assigned_parent_time=self._assigned_parent_time,
295+
validate=validate)
296+
node_orders = [node_order for _, _, _, node_order in schedules]
297+
schedules = [schedule for schedule, _, _, _ in schedules]
298+
self._node_orders = node_orders
291299

292300
return DefaultSchedulePipeline(self, wg, schedules)
293301

sampo/pipeline/lag_optimization.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ class LagOptimizationStrategy(Enum):
55
TRUE = True
66
FALSE = False
77
AUTO = None
8-
NONE = None

sampo/scheduler/heft/prioritization.py

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,12 @@
11
from operator import itemgetter
22

3+
from sampo.scheduler.utils.critical_path import ford_bellman
34
from sampo.utilities.priority import extract_priority_groups_from_nodes
45
from sampo.scheduler.utils.time_computaion import work_priority, calculate_working_time_cascade
56
from sampo.schemas.graph import GraphNode
67
from sampo.schemas.time_estimator import WorkTimeEstimator
78

89

9-
def ford_bellman(nodes: list[GraphNode],
10-
weights: dict[str, float],
11-
node_id2parent_ids: dict[str, set[str]]) -> dict[str, float]:
12-
"""
13-
Runs heuristic ford-bellman algorithm for given graph and weights.
14-
"""
15-
path_weights: dict[str, float] = {node.id: 0 for node in nodes}
16-
# cache graph edges
17-
edges: list[tuple[str, str, float]] = sorted([(finish, start.id, weights[finish])
18-
for start in nodes
19-
for finish in node_id2parent_ids[start.id]
20-
if finish in path_weights])
21-
if not edges:
22-
return path_weights
23-
24-
# for changes heuristic
25-
changed = False
26-
# run standard ford-bellman on reversed edges
27-
# optimize dict access to finish weight
28-
for i in range(len(nodes)):
29-
cur_finish = edges[0][0]
30-
cur_finish_weight = path_weights[cur_finish]
31-
# relax on edges
32-
for finish, start, weight in edges:
33-
# we are running through the equality class by finish node
34-
# so if it changes renew the parameters of current equality class
35-
if cur_finish != finish:
36-
path_weights[cur_finish] = cur_finish_weight
37-
cur_finish = finish
38-
cur_finish_weight = path_weights[cur_finish]
39-
new_weight = path_weights[start] + weight
40-
if new_weight < cur_finish_weight:
41-
cur_finish_weight = new_weight
42-
changed = True
43-
# if we were done completely nothing with actual minimum weights, the algorithm ends
44-
if not changed:
45-
break
46-
# we go here if changed = True
47-
# so the last equality class weight can be changed, save it
48-
path_weights[cur_finish] = cur_finish_weight
49-
# next iteration should start without change info from previous
50-
changed = False
51-
52-
return path_weights
53-
54-
5510
def prioritization_nodes(nodes: list[GraphNode],
5611
node_id2parent_ids: dict[str, set[str]],
5712
work_estimator: WorkTimeEstimator) -> list[GraphNode]:

sampo/scheduler/utils/__init__.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ def get_head_nodes_with_connections_mappings(wg: WorkGraph
3838
Args:
3939
wg: The `WorkGraph` to analyze.
4040
41+
Returns:
42+
A tuple containing:
43+
- A list of `GraphNode` objects representing the head nodes,
44+
sorted in topological order based on their reconstructed dependencies.
45+
- A dictionary mapping the ID of each head node to a set of IDs of
46+
its new 'parent' head nodes. These represent external dependencies
47+
where a parent of any node within the current head node's inseparable
48+
chain belongs to another head node's chain.
49+
- A dictionary mapping the ID of each head node to a set of IDs of
50+
its new 'child' head nodes. Similar to parents, these represent
51+
external dependencies where a child of any node within the current
52+
head node's inseparable chain belongs to another head node's chain.
53+
"""
54+
return get_head_nodes_with_connections_mappings_nodes(wg.nodes)
55+
56+
57+
def get_head_nodes_with_connections_mappings_nodes(nodes: list[GraphNode]) -> tuple[list[GraphNode], dict[str, set[str]], dict[str, set[str]]]:
58+
"""
59+
Identifies 'head nodes' in a WorkGraph and reconstructs their inter-node dependencies.
60+
61+
Head nodes are defined as the first nodes of inseparable chains or standalone nodes
62+
that are not part of an inseparable chain (i.e., they are not 'inseparable sons').
63+
This function effectively flattens the graph by treating inseparable chains as
64+
single logical entities represented by their head node, and then re-establishes
65+
parent-child relationships between these head nodes.
66+
67+
Args:
68+
nodes: The `WorkGraph` to analyze.
69+
4170
Returns:
4271
A tuple containing:
4372
- A list of `GraphNode` objects representing the head nodes,
@@ -54,7 +83,8 @@ def get_head_nodes_with_connections_mappings(wg: WorkGraph
5483
# Filter the work graph nodes to identify all 'head nodes'.
5584
# A head node is one that is not an 'inseparable son', meaning it's either
5685
# the start of an inseparable chain or a standalone node.
57-
nodes = [node for node in wg.nodes if not node.is_inseparable_son()]
86+
nodes = [node for node in nodes if not node.is_inseparable_son()]
87+
node_dict = {node.id: node for node in nodes}
5888

5989
# Construct a mapping from any node within an inseparable chain to its
6090
# corresponding head node.
@@ -87,6 +117,6 @@ def get_head_nodes_with_connections_mappings(wg: WorkGraph
87117
tsorted_nodes_ids = toposort_flatten(node_id2parent_ids, sort=True)
88118

89119
# Map the sorted head node IDs back to their corresponding GraphNode objects.
90-
tsorted_nodes = [wg[node_id] for node_id in tsorted_nodes_ids]
120+
tsorted_nodes = [node_dict[node_id] for node_id in tsorted_nodes_ids]
91121

92122
return tsorted_nodes, node_id2parent_ids, node_id2child_ids

0 commit comments

Comments
 (0)