diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index efc8d2a4b9..b36b8394ad 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -92,6 +92,20 @@ endif cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/ rm -rf dist/working + # Provides networkx graph analysis for project dependency calculations + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/b0/21/adfbf6168631e28577e4af9eb9f26d75fe72b2bb1d33762a5f2c425e6c2a/networkx-2.5.1.tar.gz + cd dist/working && tar -xzvf networkx* + cp -r dist/working/networkx-2.5.1/networkx dist/blenderbim/libs/site/packages/ + rm -rf dist/working + + # Required by networkx + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz + cd dist/working && tar -xzvf decorator* + cp -r dist/working/decorator-5.0.9/src/decorator.py dist/blenderbim/libs/site/packages/ + rm -rf dist/working + # Provides jsgantt-improved supports for web-based construction sequencing gantt charts mkdir dist/working cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 2b3b66fa3e..52781c9911 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -66,6 +66,7 @@ classes = ( operator.SelectTaskRelatedProducts, operator.VisualiseWorkScheduleDate, operator.VisualiseWorkScheduleDateRange, + operator.RecalculateSchedule, operator.BlenderBIM_DatePicker, operator.BlenderBIM_DatePickerSetDate, operator.BlenderBIM_RedrawDatePicker, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index b5aea85e7b..d353a2c08e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1439,7 +1439,9 @@ class BlenderBIM_DatePicker(bpy.types.Operator): return {"FINISHED"} def draw(self, context): - self.selected_date = helper.get_scene_prop("DatePickerProperties.selected_date") or helper.canonicalise_time(datetime.now()) + self.selected_date = helper.get_scene_prop("DatePickerProperties.selected_date") or helper.canonicalise_time( + datetime.now() + ) current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True) current_month = (current_date.year, current_date.month) lines = calendar.monthcalendar(*current_month) @@ -1511,3 +1513,17 @@ class BlenderBIM_RedrawDatePicker(bpy.types.Operator): context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set) return {"FINISHED"} + + +class RecalculateSchedule(bpy.types.Operator): + bl_idname = "bim.recalculate_schedule" + bl_label = "Recalculate Schedule" + work_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "sequence.recalculate_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule) + ) + Data.load(self.file) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 6b0e12bf99..34949d411c 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -114,6 +114,7 @@ class BIM_PT_work_schedules(Panel): row.prop(self.props, "should_show_calendars", text="", icon="VIEW_ORTHO") row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO") row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id + row.operator("bim.recalculate_schedule", text="", icon="FILE_REFRESH").work_schedule = work_schedule_id row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL") elif self.props.active_work_schedule_id: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py new file mode 100644 index 0000000000..aeae2b01ef --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py @@ -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, + }, + )