First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,114 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Manage work schedules, tasks, calendars, and more for 4D
These are typically used for construction planning, but may also be used in
managing recurring facility maintenance schedules.
"""
from .. import wrap_usecases
from .add_date_time import add_date_time
from .add_task import add_task
from .add_task_time import add_task_time
from .add_time_period import add_time_period
from .add_work_calendar import add_work_calendar
from .add_work_plan import add_work_plan
from .add_work_schedule import add_work_schedule
from .add_work_time import add_work_time
from .assign_lag_time import assign_lag_time
from .assign_process import assign_process
from .assign_product import assign_product
from .assign_recurrence_pattern import assign_recurrence_pattern
from .assign_sequence import assign_sequence
from .assign_work_plan import assign_work_plan
from .calculate_task_duration import calculate_task_duration
from .cascade_schedule import cascade_schedule
from .copy_work_schedule import copy_work_schedule
from .create_baseline import create_baseline
from .duplicate_task import duplicate_task
from .edit_lag_time import edit_lag_time
from .edit_recurrence_pattern import edit_recurrence_pattern
from .edit_sequence import edit_sequence
from .edit_task import edit_task
from .edit_task_time import edit_task_time
from .edit_work_calendar import edit_work_calendar
from .edit_work_plan import edit_work_plan
from .edit_work_schedule import edit_work_schedule
from .edit_work_time import edit_work_time
try:
from .recalculate_schedule import recalculate_schedule
except ModuleNotFoundError as e:
print(f"Note: API not available due to missing dependencies: sequence.recalculate_schedule - {e}")
from .remove_task import remove_task
from .remove_time_period import remove_time_period
from .remove_work_calendar import remove_work_calendar
from .remove_work_plan import remove_work_plan
from .remove_work_schedule import remove_work_schedule
from .remove_work_time import remove_work_time
from .unassign_lag_time import unassign_lag_time
from .unassign_process import unassign_process
from .unassign_product import unassign_product
from .unassign_recurrence_pattern import unassign_recurrence_pattern
from .unassign_sequence import unassign_sequence
wrap_usecases(__path__, __name__)
__all__ = [
"add_date_time",
"add_task",
"add_task_time",
"add_time_period",
"add_work_calendar",
"add_work_plan",
"add_work_schedule",
"add_work_time",
"assign_lag_time",
"assign_process",
"assign_product",
"assign_recurrence_pattern",
"assign_sequence",
"assign_work_plan",
"calculate_task_duration",
"cascade_schedule",
"copy_work_schedule",
"create_baseline",
"duplicate_task",
"edit_lag_time",
"edit_recurrence_pattern",
"edit_sequence",
"edit_task",
"edit_task_time",
"edit_work_calendar",
"edit_work_plan",
"edit_work_schedule",
"edit_work_time",
"recalculate_schedule",
"remove_task",
"remove_time_period",
"remove_work_calendar",
"remove_work_plan",
"remove_work_schedule",
"remove_work_time",
"unassign_lag_time",
"unassign_process",
"unassign_product",
"unassign_recurrence_pattern",
"unassign_sequence",
]
@@ -0,0 +1,60 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from typing import Union
import ifcopenshell.util.date
def add_date_time(file: ifcopenshell.file, dt: datetime) -> Union[str, ifcopenshell.entity_instance]:
"""Add a new date time.
Depending on ``file``'s schema method will:
- IFC2X3 - create IfcDateAndTime entity
- IFC4+ - create IfcDatetime formatted string
:param dt: datetime to convert to IFC.
:return: IfcDateAndTime entity or IfcDatetime string.
Example:
.. code:: python
dt = datetime(2025, 3, 1, 12, 31, 24)
datetime_ifc = ifcopenshell.api.sequence.add_date_time(self.file, dt)
# IFC2X3: #1=IfcDateAndTime(#2,#3)
# IFC4+: "2025-03-01T12:31:24"
print(datetime_ifc)
"""
if file.schema == "IFC2X3":
ifc_dt = file.create_entity("IfcDateAndTime")
calendar_date_data = ifcopenshell.util.date.datetime2ifc(dt, "IfcCalendarDate")
assert isinstance(calendar_date_data, dict)
ifc_dt.DateComponent = file.create_entity("IfcCalendarDate", **calendar_date_data)
local_time_data = ifcopenshell.util.date.datetime2ifc(dt, "IfcLocalTime")
assert isinstance(local_time_data, dict)
ifc_dt.TimeComponent = file.create_entity("IfcLocalTime", **local_time_data)
return ifc_dt
dt_str = ifcopenshell.util.date.datetime2ifc(dt, "IfcDateTime")
assert isinstance(dt_str, str)
return dt_str
@@ -0,0 +1,152 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional
import ifcopenshell
import ifcopenshell.api.control
import ifcopenshell.api.nest
import ifcopenshell.api.root
def add_task(
file: ifcopenshell.file,
work_schedule: Optional[ifcopenshell.entity_instance] = None,
parent_task: Optional[ifcopenshell.entity_instance] = None,
name: Optional[str] = None,
description: Optional[str] = None,
identification: Optional[str] = None,
predefined_type: str = "NOTDEFINED",
) -> ifcopenshell.entity_instance:
"""Adds a new task
Tasks are typically used for two purposes: construction scheduling and
facility management.
In construction scheduling, a task represents a job to be done in a work
schedule. Tasks are organised in a hierarchical manner known as a work
breakdown structure (WBS) and have lots of sequential relationships
(e.g. this task must finish before the next task can start) and date
information (e.g. durations, start dates). This is often represented as
a gantt chart and used to analyse critical paths to try and reduce
project time to stay on-time and within budget.
In facility management, a task represents a maintenance task to maintain
a piece of equipment. Tasks are broken down into a punch list, or simply
a bulleted or ordered sequence of tasks to be performed (e.g. turn off
equipment, check power connection, etc) in order to maintain the
equipment. Tasks will also typically have recurring scheduled dates in
line with the maintenance schedule. These maintenance tasks and
procedures are typically published as part of an operations and
maintenance manual.
All tasks must be grouped in a work schedule, either directly as a root
or top-level task, or indirectly as a child or subtask of a parent task.
In construction scheduling, tasks may be nested many times to create the
work breakdown structure, and the "leaf" tasks (i.e. tasks with no more
subtasks) are considered to be the activities with dates, whereas all
parent tasks are part of the breakdown structure used for categorisation
purposes. In facility management, top-level tasks represent the overall
maintenance job to be performed, and child tasks represent an ordered
list of things to do for that maintenance. These form a 2-level
hierarchy. No further child tasks are recommended.
:param work_schedule: The work schedule to group the task in, if the
task is to be a top-level or root task. This is mutually exclusive
with the parent_task parameter.
:param parent_task: The parent task, if the task is to be a subtask or
child task. This is mutually exclusive with the work_schedule
parameter.
:param name: The name of the task.
:param description: The description of the task.
:param identification: The identification code of the task.
:param predefined_type: The predefined type of the task. Common ones
include CONSTRUCTION, DEMOLITION, or MAINTENANCE. Consultant the
IFC documentation for IfcTaskTypeEnum for more information.
:return: The newly created IfcTask
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Add a root task to represent the design milestones, and major
# project phases.
ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Milestones", identification="A")
ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Design", identification="B")
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's start creating our work breakdown structure.
ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Early Works", identification="C1")
ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Substructure", identification="C2")
superstructure = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Superstructure", identification="C3")
# Notice how the leaf task is the actual activity
ifcopenshell.api.sequence.add_task(model,
parent_task=superstructure, name="Ground Floor FRP", identification="C3.1")
# Let's imagine we are digitising an operations and maintenance
# manual for the mechanical discipline.
maintenance = ifcopenshell.api.sequence.add_work_schedule(model, name="Mechanical Maintenance")
# Imagine we have to clean the condenser coils for a chiller every
# month. Like the schedule above, to keep things simple we won't
# show scheduling times and calendars. This root task represents the
# overall maintenance task.
cleaning = ifcopenshell.api.sequence.add_task(model,
work_schedule=maintenance, name="Condenser coil cleaning")
# These subtasks represent the punch list of maintenance tasks.
ifcopenshell.api.sequence.add_task(model, parent_task=cleaning, identification="1",
description="Prior to work, wear safety shoes, gloves, and goggles.")
ifcopenshell.api.sequence.add_task(model, parent_task=cleaning, identification="2",
description="Prepare jet pump, screwdriver, hose clamp, and control panel door key.")
ifcopenshell.api.sequence.add_task(model, parent_task=cleaning, identification="3",
description="Switch OFF the chiller unit.")
ifcopenshell.api.sequence.add_task(model, parent_task=cleaning, identification="3",
description="Open the isolator switch.")
ifcopenshell.api.sequence.add_task(model, parent_task=cleaning, identification="3",
description="Setup the water pressure by tapping to a water supply and connecting to a ...")
"""
task = ifcopenshell.api.root.create_entity(file, ifc_class="IfcTask", name=name, predefined_type=predefined_type)
if description:
task.Description = description
if identification:
task.Identification = identification
task.IsMilestone = False
if work_schedule:
ifcopenshell.api.control.assign_control(file, work_schedule, [task])
elif parent_task:
rel = ifcopenshell.api.nest.assign_object(
file,
related_objects=[task],
relating_object=parent_task,
)
if file.schema != "IFC2X3" and parent_task.Identification:
assert rel
task.Identification = parent_task.Identification + "." + str(len(rel.RelatedObjects))
return task
@@ -0,0 +1,66 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def add_task_time(
file: ifcopenshell.file, task: ifcopenshell.entity_instance, is_recurring: bool = False
) -> ifcopenshell.entity_instance:
"""Adds a task time to a task
Some tasks, such as activities within a work breakdown structure or
overall maintenance tasks will have time related information. This
includes start dates, durations, end dates, and possible recurring times
(especially for maintenance tasks).
:param task: The task to add time data to.
:param is_recurring: Whether or not the time should recur.
:return: The newly created IfcTaskTime.
Example:
.. code:: python
# Let's imagine we are creating a construction schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Create a portion of a work breakdown structure.
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
superstructure = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Superstructure", identification="C3")
task = ifcopenshell.api.sequence.add_task(model,
parent_task=superstructure, name="Ground Floor FRP", identification="C3.1")
# Add time data. Note that time data is blank by default.
time = ifcopenshell.api.sequence.add_task_time(model, task=task)
# Let's say our task starts on the first of January when everybody
# is still drunk from the new years celebration, and lasts for 2
# days. Note we don't need to specify the end date, as that is
# derived from the start plus the duration. In this simple example,
# no calendar has been specified, so we are working 24/7. Yikes!
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
"""
if is_recurring:
task_time = file.create_entity("IfcTaskTimeRecurring")
else:
task_time = file.create_entity("IfcTaskTime")
task.TaskTime = task_time
return task_time
@@ -0,0 +1,90 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from datetime import time
from typing import Optional, Union
import ifcopenshell.api
import ifcopenshell.util.date
import ifcopenshell.util.sequence
def add_time_period(
file: ifcopenshell.file,
recurrence_pattern: ifcopenshell.entity_instance,
start_time: Optional[Union[str, time]] = None,
end_time: Optional[Union[str, time]] = None,
) -> ifcopenshell.entity_instance:
"""Adds a time period to a recurrence pattern
A recurring time may be an all-day event, or only during certain time
periods of the day. For example, you might say that every 1st of January
recurring is a public holiday, which is an all-day event. Alternatively,
you might say that you work every (i.e. recurringly) Monday to Friday,
from 9am to 5pm. The 9am to 5pm is the time period.
There may also be multiple recurrence patterns, such as from 9am to
12pm, and then another from 1pm to 5pm (to indicate an hour break for
lunch).
:param recurrence_pattern: The IfcRecurrencePattern to add the time
period to. See ifcopenshell.api.sequence.assign_recurrence_pattern.
:param start_time: The start time of the time period, in a format
compatible with IfcTime, such as an ISO format time string or a
datetime.time object.
:param end_time: The end time of the time period, in a format
compatible with IfcTime, such as an ISO format time string or a
datetime.time object.
:return: The newly created IfcTimePeriod
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# State that we work from weekdays 1 to 5 (i.e. Monday to Friday)
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
# The morning work session, lunch, then the afternoon work session.
ifcopenshell.api.sequence.add_time_period(model,
recurrence_pattern=pattern, start_time="09:00", end_time="12:00")
ifcopenshell.api.sequence.add_time_period(model,
recurrence_pattern=pattern, start_time="13:00", end_time="17:00")
"""
time_period = file.create_entity("IfcTimePeriod")
time_period.StartTime = ifcopenshell.util.date.datetime2ifc(start_time, "IfcTime")
time_period.EndTime = ifcopenshell.util.date.datetime2ifc(end_time, "IfcTime")
time_periods = list(recurrence_pattern.TimePeriods or [])
time_periods.append(time_period)
recurrence_pattern.TimePeriods = time_periods
ifcopenshell.util.sequence.is_working_day.cache_clear()
ifcopenshell.util.sequence.is_calendar_applicable.cache_clear()
return time_period
@@ -0,0 +1,92 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.project
import ifcopenshell.api.root
def add_work_calendar(
file: ifcopenshell.file, name: str = "Unnamed", predefined_type: str = "NOTDEFINED"
) -> ifcopenshell.entity_instance:
"""Add a work calendar
A work calendar defines when work is allowed to occur and when the
holidays are. This is a fundamental concept in construction planning.
Every task in a work schedule will have an associated calendar. Some
task and resources work 24/7, whereas others work Monday to Friday, or
5.5 day weeks, etc. This is important, as tasks durations may only occur
during working times in a work calendar.
Work calendars can also be used to associate with events, such as
indicating that during certain days and times of the year, motion
sensors should turn on the lights, and other smart building controls.
:param name: The name of the calendar. Typically something like
"5 Day Working Week" or "24/7".
:param predefined_type: The type of calendar, typically used to more
specifically define shifts, such as FIRSTSHIFT, SECONDSHIFT, or
THIRDSHIFT. Leave as NOTDEFINED for basic calendar usage.
:return: The newly created IfcWorkCalendar
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Add a root task to represent the construction tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model, name="5 Day Week")
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# State that we work from weekdays 1 to 5 (i.e. Monday to Friday), 9am to 5pm
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
ifcopenshell.api.sequence.add_time_period(model,
recurrence_pattern=pattern, start_time="09:00", end_time="17:00")
# We associate the calendar with the construction root task. All
# subtasks underneath the construction work task will also inherit
# this calendar by default (though you can override them).
ifcopenshell.api.control.assign_control(model, relating_control=calendar, related_objects=[task])
"""
work_calendar = ifcopenshell.api.root.create_entity(
file,
ifc_class="IfcWorkCalendar",
predefined_type=predefined_type,
name=name,
)
context = file.by_type("IfcContext")[0]
ifcopenshell.api.project.assign_declaration(
file,
definitions=[work_calendar],
relating_context=context,
)
return work_calendar
@@ -0,0 +1,82 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, time
from typing import Optional, Union
import ifcopenshell.api.owner.settings
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.sequence
def add_work_plan(
file: ifcopenshell.file,
name: Optional[str] = None,
predefined_type: str = "NOTDEFINED",
start_time: Optional[Union[str, time]] = None,
) -> ifcopenshell.entity_instance:
"""Add a new work plan
A work plan is a group of work schedules. Since work schedules may have
different purposes, such as for maintenance or construction scheduling,
baseline comparison, or phasing, work plans can be used to group related
work schedules. At a minimum, it is recommended to use work plans to
indicate whether the work schedules are for facility management or for
construction scheduling.
:param name: The name of the work plan. Recommended to be "Maintenance"
or "Construction" for the two main purposes.
:param predefined_type: The type of work plan, used for baselining.
Leave as "NOTDEFINED" if unsure.
:param start_time: The earliest start time when the schedules grouped
within the work plan are relevant.
:return: The newly created IfcWorkPlan
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# This is one of our schedules in our work plan.
schedule = ifcopenshell.api.sequence.add_work_schedule(model,
name="Construction Schedule A", work_plan=work_plan)
"""
start_time = start_time or datetime.now()
work_plan = ifcopenshell.api.root.create_entity(
file,
ifc_class="IfcWorkPlan",
predefined_type=predefined_type,
name=name,
)
work_plan.CreationDate = ifcopenshell.api.sequence.add_date_time(file, datetime.now())
user = ifcopenshell.api.owner.settings.get_user(file)
if user:
work_plan.Creators = [user.ThePerson]
work_plan.StartTime = ifcopenshell.api.sequence.add_date_time(file, datetime.now())
if file.schema != "IFC2X3":
context = file.by_type("IfcContext")[0]
ifcopenshell.api.project.assign_declaration(
file,
definitions=[work_plan],
relating_context=context,
)
return work_plan
@@ -0,0 +1,106 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, time
from typing import Optional, Union
import ifcopenshell.api.aggregate
import ifcopenshell.api.owner.settings
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.sequence
def add_work_schedule(
file: ifcopenshell.file,
name: str = "Unnamed",
predefined_type: str = "NOTDEFINED",
object_type: Union[str, None] = None,
start_time: Optional[Union[str, time]] = None,
work_plan: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Add a new work schedule
A work schedule is a group of tasks, where the tasks are typically
either for maintenance or for construction scheduling.
:param name: The name of the work schedule.
:param predefined_type: The type of schedule, chosen from ACTUAL,
BASELINE, and PLANNED. Typically you would start with PLANNED, then
convert to a BASELINE when changes are made with separate schedules,
then have a parallel ACTUAL schedule.
:param object_type: Work schedule Object Type. Should be provided
in case if ``predefined_type`` is USERDEFINED.
:param start_time: The earlier start time when the schedule is relevant.
May be represented with an ISO standard string.
:param work_plan: The IfcWorkPlan the schedule will be part of. If not
provided, the schedule will not be grouped in a work plan and would
exist as a top level schedule in the project. This is not
recommended.
:return: The newly created IfcWorkSchedule
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# Let's imagine this is one of our schedules in our work plan.
schedule = ifcopenshell.api.sequence.add_work_schedule(model,
name="Construction Schedule A", work_plan=work_plan)
# Add a root task to represent the design milestones, and major
# project phases.
ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Milestones", identification="A")
ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Design", identification="B")
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
"""
start_time = start_time or datetime.now()
work_schedule = ifcopenshell.api.root.create_entity(
file,
ifc_class="IfcWorkSchedule",
predefined_type=predefined_type,
name=name,
)
work_schedule.CreationDate = ifcopenshell.api.sequence.add_date_time(file, datetime.now())
user = ifcopenshell.api.owner.settings.get_user(file)
if user:
work_schedule.Creators = [user.ThePerson]
work_schedule.StartTime = ifcopenshell.api.sequence.add_date_time(file, start_time)
if object_type:
work_schedule.ObjectType = object_type
if work_plan:
ifcopenshell.api.aggregate.assign_object(
file,
products=[work_schedule],
relating_object=work_plan,
)
elif file.schema != "IFC2X3":
# TODO: this is an ambiguity by buildingSMART
# See https://forums.buildingsmart.org/t/is-the-ifcworkschedule-project-declaration-mutually-exclusive-to-aggregation-within-a-relating-ifcworkplan/3510
context = file.by_type("IfcContext")[0]
ifcopenshell.api.project.assign_declaration(
file,
definitions=[work_schedule],
relating_context=context,
)
return work_schedule
@@ -0,0 +1,84 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Literal
import ifcopenshell
TIME_TYPE = Literal["WorkingTimes", "ExceptionTimes"]
def add_work_time(
file: ifcopenshell.file, work_calendar: ifcopenshell.entity_instance, time_type: TIME_TYPE = "WorkingTimes"
) -> ifcopenshell.entity_instance:
"""Add either working times or holiday times to a calendar
A calendar defines when work occurs by defining working times and
holiday times. First, the working times are defined, then the holidays
may override the working times. For this reason, holidays are also known
as exception times. For example, you might define the working times as
every Monday to Friday, then define a few holidays in the year, such as
the 1st of January. If the 1st of January is on a weekday, it will
override the work time.
:param work_calendar: The IfcWorkCalendar to add the work or holiday
time definition to.
:param time_type: Either WorkingTimes or ExceptionTimes, depending on
what you want to define.
:return: The newly created IfcWorkTime
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# State that we work from weekdays 1 to 5 (i.e. Monday to Friday)
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
# Let's set some holidays
holidays = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="ExceptionTimes")
# We create a yearly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="YEARLY_BY_DAY_OF_MONTH")
# The holiday is every 1st of January
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"DayComponent": [1], "MonthComponent": [1]})
"""
work_time = file.create_entity("IfcWorkTime")
if time_type == "WorkingTimes":
working_times = list(work_calendar.WorkingTimes or [])
working_times.append(work_time)
work_calendar.WorkingTimes = working_times
elif time_type == "ExceptionTimes":
exception_times = list(work_calendar.ExceptionTimes or [])
exception_times.append(work_time)
work_calendar.ExceptionTimes = exception_times
return work_time
@@ -0,0 +1,89 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.date
def assign_lag_time(
file: ifcopenshell.file, rel_sequence: ifcopenshell.entity_instance, lag_value: str, duration_type: str = "WORKTIME"
) -> ifcopenshell.entity_instance:
"""Assign a lag time to a sequence relationship between tasks
A task sequence (e.g. finish to start) may optionally have a lag time
defined. This is a fundamental concept in construction scheduling. The
lag is defined as a duration, and the duration is typically either
calendar based (i.e. follows the working times and holidays of the
calendar) or elapsed time based (i.e. 24/7).
A sequence may only have a single lag time defined. Negative lag times
are allowed.
:param rel_sequence: The IfcRelSequence to assign the lag time to.
:param lag_value: An ISO standardised duration string.
:param duration_type: Choose from WORKTIME for the associated
calendar-based lag times (this is the most common scenario and is
recommended as a default), or ELAPSEDTIME to not follow the
calendar. You may also choose NOTDEFINED but the behaviour of this
is unclear.
:return: The newly created IfcLagTime
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's imagine a root construction task
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's imagine we're doing a typically formwork, reinforcement,
# pour sequence. Let's start with the formwork. It'll take us 2
# days.
formwork = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Formwork", identification="C.1")
time = ifcopenshell.api.sequence.add_task_time(model, task=formwork)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
# Now let's do the reinforcement. It'll take us another 2 days.
reinforcement = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Reinforcement", identification="C.2")
time = ifcopenshell.api.sequence.add_task_time(model, task=reinforcement)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
# Now let's say the formwork must finish before the reinforcement
# can start. This is a typical finish to start relationship (FS).
sequence = ifcopenshell.api.sequence.assign_sequence(model,
relating_process=formwork, related_process=reinforcement)
# Now typically there would be no lag time between formwork and
# reinforcement, but let's pretend that we had to allow 1 day gap
# for whatever reason.
ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D")
"""
duration = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration"))
lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=duration)
if rel_sequence.is_a("IfcRelSequence"):
if (current_lag_time := rel_sequence.TimeLag) and file.get_total_inverses(current_lag_time) == 1:
file.remove(current_lag_time)
rel_sequence.TimeLag = lag_time
return lag_time
@@ -0,0 +1,130 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
def assign_process(
file: ifcopenshell.file,
relating_process: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
"""Assigns an object as an input, control, or resource of a process
Processes work using the ICOM (Input, Controls, Outputs, Mechanisms)
paradigm in IFC. This process model is commonly used in modeling
manufacturing functions.
For example, processes (such as tasks) consume Inputs and transform them
into Outputs. The process may only occur within the limits of Controls
(e.g. cost items) and may require Mechanisms (ISO9000 calls them
Mechanisms, whereas IFC calls them resources, such as raw materials,
labour, or equipment).
+----------+
| Controls |
+----------+
|
V
+--------+ +---------+ +---------+
| Inputs | --> | Process | --> | Outputs |
+--------+ +---------+ +---------+
^
|
+-----------+
| Resources |
+-----------+
There are three main scenarios where an object may be related to a
task: defining inputs, controls, and resources of a process.
For inputs, a product (i.e. wall) may be defined as an input to a task,
such as when the task is to demolish the wall (i.e. the wall is an
input, and there is no output).
For controls, a cost item may be defined as a control to a task.
For resources, any construction resource may be assigned to a task.
.. warning::
This function creates an **Input** relationship
(``IfcRelAssignsToProcess``), meaning the product is *consumed* or
*operated on* by the task — the typical case is demolition or
maintenance.
If the task *constructs or installs* a product (e.g. erecting a wall
or fitting a window), use :func:`assign_product` instead, which
creates an **Output** relationship (``IfcRelAssignsToProduct``).
:param relating_process: The IfcProcess (typically IfcTask) that the
input, control, or resource is related to.
:param related_object: The IfcProduct (for input), IfcCostItem (for
control) or IfcConstructionResource (for resource).
:return: The newly created IfcRelAssignsToProcess relationship
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a demolition task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Demolish existing", identification="A", predefined_type="DEMOLITION")
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# The wall is an INPUT to the demolition task (it will be consumed).
ifcopenshell.api.sequence.assign_process(model, relating_process=task, related_object=wall)
# For a construction task that BUILDS a wall, use assign_product instead:
# build_task = ifcopenshell.api.sequence.add_task(model, ..., predefined_type="CONSTRUCTION")
# ifcopenshell.api.sequence.assign_product(model, relating_product=wall, related_object=build_task)
"""
if related_object.HasAssignments:
for assignment in related_object.HasAssignments:
if assignment.is_a("IfcRelAssignsToProcess") and assignment.RelatingProcess == relating_process:
return
operates_on = None
if relating_process.OperatesOn:
operates_on = relating_process.OperatesOn[0]
if operates_on:
related_objects = list(operates_on.RelatedObjects)
related_objects.append(related_object)
operates_on.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(file, element=operates_on)
else:
operates_on = file.create_entity(
"IfcRelAssignsToProcess",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"RelatedObjects": [related_object],
"RelatingProcess": relating_process,
}
)
return operates_on
@@ -0,0 +1,91 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
def assign_product(
file: ifcopenshell.file,
relating_product: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
"""Assigns a product to be produced as a result of a process
A construction task may result in products (e.g. a wall) being
constructed. These task "Outputs" are defined in IFC through product
relationships.
Not all tasks have Outputs. For example, maintenance tasks will
typically not have any outputs.
See ifcopenshell.api.sequence.assign_process for Inputs and other types
of process relationships that can be described in manufacturing
process modeling.
:param relating_product: The IfcProduct that was constructed as a result
of the task.
:param related_object: The IfcProcess (typically IfcTask) of the
construction task.
:return: The newly created IfcRelAssignsToProduct relationship
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a construction task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Build wall", identification="A", predefined_type="CONSTRUCTION")
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's construct that wall!
ifcopenshell.api.sequence.assign_product(model, relating_product=wall, related_object=task)
"""
if related_object.HasAssignments:
for assignment in related_object.HasAssignments:
if assignment.is_a("IfcRelAssignsToProduct") and assignment.RelatingProduct == relating_product:
return assignment
referenced_by = None
if relating_product.ReferencedBy:
referenced_by = relating_product.ReferencedBy[0]
if referenced_by:
related_objects = list(referenced_by.RelatedObjects)
related_objects.append(related_object)
referenced_by.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(file, element=referenced_by)
else:
referenced_by = file.create_entity(
"IfcRelAssignsToProduct",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"RelatedObjects": [related_object],
"RelatingProduct": relating_product,
}
)
return referenced_by
@@ -0,0 +1,118 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.util.sequence
def assign_recurrence_pattern(
file: ifcopenshell.file,
parent: ifcopenshell.entity_instance,
recurrence_type: ifcopenshell.util.sequence.RECURRENCE_TYPE = "WEEKLY",
) -> ifcopenshell.entity_instance:
"""Define a time to recur at a particular interval
There are two scenarios where you might want to define a recurring time
pattern.
You might want a task to be scheduled at a recurring interval,
this is common for maintenance tasks which need to be performed monthly,
every 6 months, every year, etc.
Alternatively, you might be defining a work calendar, which defines
working days or holidays. The working days might be every week from
monday to friday ("every" week means it recurs every week), or the
holidays might be the same every year.
The types of recurrence are:
- DAILY: every Nth (interval) day for up to X (Occurrences) occurrences.
e.g. Every day, every 2 days, every day up to 5 times, etc
- WEEKLY: every Nth (interval) MTWTFSS (WeekdayComponent) for up to X
(Occurrences) occurrences. e.g. Every Monday, every weekday, every
other saturday, etc
- MONTHLY_BY_DAY_OF_MONTH: every Nth (DayComponent) of every Xth
(Interval) Month up to Y (Occurrences) occurrences. e.g. Every 15th of
the Month.
- MONTHLY_BY_POSITION: Every Nth (Position) MTWTFSS (WeekdayComponent)
of every Xth (Interval) Month up to Y (Occurrences) occurrences. e.g.
Every second Tuesday of the Month.
- YEARLY_BY_DAY_OF_MONTH: every Nth (DayComponent) of every JFMAMJJASOND
(MonthComponent) month of every Yth (Interval) Year up to Z
(Occurrences) occurrences. e.g. every 25th of December.
- YEARLY_BY_POSITION: every Nth (Position) MTWTFSS (WeekdayComponent) of
every JFMAMJJASOND (MonthComponent) month of every Yth (Interval)
Year up to Z (Occurrences) occurrences. e.g. every third Wednesday
of January.
These recurrence patterns are fairly standard in all calendar and
scheduling applications.
:param parent: Either an IfcTaskTimeRecurring if you are defining a
recurring schedule for a task, or IfcWorkTime if you are defining a
recurring pattern for a workdays or holidays in a calendar.
:param recurrence_type: One of the types of recurrences.
:return: The newly created IfcRecurrencePattern
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# State that we work from weekdays 1 to 5 (i.e. Monday to Friday)
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
# Let's imagine we are creating a maintenance schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Equipment Maintenance")
# Now let's imagine we have a task to maintain the chillers
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Chiller maintenance")
# Because it is a maintenance task, we must schedule a recurring time
time = ifcopenshell.api.sequence.add_task_time(model, task=task, is_recurring=True)
# We create a monthly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="MONTHLY_BY_DAY_OF_MONTH")
# Specifically, the maintenance task must occur every 6 months
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"DayComponent": [1], "Interval": 6})
"""
recurrence = file.create_entity("IfcRecurrencePattern", recurrence_type)
if parent.is_a("IfcWorkTime"):
if (old_recurrence := parent.RecurrencePattern) and file.get_total_inverses(old_recurrence) == 1:
file.remove(old_recurrence)
parent.RecurrencePattern = recurrence
elif parent.is_a("IfcTaskTimeRecurring"):
if (recurrence_old := parent.Recurrence) and file.get_total_inverses(recurrence_old) == 1:
file.remove(recurrence_old)
parent.Recurrence = recurrence
return recurrence
@@ -0,0 +1,123 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.api.sequence
import ifcopenshell.guid
def assign_sequence(
file: ifcopenshell.file,
relating_process: ifcopenshell.entity_instance,
related_process: ifcopenshell.entity_instance,
sequence_type: str = "FINISH_START",
) -> ifcopenshell.entity_instance:
"""Assign a sequential relationship between tasks
Tasks in construction sequencing typically have sequence relationships
between them, indicating that one task must happen after another. This
is used to automatically compute new start and end dates and cascade
changes when dates are changed. This is also used to calculate critical
paths and floats.
There are four types of sequence relationships, known as finish to
start, finish to finish, start to start, and start to finish, sometimes
abbreviated as a (FS, FF, SS, and SF). The most common is the finish to
start relationship, indicating that the previous task must finish before
the next task can start.
You must not create cyclical task sequences. This makes the computer
unhappy.
Note that "previous" or "next" does not necessarily mean the task
chronologically happens before or after. They simply indicate the order
of the sequence relationship. For this reason, they are often called
predecessor and successor tasks in the planning profession.
:param relating_process: The previous / predecessor task.
:param related_process: The next / successor task.
:param sequence_type: Choose from FINISH_START, FINISH_FINISH,
START_START, or START_FINISH.
:return: The newly created IfcRelSequence
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's imagine a root construction task
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's imagine we're doing a typically formwork, reinforcement,
# pour sequence. Let's start with the formwork. It'll take us 2
# days.
formwork = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Formwork", identification="C.1")
time = ifcopenshell.api.sequence.add_task_time(model, task=formwork)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
# Now let's do the reinforcement. It'll take us another 2 days.
reinforcement = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Reinforcement", identification="C.2")
time = ifcopenshell.api.sequence.add_task_time(model, task=reinforcement)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
# Now the pour it It'll only take 1 day.
pour = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Reinforcement", identification="C.3")
time = ifcopenshell.api.sequence.add_task_time(model, task=pour)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P1D"})
# Now let's say the formwork must finish before the reinforcement
# can start, and the reinforcement must finish before the pour can
# start. This is a typical finish to start relationship (FS).
ifcopenshell.api.sequence.assign_sequence(model,
relating_process=formwork, related_process=reinforcement)
ifcopenshell.api.sequence.assign_sequence(model,
relating_process=reinforcement, related_process=pour)
# Notice how we set all the scheduled start dates arbitrarily at
# 2000-01-01. This is because we can ask IfcOpenShell to
# automatically cascade the dates, starting from any task. This will
# update the reinforcement date to be 2000-01-03 and the pour date
# to be 2000-01-05.
ifcopenshell.api.sequence.cascade_schedule(model, task=formwork)
"""
for rel in related_process.IsSuccessorFrom or []:
if rel.RelatingProcess == relating_process:
return rel
rel = file.create_entity(
"IfcRelSequence",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"RelatingProcess": relating_process,
"RelatedProcess": related_process,
"SequenceType": sequence_type,
}
)
ifcopenshell.api.sequence.cascade_schedule(file, task=relating_process)
return rel
@@ -0,0 +1,64 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Union
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.project
def assign_work_plan(
file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance, work_plan: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns a work schedule to a work plan
Typically, work schedules would be assigned to a work plan at creation.
However you may also delay this and do it manually afterwards.
:param work_schedule: The IfcWorkSchedule that will be assigned to the
work plan.
:param work_plan: The IfcWorkPlan for the schedule to be assigned to.
:return: The IfcRelAggregates relationship
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# Alternatively, if you create a schedule without a work plan ...
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# ... you can assign the work plan afterwards.
ifcopenshell.api.sequence.assign_work_plan(work_schedule=schedule, work_plan=work_plan)
"""
# TODO: this is an ambiguity by buildingSMART
# See https://forums.buildingsmart.org/t/is-the-ifcworkschedule-project-declaration-mutually-exclusive-to-aggregation-within-a-relating-ifcworkplan/3510
ifcopenshell.api.project.unassign_declaration(
file,
definitions=[work_schedule],
relating_context=file.by_type("IfcContext")[0],
)
rel_aggregates = ifcopenshell.api.aggregate.assign_object(
file,
products=[work_schedule],
relating_object=work_plan,
)
return rel_aggregates
@@ -0,0 +1,148 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import math
from typing import Union
import ifcopenshell.api.sequence
import ifcopenshell.util.date
import ifcopenshell.util.element
def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None:
"""Calculates the task duration based on resource usage
If a task has labour or equipment resources assigned to it, its duration
may be parametrically derived from the scheduled work of the resource.
For example, a labour resource with scheduled work of 10 working days
and a resource utilisation of 200% (i.e. two labour teams) will imply
that the task duration is 5 working days.
If this data is not available, such as if the task has no resources,
then nothing happens.
:param task: The IfcTask to calculate the duration for.
:return: None
Example:
.. code:: python
# Add our own crew
crew = ifcopenshell.api.resource.add_resource(model, ifc_class="IfcCrewResource")
# Add some labour to our crew.
labour = ifcopenshell.api.resource.add_resource(model,
parent_resource=crew, ifc_class="IfcLaborResource")
# Labour resource is quantified in terms of time.
quantity = ifcopenshell.api.resource.add_resource_quantity(model,
resource=labour, ifc_class="IfcQuantityTime")
# Store the unit time used in hours
ifcopenshell.api.resource.edit_resource_quantity(model,
physical_quantity=quantity, attributes={"TimeValue": 8.0})
# Let's imagine we've used the resource for 10 days with a
# utilisation of 200%.
time = ifcopenshell.api.resource.add_resource_time(model, resource=labour)
ifcopenshell.api.resource.edit_resource_time(model,
resource_time=time, attributes={"ScheduleWork": "PT80H", "ScheduleUsage": 2})
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a construction task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Foundations", identification="A")
# Assign our resource to the task.
ifcopenshell.api.sequence.assign_process(model, relating_process=task, related_object=labour)
# Now we can calculate the task duration based on the resource. This
# will set task.TaskTime.ScheduleDuration to be P5D.
ifcopenshell.api.sequence.calculate_task_duration(model, task=task)
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(task)
class Usecase:
file: ifcopenshell.file
def execute(self, task: ifcopenshell.entity_instance) -> None:
self.task = task
self.seconds_per_workday = self.calculate_seconds_per_workday()
duration = self.calculate_max_resource_usage_duration()
if duration:
self.set_task_duration(duration)
def calculate_seconds_per_workday(self) -> float:
def get_work_schedule(task):
for rel in task.HasAssignments or []:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"):
return rel.RelatingControl
for rel in task.Nests or []:
return get_work_schedule(rel.RelatingObject)
default_seconds_per_workday = 8 * 60 * 60
work_schedule = get_work_schedule(self.task)
if not work_schedule:
return default_seconds_per_workday
psets = ifcopenshell.util.element.get_psets(work_schedule)
if (
not psets
or "Pset_WorkControlCommon" not in psets
or "WorkDayDuration" not in psets["Pset_WorkControlCommon"]
):
return default_seconds_per_workday
work_day_duration = ifcopenshell.util.date.ifc2datetime(psets["Pset_WorkControlCommon"]["WorkDayDuration"])
return work_day_duration.seconds
def calculate_max_resource_usage_duration(self) -> float:
max_duration = 0
for rel in self.task.OperatesOn or []:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcConstructionResource"):
duration = self.calculate_duration_in_days(related_object)
if duration and duration > max_duration:
max_duration = duration
return max_duration
def calculate_duration_in_days(self, resource: ifcopenshell.entity_instance) -> Union[float, None]:
def is_hourly_work(schedule_work):
return "T" in schedule_work
if not resource.Usage or not resource.Usage.ScheduleWork:
return
schedule_usage = resource.Usage.ScheduleUsage or 1
schedule_duration = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork)
if is_hourly_work(resource.Usage.ScheduleWork):
schedule_seconds = (schedule_duration.days * 24 * 60 * 60) + schedule_duration.seconds
else:
partial_days = schedule_duration.seconds / (24 * 60 * 60)
schedule_seconds = (schedule_duration.days + partial_days) * self.seconds_per_workday
return math.ceil((schedule_seconds / self.seconds_per_workday) / schedule_usage)
def set_task_duration(self, duration: float) -> None:
if not (task_time := self.task.TaskTime):
task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=self.task)
task_time.ScheduleDuration = f"P{duration}D"
@@ -0,0 +1,345 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import datetime
from typing import Optional, Union
import ifcopenshell.util.date
import ifcopenshell.util.sequence
from ifcopenshell.util.sequence import DURATION_TYPE
def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> 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.
:return: 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.sequence.add_task(model,
work_schedule=work_schedule, name=name, predefined_type="CONSTRUCTION")
# Give it a time
task_time = ifcopenshell.api.sequence.add_task_time(model, task=task)
# Arbitrarily set the task's scheduled time duration to be 1 week
ifcopenshell.api.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.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.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.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.sequence.cascade_schedule(model, task=start_task)
# Calculate the critical path and floats.
ifcopenshell.api.sequence.recalculate_schedule(model, work_schedule=schedule)
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(task)
class Usecase:
file: ifcopenshell.file
def execute(self, task: ifcopenshell.entity_instance):
self.calendar_cache = {}
self.cascade_task(task, is_first_task=True)
def cascade_task(
self,
task: ifcopenshell.entity_instance,
is_first_task: bool = False,
task_sequence: Optional[list[ifcopenshell.entity_instance]] = None,
) -> 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 RecursionError("Recursive tasks found. Could not cascade schedule.")
if not task.TaskTime:
return
duration = (
ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration)
if task.TaskTime.ScheduleDuration
else datetime.timedelta()
)
finishes = []
starts = []
for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "predecessor"):
predecessor = rel.RelatingProcess
predecessor_duration = (
ifcopenshell.util.date.ifc2datetime(predecessor.TaskTime.ScheduleDuration)
if predecessor.TaskTime and predecessor.TaskTime.ScheduleDuration
else datetime.timedelta()
)
if rel.SequenceType == "FINISH_START":
finish = self.get_task_time_attribute(predecessor, "ScheduleFinish")
if not finish:
continue
days = 0 if predecessor_duration.days == 0 else 1
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
)
duration_type = rel.TimeLag.DurationType
if days:
starts.append(
datetime.datetime.combine(
self.offset_date(finish, days, duration_type, self.get_calendar(task)),
datetime.time(9),
)
)
starts.append(
datetime.datetime.combine(
self.offset_date(
finish,
days,
duration_type,
self.get_calendar(predecessor),
),
datetime.time(9),
)
)
else:
starts.append(finish)
elif rel.SequenceType == "START_START":
start = self.get_task_time_attribute(predecessor, "ScheduleStart")
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
)
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)))
else:
starts.append(start)
elif rel.SequenceType == "FINISH_FINISH":
finish = self.get_task_time_attribute(predecessor, "ScheduleFinish")
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
)
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)))
else:
finishes.append(finish)
elif rel.SequenceType == "START_FINISH":
start = self.get_task_time_attribute(predecessor, "ScheduleStart")
if not start:
continue
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
)
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)),
datetime.time(17),
)
)
finishes.append(
datetime.datetime.combine(
self.offset_date(
start,
days,
duration_type,
self.get_calendar(predecessor),
),
datetime.time(17),
)
)
else:
finishes.append(start)
if starts and finishes:
start = max(starts)
finish = max(finishes)
potential_finish = ifcopenshell.util.sequence.get_start_or_finish_date(
start,
duration,
task.TaskTime.DurationType,
self.get_calendar(task),
date_type="FINISH",
)
if potential_finish > finish:
start_ifc = ifcopenshell.util.date.datetime2ifc(start, "IfcDateTime")
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")
else:
finish_ifc = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
if task.TaskTime.ScheduleFinish == finish_ifc and not is_first_task:
return
task.TaskTime.ScheduleFinish = finish_ifc
task.TaskTime.ScheduleStart = ifcopenshell.util.date.datetime2ifc(
ifcopenshell.util.sequence.get_start_or_finish_date(
finish,
duration,
task.TaskTime.DurationType,
self.get_calendar(task),
date_type="START",
),
"IfcDateTime",
)
elif finishes:
finish = max(finishes)
finish_ifc = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
if task.TaskTime.ScheduleFinish == finish_ifc and not is_first_task:
return
task.TaskTime.ScheduleFinish = finish_ifc
task.TaskTime.ScheduleStart = ifcopenshell.util.date.datetime2ifc(
ifcopenshell.util.sequence.get_start_or_finish_date(
finish,
duration,
task.TaskTime.DurationType,
self.get_calendar(task),
date_type="START",
),
"IfcDateTime",
)
elif starts:
start = max(starts)
start_ifc = ifcopenshell.util.date.datetime2ifc(start, "IfcDateTime")
if task.TaskTime.ScheduleStart == start_ifc and not is_first_task:
return
task.TaskTime.ScheduleStart = start_ifc
task.TaskTime.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(
ifcopenshell.util.sequence.get_start_or_finish_date(
start,
duration,
task.TaskTime.DurationType,
self.get_calendar(task),
date_type="FINISH",
),
"IfcDateTime",
)
for rel in task.IsPredecessorTo:
self.cascade_task(rel.RelatedProcess, task_sequence=task_sequence + [task])
for rel in task.IsNestedBy:
[
self.cascade_task(nested_task, task_sequence=task_sequence + [task])
for nested_task in rel.RelatedObjects or []
]
def get_lag_time_days(self, lag_time: ifcopenshell.entity_instance) -> int:
return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days
def get_calendar(self, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if task.id() not in self.calendar_cache:
self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(task)
return self.calendar_cache[task.id()]
def offset_date(
self, date: datetime.datetime, days: int, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance
) -> datetime.datetime:
return ifcopenshell.util.sequence.offset_date(date, datetime.timedelta(days=days), duration_type, calendar)
def get_task_time_attribute(
self, task: ifcopenshell.entity_instance, attribute: str
) -> Union[datetime.datetime, None]:
if task.TaskTime:
value = getattr(task.TaskTime, attribute)
if value:
return ifcopenshell.util.date.ifc2datetime(value)
@@ -0,0 +1,51 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.control
import ifcopenshell.api.sequence
import ifcopenshell.util.element
def copy_work_schedule(
file: ifcopenshell.file,
work_schedule: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
"""Copy a work schedule.
:param work_schedule: IfcWorkSchedule to copy.
:return: The duplicated IfcWorkSchedule entity.
Example:
.. code:: python
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
schedule = ifcopenshell.api.sequence.add_work_schedule(model,
name="Construction Schedule A", work_plan=work_plan)
new_schedule = ifcopenshell.api.sequence.copy_work_schedule(model, schedule)
"""
# Shared code logic with copy_cost_schedule.
new_schedule = ifcopenshell.util.element.copy(file, work_schedule)
for rel in work_schedule.Controls:
for task in rel.RelatedObjects:
duplicated_tasks = ifcopenshell.api.sequence.duplicate_task(file, task)[1]
# All other nested items are not connected to the work schedule explicitly.
duplicated_task = duplicated_tasks[0]
ifcopenshell.api.control.assign_control(file, new_schedule, [duplicated_task])
return new_schedule
@@ -0,0 +1,105 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021-2022 Dion Moult <dion@thinkmoult.com>, Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, Union
import ifcopenshell
import ifcopenshell.api.control
import ifcopenshell.api.owner
import ifcopenshell.api.sequence
import ifcopenshell.guid
import ifcopenshell.util.sequence
def create_baseline(
file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance, name: Optional[str] = None
) -> None:
"""Creates a baseline for your Work Schedule
Using a IfcWorkSchdule having PredefinedType=PLANNED,
We can create a baseline for our work schedule. This IfcWorkSchedule will have PredefinedType=BASELINE
and the IfcWorkSchedule.CreationDate indicating the date of the baseline creation, and IfcWorkSchedule.Name indicating the name of the baseline.
The following relationships are also baselined:
* Same Tasks & attributes
* Same Task Relationships
* Same Construction Resources
* Same Resource Relationships
:param work_schedule: The planned work_schedule to baseline
:param name: baseline work schedule name
:return: The baseline work_schedule
Example:
.. code:: python
# We have a Work Schedule
planned_work_schedule = WorkSchedule(name="Design new feature",predefinedType="PLANNED", deadline="2023-03-01")
# And now we have a baseline for our Work Schedule
baseline_work_schedule = ifcopenshell.api.sequence.create_baseline(file, work_schedule=planned_work_schedule, name="Baseline 1")
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(work_schedule, name)
class Usecase:
file: ifcopenshell.file
def execute(self, work_schedule: ifcopenshell.entity_instance, name: Union[str, None]) -> None:
# create work schedule
if not work_schedule.PredefinedType == "PLANNED":
return
baseline_work_schedule = ifcopenshell.api.sequence.add_work_schedule(
self.file, name=work_schedule.Name, predefined_type="BASELINE"
)
baseline_work_schedule.Name = name
self.create_baseline_reference(work_schedule, baseline_work_schedule)
for summary_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
res = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task)
assert isinstance(res, list)
current, duplicate = res
ifcopenshell.api.control.assign_control(
self.file, relating_control=baseline_work_schedule, related_objects=[duplicate[0]]
)
for i, task in enumerate(current):
self.create_baseline_reference(task, duplicate[i])
def create_baseline_reference(
self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
referenced_by = None
if relating_object.Declares:
referenced_by = relating_object.Declares[0]
if referenced_by:
related_objects = list(referenced_by.RelatedObjects)
related_objects.append(related_object)
referenced_by.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(self.file, element=referenced_by)
else:
referenced_by = self.file.create_entity(
"IfcRelDefinesByObject",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.owner.create_owner_history(self.file),
RelatedObjects=[related_object],
RelatingObject=relating_object,
)
return referenced_by
@@ -0,0 +1,193 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021-2022 Dion Moult <dion@thinkmoult.com>, Yassine Oualid <yassine@sigmadimensions.com>
#
# This self.file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.nest
import ifcopenshell.api.owner
import ifcopenshell.api.sequence
import ifcopenshell.guid
import ifcopenshell.util.date
import ifcopenshell.util.element
# TODO: inconsistent name with other copy_xxx api methods.
def duplicate_task(
file: ifcopenshell.file, task: ifcopenshell.entity_instance
) -> tuple[list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance]]:
"""Duplicates a task in the project
The following relationships are also duplicated:
* The copy will have the same attributes and property sets as the original task
* The copy will be assigned to the parent task or work schedule
* The copy will have duplicated nested tasks
:param task: The task to be duplicated
:return: A tuple that consists of two lists of tasks:
- Original task and it's nested tasks.
- Their corresponding duplicated tasks.
Example:
.. code:: python
# We have a task
original_task = ifcopenshell.api.sequence.add_task(
model, work_schedule=work_schedule,
name="Design new feature",
)
# And now we have two
original_tasks, duplicated_tasks = ifcopenshell.api.sequence.duplicate_task(original_task)
print(duplicated_tasks[0]) # A copy of ``original_task``.
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(task)
class Usecase:
file: ifcopenshell.file
current: list[ifcopenshell.entity_instance]
duplicate: list[ifcopenshell.entity_instance]
def execute(
self, task: ifcopenshell.entity_instance
) -> tuple[list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance]]:
self.current = []
self.duplicate = []
self.duplicate_task(task)
self.copy_sequence_relationship()
return self.current, self.duplicate
def duplicate_task(self, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
new_task = ifcopenshell.util.element.copy_deep(self.file, task)
self.current.append(task)
self.duplicate.append(new_task)
self.copy_indirect_attributes(task, new_task)
return new_task
def copy_indirect_attributes(
self, from_element: ifcopenshell.entity_instance, to_element: ifcopenshell.entity_instance
) -> None:
for inverse in self.file.get_inverse(from_element):
if inverse.is_a("IfcRelDefinesByProperties"):
inverse = ifcopenshell.util.element.copy(self.file, inverse)
inverse.RelatedObjects = [to_element]
pset = ifcopenshell.util.element.copy_deep(self.file, inverse.RelatingPropertyDefinition)
inverse.RelatingPropertyDefinition = pset
elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element:
nested_tasks = [e for e in inverse.RelatedObjects]
if nested_tasks:
new_tasks = []
for t in nested_tasks:
new_task = self.duplicate_task(t)
new_tasks.append(new_task)
inverse = ifcopenshell.util.element.copy(self.file, inverse)
inverse.RelatingObject = to_element
inverse.RelatedObjects = new_tasks
ifcopenshell.api.nest.unassign_object(self.file, related_objects=new_tasks)
ifcopenshell.api.nest.assign_object(
self.file,
related_objects=new_tasks,
relating_object=to_element,
)
elif inverse.is_a("IfcRelSequence") and (
inverse.RelatingProcess == from_element or inverse.RelatedProcess == from_element
):
continue
elif inverse.is_a("IfcRelAssignsToControl") and inverse.RelatingControl.is_a("IfcWorkSchedule"):
continue
elif inverse.is_a("IfcRelDefinesByObject"):
continue
else:
for i, value in enumerate(inverse):
if value == from_element:
new_inverse = ifcopenshell.util.element.copy(self.file, inverse)
new_inverse[i] = to_element
elif isinstance(value, (tuple, list)) and from_element in value:
new_value = list(value)
new_value.append(to_element)
inverse[i] = new_value
def copy_sequence_relationship(self) -> None:
original_tasks = self.current
duplicated_tasks = self.duplicate
for i, original_task in enumerate(original_tasks):
for inverse in self.file.get_inverse(original_task):
if inverse.is_a("IfcRelSequence") and (
inverse.RelatingProcess == original_task or inverse.RelatedProcess == original_task
):
original_task_index = original_tasks.index(original_task)
duplicated_task = duplicated_tasks[original_task_index]
relating_process, related_process = None, None
if inverse.RelatingProcess == original_task:
relating_process = duplicated_task
else:
related_process = duplicated_task
if inverse.RelatedProcess in original_tasks:
related_process_index = original_tasks.index(inverse.RelatedProcess)
related_process = duplicated_tasks[related_process_index]
else: # thus the related process is not part of the duplicated tasks
related_process = inverse.RelatedProcess
if inverse.RelatingProcess in original_tasks:
relating_process_index = original_tasks.index(inverse.RelatingProcess)
relating_process = duplicated_tasks[relating_process_index]
else: # thus the relating process is not part of the duplicated tasks
relating_process = inverse.RelatingProcess
if relating_process and related_process:
rel = ifcopenshell.api.sequence.assign_sequence(
self.file,
relating_process=relating_process,
related_process=related_process,
)
if inverse.TimeLag:
ifcopenshell.api.sequence.assign_lag_time(
self.file,
rel_sequence=rel,
lag_value=(
ifcopenshell.util.date.ifc2datetime(inverse.TimeLag.LagValue.wrappedValue)
if inverse.TimeLag.LagValue
else None
),
duration_type=inverse.TimeLag.DurationType,
)
def create_object_reference(
self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
referenced_by = None
if relating_object.Declares:
referenced_by = relating_object.Declares[0]
if referenced_by:
related_objects = list(referenced_by.RelatedObjects)
related_objects.append(related_object)
referenced_by.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(self.file, element=referenced_by)
else:
referenced_by = self.file.create_entity(
"IfcRelDefinesByObject",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file),
"RelatedObjects": [related_object],
"RelatingObject": relating_object,
}
)
return referenced_by
@@ -0,0 +1,84 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell.api.sequence
import ifcopenshell.util.date
def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcLagTime
For more information about the attributes and data types of an
IfcLagTime, consult the IFC documentation.
:param lag_time: The IfcLagTime entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's imagine a root construction task
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's imagine we're doing a typically formwork, reinforcement,
# pour sequence. Let's start with the formwork. It'll take us 2
# days.
formwork = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Formwork", identification="C.1")
time = ifcopenshell.api.sequence.add_task_time(model, task=formwork)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
# Now let's do the reinforcement. It'll take us another 2 days.
reinforcement = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Reinforcement", identification="C.2")
time = ifcopenshell.api.sequence.add_task_time(model, task=reinforcement)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
# Now let's say the formwork must finish before the reinforcement
# can start. This is a typical finish to start relationship (FS).
sequence = ifcopenshell.api.sequence.assign_sequence(model,
relating_process=formwork, related_process=reinforcement)
# Now typically there would be no lag time between formwork and
# reinforcement, but let's pretend that we had to allow 1 day gap
# for whatever reason.
lag = ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D")
# Or, let's make it 2 days instead.
ifcopenshell.api.sequence.edit_lag_time(model, lag_time=lag, attributes={"LagValue": "P2D"})
"""
for name, value in attributes.items():
if name == "LagValue" and value is not None:
if isinstance(value, float):
value = file.createIfcRatioMeasure(value)
else:
value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(value, "IfcDuration"))
setattr(lag_time, name, value)
for rel in [r for r in file.get_inverse(lag_time) if r.is_a("IfcRelSequence")]:
ifcopenshell.api.sequence.cascade_schedule(file, task=rel.RelatedProcess)
@@ -0,0 +1,60 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell
import ifcopenshell.util.sequence
def edit_recurrence_pattern(
file: ifcopenshell.file, recurrence_pattern: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcRecurrencePattern
For more information about the attributes and data types of an
IfcRecurrencePattern, consult the IFC documentation.
:param recurrence_pattern: The IfcRecurrencePattern entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# State that we work from weekdays 1 to 5 (i.e. Monday to Friday)
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
"""
for name, value in attributes.items():
setattr(recurrence_pattern, name, value)
ifcopenshell.util.sequence.is_working_day.cache_clear()
ifcopenshell.util.sequence.is_calendar_applicable.cache_clear()
@@ -0,0 +1,66 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell
import ifcopenshell.api.sequence
def edit_sequence(
file: ifcopenshell.file, rel_sequence: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcRelSequence
For more information about the attributes and data types of an
IfcRelSequence, consult the IFC documentation.
:param rel_sequence: The IfcRelSequence entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's imagine a root construction task
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's imagine we're building 2 zones, one after another.
zone1 = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Zone 1", identification="C.1")
zone2 = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Zone 2", identification="C.2")
# Zone 1 finishes, then zone 2 starts.
sequence = ifcopenshell.api.sequence.assign_sequence(model,
relating_process=zone1, related_process=zone2)
# What if they both started at the same time?
ifcopenshell.api.sequence.edit_sequence(model,
rel_sequence=sequence, attributes={"SequenceType": "START_START"})
"""
for name, value in attributes.items():
setattr(rel_sequence, name, value)
if "SequenceType" in attributes.keys():
ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess)
@@ -0,0 +1,50 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell
def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcTask
For more information about the attributes and data types of an
IfcTask, consult the IFC documentation.
:param task: The IfcTask entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Add a root task to represent the design milestones, and major
# project phases.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Milestones", identification="A")
# Change the identification
ifcopenshell.api.sequence.edit_task(model, task=task, attributes={"Identification": "M"})
"""
for name, value in attributes.items():
setattr(task, name, value)
@@ -0,0 +1,153 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import datetime
from typing import Any
import ifcopenshell.api.resource
import ifcopenshell.api.sequence
import ifcopenshell.util.constraint
import ifcopenshell.util.date
import ifcopenshell.util.sequence
def edit_task_time(
file: ifcopenshell.file,
task_time: ifcopenshell.entity_instance,
attributes: dict[str, Any],
) -> None:
"""Edits the attributes of an IfcTaskTime
For more information about the attributes and data types of an
IfcTaskTime, consult the IFC documentation.
:param task_time: The IfcTaskTime entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Create a task to do formwork
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Formwork", identification="A")
# Let's say it takes 2 days and starts on the 1st of January, 2000
time = ifcopenshell.api.sequence.add_task_time(model, task=formwork)
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(task_time, attributes)
class Usecase:
file: ifcopenshell.file
def execute(self, task_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
self.task_time = task_time
self.task = self.get_task()
self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task)
# If the user specifies both an end date and a duration, the duration takes priority
if attributes.get("ScheduleDuration", None) and "ScheduleFinish" in attributes.keys():
del attributes["ScheduleFinish"]
duration_type = attributes.get("DurationType", self.task_time.DurationType)
finish = attributes.get("ScheduleFinish", None)
if finish:
if isinstance(finish, str):
finish = datetime.datetime.fromisoformat(finish)
attributes["ScheduleFinish"] = datetime.datetime.combine(
ifcopenshell.util.sequence.get_soonest_working_day(finish, duration_type, self.calendar),
datetime.time(17),
)
start = attributes.get("ScheduleStart", None)
if start:
if isinstance(start, str):
start = datetime.datetime.fromisoformat(start)
attributes["ScheduleStart"] = datetime.datetime.combine(
ifcopenshell.util.sequence.get_soonest_working_day(start, duration_type, self.calendar),
datetime.time(9),
)
for name, value in attributes.items():
if value is not None:
if "Start" in name or "Finish" in name or name == "StatusTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "ScheduleDuration" or name == "ActualDuration" or name == "RemainingTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
setattr(self.task_time, name, value)
if "ScheduleDuration" in attributes.keys() and task_time.ScheduleDuration and task_time.ScheduleStart:
self.calculate_finish()
elif attributes.get("ScheduleStart", None) and task_time.ScheduleDuration:
self.calculate_finish()
elif attributes.get("ScheduleFinish", None) and task_time.ScheduleStart:
self.calculate_duration()
if task_time.ScheduleDuration and (
"ScheduleStart" in attributes.keys()
or "ScheduleFinish" in attributes.keys()
or "ScheduleDuration" in attributes.keys()
):
ifcopenshell.api.sequence.cascade_schedule(self.file, task=self.task)
if task_time.ScheduleDuration:
self.handle_resource_calculation()
def calculate_finish(self):
finish = ifcopenshell.util.sequence.get_start_or_finish_date(
ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart),
ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleDuration),
self.task_time.DurationType,
self.calendar,
date_type="FINISH",
)
self.task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
def calculate_duration(self):
start = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart)
finish = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleFinish)
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
duration = datetime.timedelta(days=1)
while current_date < finish_date:
if self.task_time.DurationType == "ELAPSEDTIME" or not self.calendar:
duration += datetime.timedelta(days=1)
elif ifcopenshell.util.sequence.is_working_day(current_date, self.calendar):
duration += datetime.timedelta(days=1)
current_date += datetime.timedelta(days=1)
self.task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration")
def get_task(self) -> ifcopenshell.entity_instance:
return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
def handle_resource_calculation(self):
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_recursive=False)
for resource in resources:
if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"):
ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource)
# TODO: If the duration changes, this implies the productivity rate must change to accomModate the new Schedule Work to be calculated.
# elif ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleUsage"):
# ifcopenshell.api.resource.calculate_resource_work(self.file, resource=resource)
@@ -0,0 +1,47 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell
def edit_work_calendar(
file: ifcopenshell.file, work_calendar: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcWorkCalendar
For more information about the attributes and data types of an
IfcWorkCalendar, consult the IFC documentation.
:param work_calendar: The IfcWorkCalendar entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model, name="5 Day Week")
# Let's give it a description
ifcopenshell.api.sequence.edit_work_calendar(model,
work_calendar=calendar, attributes={"Description": "Monday to Friday 8 hour days"})
"""
for name, value in attributes.items():
setattr(work_calendar, name, value)
@@ -0,0 +1,53 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell.util.date
def edit_work_plan(
file: ifcopenshell.file, work_plan: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcWorkPlan
For more information about the attributes and data types of an
IfcWorkPlan, consult the IFC documentation.
:param work_plan: The IfcWorkPlan entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# Let's give it a description
ifcopenshell.api.sequence.edit_work_plan(model,
work_plan=work_plan, attributes={"Description": "Construction of phase 1"})
"""
for name, value in attributes.items():
if value:
if "Date" in name or "Time" in name:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "Duration" or name == "TotalFloat":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
setattr(work_plan, name, value)
@@ -0,0 +1,57 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell.util.date
def edit_work_schedule(
file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcWorkSchedule
For more information about the attributes and data types of an
IfcWorkSchedule, consult the IFC documentation.
:param work_schedule: The IfcWorkSchedule entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# Let's imagine this is one of our schedules in our work plan.
schedule = ifcopenshell.api.sequence.add_work_schedule(model,
name="Construction Schedule A", work_plan=work_plan)
# Let's give it a description
ifcopenshell.api.sequence.edit_work_schedule(model,
work_schedule=work_schedule, attributes={"Description": "3 crane design option"})
"""
for name, value in attributes.items():
if value:
if "Date" in name or "Time" in name:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "Duration" or name == "TotalFloat":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
setattr(work_schedule, name, value)
@@ -0,0 +1,65 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
import ifcopenshell.util.date
def edit_work_time(
file: ifcopenshell.file,
work_time: ifcopenshell.entity_instance,
attributes: dict[str, Any],
) -> None:
"""Edits the attributes of an IfcWorkTime
For more information about the attributes and data types of an
IfcWorkTime, consult the IFC documentation.
:param work_time: The IfcWorkTime entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# If we don't specify any recurring time periods in our work time,
# we need to specify a start and end date of the work time. It
# starts at 0:00 on the start date and 24:00 at the end date.
ifcopenshell.api.sequence.edit_work_time(model,
work_time=work_time, attributes={"StartDate": "2000-01-01", "FinishDate": "2000-01-02"})
"""
for name, value in attributes.items():
if name in ("Start", "StartDate"):
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
# 4 IfcWorktime Start
work_time[4] = value
elif name in ("Finish", "FinishDate"):
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
# 5 IfcWorktime Finish
work_time[5] = value
else:
setattr(work_time, name, value)
@@ -0,0 +1,521 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import datetime
import networkx as nx
import ifcopenshell.api.sequence
import ifcopenshell.util.date
import ifcopenshell.util.sequence
def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance) -> None:
"""Calculate the critical path and floats for a work schedule
This implements critical path analysis, using the forward pass and
backward pass method. When run, any tasks that have no float will be
marked as critical, and both the total and free floats will be
populated for all task times.
Cyclical relationships are detected and will result in a recursion
error.
:param work_schedule: The IfcWorkSchedule to perform the calculation on.
:return: None
Example:
.. code:: python
# See the example for ifcopenshell.api.sequence.cascade_schedule for
# details of how to set up a basic set of tasks and calculate the
# critical path. Typically cascade_schedule is run prior to ensure
# that dates are correct.
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(work_schedule)
class Usecase:
file: ifcopenshell.file
def execute(self, work_schedule: ifcopenshell.entity_instance) -> None:
self.work_schedule = work_schedule
# The method implemented is the same as shown here:
# https://www.youtube.com/watch?v=qTErIV6OqLg
self.start_dates = []
self.build_network_graph()
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 RecursionError("Task graph is cyclic and so critical path method cannot be performed.")
self.pending_nodes = set(self.g.nodes)
while self.pending_nodes:
remaining_nodes = set()
for pending_node in self.pending_nodes:
if not self.backward_pass(pending_node):
remaining_nodes.add(pending_node)
self.pending_nodes = remaining_nodes
self.update_task_times()
def build_network_graph(self) -> None:
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, duration_type="ELAPSEDTIME", calendar=None)
self.g.add_node("finish", duration=0, duration_type="ELAPSEDTIME", calendar=None)
for rel in self.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: ifcopenshell.entity_instance) -> None:
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
duration_type = task.TaskTime.DurationType
else:
duration = 0
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(
[
(
rel.RelatingProcess.id(),
task.id(),
{
"lag_time": (
0
if not rel.TimeLag
else ifcopenshell.util.date.ifc2datetime(rel.TimeLag.LagValue.wrappedValue).days
),
"type": self.sequence_type_map[rel.SequenceType],
},
)
for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, sequence="predecessor")
]
)
predecessor_types = [
rel.SequenceType for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "predecessor")
]
successor_types = [
rel.SequenceType for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "successor")
]
if not 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))
self.g.nodes[task.id()]["early_start"] = ifcopenshell.util.date.ifc2datetime(
task.TaskTime.ScheduleStart
) # we assume this task is constrained to start on this date
if not successor_types:
self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FF"}))
def update_task_times(self) -> None:
for ifc_definition_id in self.g.nodes:
if ifc_definition_id in ("start", "finish"):
continue
data = self.g.nodes[ifc_definition_id]
task = self.file.by_id(ifc_definition_id)
if not task.TaskTime:
continue
ifcopenshell.api.sequence.edit_task_time(
self.file,
task_time=task.TaskTime,
attributes={
"FreeFloat": ifcopenshell.util.date.datetime2ifc(data["free_float"], "IfcDuration"),
"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: datetime.datetime, days: int, node: dict) -> datetime.datetime:
return ifcopenshell.util.sequence.offset_date(
date, datetime.timedelta(days=days), node["duration_type"], node["calendar"]
)
def forward_pass(self, node) -> bool:
successors = self.g.successors(node)
predecessors = list(self.g.predecessors(node))
data = self.g.nodes[node]
if node == "start":
data["early_start"] = min(self.start_dates)
else:
finishes = []
starts = []
if data.get("early_start") is not None:
data["early_finish"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["early_start"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="FINISH",
)
return True # we're done! We assume this task is constrained and finish processing it
for predecessor in predecessors:
predecessor_data = self.g.nodes[predecessor]
edge = self.g[predecessor][node]
if edge["type"] == "FS":
finish = predecessor_data.get("early_finish")
if finish is None:
return
days = 0 if predecessor_data["duration"] == 0 else 1
if edge["lag_time"]:
days += edge["lag_time"]
if days:
starts.append(datetime.datetime.combine(self.offset_date(finish, days, data), datetime.time(9)))
starts.append(
datetime.datetime.combine(
self.offset_date(finish, days, predecessor_data),
datetime.time(9),
)
)
else:
starts.append(finish)
elif edge["type"] == "SS":
start = predecessor_data.get("early_start")
if start is None:
return
if edge["lag_time"]:
starts.append(self.offset_date(start, edge["lag_time"], data))
starts.append(self.offset_date(start, edge["lag_time"], predecessor_data))
else:
starts.append(start)
elif edge["type"] == "FF":
finish = predecessor_data.get("early_finish")
if finish is None:
return
if edge["lag_time"]:
finishes.append(self.offset_date(finish, edge["lag_time"], data))
finishes.append(self.offset_date(finish, edge["lag_time"], predecessor_data))
else:
finishes.append(finish)
elif edge["type"] == "SF":
start = predecessor_data.get("early_start")
if start is None:
return
days = -1
if edge["lag_time"]:
days += edge["lag_time"]
if days or edge["lag_time"]:
finishes.append(
datetime.datetime.combine(self.offset_date(start, days, data), datetime.time(17))
)
finishes.append(
datetime.datetime.combine(
self.offset_date(start, days, predecessor_data),
datetime.time(17),
)
)
else:
finishes.append(start)
if starts and finishes:
data["early_start"] = max(starts)
data["early_finish"] = max(finishes)
potential_finish = ifcopenshell.util.sequence.get_start_or_finish_date(
data["early_start"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="FINISH",
)
if potential_finish > data["early_finish"]:
data["early_finish"] = potential_finish
else:
data["early_start"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["early_finish"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="START",
)
elif finishes:
data["early_finish"] = max(finishes)
elif starts:
data["early_start"] = max(starts)
else:
print("How did this happen?")
if data.get("early_finish") is None:
data["early_finish"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["early_start"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="FINISH",
)
elif data.get("early_start") is None:
data["early_start"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["early_finish"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="START",
)
return True
def backward_pass(self, node) -> bool:
successors = list(self.g.successors(node))
predecessors = self.g.predecessors(node)
data = self.g.nodes[node]
free_floats = []
if node == "finish":
data["late_finish"] = data["early_finish"]
else:
finishes = []
starts = []
for successor in successors:
successor_data = self.g.nodes[successor]
edge = self.g[node][successor]
if edge["type"] == "FS":
start = successor_data.get("late_start")
if start is None:
return
days = 1
if edge["lag_time"]:
days += edge["lag_time"]
if days or edge["lag_time"]:
finishes.append(
datetime.datetime.combine(self.offset_date(start, -days, data), datetime.time(17))
)
finishes.append(
datetime.datetime.combine(
self.offset_date(start, -days, successor_data),
datetime.time(17),
)
)
else:
finishes.append(start)
free_floats.append(
self.calculate_free_float(
data["early_finish"].date() + datetime.timedelta(days=1),
successor_data["early_start"].date(),
edge["lag_time"],
data,
successor_data,
)
)
elif edge["type"] == "SS":
start = successor_data.get("late_start")
if start is None:
return
if edge["lag_time"]:
starts.append(self.offset_date(start, -edge["lag_time"], data))
starts.append(self.offset_date(start, -edge["lag_time"], successor_data))
else:
starts.append(start)
free_floats.append(
self.calculate_free_float(
data["early_start"],
successor_data["early_start"],
edge["lag_time"],
data,
successor_data,
)
)
elif edge["type"] == "FF":
finish = successor_data.get("late_finish")
if finish is None:
return
if edge["lag_time"]:
finishes.append(self.offset_date(finish, -edge["lag_time"], data))
finishes.append(self.offset_date(finish, -edge["lag_time"], successor_data))
else:
finishes.append(finish)
free_floats.append(
self.calculate_free_float(
data["early_finish"],
successor_data["early_finish"],
edge["lag_time"],
data,
successor_data,
)
)
elif edge["type"] == "SF":
finish = successor_data.get("late_finish")
if finish is None:
return
days = 0 if successor_data["duration"] == 0 else -1
if edge["lag_time"]:
days += edge["lag_time"]
if days:
starts.append(
datetime.datetime.combine(self.offset_date(finish, -days, data), datetime.time(9))
)
starts.append(
datetime.datetime.combine(
self.offset_date(finish, -days, successor_data),
datetime.time(9),
)
)
else:
starts.append(finish)
free_floats.append(
self.calculate_free_float(
data["early_start"],
successor_data["early_finish"],
edge["lag_time"],
data,
successor_data,
)
)
if starts and finishes:
data["late_start"] = min(starts)
data["late_finish"] = min(finishes)
if self.offset_date(data["late_start"], data["duration"], data) < data["late_finish"]:
data["late_finish"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["late_start"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="FINISH",
)
else:
data["late_start"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["late_finish"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="START",
)
elif finishes:
data["late_finish"] = min(finishes)
elif starts:
data["late_start"] = min(starts)
else:
print("How did this happen?")
if data.get("late_finish") is None:
data["late_finish"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["late_start"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="FINISH",
)
elif data.get("late_start") is None:
data["late_start"] = ifcopenshell.util.sequence.get_start_or_finish_date(
data["late_finish"],
datetime.timedelta(days=data["duration"]),
data["duration_type"],
data["calendar"],
date_type="START",
)
if data["duration_type"] == "WORKTIME":
data["total_float"] = datetime.timedelta(
days=ifcopenshell.util.sequence.count_working_days(
data["early_finish"], data["late_finish"], data["calendar"]
)
)
else:
data["total_float"] = data["late_finish"] - data["early_finish"]
# If the float is within the span of a single day, it may show as a 8 hours
if data["total_float"].seconds == 60 * 60 * 8:
data["total_float"] = datetime.timedelta(days=data["total_float"].days + 1)
data["free_float"] = min(free_floats) if free_floats else None
# If the float is within the span of a single day, it may show as a 8 hours
if data["free_float"] and data["free_float"].seconds == 60 * 60 * 8:
data["free_float"] = datetime.timedelta(days=data["free_float"].days + 1)
return True
def calculate_free_float(
self,
predecessor_date: datetime.datetime,
successor_date: datetime.datetime,
lag_time: int,
predecessor_data: dict,
successor_data: dict,
) -> datetime.timedelta:
if not lag_time:
min_successor_date = successor_date
else:
min_successor_date = min(
(
self.offset_date(successor_date, -lag_time, predecessor_data),
self.offset_date(successor_date, -lag_time, successor_data),
)
)
if predecessor_data["duration_type"] == "WORKTIME":
return datetime.timedelta(
days=ifcopenshell.util.sequence.count_working_days(
predecessor_date, min_successor_date, predecessor_data["calendar"]
)
)
return min_successor_date - predecessor_date
@@ -0,0 +1,135 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.nest
import ifcopenshell.api.project
import ifcopenshell.api.pset
import ifcopenshell.api.sequence
import ifcopenshell.util.element
def remove_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None:
"""Removes a task
All subtasks are also removed recursively. Any relationships such as
sequences or controls are also removed.
:param task: The IfcTask to remove.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Add a root task to represent the design milestones, and major
# project phases.
ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Milestones", identification="A")
design = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Design", identification="B")
ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Ah, let's delete the design section, who needs it anyway we'll
# just fix it on site.
ifcopenshell.api.sequence.remove_task(model, task=design)
"""
# TODO: do a deep purge
ifcopenshell.api.project.unassign_declaration(
file,
definitions=[task],
relating_context=file.by_type("IfcContext")[0],
)
if task_time := task.TaskTime:
if task_time.is_a("IfcTaskTimeRecurring"):
ifcopenshell.api.sequence.unassign_recurrence_pattern(file, task_time.Recurrence)
file.remove(task_time)
# Handle IfcRelNests.
if rels := task.IsNestedBy:
subtasks = rels[0].RelatedObjects
# Use batching for optimization.
ifcopenshell.api.nest.unassign_object(file, subtasks)
for task_ in subtasks:
ifcopenshell.api.sequence.remove_task(file, task=task_)
if task.Nests:
ifcopenshell.api.nest.unassign_object(file, [task])
for inverse in file.get_inverse(task):
if inverse.is_a("IfcRelSequence"):
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelAssignsToControl"):
if inverse.RelatingControl == task or len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
else:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(task)
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelDefinesByProperties"):
ifcopenshell.api.pset.remove_pset(
file,
product=task,
pset=inverse.RelatingPropertyDefinition,
)
elif inverse.is_a("IfcRelAssignsToProcess"):
if inverse.RelatingProcess == task or len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelAssignsToProduct"):
if inverse.RelatingProduct == task or len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
else:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(task)
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelAssignsToObject"):
if inverse.RelatingObject == task or len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
else:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(task)
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelAssignsToProcess"):
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
history = task.OwnerHistory
file.remove(task)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -0,0 +1,56 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
def remove_time_period(file: ifcopenshell.file, time_period: ifcopenshell.entity_instance) -> None:
"""Removes a time period
:param time_period: The IfcTimePeriod to remove.
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# State that we work from weekdays 1 to 5 (i.e. Monday to Friday)
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
# The morning work session, lunch, then the afternoon work session.
morning = ifcopenshell.api.sequence.add_time_period(model,
recurrence_pattern=pattern, start_time="09:00", end_time="12:00")
afternoon = ifcopenshell.api.sequence.add_time_period(model,
recurrence_pattern=pattern, start_time="13:00", end_time="17:00")
# Let's take the afternoon off!
ifcopenshell.api.sequence.remove_time_period(model, time_period=afternoon)
"""
file.remove(time_period)
@@ -0,0 +1,71 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.control
import ifcopenshell.api.project
import ifcopenshell.api.sequence
import ifcopenshell.util.element
def remove_work_calendar(file: ifcopenshell.file, work_calendar: ifcopenshell.entity_instance) -> None:
"""Removes a work calendar
All relationships are also removed, such as if a task is set to use that
calendar.
:param work_calendar: The IfcWorkCalendar to remove
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model, name="5 Day Week")
# And remove it immediately
ifcopenshell.api.sequence.remove_work_calendar(model, work_calendar=calendar)
"""
# TODO: do a deep purge
ifcopenshell.api.project.unassign_declaration(
file,
definitions=[work_calendar],
relating_context=file.by_type("IfcContext")[0],
)
if work_calendar.Controls:
for rel in work_calendar.Controls:
for related_object in rel.RelatedObjects:
ifcopenshell.api.control.unassign_control(
file,
relating_control=work_calendar,
related_objects=[related_object],
)
# Currently in API work times are created already attached
# to the work calendar, so they are never reused.
for working_time in work_calendar.WorkingTimes or []:
ifcopenshell.api.sequence.remove_work_time(file, work_time=working_time)
for exception_time in work_calendar.ExceptionTimes or []:
ifcopenshell.api.sequence.remove_work_time(file, work_time=exception_time)
history = work_calendar.OwnerHistory
file.remove(work_calendar)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -0,0 +1,57 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.project
import ifcopenshell.util.element
def remove_work_plan(file: ifcopenshell.file, work_plan: ifcopenshell.entity_instance) -> None:
"""Removes a work plan
Note that schedules that are grouped under the work plan are not
removed.
:param work_plan: The IfcWorkPlan to remove.
:return: None
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# And remove it immediately
ifcopenshell.api.sequence.remove_work_plan(model, work_plan=work_plan)
"""
ifcopenshell.api.project.unassign_declaration(
file,
definitions=[work_plan],
relating_context=file.by_type("IfcContext")[0],
)
related_objects = [obj for rel in work_plan.IsDecomposedBy for obj in rel.RelatedObjects]
if related_objects:
ifcopenshell.api.aggregate.unassign_object(file, related_objects)
history = work_plan.OwnerHistory
file.remove(work_plan)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -0,0 +1,83 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.project
import ifcopenshell.api.sequence
import ifcopenshell.util.element
def remove_work_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance) -> None:
"""Removes a work schedule
All tasks in the work schedule are also removed recursively.
:param work_schedule: The IfcWorkSchedule to remove.
:return: None
Example:
.. code:: python
# This will hold all our construction schedules
work_plan = ifcopenshell.api.sequence.add_work_plan(model, name="Construction")
# Let's imagine this is one of our schedules in our work plan.
schedule = ifcopenshell.api.sequence.add_work_schedule(model,
name="Construction Schedule A", work_plan=work_plan)
# And remove it immediately
ifcopenshell.api.sequence.remove_work_schedule(model, work_schedule=schedule)
"""
# TODO: do a deep purge
ifcopenshell.api.project.unassign_declaration(
file, definitions=[work_schedule], relating_context=file.by_type("IfcContext")[0]
)
if work_schedule.Declares:
for rel in work_schedule.Declares:
for work_schedule_ in rel.RelatedObjects:
ifcopenshell.api.sequence.remove_work_schedule(file, work_schedule=work_schedule_)
# Unassign from work plans.
if work_schedule.Decomposes:
ifcopenshell.api.aggregate.unassign_object(file, [work_schedule])
for inverse in file.get_inverse(work_schedule):
if inverse.is_a("IfcRelDefinesByObject"):
if inverse.RelatingObject == work_schedule or len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
else:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(work_schedule)
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelAssignsToControl"):
[
ifcopenshell.api.sequence.remove_task(file, task=related_object)
for related_object in inverse.RelatedObjects
if related_object.is_a("IfcTask")
]
history = work_schedule.OwnerHistory
file.remove(work_schedule)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -0,0 +1,48 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.sequence
def remove_work_time(file: ifcopenshell.file, work_time: ifcopenshell.entity_instance) -> None:
"""Removes a work time
:param work_time: The IfcWorkTime to remove.
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# And remove it immediately
ifcopenshell.api.sequence.remove_work_time(model, work_time=work_time)
"""
# Currently in API recurrence patterns are created during assignment
# and removed during unassignment, so they are never reused.
if recurrence_pattern := work_time.RecurrencePattern:
ifcopenshell.api.sequence.unassign_recurrence_pattern(file, recurrence_pattern)
file.remove(work_time)
@@ -0,0 +1,62 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.sequence
def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity_instance) -> None:
"""Removes any lag time in a sequence
The schedule is cascaded afterwards.
:param rel_sequence: The sequence to remove the lag time from.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's imagine a root construction task
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's imagine we're building 2 zones, one after another.
zone1 = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Zone 1", identification="C.1")
zone2 = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Zone 2", identification="C.2")
# Zone 1 finishes, then zone 2 starts.
sequence = ifcopenshell.api.sequence.assign_sequence(model,
relating_process=zone1, related_process=zone2)
# What if you had to wait 1 week before you could start zone 2?
ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1W")
# What if you didn't?
ifcopenshell.api.sequence.unassign_lag_time(model, rel_sequence=sequence)
"""
if file.get_total_inverses(current_lag_time := rel_sequence.TimeLag) == 1:
file.remove(current_lag_time)
else:
rel_sequence.TimeLag = None
ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess)
@@ -0,0 +1,72 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.util.element
def unassign_process(
file: ifcopenshell.file,
relating_process: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> None:
"""Unassigns a process and object relationship
See ifcopenshell.api.sequence.assign_process for details.
:param relating_process: The IfcTask in the relationship.
:param related_object: The related object.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a construction task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Demolish existing", identification="A", predefined_type="DEMOLITION")
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's demolish that wall!
ifcopenshell.api.sequence.assign_process(model, relating_process=task, related_object=wall)
# Change our mind.
ifcopenshell.api.sequence.unassign_process(model, relating_process=task, related_object=wall)
"""
for rel in related_object.HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProcess") or rel.RelatingProcess != relating_process:
continue
if len(rel.RelatedObjects) == 1:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(related_object)
rel.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(file, element=rel)
return rel
@@ -0,0 +1,72 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.util.element
def unassign_product(
file: ifcopenshell.file,
relating_product: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> None:
"""Unassigns a product and object relationship
See ifcopenshell.api.sequence.assign_product for details.
:param relating_product: The IfcProduct in the relationship.
:param related_object: The IfcTask in the relationship.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a construction task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Build wall", identification="A", predefined_type="CONSTRUCTION")
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's construct that wall!
ifcopenshell.api.sequence.assign_product(relating_product=wall, related_object=task)
# Change our mind.
ifcopenshell.api.sequence.unassign_product(relating_product=wall, related_object=task)
"""
for rel in related_object.HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != relating_product:
continue
if len(rel.RelatedObjects) == 1:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(related_object)
rel.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(file, element=rel)
return rel
@@ -0,0 +1,53 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def unassign_recurrence_pattern(file: ifcopenshell.file, recurrence_pattern: ifcopenshell.entity_instance) -> None:
"""Unassigns a recurrence pattern
Note that a recurring task time must have a recurrence pattern, so if
you remove it, be sure to clean up after this API call
(e.g. remove IfcTaskTimeRecurring entity
or assign a different recurrence patern to it
or replace IfcTaskTimeRecurring with IfcTaskTime).
:param recurrence_pattern: The IfcRecurrencePattern to remove.
:return: None
Example:
.. code:: python
# Let's create a new calendar.
calendar = ifcopenshell.api.sequence.add_work_calendar(model)
# Let's start defining the times that we work during the week.
work_time = ifcopenshell.api.sequence.add_work_time(model,
work_calendar=calendar, time_type="WorkingTimes")
# We create a weekly recurrence
pattern = ifcopenshell.api.sequence.assign_recurrence_pattern(model,
parent=work_time, recurrence_type="WEEKLY")
# Change our mind, let's just maintain it whenever we feel like it.
ifcopenshell.api.sequence.unassign_recurrence_pattern(recurrence_pattern=pattern)
"""
for time_period in recurrence_pattern.TimePeriods or []:
file.remove(time_period)
file.remove(recurrence_pattern)
@@ -0,0 +1,66 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.sequence
import ifcopenshell.util.element
def unassign_sequence(
file: ifcopenshell.file,
relating_process: ifcopenshell.entity_instance,
related_process: ifcopenshell.entity_instance,
) -> None:
"""Removes a sequence relationship between tasks
:param relating_process: The previous / predecessor task.
:param related_process: The next / successor task.
:return: None
Example:
.. code:: python
# Let's imagine we are creating a construction schedule. All tasks
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's imagine a root construction task
construction = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Construction", identification="C")
# Let's imagine we're building 2 zones, one after another.
zone1 = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Zone 1", identification="C.1")
zone2 = ifcopenshell.api.sequence.add_task(model,
parent_task=construction, name="Zone 2", identification="C.2")
# Zone 1 finishes, then zone 2 starts.
ifcopenshell.api.sequence.assign_sequence(model, relating_process=zone1, related_process=zone2)
# Let's make them unrelated
ifcopenshell.api.sequence.unassign_sequence(model,
relating_process=zone1, related_process=zone2)
"""
for rel in related_process.IsSuccessorFrom or []:
if rel.RelatingProcess == relating_process:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
ifcopenshell.api.sequence.cascade_schedule(file, task=related_process)