Coverage for bim2sim/elements/mapping/filter.py: 71%

113 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-16 08:28 +0000

1"""Module containing filters to identify IFC elements of interest""" 

2from typing import Iterable, Tuple, Dict, Any, Type, List 

3import logging 

4 

5from bim2sim.elements.base_elements import ProductBased 

6from bim2sim.elements.mapping.ifc2python import (getSpatialChildren, 

7 getHierarchicalChildren) 

8 

9logger = logging.getLogger(__name__) 

10 

11 

12class Filter: 

13 """Base filter""" 

14 

15 def __init__(self): 

16 pass 

17 

18 def matches(self, ifcelement): 

19 """Check if element matches filter conditions""" 

20 raise NotImplementedError("Must overwride method 'matches'") 

21 

22 def run(self): 

23 """Apply the Filter on IFC File""" 

24 raise NotImplementedError("Must overwride method 'run'") 

25 

26 def __repr__(self): 

27 return "<%s>" % (self.__class__.__name__) 

28 

29 

30class TypeFilter(Filter): 

31 """Filter for subsets of IFC types""" 

32 

33 def __init__(self, ifc_types: Iterable): 

34 super().__init__() 

35 self.ifc_types = ifc_types 

36 

37 def matches(self, ifcelement): 

38 __doc__ = super().matches.__doc__ 

39 return ifcelement.type in self.ifc_types # TODO: string based 

40 

41 def run(self, ifc) -> Tuple[Dict[Any, Type[ProductBased]], List[Any]]: 

42 """Scan IFC file by IFC types. 

43 

44 Args: 

45 ifc: The IFC file to scan 

46 

47 Returns: 

48 A tuple containing: 

49 - Dict mapping IFC entities to their types 

50 - List of unknown IFC entities 

51 """ 

52 unknown_ifc_entities = [] 

53 result = {} 

54 

55 for ifc_type in self.ifc_types: 

56 try: 

57 entities = ifc.by_type(ifc_type) 

58 except RuntimeError: 

59 logger.info("No entity of type '%s' found", ifc_type) 

60 entities = [] 

61 

62 for entity in entities: 

63 result[entity] = ifc_type 

64 

65 return result, unknown_ifc_entities 

66 

67 

68class StoreyFilter(Filter): 

69 """A filter that removes building storeys not in a specified list of GUIDs. 

70 

71 This filter removes building storeys that don't match the provided GUIDs, 

72 along with all their spatial and hierarchical children. 

73 

74 Attributes: 

75 storey_guids: A list of GlobalId strings for storeys to keep. 

76 """ 

77 

78 def __init__(self, storey_guids: list): 

79 """Initialize the StoreyFilter with a list of storey GUIDs to keep. 

80 

81 Args: 

82 storey_guids: A list of string GUIDs for storeys that should be 

83 kept. All other storeys and their children will be removed. 

84 """ 

85 super().__init__() 

86 self.storey_guids = storey_guids 

87 

88 def run(self, ifc_file, entity_type_dict, unknown): 

89 """Run the filter to remove unwanted storeys and their children. 

90 

91 Args: 

92 ifc_file: The IfcOpenShell file object to process. 

93 entity_type_dict: Dictionary mapping entities to their types. 

94 unknown: List of entities with unknown types. 

95 

96 Returns: 

97 tuple: A tuple containing: 

98 - Updated entity_type_dict with unwanted entities removed 

99 - Updated unknown list with unwanted entities removed 

100 

101 Raises: 

102 TypeError: If the ifc_file is not a valid IfcOpenShell file. 

103 RuntimeError: If there is an error processing the storeys. 

104 """ 

105 try: 

106 # Get all storeys 

107 all_storeys = ifc_file.by_type('IfcBuildingStorey') 

108 if not all_storeys: 

109 logger.warning( 

110 "No IfcBuildingStorey elements found in the model") 

111 return entity_type_dict, unknown 

112 

113 # Check if storey_guids is empty or if none of the guids match 

114 # existing storeys 

115 if not self.storey_guids or not any( 

116 storey.GlobalId in self.storey_guids for storey in 

117 all_storeys): 

118 logger.info( 

119 "No valid storey GUIDs provided - no filtering will be " 

120 "performed") 

121 return entity_type_dict, unknown 

122 

123 # Identify storeys to remove 

124 storeys_to_remove = [storey for storey in all_storeys if 

125 storey.GlobalId not in self.storey_guids] 

126 

127 if not storeys_to_remove: 

128 logger.info("No storeys need to be removed") 

129 return entity_type_dict, unknown 

130 

131 logger.info(f"Removing {len(storeys_to_remove)} storeys") 

132 

133 # Collect all entities to remove 

134 entities_to_remove = [] 

135 for storey in storeys_to_remove: 

136 try: 

137 spatial_children = getSpatialChildren(storey) 

138 hierarchical_children = getHierarchicalChildren(storey) 

