From 6f8861c030fd98b97fb6c1ae022a223a6dbf2d69 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 4 Jun 2021 15:20:38 +1000 Subject: [PATCH] ES, EF, LS, LF calculations in work schedules now are calendar based. Thanks SigmaDimensions! --- .../api/sequence/recalculate_schedule.py | 112 +++++++++++------- .../ifcopenshell/util/sequence.py | 39 +++++- 2 files changed, 107 insertions(+), 44 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py index 50e42226ee..abcecfe8f7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py @@ -2,6 +2,7 @@ import datetime import networkx as nx import ifcopenshell.api import ifcopenshell.util.date +import ifcopenshell.util.sequence class Usecase: @@ -12,8 +13,9 @@ class Usecase: 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 + # The method implemented is the same as shown here: + # https://www.youtube.com/watch?v=qTErIV6OqLg + self.start_dates = [] self.build_network_graph() self.pending_nodes = set(self.g.nodes) @@ -34,13 +36,6 @@ class Usecase: self.update_task_times() - # TODO: normalise durations to elapsed times. Leave this debug print until we finish this TODO. - for n in self.g.nodes: - if n == 'start' or n == 'finish': - print(n, self.g.nodes[n]) - else: - print(self.file.by_id(n), self.g.nodes[n]) - def build_network_graph(self): self.sequence_type_map = { None: "FS", @@ -53,8 +48,8 @@ class Usecase: } self.g = nx.DiGraph() self.edges = [] - self.g.add_node("start", duration=0) - self.g.add_node("finish", duration=0) + self.g.add_node("start", duration=0, duration_type="ELAPSEDTIME", calendar=None) + self.g.add_node("finish", duration=0, duration_type="ELAPSEDTIME", calendar=None) for rel in self.settings["work_schedule"].Controls: for related_object in rel.RelatedObjects: if not related_object.is_a("IfcTask"): @@ -70,9 +65,18 @@ class Usecase: if task.TaskTime and task.TaskTime.ScheduleDuration: duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration).days + duration_type = task.TaskTime.DurationType else: duration = 0 - self.g.add_node(task.id(), duration=duration) + duration_type = "ELAPSEDTIME" + + self.g.add_node( + task.id(), + duration=duration, + duration_type=duration_type, + calendar=ifcopenshell.util.sequence.derive_calendar(task), + ) + self.edges.extend( [ ( @@ -95,6 +99,8 @@ class Usecase: "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 task.TaskTime and task.TaskTime.ScheduleStart: + self.start_dates.append(ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleStart)) 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"})) @@ -108,52 +114,64 @@ class Usecase: 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, + "TotalFloat": ifcopenshell.util.date.datetime2ifc(data["total_float"], "IfcDuration"), + "IsCritical": data["total_float"].days == 0, + "EarlyStart": ifcopenshell.util.date.datetime2ifc(data["early_start"], "IfcDateTime"), + "EarlyFinish": ifcopenshell.util.date.datetime2ifc(data["early_finish"], "IfcDateTime"), + "LateStart": ifcopenshell.util.date.datetime2ifc(data["late_start"], "IfcDateTime"), + "LateFinish": ifcopenshell.util.date.datetime2ifc(data["late_finish"], "IfcDateTime"), }, ) + def offset_date(self, date, days, node): + return ifcopenshell.util.sequence.get_finish_date( + date, datetime.timedelta(days=days), node["duration_type"], node["calendar"] + ) + def forward_pass(self, node): successors = self.g.successors(node) predecessors = list(self.g.predecessors(node)) data = self.g.nodes[node] if node == "start": - data["early_start"] = 0 + data["early_start"] = min(self.start_dates) else: finishes = [] starts = [] for predecessor in predecessors: + predecessor_data = self.g.nodes[predecessor] edge = self.g[predecessor][node] if edge["type"] == "FS": - finish = self.g.nodes[predecessor].get("early_finish") + finish = predecessor_data.get("early_finish") if finish is None: return - starts.append(finish + edge["lag_time"]) + starts.append(self.offset_date(finish, edge["lag_time"], data)) + starts.append(self.offset_date(finish, edge["lag_time"], predecessor_data)) elif edge["type"] == "SS": - start = self.g.nodes[predecessor].get("early_start") + start = predecessor_data.get("early_start") if start is None: return - starts.append(start + edge["lag_time"]) + starts.append(self.offset_date(start, edge["lag_time"], data)) + starts.append(self.offset_date(start, edge["lag_time"], predecessor_data)) elif edge["type"] == "FF": - finish = self.g.nodes[predecessor].get("early_finish") + finish = predecessor_data.get("early_finish") if finish is None: return - finishes.append(finish + edge["lag_time"]) + finishes.append(self.offset_date(finish, edge["lag_time"], data)) + finishes.append(self.offset_date(finish, edge["lag_time"], predecessor_data)) elif edge["type"] == "SF": - start = self.g.nodes[predecessor].get("early_start") + start = predecessor_data.get("early_start") if start is None: return - finishes.append(start + edge["lag_time"]) + finishes.append(self.offset_date(start, edge["lag_time"], data)) + finishes.append(self.offset_date(start, edge["lag_time"], predecessor_data)) if starts and finishes: data["early_start"] = max(starts) data["early_finish"] = max(finishes) - if data["early_start"] + data["duration"] > data["early_finish"]: - data["early_finish"] = data["early_start"] + data["duration"] + if self.offset_date(data["early_start"], data["duration"], data) > data["early_finish"]: + data["early_finish"] = self.offset_date(data["early_start"], data["duration"], data) else: - data["early_start"] = data["early_finish"] - data["duration"] + data["early_start"] = self.offset_date(data["early_finish"], -data["duration"], data) elif finishes: data["early_finish"] = max(finishes) elif starts: @@ -162,9 +180,9 @@ class Usecase: print("How did this happen?") if data.get("early_finish") is None: - data["early_finish"] = data["early_start"] + data["duration"] + data["early_finish"] = self.offset_date(data["early_start"], data["duration"], data) elif data.get("early_start") is None: - data["early_start"] = data["early_finish"] - data["duration"] + data["early_start"] = self.offset_date(data["early_finish"], -data["duration"], data) return True @@ -179,34 +197,39 @@ class Usecase: finishes = [] starts = [] for successor in successors: + successor_data = self.g.nodes[successor] edge = self.g[node][successor] if edge["type"] == "FS": - start = self.g.nodes[successor].get("late_start") + start = successor_data.get("late_start") if start is None: return - finishes.append(start - edge["lag_time"]) + finishes.append(self.offset_date(start, -edge["lag_time"], data)) + finishes.append(self.offset_date(start, -edge["lag_time"], successor_data)) elif edge["type"] == "SS": - start = self.g.nodes[successor].get("late_start") + start = successor_data.get("late_start") if start is None: return - starts.append(start - edge["lag_time"]) + starts.append(self.offset_date(start, -edge["lag_time"], data)) + starts.append(self.offset_date(start, -edge["lag_time"], successor_data)) elif edge["type"] == "FF": - finish = self.g.nodes[successor].get("late_finish") + finish = successor_data.get("late_finish") if finish is None: return - finishes.append(finish - edge["lag_time"]) + finishes.append(self.offset_date(finish, -edge["lag_time"], data)) + finishes.append(self.offset_date(finish, -edge["lag_time"], successor_data)) elif edge["type"] == "SF": - finish = self.g.nodes[successor].get("late_finish") + finish = successor_data.get("late_finish") if finish is None: return - starts.append(finish - edge["lag_time"]) + starts.append(self.offset_date(finish, -edge["lag_time"], data)) + starts.append(self.offset_date(finish, -edge["lag_time"], successor_data)) if starts and finishes: data["late_start"] = min(starts) data["late_finish"] = min(finishes) - if data["late_start"] + data["duration"] < data["late_finish"]: - data["late_finish"] = data["late_start"] + data["duration"] + if self.offset_date(data["late_start"], data["duration"], data) < data["late_finish"]: + data["late_finish"] = self.offset_date(data["late_start"], data["duration"], data) else: - data["late_start"] = data["late_finish"] - data["duration"] + data["late_start"] = self.offset_date(data["late_finish"], -data["duration"], data) elif finishes: data["late_finish"] = min(finishes) elif starts: @@ -215,10 +238,13 @@ class Usecase: print("How did this happen?") if data.get("late_finish") is None: - data["late_finish"] = data["late_start"] + data["duration"] + data["late_finish"] = self.offset_date(data["late_start"], data["duration"], data) elif data.get("late_start") is None: - data["late_start"] = data["late_finish"] - data["duration"] + data["late_start"] = self.offset_date(data["late_finish"], -data["duration"], data) data["total_float"] = data["late_finish"] - data["early_finish"] + # Waiting for yassine to confirm relationships + # data["free_float"] = min([self.g.nodes[s]["early_start"] for s in successors]) + return True diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 91d96f1751..6fdd713386 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -1,5 +1,6 @@ import datetime import ifcopenshell.util.date +from functools import lru_cache def derive_calendar(task): @@ -25,6 +26,41 @@ def count_working_days(start, finish, calendar): return result +def get_finish_date(start, duration, duration_type, calendar): + current_date = datetime.date(start.year, start.month, start.day) + abs_duration = abs(duration.days) + date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1) + while abs_duration > 0: + if duration_type == "ELAPSEDTIME" or not calendar: + abs_duration -= 1 + elif ifcopenshell.util.sequence.is_working_day(current_date, calendar): + abs_duration -= 1 + current_date += date_offset + + if duration.days > 0: + current_date = get_soonest_working_day(current_date, duration_type, calendar) + else: + current_date = get_recent_working_day(current_date, duration_type, calendar) + return current_date + + +def get_soonest_working_day(start, duration_type, calendar): + if duration_type == "ELAPSEDTIME" or not calendar: + return start + while not is_working_day(start, calendar): + start += datetime.timedelta(days=1) + return start + + +def get_recent_working_day(start, duration_type, calendar): + if duration_type == "ELAPSEDTIME" or not calendar: + return start + while not is_working_day(start, calendar): + start -= datetime.timedelta(days=1) + return start + + +@lru_cache(maxsize=None) def is_working_day(day, calendar): is_working_day = False for work_time in calendar.WorkingTimes or []: @@ -37,13 +73,14 @@ def is_working_day(day, calendar): if is_work_time_applicable_to_day(work_time, day): is_working_day = False break - print('is day working day ', day, is_working_day) return is_working_day def is_work_time_applicable_to_day(work_time, day): start = None finish = None + if isinstance(day, datetime.datetime): + day = datetime.date(day.year, day.month, day.day) if work_time.Start: start = ifcopenshell.util.date.ifc2datetime(work_time.Start)