2022-01-19 12:18:33 +11:00
# 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/>.
2021-06-02 17:30:05 +10:00
import datetime
import networkx as nx
import ifcopenshell . api
import ifcopenshell . util . date
2021-06-04 15:20:38 +10:00
import ifcopenshell . util . sequence
2021-06-02 17:30:05 +10:00
class Usecase :
2023-01-03 17:31:48 +11:00
def __init__ ( self , file , work_schedule = 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.
:type work_schedule: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
2023-01-10 10:16:28 +11:00
Example:
.. code:: python
2023-01-03 17:31:48 +11:00
# 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.
"""
2021-06-02 17:30:05 +10:00
self . file = file
2023-01-03 17:31:48 +11:00
self . settings = { " work_schedule " : work_schedule }
2021-06-02 17:30:05 +10:00
def execute ( self ) :
2021-06-04 15:20:38 +10:00
# The method implemented is the same as shown here:
# https://www.youtube.com/watch?v=qTErIV6OqLg
self . start_dates = [ ]
2021-06-02 17:30:05 +10:00
self . build_network_graph ( )
2021-06-03 13:40:18 +10:00
2022-03-25 15:46:19 +11:00
if not self . start_dates :
return
2022-07-19 12:39:54 +10:00
is_cyclic = False
attempts = 0
2021-06-03 13:40:18 +10:00
self . pending_nodes = set ( self . g . nodes )
2022-07-19 12:39:54 +10:00
max_worst_case_attempts = pow ( len ( self . pending_nodes ) , 2 )
2021-06-03 13:40:18 +10:00
while self . pending_nodes :
2022-07-19 12:39:54 +10:00
attempts + = 1
2021-06-03 13:40:18 +10:00
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
2022-07-19 12:39:54 +10:00
# 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
2022-09-09 02:25:20 +01:00
break # We have an infinite loop due to a cyclic graph
2022-07-19 12:39:54 +10:00
if is_cyclic :
2022-09-09 02:25:20 +01:00
raise RecursionError (
" Task graph is cyclic and so critical path method cannot be performed. "
)
2022-07-19 12:39:54 +10:00
return
2021-06-03 13:40:18 +10:00
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
2021-06-02 17:30:05 +10:00
self . update_task_times ( )
2021-06-03 13:40:18 +10:00
2021-06-02 17:30:05 +10:00
def build_network_graph ( self ) :
self . sequence_type_map = {
None : " FS " ,
" START_START " : " SS " ,
" START_FINISH " : " SF " ,
" FINISH_START " : " FS " ,
" FINISH_FINISH " : " FF " ,
" USERDEFINED " : " FS " ,
" NOTDEFINED " : " FS " ,
}
self . g = nx . DiGraph ( )
self . edges = [ ]
2021-06-04 15:20:38 +10:00
self . g . add_node ( " start " , duration = 0 , duration_type = " ELAPSEDTIME " , calendar = None )
2022-09-09 02:25:20 +01:00
self . g . add_node (
" finish " , duration = 0 , duration_type = " ELAPSEDTIME " , calendar = None
)
2021-06-02 17:30:05 +10:00
for rel in self . settings [ " work_schedule " ] . Controls :
for related_object in rel . RelatedObjects :
if not related_object . is_a ( " IfcTask " ) :
continue
self . add_node ( related_object )
self . g . add_edges_from ( self . edges )
def add_node ( self , task ) :
if task . IsNestedBy :
for rel in task . IsNestedBy :
[ self . add_node ( o ) for o in rel . RelatedObjects ]
return
if task . TaskTime and task . TaskTime . ScheduleDuration :
2022-09-09 02:25:20 +01:00
duration = ifcopenshell . util . date . ifc2datetime (
task . TaskTime . ScheduleDuration
) . days
2021-06-04 15:20:38 +10:00
duration_type = task . TaskTime . DurationType
2021-06-02 17:30:05 +10:00
else :
duration = 0
2021-06-04 15:20:38 +10:00
duration_type = " ELAPSEDTIME "
self . g . add_node (
task . id ( ) ,
duration = duration ,
duration_type = duration_type ,
calendar = ifcopenshell . util . sequence . derive_calendar ( task ) ,
)
2021-06-02 17:30:05 +10:00
self . edges . extend (
[
(
rel . RelatingProcess . id ( ) ,
2023-09-20 01:10:34 +01:00
task . id ( ) ,
2021-06-02 17:30:05 +10:00
{
" lag_time " : 0
2021-06-17 13:23:09 +10:00
if not rel . TimeLag
2022-09-09 02:25:20 +01:00
else ifcopenshell . util . date . ifc2datetime (
rel . TimeLag . LagValue . wrappedValue
) . days ,
2021-06-02 17:30:05 +10:00
" type " : self . sequence_type_map [ rel . SequenceType ] ,
} ,
)
2023-09-20 01:10:34 +01:00
for rel in ifcopenshell . util . sequence . get_sequence_assignment ( task , sequence = " predecessor " )
2021-06-02 17:30:05 +10:00
]
)
2023-09-20 01:10:34 +01:00
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 " ) ]
2021-06-03 13:40:18 +10:00
2022-03-25 15:53:05 +11:00
if not predecessor_types :
2021-06-02 17:30:05 +10:00
self . edges . append ( ( " start " , task . id ( ) , { " lag_time " : 0 , " type " : " FS " } ) )
2021-06-04 15:20:38 +10:00
if task . TaskTime and task . TaskTime . ScheduleStart :
2022-09-09 02:25:20 +01:00
self . start_dates . append (
ifcopenshell . util . date . ifc2datetime ( task . TaskTime . ScheduleStart )
)
2023-09-20 01:10:34 +01:00
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
2022-03-25 15:53:05 +11:00
if not successor_types :
self . edges . append ( ( task . id ( ) , " finish " , { " lag_time " : 0 , " type " : " FF " } ) )
2021-06-02 17:30:05 +10:00
def update_task_times ( self ) :
for ifc_definition_id in self . g . nodes :
2022-03-25 15:49:08 +11:00
if ifc_definition_id in ( " start " , " finish " ) :
continue
2021-06-02 17:30:05 +10:00
data = self . g . nodes [ ifc_definition_id ]
2022-03-25 15:49:08 +11:00
task = self . file . by_id ( ifc_definition_id )
if not task . TaskTime :
2021-06-02 17:30:05 +10:00
continue
ifcopenshell . api . run (
" sequence.edit_task_time " ,
self . file ,
2022-03-25 15:49:08 +11:00
task_time = task . TaskTime ,
2021-06-02 17:30:05 +10:00
attributes = {
2022-09-09 02:25:20 +01:00
" FreeFloat " : ifcopenshell . util . date . datetime2ifc (
data [ " free_float " ] , " IfcDuration "
) ,
" TotalFloat " : ifcopenshell . util . date . datetime2ifc (
data [ " total_float " ] , " IfcDuration "
) ,
2021-06-04 15:20:38 +10:00
" IsCritical " : data [ " total_float " ] . days == 0 ,
2022-09-09 02:25:20 +01:00
" 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 "
) ,
2021-06-02 17:30:05 +10:00
} ,
)
2021-06-03 13:40:18 +10:00
2021-06-04 15:20:38 +10:00
def offset_date ( self , date , days , node ) :
2022-03-25 15:53:05 +11:00
return ifcopenshell . util . sequence . offset_date (
2021-06-04 15:20:38 +10:00
date , datetime . timedelta ( days = days ) , node [ " duration_type " ] , node [ " calendar " ]
)
2021-06-03 13:40:18 +10:00
def forward_pass ( self , node ) :
successors = self . g . successors ( node )
predecessors = list ( self . g . predecessors ( node ) )
data = self . g . nodes [ node ]
if node == " start " :
2021-06-04 15:20:38 +10:00
data [ " early_start " ] = min ( self . start_dates )
2021-06-03 13:40:18 +10:00
else :
finishes = [ ]
starts = [ ]
2023-09-20 01:10:34 +01:00
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
2021-06-03 13:40:18 +10:00
for predecessor in predecessors :
2021-06-04 15:20:38 +10:00
predecessor_data = self . g . nodes [ predecessor ]
2021-06-03 13:40:18 +10:00
edge = self . g [ predecessor ] [ node ]
if edge [ " type " ] == " FS " :
2021-06-04 15:20:38 +10:00
finish = predecessor_data . get ( " early_finish " )
2021-06-03 13:40:18 +10:00
if finish is None :
return
2022-03-25 15:53:05 +11:00
days = 0 if predecessor_data [ " duration " ] == 0 else 1
if edge [ " lag_time " ] :
days + = edge [ " lag_time " ]
if days :
starts . append (
datetime . datetime . combine (
2022-09-09 02:25:20 +01:00
self . offset_date ( finish , days , data ) , datetime . time ( 9 )
)
)
starts . append (
datetime . datetime . combine (
self . offset_date ( finish , days , predecessor_data ) ,
datetime . time ( 9 ) ,
2022-03-25 15:53:05 +11:00
)
)
2021-06-17 11:05:43 +10:00
else :
2022-03-25 15:53:05 +11:00
starts . append ( finish )
2021-06-03 13:40:18 +10:00
elif edge [ " type " ] == " SS " :
2021-06-04 15:20:38 +10:00
start = predecessor_data . get ( " early_start " )
2021-06-03 13:40:18 +10:00
if start is None :
return
2022-03-25 15:53:05 +11:00
if edge [ " lag_time " ] :
2021-06-17 11:05:43 +10:00
starts . append ( self . offset_date ( start , edge [ " lag_time " ] , data ) )
2022-09-09 02:25:20 +01:00
starts . append (
self . offset_date ( start , edge [ " lag_time " ] , predecessor_data )
)
2022-03-25 15:53:05 +11:00
else :
starts . append ( start )
2021-06-03 13:40:18 +10:00
elif edge [ " type " ] == " FF " :
2021-06-04 15:20:38 +10:00
finish = predecessor_data . get ( " early_finish " )
2021-06-03 13:40:18 +10:00
if finish is None :
return
2022-03-25 15:53:05 +11:00
if edge [ " lag_time " ] :
2022-09-09 02:25:20 +01:00
finishes . append (
self . offset_date ( finish , edge [ " lag_time " ] , data )
)
finishes . append (
self . offset_date ( finish , edge [ " lag_time " ] , predecessor_data )
)
2022-03-25 15:53:05 +11:00
else :
finishes . append ( finish )
2021-06-03 13:40:18 +10:00
elif edge [ " type " ] == " SF " :
2021-06-04 15:20:38 +10:00
start = predecessor_data . get ( " early_start " )
2021-06-03 13:40:18 +10:00
if start is None :
return
2022-03-25 15:53:05 +11:00
days = - 1
if edge [ " lag_time " ] :
days + = edge [ " lag_time " ]
if days or edge [ " lag_time " ] :
finishes . append (
2022-09-09 02:25:20 +01:00
datetime . datetime . combine (
self . offset_date ( start , days , data ) , datetime . time ( 17 )
)
2022-03-25 15:53:05 +11:00
)
finishes . append (
datetime . datetime . combine (
2022-09-09 02:25:20 +01:00
self . offset_date ( start , days , predecessor_data ) ,
datetime . time ( 17 ) ,
2022-03-25 15:53:05 +11:00
)
)
2021-06-17 11:05:43 +10:00
else :
2022-03-25 15:53:05 +11:00
finishes . append ( start )
2021-06-03 13:40:18 +10:00
if starts and finishes :
data [ " early_start " ] = max ( starts )
data [ " early_finish " ] = max ( finishes )
2022-03-25 15:53:05 +11:00
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
2021-06-03 13:40:18 +10:00
else :
2022-09-09 02:25:20 +01:00
data [
" early_start "
] = ifcopenshell . util . sequence . get_start_or_finish_date (
2022-03-25 15:53:05 +11:00
data [ " early_finish " ] ,
datetime . timedelta ( days = data [ " duration " ] ) ,
data [ " duration_type " ] ,
data [ " calendar " ] ,
date_type = " START " ,
)
2021-06-03 13:40:18 +10:00
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 :
2022-03-25 15:53:05 +11:00
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 " ,
)
2021-06-03 13:40:18 +10:00
elif data . get ( " early_start " ) is None :
2022-03-25 15:53:05 +11:00
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 " ,
)
2021-06-03 13:40:18 +10:00
return True
def backward_pass ( self , node ) :
successors = list ( self . g . successors ( node ) )
predecessors = self . g . predecessors ( node )
data = self . g . nodes [ node ]
2021-06-08 11:36:07 +10:00
free_floats = [ ]
2021-06-03 13:40:18 +10:00
if node == " finish " :
data [ " late_finish " ] = data [ " early_finish " ]
else :
finishes = [ ]
starts = [ ]
for successor in successors :
2021-06-04 15:20:38 +10:00
successor_data = self . g . nodes [ successor ]
2021-06-03 13:40:18 +10:00
edge = self . g [ node ] [ successor ]
if edge [ " type " ] == " FS " :
2021-06-04 15:20:38 +10:00
start = successor_data . get ( " late_start " )
2021-06-03 13:40:18 +10:00
if start is None :
return
2022-03-25 15:53:05 +11:00
days = 1
if edge [ " lag_time " ] :
days + = edge [ " lag_time " ]
if days or edge [ " lag_time " ] :
finishes . append (
2022-09-09 02:25:20 +01:00
datetime . datetime . combine (
self . offset_date ( start , - days , data ) , datetime . time ( 17 )
)
2022-03-25 15:53:05 +11:00
)
finishes . append (
2022-09-09 02:25:20 +01:00
datetime . datetime . combine (
self . offset_date ( start , - days , successor_data ) ,
datetime . time ( 17 ) ,
)
2022-03-25 15:53:05 +11:00
)
2021-06-17 11:05:43 +10:00
else :
2022-03-25 15:53:05 +11:00
finishes . append ( start )
2021-06-08 11:36:07 +10:00
free_floats . append (
self . calculate_free_float (
2022-03-25 15:53:05 +11:00
data [ " early_finish " ] . date ( ) + datetime . timedelta ( days = 1 ) ,
successor_data [ " early_start " ] . date ( ) ,
edge [ " lag_time " ] ,
data ,
successor_data ,
2021-06-08 11:36:07 +10:00
)
)
2021-06-03 13:40:18 +10:00
elif edge [ " type " ] == " SS " :
2021-06-04 15:20:38 +10:00
start = successor_data . get ( " late_start " )
2021-06-03 13:40:18 +10:00
if start is None :
return
2022-03-25 15:53:05 +11:00
if edge [ " lag_time " ] :
2021-06-17 11:05:43 +10:00
starts . append ( self . offset_date ( start , - edge [ " lag_time " ] , data ) )
2022-09-09 02:25:20 +01:00
starts . append (
self . offset_date ( start , - edge [ " lag_time " ] , successor_data )
)
2022-03-25 15:53:05 +11:00
else :
starts . append ( start )
2021-06-08 11:36:07 +10:00
free_floats . append (
self . calculate_free_float (
2022-09-09 02:25:20 +01:00
data [ " early_start " ] ,
successor_data [ " early_start " ] ,
edge [ " lag_time " ] ,
data ,
successor_data ,
2021-06-08 11:36:07 +10:00
)
)
2021-06-03 13:40:18 +10:00
elif edge [ " type " ] == " FF " :
2021-06-04 15:20:38 +10:00
finish = successor_data . get ( " late_finish " )
2021-06-03 13:40:18 +10:00
if finish is None :
return
2022-03-25 15:53:05 +11:00
if edge [ " lag_time " ] :
2022-09-09 02:25:20 +01:00
finishes . append (
self . offset_date ( finish , - edge [ " lag_time " ] , data )
)
finishes . append (
self . offset_date ( finish , - edge [ " lag_time " ] , successor_data )
)
2022-03-25 15:53:05 +11:00
else :
finishes . append ( finish )
2021-06-08 11:36:07 +10:00
free_floats . append (
self . calculate_free_float (
2022-09-09 02:25:20 +01:00
data [ " early_finish " ] ,
successor_data [ " early_finish " ] ,
edge [ " lag_time " ] ,
data ,
successor_data ,
2021-06-08 11:36:07 +10:00
)
)
2021-06-03 13:40:18 +10:00
elif edge [ " type " ] == " SF " :
2021-06-04 15:20:38 +10:00
finish = successor_data . get ( " late_finish " )
2021-06-03 13:40:18 +10:00
if finish is None :
return
2022-03-25 15:53:05 +11:00
days = 0 if successor_data [ " duration " ] == 0 else - 1
if edge [ " lag_time " ] :
days + = edge [ " lag_time " ]
if days :
starts . append (
2022-09-09 02:25:20 +01:00
datetime . datetime . combine (
self . offset_date ( finish , - days , data ) , datetime . time ( 9 )
)
2022-03-25 15:53:05 +11:00
)
starts . append (
2022-09-09 02:25:20 +01:00
datetime . datetime . combine (
self . offset_date ( finish , - days , successor_data ) ,
datetime . time ( 9 ) ,
)
2022-03-25 15:53:05 +11:00
)
2021-06-17 11:05:43 +10:00
else :
2022-03-25 15:53:05 +11:00
starts . append ( finish )
2021-06-08 11:36:07 +10:00
free_floats . append (
self . calculate_free_float (
2022-09-09 02:25:20 +01:00
data [ " early_start " ] ,
successor_data [ " early_finish " ] ,
edge [ " lag_time " ] ,
data ,
successor_data ,
2021-06-08 11:36:07 +10:00
)
)
2021-06-03 13:40:18 +10:00
if starts and finishes :
data [ " late_start " ] = min ( starts )
data [ " late_finish " ] = min ( finishes )
2022-09-09 02:25:20 +01:00
if (
self . offset_date ( data [ " late_start " ] , data [ " duration " ] , data )
< data [ " late_finish " ]
) :
data [
" late_finish "
] = ifcopenshell . util . sequence . get_start_or_finish_date (
2022-03-25 15:53:05 +11:00
data [ " late_start " ] ,
datetime . timedelta ( days = data [ " duration " ] ) ,
data [ " duration_type " ] ,
data [ " calendar " ] ,
date_type = " FINISH " ,
)
2021-06-03 13:40:18 +10:00
else :
2022-09-09 02:25:20 +01:00
data [
" late_start "
] = ifcopenshell . util . sequence . get_start_or_finish_date (
2022-03-25 15:53:05 +11:00
data [ " late_finish " ] ,
datetime . timedelta ( days = data [ " duration " ] ) ,
data [ " duration_type " ] ,
data [ " calendar " ] ,
date_type = " START " ,
)
2021-06-03 13:40:18 +10:00
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 :
2022-03-25 15:53:05 +11:00
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 " ,
)
2021-06-03 13:40:18 +10:00
elif data . get ( " late_start " ) is None :
2022-03-25 15:53:05 +11:00
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 " ,
)
2021-06-03 13:40:18 +10:00
2021-06-04 17:22:35 +10:00
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 " ]
2022-03-25 15:53:05 +11:00
# 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 :
2022-09-09 02:25:20 +01:00
data [ " total_float " ] = datetime . timedelta (
days = data [ " total_float " ] . days + 1
)
2021-06-03 13:40:18 +10:00
2021-06-08 11:36:07 +10:00
data [ " free_float " ] = min ( free_floats ) if free_floats else None
2022-03-25 15:53:05 +11:00
# 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 )
2021-06-04 15:20:38 +10:00
2021-06-03 13:40:18 +10:00
return True
2021-06-08 11:36:07 +10:00
2022-09-09 02:25:20 +01:00
def calculate_free_float (
self ,
predecessor_date ,
successor_date ,
lag_time ,
predecessor_data ,
successor_data ,
) :
2021-06-17 11:05:43 +10:00
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 ) ,
)
2021-06-08 11:36:07 +10:00
)
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