Generate functions for all API usecases for better static code features. See #2693.

This commit is contained in:
Dion Moult
2024-05-06 14:35:39 +10:00
parent 10f894e2ea
commit d11ec67129
330 changed files with 13283 additions and 13751 deletions
@@ -21,90 +21,93 @@ import ifcopenshell.util.date
import ifcopenshell.util.sequence
def cascade_schedule(file, task=None) -> None:
"""Cascades start and end dates of tasks based on durations
Given a start task with a start date and duration, the end date, and the
start and end of all successor tasks with durations may be automatically
computed.
Using this automatic computation is recommended is an alternative to
manually specifying dates. It is useful for doing edits and cascading
changes.
Dates can only cascade from predecessor to successors, not backwards.
Cyclical relationships are invalid and will result in a recursion error
being raised.
Note that there may be differences between how different planning
software calculate start and end dates. Some may consider Monday 5pm to
be equivalent to be Tuesday 8am, for instance.
:param task: The start task to begin cascading from.
:type task: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
# Define a convenience function to add a task chained to a predecessor
def add_task(model, name, predecessor, work_schedule):
# Add a construction task
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=work_schedule, name=name, predefined_type="CONSTRUCTION")
# Give it a time
task_time = ifcopenshell.api.run("sequence.add_task_time", model, task=task)
# Arbitrarily set the task's scheduled time duration to be 1 week
ifcopenshell.api.run("sequence.edit_task_time", model, task_time=task_time,
attributes={"ScheduleStart": datetime.date(2000, 1, 1), "ScheduleDuration": "P1W"})
# If a predecessor exists, create a finish to start relationship
if predecessor:
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=predecessor, related_process=task)
return task
# Open an existing IFC4 model you have of a building
model = ifcopenshell.open("/path/to/existing/model.ifc")
# Create a new construction schedule
schedule = ifcopenshell.api.run("sequence.add_work_schedule", model, name="Construction")
# Let's imagine a starting task for site establishment.
task = add_task(model, "Site establishment", None, schedule)
start_task = task
# Get all our storeys sorted by elevation ascending.
storeys = sorted(model.by_type("IfcBuildingStorey"), key=lambda s: get_storey_elevation(s))
# For each storey ...
for storey in storeys:
# Add a construction task to construct that storey, using our convenience function
task = add_task(model, f"Construct {storey.Name}", task, schedule)
# Assign all the products in that storey to the task as construction outputs.
for product in get_decomposition(storey):
ifcopenshell.api.run("sequence.assign_product", model, relating_product=product, related_object=task)
# Ask the computer to calculate all the dates for us from the start task.
# For example, if the first task started on the 1st of January and took a
# week, the next task will start on the 8th of January. This saves us
# manually doing date calculations.
ifcopenshell.api.run("sequence.cascade_schedule", model, task=start_task)
# Calculate the critical path and floats.
ifcopenshell.api.run("sequence.recalculate_schedule", model, work_schedule=schedule)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"task": task}
return usecase.execute()
class Usecase:
def __init__(self, file, task=None):
"""Cascades start and end dates of tasks based on durations
Given a start task with a start date and duration, the end date, and the
start and end of all successor tasks with durations may be automatically
computed.
Using this automatic computation is recommended is an alternative to
manually specifying dates. It is useful for doing edits and cascading
changes.
Dates can only cascade from predecessor to successors, not backwards.
Cyclical relationships are invalid and will result in a recursion error
being raised.
Note that there may be differences between how different planning
software calculate start and end dates. Some may consider Monday 5pm to
be equivalent to be Tuesday 8am, for instance.
:param task: The start task to begin cascading from.
:type task: ifcopenshell.entity_instance
:return: None
:rtype: None
Example:
.. code:: python
# Define a convenience function to add a task chained to a predecessor
def add_task(model, name, predecessor, work_schedule):
# Add a construction task
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=work_schedule, name=name, predefined_type="CONSTRUCTION")
# Give it a time
task_time = ifcopenshell.api.run("sequence.add_task_time", model, task=task)
# Arbitrarily set the task's scheduled time duration to be 1 week
ifcopenshell.api.run("sequence.edit_task_time", model, task_time=task_time,
attributes={"ScheduleStart": datetime.date(2000, 1, 1), "ScheduleDuration": "P1W"})
# If a predecessor exists, create a finish to start relationship
if predecessor:
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=predecessor, related_process=task)
return task
# Open an existing IFC4 model you have of a building
model = ifcopenshell.open("/path/to/existing/model.ifc")
# Create a new construction schedule
schedule = ifcopenshell.api.run("sequence.add_work_schedule", model, name="Construction")
# Let's imagine a starting task for site establishment.
task = add_task(model, "Site establishment", None, schedule)
start_task = task
# Get all our storeys sorted by elevation ascending.
storeys = sorted(model.by_type("IfcBuildingStorey"), key=lambda s: get_storey_elevation(s))
# For each storey ...
for storey in storeys:
# Add a construction task to construct that storey, using our convenience function
task = add_task(model, f"Construct {storey.Name}", task, schedule)
# Assign all the products in that storey to the task as construction outputs.
for product in get_decomposition(storey):
ifcopenshell.api.run("sequence.assign_product", model, relating_product=product, related_object=task)
# Ask the computer to calculate all the dates for us from the start task.
# For example, if the first task started on the 1st of January and took a
# week, the next task will start on the 8th of January. This saves us
# manually doing date calculations.
ifcopenshell.api.run("sequence.cascade_schedule", model, task=start_task)
# Calculate the critical path and floats.
ifcopenshell.api.run("sequence.recalculate_schedule", model, work_schedule=schedule)
"""
self.file = file
self.settings = {"task": task}
def execute(self):
self.calendar_cache = {}
self.cascade_task(self.settings["task"], is_first_task=True)
@@ -135,14 +138,10 @@ class Usecase:
finishes = []
starts = []
for rel in ifcopenshell.util.sequence.get_sequence_assignment(
task, "predecessor"
):
for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "predecessor"):
predecessor = rel.RelatingProcess
predecessor_duration = (
ifcopenshell.util.date.ifc2datetime(
predecessor.TaskTime.ScheduleDuration
)
ifcopenshell.util.date.ifc2datetime(predecessor.TaskTime.ScheduleDuration)
if predecessor.TaskTime and predecessor.TaskTime.ScheduleDuration
else datetime.timedelta()
)
@@ -154,14 +153,16 @@ class Usecase:
duration_type = "WORKTIME"
if rel.TimeLag:
# updated to handle IfcRatioMeasure as a TimeLag value
days += self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
days += (
self.get_lag_time_days(rel.TimeLag)
if rel.TimeLag.LagValue.is_a("IfcDuration")
else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
)
duration_type = rel.TimeLag.DurationType
if days:
starts.append(
datetime.datetime.combine(
self.offset_date(
finish, days, duration_type, self.get_calendar(task)
),
self.offset_date(finish, days, duration_type, self.get_calendar(task)),
datetime.time(9),
)
)
@@ -183,18 +184,14 @@ class Usecase:
if not start:
continue
if rel.TimeLag:
days = self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
days = (
self.get_lag_time_days(rel.TimeLag)
if rel.TimeLag.LagValue.is_a("IfcDuration")
else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
)
duration_type = rel.TimeLag.DurationType
starts.append(
self.offset_date(
start, days, duration_type, self.get_calendar(task)
)
)
starts.append(
self.offset_date(
start, days, duration_type, self.get_calendar(predecessor)
)
)
starts.append(self.offset_date(start, days, duration_type, self.get_calendar(task)))
starts.append(self.offset_date(start, days, duration_type, self.get_calendar(predecessor)))
else:
starts.append(start)
elif rel.SequenceType == "FINISH_FINISH":
@@ -202,18 +199,14 @@ class Usecase:
if not finish:
continue
if rel.TimeLag:
days = self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
days = (
self.get_lag_time_days(rel.TimeLag)
if rel.TimeLag.LagValue.is_a("IfcDuration")
else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
)
duration_type = rel.TimeLag.DurationType
finishes.append(
self.offset_date(
finish, days, duration_type, self.get_calendar(task)
)
)
finishes.append(
self.offset_date(
finish, days, duration_type, self.get_calendar(predecessor)
)
)
finishes.append(self.offset_date(finish, days, duration_type, self.get_calendar(task)))
finishes.append(self.offset_date(finish, days, duration_type, self.get_calendar(predecessor)))
else:
finishes.append(finish)
elif rel.SequenceType == "START_FINISH":
@@ -223,14 +216,16 @@ class Usecase:
days = -1
duration_type = "WORKTIME"
if rel.TimeLag:
days += self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
days += (
self.get_lag_time_days(rel.TimeLag)
if rel.TimeLag.LagValue.is_a("IfcDuration")
else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue
)
duration_type = rel.TimeLag.DurationType
if days or rel.TimeLag:
finishes.append(
datetime.datetime.combine(
self.offset_date(
start, days, duration_type, self.get_calendar(task)
),
self.offset_date(start, days, duration_type, self.get_calendar(task)),
datetime.time(17),
)
)
@@ -263,9 +258,7 @@ class Usecase:
if task.TaskTime.ScheduleStart == start_ifc and not is_first_task:
return
task.TaskTime.ScheduleStart = start_ifc
task.TaskTime.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(
potential_finish, "IfcDateTime"
)
task.TaskTime.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(potential_finish, "IfcDateTime")
else:
finish_ifc = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
if task.TaskTime.ScheduleFinish == finish_ifc and not is_first_task:
@@ -328,15 +321,11 @@ class Usecase:
def get_calendar(self, task):
if task.id() not in self.calendar_cache:
self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(
task
)
self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(task)
return self.calendar_cache[task.id()]
def offset_date(self, date, days, duration_type, calendar):
return ifcopenshell.util.sequence.offset_date(
date, datetime.timedelta(days=days), duration_type, calendar
)
return ifcopenshell.util.sequence.offset_date(date, datetime.timedelta(days=days), duration_type, calendar)
def get_task_time_attribute(self, task, attribute):
if task.TaskTime: