diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py index 99fc616e33..abb869b452 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py @@ -32,7 +32,20 @@ class Usecase: self.calendar_cache = {} self.cascade_task(self.settings["task"], is_first_task=True) - def cascade_task(self, task, is_first_task=False): + def cascade_task(self, task, is_first_task=False, task_sequence=None): + if task_sequence is None: + task_sequence = [] + + if task in task_sequence: + print("Warning! Recursive sequence is as follows:") + for i, debug_task in enumerate(task_sequence): + if i == 0: + print("Starting at", debug_task) + else: + print("... is a predecessor to ...", debug_task) + print("... which is cyclically a predecessor to ...", task) + raise Exception("Recursive tasks found. Could not cascade schedule.") + if not task.TaskTime: return @@ -170,7 +183,7 @@ class Usecase: ) for rel in task.IsPredecessorTo: - self.cascade_task(rel.RelatedProcess) + self.cascade_task(rel.RelatedProcess, task_sequence=task_sequence + [task]) def get_lag_time_days(self, lag_time): return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py index 93082a39f8..db6d26249a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py @@ -39,14 +39,32 @@ class Usecase: if not self.start_dates: return + is_cyclic = False + attempts = 0 self.pending_nodes = set(self.g.nodes) + max_worst_case_attempts = pow(len(self.pending_nodes), 2) while self.pending_nodes: + attempts += 1 remaining_nodes = set() for pending_node in self.pending_nodes: if not self.forward_pass(pending_node): remaining_nodes.add(pending_node) self.pending_nodes = remaining_nodes + # As we parse nodes, the remaining attempts can drop dramatically, so we recalculate the upper limit + max_remaining_attempts = pow(len(self.pending_nodes), 2) + if max_remaining_attempts < max_worst_case_attempts: + max_worst_case_attempts = max_remaining_attempts + attempts = 0 + + if attempts > max_worst_case_attempts: + is_cyclic = True + break # We have an infinite loop due to a cyclic graph + + if is_cyclic: + raise Exception("Task graph is cyclic and so critical path method cannot be performed.") + return + self.pending_nodes = set(self.g.nodes) while self.pending_nodes: remaining_nodes = set()