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
« 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
5from bim2sim.elements.base_elements import ProductBased
6from bim2sim.elements.mapping.ifc2python import (getSpatialChildren,
7 getHierarchicalChildren)
9logger = logging.getLogger(__name__)
12class Filter:
13 """Base filter"""
15 def __init__(self):
16 pass
18 def matches(self, ifcelement):
19 """Check if element matches filter conditions"""
20 raise NotImplementedError("Must overwride method 'matches'")
22 def run(self):
23 """Apply the Filter on IFC File"""
24 raise NotImplementedError("Must overwride method 'run'")
26 def __repr__(self):
27 return "<%s>" % (self.__class__.__name__)
30class TypeFilter(Filter):
31 """Filter for subsets of IFC types"""
33 def __init__(self, ifc_types: Iterable):
34 super().__init__()
35 self.ifc_types = ifc_types
37 def matches(self, ifcelement):
38 __doc__ = super().matches.__doc__
39 return ifcelement.type in self.ifc_types # TODO: string based
41 def run(self, ifc) -> Tuple[Dict[Any, Type[ProductBased]], List[Any]]:
42 """Scan IFC file by IFC types.
44 Args:
45 ifc: The IFC file to scan
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 = {}
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 = []
62 for entity in entities:
63 result[entity] = ifc_type
65 return result, unknown_ifc_entities
68class StoreyFilter(Filter):
69 """A filter that removes building storeys not in a specified list of GUIDs.
71 This filter removes building storeys that don't match the provided GUIDs,
72 along with all their spatial and hierarchical children.
74 Attributes:
75 storey_guids: A list of GlobalId strings for storeys to keep.
76 """
78 def __init__(self, storey_guids: list):
79 """Initialize the StoreyFilter with a list of storey GUIDs to keep.
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
88 def run(self, ifc_file, entity_type_dict, unknown):
89 """Run the filter to remove unwanted storeys and their children.
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.
96 Returns:
97 tuple: A tuple containing:
98 - Updated entity_type_dict with unwanted entities removed
99 - Updated unknown list with unwanted entities removed
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
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
123 # Identify storeys to remove
124 storeys_to_remove = [storey for storey in all_storeys if
125 storey.GlobalId not in self.storey_guids]
127 if not storeys_to_remove:
128 logger.info("No storeys need to be removed")
129 return entity_type_dict, unknown
131 logger.info(f"Removing {len(storeys_to_remove)} storeys")
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)}")
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)
154 logger.info(
155 f"Removed {len(entities_to_remove)} entities in total")
157 return entity_type_dict, unknown
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
165class TextFilter(Filter):
166 """Filter for unknown properties by text fragments.
168 This class provides functionality to filter IFC entities by analyzing text
169 fragments to determine their potential element classes.
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 """
179 def __init__(self, elements_classes: Iterable[ProductBased],
180 ifc_units: dict,
181 optional_locations: list = None):
182 """Initialize a TextFilter instance.
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
196 def find_matches(self, entity):
197 """Find all element classes that match the given entity.
199 Args:
200 entity: The IFC entity to check.
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
213 return matches
215 def run(self, ifc_entities: list):
216 """Run the filter on a list of IFC entities.
218 Args:
219 ifc_entities: List of IFC entities to filter.
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 = []
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)
239 return filter_results, unknown
242class GeometricFilter(Filter):
243 """Filter based on geometric position"""
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__()
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"
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
269 def matches(self, ifcelement):
270 __doc__ = super().matches.__doc__
271 raise NotImplementedError("ToDo") # TODO
274class ZoneFilter(GeometricFilter):
275 """Filter elements within given zone"""
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)