Skip to content

Commit

Permalink
New API usecase to recalculate schedule times, including:
Browse files Browse the repository at this point in the history
* Critical path
* Total float
* Early start
* Early finish
* Late start
* Late finish
  • Loading branch information
Moult committed Jun 2, 2021
1 parent 3a754c8 commit 67b636f
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 1 deletion.
14 changes: 14 additions & 0 deletions src/blenderbim/Makefile
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/blenderbim/blenderbim/bim/module/sequence/__init__.py
Expand Up @@ -66,6 +66,7 @@
operator.SelectTaskRelatedProducts,
operator.VisualiseWorkScheduleDate,
operator.VisualiseWorkScheduleDateRange,
operator.RecalculateSchedule,
operator.BlenderBIM_DatePicker,
operator.BlenderBIM_DatePickerSetDate,
operator.BlenderBIM_RedrawDatePicker,
Expand Down
18 changes: 17 additions & 1 deletion src/blenderbim/blenderbim/bim/module/sequence/operator.py
Expand Up @@ -1439,7 +1439,9 @@ def execute(self, context):
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)
Expand Down Expand Up @@ -1511,3 +1513,17 @@ def invoke(self, context, event):
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"}
1 change: 1 addition & 0 deletions src/blenderbim/blenderbim/bim/module/sequence/ui.py
Expand Up @@ -114,6 +114,7 @@ def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
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:
Expand Down
@@ -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,
},
)

0 comments on commit 67b636f

Please sign in to comment.