Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New API usecase to recalculate schedule times, including:
* Critical path * Total float * Early start * Early finish * Late start * Late finish
- Loading branch information
Showing
5 changed files
with
230 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import datetime | ||
import networkx as nx | ||
import ifcopenshell.api | ||
import ifcopenshell.util.date | ||
|
||
|
||
class Usecase: | ||
def __init__(self, file, **settings): | ||
self.file = file | ||
self.settings = {"work_schedule": None} | ||
for key, value in settings.items(): | ||
self.settings[key] = value | ||
|
||
def execute(self): | ||
# I learned everything about project dependency calcs from this YouTube playlist: | ||
# https://www.youtube.com/playlist?list=PLLRADeJk4TCK-X5vJY8focpFkau1MR7do | ||
import time | ||
|
||
self.time = time.time() | ||
self.build_network_graph() | ||
print("{} :: {:.2f}".format("Build network", time.time() - self.time)) | ||
print("TOTAL NODES AND EDGES", len(self.g.nodes), len(self.g.edges)) | ||
self.time = time.time() | ||
self.calculate_all_paths_sorted_by_duration() | ||
print("{} :: {:.2f}".format("Calc all paths", time.time() - self.time)) | ||
self.time = time.time() | ||
self.calculate_critical_path() | ||
print("{} :: {:.2f}".format("Calc critical", time.time() - self.time)) | ||
self.time = time.time() | ||
self.calculate_forward_pass() | ||
print("{} :: {:.2f}".format("Forward", time.time() - self.time)) | ||
self.time = time.time() | ||
self.calculate_backward_pass() | ||
print("{} :: {:.2f}".format("Backward", time.time() - self.time)) | ||
self.time = time.time() | ||
self.update_task_times() | ||
print("{} :: {:.2f}".format("Update", time.time() - self.time)) | ||
self.time = time.time() | ||
print("DONE!", self.critical_paths) | ||
|
||
def build_network_graph(self): | ||
self.sequence_type_map = { | ||
None: "FS", | ||
"START_START": "SS", | ||
"START_FINISH": "SF", | ||
"FINISH_START": "FS", | ||
"FINISH_FINISH": "FF", | ||
"USERDEFINED": "FS", | ||
"NOTDEFINED": "FS", | ||
} | ||
self.g = nx.DiGraph() | ||
self.edges = [] | ||
self.g.add_node("start", duration=0) | ||
self.g.add_node("finish", duration=0) | ||
for rel in self.settings["work_schedule"].Controls: | ||
for related_object in rel.RelatedObjects: | ||
if not related_object.is_a("IfcTask"): | ||
continue | ||
self.add_node(related_object) | ||
self.g.add_edges_from(self.edges) | ||
|
||
def add_node(self, task): | ||
if task.IsNestedBy: | ||
for rel in task.IsNestedBy: | ||
[self.add_node(o) for o in rel.RelatedObjects] | ||
return | ||
|
||
if task.TaskTime and task.TaskTime.ScheduleDuration: | ||
duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration).days | ||
else: | ||
duration = 0 | ||
self.g.add_node(task.id(), duration=duration) | ||
self.edges.extend( | ||
[ | ||
( | ||
rel.RelatingProcess.id(), | ||
rel.RelatedProcess.id(), | ||
{ | ||
"lag_time": 0 | ||
if not rel.TimeLag or not rel.TimeLag.LagValue | ||
else ifcopenshell.util.date.ifc2datetime(rel.TimeLag.LagValue.wrappedValue).days, | ||
"type": self.sequence_type_map[rel.SequenceType], | ||
}, | ||
) | ||
for rel in task.IsSuccessorFrom or [] | ||
] | ||
) | ||
predecessor_types = [rel.SequenceType for rel in task.IsSuccessorFrom] | ||
successor_types = [rel.SequenceType for rel in task.IsPredecessorTo] | ||
# This is less correct, but less computation | ||
if not task.IsSuccessorFrom: | ||
self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"})) | ||
# This I think is more correct, but unlikely to be necessary in most | ||
# graphs, and simply adds more computation time | ||
# if not predecessor_types or ( | ||
# "FINISH_START" not in predecessor_types and "START_START" not in predecessor_types | ||
# ): | ||
# self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"})) | ||
if not successor_types or ("FINISH_START" not in successor_types and "FINISH_FINISH" not in successor_types): | ||
self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FS"})) | ||
|
||
def calculate_all_paths_sorted_by_duration(self): | ||
self.paths = [] | ||
total_paths = 0 | ||
for path in nx.algorithms.simple_paths.all_simple_paths(self.g, "start", "finish"): | ||
total_duration = 0 | ||
for i, node in enumerate(path): | ||
try: | ||
next_edge = self.g[node][path[i + 1]] | ||
prev_edge = self.g[path[i - 1]][node] | ||
except: | ||
continue | ||
if prev_edge["type"][1] == "S" and next_edge["type"][0] == "F": | ||
total_duration += self.g.nodes[node]["duration"] | ||
elif prev_edge["type"][1] == "F" and next_edge["type"][0] == "S": | ||
total_duration -= self.g.nodes[node]["duration"] | ||
total_duration += next_edge["lag_time"] | ||
self.paths.append((total_duration, path)) | ||
total_paths += 1 | ||
if total_paths % 2000 == 0: | ||
print(total_paths, total_duration) | ||
self.paths = list(reversed(sorted(self.paths, key=lambda x: x[0]))) | ||
|
||
def calculate_critical_path(self): | ||
self.critical_paths = [p for p in self.paths if p[0] == self.paths[0][0]] | ||
|
||
def calculate_forward_pass(self): | ||
for path_data in self.paths: | ||
path = path_data[1] | ||
for i, node in enumerate(path): | ||
data = self.g.nodes[node] | ||
|
||
if node == "start": | ||
data["early_start"] = 0 | ||
else: | ||
prev_node = self.g.nodes[path[i - 1]] | ||
prev_edge = self.g[path[i - 1]][node] | ||
if prev_edge["type"] == "FS" and data.get("early_start") is None: | ||
data["early_start"] = prev_node["early_finish"] + prev_edge["lag_time"] | ||
elif prev_edge["type"] == "FF" and data.get("early_finish") is None: | ||
data["early_finish"] = prev_node["early_finish"] + prev_edge["lag_time"] | ||
elif prev_edge["type"] == "SS" and data.get("early_start") is None: | ||
data["early_start"] = prev_node["early_start"] + prev_edge["lag_time"] | ||
elif prev_edge["type"] == "SF" and data.get("early_finish") is None: | ||
data["early_finish"] = prev_node["early_start"] + prev_edge["lag_time"] | ||
|
||
if data.get("early_finish") is None: | ||
data["early_finish"] = data["early_start"] + data["duration"] | ||
elif data.get("early_start") is None: | ||
data["early_start"] = data["early_finish"] - data["duration"] | ||
#print(data) | ||
|
||
def calculate_backward_pass(self): | ||
critical_duration = self.critical_paths[0][0] | ||
for path_data in self.paths: | ||
path = list(reversed(path_data[1])) | ||
for i, node in enumerate(path): | ||
data = self.g.nodes[node] | ||
|
||
if node == "finish": | ||
data["late_finish"] = critical_duration | ||
else: | ||
prev_node = self.g.nodes[path[i - 1]] | ||
prev_edge = self.g[node][path[i - 1]] | ||
if prev_edge["type"] == "FS" and data.get("late_finish") is None: | ||
data["late_finish"] = prev_node["late_start"] - prev_edge["lag_time"] | ||
elif prev_edge["type"] == "FF" and data.get("late_finish") is None: | ||
data["late_finish"] = prev_node["late_finish"] - prev_edge["lag_time"] | ||
elif prev_edge["type"] == "SS" and data.get("late_start") is None: | ||
data["late_start"] = prev_node["late_start"] - prev_edge["lag_time"] | ||
elif prev_edge["type"] == "SF" and data.get("late_start") is None: | ||
data["late_start"] = prev_node["late_finish"] - prev_edge["lag_time"] | ||
|
||
if data.get("late_finish") is None: | ||
data["late_finish"] = data["late_start"] + data["duration"] | ||
elif data.get("late_start") is None: | ||
data["late_start"] = data["late_finish"] - data["duration"] | ||
|
||
data["total_float"] = data["late_finish"] - data["early_finish"] | ||
#print("DATA", data) | ||
|
||
def update_task_times(self): | ||
for ifc_definition_id in self.g.nodes: | ||
data = self.g.nodes[ifc_definition_id] | ||
if not data["duration"]: | ||
continue | ||
ifcopenshell.api.run( | ||
"sequence.edit_task_time", | ||
self.file, | ||
task_time=self.file.by_id(ifc_definition_id).TaskTime, | ||
attributes={ | ||
"TotalFloat": ifcopenshell.util.date.datetime2ifc( | ||
datetime.timedelta(days=data["total_float"]), "IfcDuration" | ||
), | ||
"IsCritical": data["total_float"] == 0, | ||
}, | ||
) |