Coverage for bim2sim/tasks/bps/sb_2b_generation.py: 22%

98 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +0000

1import logging 

2 

3import ifcopenshell 

4from ifcopenshell import guid 

5from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut 

6from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex 

7from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape 

8from OCC.Core.Extrema import Extrema_ExtFlag_MIN 

9from OCC.Core.TopoDS import TopoDS_Face 

10from OCC.Core.gp import gp_Pnt 

11 

12from bim2sim.elements.bps_elements import SpaceBoundary2B, ThermalZone, Door, \ 

13 Window 

14from bim2sim.tasks.base import ITask 

15from bim2sim.tasks.bps import CorrectSpaceBoundaries 

16from bim2sim.utilities.common_functions import get_spaces_with_bounds 

17from bim2sim.utilities.pyocc_tools import PyOCCTools 

18 

19logger = logging.getLogger(__name__) 

20 

21 

22class AddSpaceBoundaries2B(ITask): 

23 """Fill gaps in set of space boundary per space with 2B space boundaries.""" 

24 

25 reads = ('elements',) 

26 touches = ('elements',) 

27 

28 def run(self, elements: dict) -> tuple[dict]: 

29 """Create 2b space boundaries to fill gaps in spaces. 

30 

31 This task generates space boundaries of type 2b to fill gaps in the 

32 space surrounding space boundaries. The resulting set of space 

33 boundaries forms a watertight shape. 

34 

35 Args: 

36 elements (dict): dictionary in the format dict[guid: element], 

37 holds preprocessed elements including space boundaries. 

38 Returns: 

39 elements (dict): dictionary in the format dict[guid: element], 

40 holds preprocessed elements including space boundaries and 

41 generated 2b space boundaries. 

42 """ 

43 if not self.playground.sim_settings.close_space_boundary_gaps: 

44 return elements, 

45 try: 

46 total_area_before_split = 0 

47 inst_2b = self._compute_2b_bound_gaps(elements) 

48 for sb in inst_2b.values(): 

49 total_area_before_split += sb.bound_area 

50 CorrectSpaceBoundaries.split_non_convex_bounds( 

51 CorrectSpaceBoundaries(self.playground), 

52 inst_2b, 

53 self.playground.sim_settings.split_bounds) 

54 total_area_after_split = 0 

55 for sb in inst_2b.values(): 

56 total_area_after_split += sb.bound_area 

57 rel_dif_per = (abs(total_area_before_split-total_area_after_split)/ 

58 total_area_before_split * 100) 

59 if rel_dif_per > 1e-3: 

60 self.logger.warning( 

61 f"Difference of {rel_dif_per.m} between total 2B area " 

62 f"before and after splitting.") 

63 except Exception as ex: 

64 logger.warning(f"Unexpected {ex=}. No 2b Space Boundaries added." 

65 f" {type(ex)=}") 

66 return elements, 

67 elements.update(inst_2b) 

68 

69 return elements, 

70 

71 def _compute_2b_bound_gaps(self, elements: dict)\ 

72 -> dict[str:SpaceBoundary2B]: 

73 """Compute 2b space boundaries for gaps between 2a space boundaries. 

74 

75 This function computes 2b space boundaries for non-watertight sets of 

76 space boundaries of type 2a. The 2b generation algorithm cuts all 

77 individual existing space boundary shapes from the space shape. The 

78 resulting surfaces close the gaps in the space boundary shapes. These 

79 new shapes are assigned to be shapes of type 2b for simplicity. The 

80 algorithm could be further improved by a verification of the space 

81 boundary type (type 2a or 2b). 

82 

83 Args: 

84 elements: dict[guid: element] 

85 

86 Returns: 

87 dict[guid: SpaceBoundary2B] 

88 """ 

89 logger.info("Generate space boundaries of type 2B") 

90 inst_2b = dict() 

91 spaces = get_spaces_with_bounds(elements) 

92 for space_obj in spaces: 

93 # compare surface area of IfcSpace shape with sum of space 

94 # boundary shapes of this thermal zone. 