139 entities_to_remove.extend(spatial_children) 

140 entities_to_remove.extend(hierarchical_children) 

141 entities_to_remove.append( 

142 storey) # Also remove the storey itself 

143 except Exception as e: 

144 logger.error( 

145 f"Error processing storey {storey.GlobalId}: {str(e)}") 

146 

147 # Remove entities from dictionaries 

148 for entity_to_remove in entities_to_remove: 

149 if entity_to_remove in entity_type_dict: 

150 del entity_type_dict[entity_to_remove] 

151 if entity_to_remove in unknown: 

152 unknown.remove(entity_to_remove) 

153 

154 logger.info( 

155 f"Removed {len(entities_to_remove)} entities in total") 

156 

157 return entity_type_dict, unknown 

158 

159 except Exception as e: 

160 error_msg = f"Error in StoreyFilter: {str(e)}" 

161 logger.error(error_msg) 

162 raise RuntimeError(error_msg) from e 

163 

164 

165class TextFilter(Filter): 

166 """Filter for unknown properties by text fragments. 

167 

168 This class provides functionality to filter IFC entities by analyzing text 

169 fragments to determine their potential element classes. 

170 

171 Attributes: 

172 elements_classes: Collection of ProductBased classes to check for 

173 matches. 

174 ifc_units: Dictionary containing IFC unit information. 

175 optional_locations: Additional locations to check patterns beyond 

176 names. 

177 """ 

178 

179 def __init__(self, elements_classes: Iterable[ProductBased], 

180 ifc_units: dict, 

181 optional_locations: list = None): 

182 """Initialize a TextFilter instance. 

183 

184 Args: 

185 elements_classes: Collection of ProductBased classes to check 

186 for matches. 

187 ifc_units: Dictionary containing IFC unit information. 

188 optional_locations: Additional locations to check patterns. 

189 Defaults to None. 

190 """ 

191 super().__init__() 

192 self.elements_classes = elements_classes 

193 self.ifc_units = ifc_units 

194 self.optional_locations = optional_locations 

195 

196 def find_matches(self, entity): 

197 """Find all element classes that match the given entity. 

198 

199 Args: 

200 entity: The IFC entity to check. 

201 

202 Returns: 

203 dict: Dictionary with matching classes as keys and their match 

204 fragments as values. 

205 """ 

206 matches = {} 

207 for cls in self.elements_classes: 

208 fragments = cls.filter_for_text_fragments( 

209 entity, self.ifc_units, self.optional_locations) 

210 if fragments: 

211 matches[cls] = fragments 

212 

213 return matches 

214 

215 def run(self, ifc_entities: list): 

216 """Run the filter on a list of IFC entities. 

217 

218 Args: 

219 ifc_entities: List of IFC entities to filter. 

220 

221 Returns: 

222 tuple: 

223 - filter_results: Dictionary mapping IFC entities to 

224 matching classes 

225 and their fragments. 

226 - unknown: List of IFC entities that didn't match any class. 

227 """ 

228 filter_results = {} 

229 unknown = [] 

230 

231 for entity in ifc_entities: 

232 matches = self.find_matches(entity) 

233 if matches: 

234 # Store both the classes and their matching fragments 

235 filter_results[entity] = matches 

236 else: 

237 unknown.append(entity) 

238 

239 return filter_results, unknown 

240 

241 

242class GeometricFilter(Filter): 

243 """Filter based on geometric position""" 

244 

245 def __init__(self, 

246 x_min: float = None, x_max: float = None, 

247 y_min: float = None, y_max: float = None, 

248 z_min: float = None, z_max: float = None): 

249 """None = unlimited""" 

250 super().__init__() 

251 

252 assert any([not lim is None for lim in 

253 [x_min, x_max, y_min, y_max, z_min, z_max]]), \ 

254 "Filter without limits has no effect." 

255 assert (x_min is None or x_max is None) or x_min < x_max, \ 

256 "Invalid arguments for x_min and x_max" 

257 assert (y_min is None or y_max is None) or y_min < y_max, \ 

258 "Invalid arguments for y_min and y_max" 

259 assert (z_min is None or z_max is None) or z_min < z_max, \ 

260 "Invalid arguments for z_min and z_max" 

261 

262 self.x_min = x_min 

263 self.x_max = x_max 

264 self.y_min = y_min 

265 self.y_max = y_max 

266 self.z_min = z_min 

267 self.z_max = z_max 

268 

269 def matches(self, ifcelement): 

270 __doc__ = super().matches.__doc__ 

271 raise NotImplementedError("ToDo") # TODO 

272 

273 

274class ZoneFilter(GeometricFilter): 

275 """Filter elements within given zone""" 

276 

277 def __init__(self, zone): 

278 raise NotImplementedError("ToDo") # TODO 

279 # super().__init__(x_min, x_max, y_min, y_max, z_min, z_max)