Coverage for bim2sim/plugins/PluginTEASER/bim2sim_teaser/export/__init__.py: 0%
172 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1"""Package for TEASER export"""
3import logging
4from threading import Lock
5from typing import Union, Type, Dict, Container, Tuple, Callable, List
7import pint
9from bim2sim.elements.aggregation.bps_aggregations import \
10 InnerFloorDisaggregated
11from bim2sim.elements.bps_elements import InnerFloor, ThermalZone
12from bim2sim.elements.aggregation.bps_aggregations import AggregatedThermalZone
13from bim2sim.kernel import log
14from bim2sim.elements.base_elements import Element
15from bim2sim.elements.base_elements import Dummy as ElementDummy
16from bim2sim.utilities.types import BoundaryOrientation
18lock = Lock()
20logger = logging.getLogger(__name__)
21user_logger = log.get_user_logger(__name__)
24class FactoryError(Exception):
25 """Error in Model factory"""
28class TEASERExportInstance:
29 """TEASER model instance"""
31 library: str = None
32 represents: Union[Element, Container[Element]] = None
33 lookup: Dict[Type[Element], List[Type['TEASERExportInstance']]] = {}
34 dummy: Type['TEASERExportInstance'] = None
35 _initialized = False
36 export_elements: List[object] = []
37 requested_elements: List[Element] = []
39 def __init__(self, element: Element):
40 self.element = element
41 self.export_elements.append(self)
42 if element not in self.requested_elements:
43 self.requested_elements.append(element)
44 self.requested: Dict[str, Tuple[Callable, str, str]] = {}
45 self.request_params()
47 @staticmethod
48 def _lookup_add(key, value):
49 """Adds key and value to Instance.lookup. Returns conflict."""
50 if key in TEASERExportInstance.lookup and value not in TEASERExportInstance.lookup[key]:
51 logger.warning(
52 f"Multiple representations in TEASER Export for "
53 f"({key}) with "
54 f"{[inst.__name__ for inst in TEASERExportInstance.lookup[key]]}'")
55 TEASERExportInstance.lookup[key].append(value)
56 else:
57 TEASERExportInstance.lookup[key] = [value]
59 @staticmethod
60 def init_factory(libraries):
61 """initialize lookup for factory"""
62 conflict = False
63 TEASERExportInstance.dummy = Dummy
64 for library in libraries:
65 if TEASERExportInstance not in library.__bases__:
66 logger.warning(
67 "Got Library not directly inheriting from Instance.")
68 if library.library:
69 logger.info("Got library '%s'", library.library)
70 else:
71 logger.error("Attribute library not set for '%s'",
72 library.__name__)
73 raise AssertionError("Library not defined")
74 for cls in library.get_library_classes(library):
75 if cls.represents is None:
76 logger.warning(
77 "'%s' represents no model and can't be used",
78 cls.__name__)
79 continue
81 if isinstance(cls.represents, Container):
82 for rep in cls.represents:
83 TEASERExportInstance._lookup_add(rep, cls)
84 else:
85 TEASERExportInstance._lookup_add(cls.represents, cls)
87 if conflict:
88 raise AssertionError(
89 "Conflict(s) in Models. (See log for details).")
91 TEASERExportInstance._initialized = True
93 models = set([inst[0] for inst in [*TEASERExportInstance.lookup.values()]])
94 models_txt = "\n".join(
95 sorted([" - %s" % inst.__name__ for inst in models]))
96 logger.debug("TEASER initialized with %d models:\n%s",
97 len(models), models_txt)
99 @staticmethod
100 def get_library_classes(library) -> List[Type['TEASERExportInstance']]:
101 classes = []
102 for cls in library.__subclasses__():
103 sub_cls = cls.__subclasses__()
104 if sub_cls:
105 classes.extend(sub_cls)
106 else:
107 classes.append(cls)
108 return classes
110 @staticmethod
111 def factory(element, parent):
112 """Create model depending on ifc_element"""
113 if not TEASERExportInstance._initialized:
114 raise FactoryError("Factory not initialized.")
116 export_cls = TEASERExportInstance.lookup.get(type(element), TEASERExportInstance.dummy)
117 if len(export_cls) > 1:
118 # handle Floor representation with SBs
119 if isinstance(element, InnerFloorDisaggregated) or isinstance(
120 element, InnerFloor):
121 export_cls = TEASERExportInstance.handle_ceiling_floor(
122 element, parent)
123 else:
124 logger.error(f"Multiple export classes for {element} where no"
125 f"special handling is given.")
126 else:
127 export_cls = export_cls[0]
128 return export_cls(element, parent)
130 @staticmethod
131 def handle_ceiling_floor(element, parent):
132 """Handle if a bim2sim Floor is a TEASER Ceiling or Floor.
134 This function uses information of Space Boundaries to determine the
135 correct type of the given element. TEASER has Ceiling and Floor while
136 IFC and thus also bim2sim only knows Floors.
138 Args:
139 element: bim2sim element
140 parent: parent, in this case a ThermalZone or AggregatedThermalZone
141 element
143 Returns:
144 export_cls: Either Ceiling or Floor class of TEASER
146 """
147 # In non aggregated ThermalZone the bim2sim Floor can be
148 # either TEASER Ceiling or TEASER Floor
149 # use type() to check only for ThermalZone not subclasses
150 from bim2sim.plugins.PluginTEASER.bim2sim_teaser.models import \
151 Ceiling, Floor
152 sbs_ele_inside_zone = []
153 for sb_ele in element.space_boundaries:
154 if isinstance(
155 parent.element, AggregatedThermalZone):
156 tz_sbs = []
157 for tz in parent.element.elements:
158 for sb in tz.space_boundaries:
159 tz_sbs.append(sb)
160 else:
161 tz_sbs = parent.element.space_boundaries
162 if sb_ele in tz_sbs:
163 sbs_ele_inside_zone.append(sb_ele)
164 if len(sbs_ele_inside_zone) > 1:
165 if not isinstance(
166 parent.element, AggregatedThermalZone):
167 logger.warning(
168 f"For {element} multiple SBs of the same element"
169 f" were found inside one not aggregated "
170 f"ThermalZone: {sbs_ele_inside_zone}."
171 f"This might indicate, that something went"
172 f" wrong with prior Disaggregation but can also just mean "
173 f"that the related IfcSpace covers multiple IfcStoreys.")
174 export_cls = Ceiling
175 # In aggregated ThermalZone where the bim2sim Floor is
176 # inside the ThermalZone and not a boundary of the
177 # ThermalZone, it is handled as a Ceiling
178 elif isinstance(parent.element, AggregatedThermalZone):
179 export_cls = Ceiling
180 else:
181 sb_ele = sbs_ele_inside_zone[0]
182 top_bottom = sb_ele.top_bottom
183 if top_bottom == BoundaryOrientation.top:
184 export_cls = Ceiling
185 elif top_bottom == BoundaryOrientation.bottom:
186 export_cls = Floor
187 # This might be the case of slabs with opening inside a IfcSpace
188 elif top_bottom == BoundaryOrientation.vertical:
189 export_cls = Ceiling
190 else:
191 logger.error(
192 f"SB information is unclear, can't determine if {element}"
193 f"is a Floor or a Ceiling.")
194 return export_cls
196 def request_param(self, name: str, check=None, export_name: str = None,
197 export_unit: str = ''):
198 """Parameter gets marked as required and will be checked.
200 Hint: run collect_params() to collect actual values after requests.
202 :param name: name of parameter to request
203 :param check: validation function for parameter
204 :param export_name: name of parameter in export. Defaults to name
205 :param export_unit: unit of parameter in export. Converts to SI
206 units if not specified otherwise"""
207 self.element.request(name)
208 if export_name is None:
209 export_name = name
210 self.requested[name] = (check, export_name or name, export_unit)
212 def request_params(self):
213 """Request all required parameters."""
214 # overwrite this in child classes
215 pass
217 def collect_params(self):
218 """Collect all requested parameters.
220 First checks if the parameter is a list or a quantity, next uses the
221 check function provided by the request_param function to check every
222 value of the parameter, afterward converts the parameter values to the
223 special units provided by the request_param function, finally stores
224 the parameter on the model instance."""
226 for name, (
227 check, export_name, special_units) in self.requested.items():
228 param = getattr(self.element, name)
229 # check if parameter is a list, to check every value
230 if isinstance(param, list):
231 new_param = []
232 for item in param:
233 if self.check_param(item, check):
234 if special_units or isinstance(item, pint.Quantity):
235 item = self._convert_param(item, special_units).m
236 new_param.append(item)
237 else:
238 new_param = None
239 logger.warning("Parameter check failed for '%s' with "
240 "value: %s", name, param)
241 break
242 setattr(self, export_name, new_param)
243 else:
244 if self.check_param(param, check):
245 if special_units or isinstance(param, pint.Quantity):
246 param = self._convert_param(param, special_units).m
247 setattr(self, export_name, param)
248 else:
249 setattr(self, export_name, None)
250 logger.warning(
251 "Parameter check failed for '%s' with value: %s",
252 name, param)
254 @staticmethod
255 def check_param(param, check):
256 """Check if parameter is valid.
258 :param param: parameter to check
259 :param check: validation function for parameter"""
260 if check is not None:
261 if not check(param):
262 return False
263 return True
265 @staticmethod
266 def _convert_param(param: pint.Quantity, special_units) -> pint.Quantity:
267 """Convert to SI units or special units."""
268 if special_units:
269 if special_units != param.u:
270 converted = param.m_as(special_units)
271 else:
272 converted = param
273 else:
274 converted = param.to_base_units()
275 return converted
277 @staticmethod
278 def check_numeric(min_value=None, max_value=None):
279 """Generic check function generator
280 returns check function"""
281 if not isinstance(min_value, (pint.Quantity, type(None))):
282 raise AssertionError("min_value is no pint quantity with unit")
283 if not isinstance(max_value, (pint.Quantity, type(None))):
284 raise AssertionError("max_value is no pint quantity with unit")
286 def inner_check(value):
287 if not isinstance(value, pint.Quantity):
288 return False
289 if min_value is None and max_value is None:
290 return True
291 if min_value is not None and max_value is None:
292 return min_value <= value
293 if max_value is not None:
294 return value <= max_value
295 return min_value <= value <= max_value
297 return inner_check
299 def __repr__(self):
300 return "<%s_%s>" % (type(self).__name__, self.name)
303class Dummy(TEASERExportInstance):
304 represents = ElementDummy