95 space_surf_area = PyOCCTools.get_shape_area(space_obj.space_shape) 

96 sb_area = 0 

97 for bound in space_obj.space_boundaries: 

98 if bound.parent_bound: 

99 continue 

100 sb_area += PyOCCTools.get_shape_area(bound.bound_shape) 

101 if (space_surf_area - sb_area) < 1e-2: 

102 continue 

103 # spaces which reach this point have gaps in their boundaries. 

104 space_obj.b_bound_shape = space_obj.space_shape 

105 for bound in space_obj.space_boundaries: 

106 if bound.bound_area.m == 0: 

107 continue 

108 if PyOCCTools.get_shape_area(space_obj.b_bound_shape) == 0: 

109 continue 

110 # exclude surfaces that are too far from space shape. 

111 distance = BRepExtrema_DistShapeShape( 

112 space_obj.b_bound_shape, 

113 bound.bound_shape, 

114 Extrema_ExtFlag_MIN).Value() 

115 if distance > 1e-6: 

116 continue 

117 # cut the current shape from the (leftover) space shape. 

118 space_obj.b_bound_shape = BRepAlgoAPI_Cut( 

119 space_obj.b_bound_shape, bound.bound_shape).Shape() 

120 # extract faces from the leftover shape. 

121 faces = PyOCCTools.get_faces_from_shape(space_obj.b_bound_shape) 

122 if faces: 

123 # create a new 2b space boundary for each face.. 

124 inst_2b.update(self.create_2b_space_boundaries(faces, space_obj)) 

125 return inst_2b 

126 

127 @staticmethod 

128 def create_2b_space_boundaries(faces: list[TopoDS_Face], 

129 space_obj: ThermalZone)\ 

130 -> dict[str: SpaceBoundary2B]: 

131 """Create new 2b space boundaries. 

132 

133 Create a new 2b space boundary for each face in the list and 

134 assign it to the space instance. 

135 

136 Args: 

137 faces: list of TopoDS_Face 

138 space_obj: ThermalZone instance 

139 

140 Returns: 

141 dict[guid: SpaceBoundary2B] 

142 

143 """ 

144 settings = ifcopenshell.geom.main.settings() 

145 settings.set(settings.USE_PYTHON_OPENCASCADE, True) 

146 settings.set(settings.USE_WORLD_COORDS, True) 

147 settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False) 

148 settings.set(settings.INCLUDE_CURVES, True) 

149 inst_2b = dict() 

150 bound_obj = [] 

151 

152 # generate a list of IFCBased elements (e.g. Wall) that are the 

153 # space surrounding elements. Initialize a shape (geometry) for these 

154 # elements. 

155 for bound in space_obj.space_boundaries: 

156 if bound.bound_element and bound.bound_element.ifc.Representation: 

157 bi = bound.bound_element.ifc 

158 bound.bound_element.shape = ifcopenshell.geom.create_shape( 

159 settings, bi).geometry 

160 bound_obj.append(bound.bound_element) 

161 

162 for i, face in enumerate(faces): 

163 b_bound = SpaceBoundary2B() 

164 b_bound.bound_shape = face 

165 if b_bound.bound_area.m < 1e-3: 

166 continue 

167 b_bound.guid = guid.new() 

168 b_bound.bound_thermal_zone = space_obj 

169 # get the building element that is bounded by the current 2b bound 

170 for instance in bound_obj: 

171 if isinstance(instance, Door) or isinstance(instance, Window): 

172 continue 

173 center_shape = BRepBuilderAPI_MakeVertex( 

174 gp_Pnt(b_bound.bound_center)).Shape() 

175 distance = BRepExtrema_DistShapeShape( 

176 center_shape, instance.shape, Extrema_ExtFlag_MIN).Value() 

177 if distance < 1e-3: 

178 b_bound.bound_element = instance 

179 break 

180 space_obj.space_boundaries.append(b_bound) 

181 b_bound.bound_element.space_boundaries.append(b_bound) 

182 inst_2b[b_bound.guid] = b_bound 

183 return inst_2b