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

1"""Package for TEASER export""" 

2 

3import logging 

4from threading import Lock 

5from typing import Union, Type, Dict, Container, Tuple, Callable, List 

6 

7import pint 

8 

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 

17 

18lock = Lock() 

19 

20logger = logging.getLogger(__name__) 

21user_logger = log.get_user_logger(__name__) 

22 

23 

24class FactoryError(Exception): 

25 """Error in Model factory""" 

26 

27 

28class TEASERExportInstance: 

29 """TEASER model instance""" 

30 

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] = [] 

38 

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() 

46 

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] 

58 

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 

80 

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) 

86 

87 if conflict: 

88 raise AssertionError( 

89 "Conflict(s) in Models. (See log for details).") 

90 

91 TEASERExportInstance._initialized = True 

92 

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) 

98 

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 

109 

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.") 

115 

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) 

129 

130 @staticmethod 

131 def handle_ceiling_floor(element, parent): 

132 """Handle if a bim2sim Floor is a TEASER Ceiling or Floor. 

133 

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. 

137 

138 Args: 

139 element: bim2sim element 

140 parent: parent, in this case a ThermalZone or AggregatedThermalZone 

141 element 

142 

143 Returns: 

144 export_cls: Either Ceiling or Floor class of TEASER 

145 

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 

195 

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. 

199 

200 Hint: run collect_params() to collect actual values after requests. 

201 

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) 

211 

212 def request_params(self): 

213 """Request all required parameters.""" 

214 # overwrite this in child classes 

215 pass 

216 

217 def collect_params(self): 

218 """Collect all requested parameters. 

219 

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.""" 

225 

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) 

253 

254 @staticmethod 

255 def check_param(param, check): 

256 """Check if parameter is valid. 

257 

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 

264 

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 

276 

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") 

285 

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 

296 

297 return inner_check 

298 

299 def __repr__(self): 

300 return "<%s_%s>" % (type(self).__name__, self.name) 

301 

302 

303class Dummy(TEASERExportInstance): 

304 represents = ElementDummy