Coverage for bim2sim/tasks/bps/enrich_material.py: 89%

169 statements  

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

1import ast 

2import logging 

3from dataclasses import dataclass 

4from pathlib import Path 

5from typing import Dict, List 

6 

7from bim2sim.elements.base_elements import Material 

8from bim2sim.elements.bps_elements import Layer, LayerSet, Building 

9from bim2sim.kernel.decision import DecisionBunch 

10from bim2sim.sim_settings import BuildingSimSettings 

11from bim2sim.tasks.base import ITask 

12from bim2sim.tasks.base import Playground 

13from bim2sim.utilities.common_functions import filter_elements, \ 

14 get_type_building_elements, get_material_templates 

15from bim2sim.utilities.types import LOD, AttributeDataSource 

16 

17 

18class EnrichMaterial(ITask): 

19 """Enriches material properties that were recognized as invalid 

20 LOD.layers = Medium & Full""" 

21 

22 reads = ('elements',) 

23 

24 mapping_templates_bim2sim = { 

25 "OuterWall": ["OuterWall", "OuterWallDisaggregated"], 

26 "InnerWall": ["InnerWall", "InnerWallDisaggregated"], 

27 "Window": ["Window"], 

28 "Roof": ["Roof"], 

29 "Floor": ["InnerFloor", "InnerFloorDisaggregated"], 

30 "GroundFloor": ["GroundFloor", "GroundFloorDisaggregated"], 

31 "Door": ["OuterDoor", "InnerDoor", "OuterDoorDisaggregated", 

32 "InnerDoorDisaggregated"], 

33 } 

34 

35 def __init__(self, playground: Playground): 

36 super().__init__(playground) 

37 self.layer_sets_added = [] 

38 self.template_materials = {} 

39 

40 def run(self, elements: dict): 

41 """Enriches materials and layer sets of building elements. 

42 

43 Enrichment data in the files MaterialTemplates.json and 

44 TypeElements_IWU.json is taken from TEASER. The underlying data 

45 comes from IWU data. For more detailed information please review TEASER 

46 code documentation: 

47 https://rwth-ebc.github.io/TEASER//master/docs/index.html 

48 """ 

49 # TODO add data_source also to non attributes values: 

50 # layer and layerset 

51 [element_templates, material_template] = \ 

52 yield from self.get_templates(elements) 

53 if self.playground.sim_settings.layers_and_materials is LOD.low: 

54 self.create_new_layer_sets_and_materials( 

55 elements, element_templates, material_template) 

56 

57 if self.playground.sim_settings.layers_and_materials is LOD.full: 

58 # TODO #676 

59 raise NotImplementedError("layers_and_materials full is currently" 

60 " not supported.") 

61 for layer_set in self.layer_sets_added: 

62 elements[layer_set.guid] = layer_set 

63 for layer in layer_set.layers: 

64 elements[layer.guid] = layer 

65 for material in self.template_materials.values(): 

66 elements[material.guid] = material 

67 

68 def create_new_layer_sets_and_materials( 

69 self, elements: dict, 

70 element_templates: dict, 

71 material_template: dict): 

72 """Create a new layer set including layers and materials. 

73 

74 This creates a completely new layer set, including the relevant layers 

75 and materials. Materials are only created once, even if they occur in 

76 multiple layer sets/layers. 

77 Additionally, some information on element level are overwritten with 

78 data from the templates, like inner_convection etc. 

79 """ 

80 # TODO multi building support would require to have relations 

81 # between each bim2sim element and its bim2sim Building 

82 # instance. For now we always take the first building. 

83 element_template = element_templates[ 

84 list(element_templates.keys())[0]] 

85 for template_name, ele_types in ( 

86 self.mapping_templates_bim2sim.items()): 

87 layer_set = self.create_layer_set_from_template( 

88 element_template[template_name], material_template) 

89 ele_enrichment_data = self.enrich_element_data_from_template( 

90 element_template[template_name]) 

91 elements_to_enrich = [] 

92 for ele_type in ele_types: 

93 elements_to_enrich.extend(filter_elements(elements, ele_type)) 

94 for element in elements_to_enrich: 

95 # set layer_set 

96 element.layerset = layer_set 

97 # TODO set layer_set also to not disaggregated parent, t.b.d. 

98 # if hasattr(element, "disagg_parent"): 

99 # element.disagg_parent.layerset = layer_set 

100 # overwrite element level attributes like inner_convection 

101 for att, value in ele_enrichment_data.items(): 

102 if hasattr(element, att): 

103 setattr(element, att, value) 

104 # overwrite thickness/width of element with enriched layer_set 

105 # thickness 

106 if hasattr(element, "width"): 

107 element.width = ( 

108 layer_set.thickness, AttributeDataSource.enrichment) 

109 

110 @staticmethod 

111 def enrich_element_data_from_template(element_template: dict) -> dict: 

112 """Get all element level enrichment data from templates.""" 

113 ele_enrichment_data = {key: info for key, info in 

114 element_template.items() 

115 if type(info) not in [list, dict]} 

116 return ele_enrichment_data 

117 

118 def create_layer_set_from_template(self, element_template: dict, 

119 material_template: dict) -> LayerSet: 

120 """Create layer set from template including layers and materials.""" 

121 layer_set = LayerSet() 

122 for layer_template in element_template['layer'].values(): 

123 layer = Layer() 

124 layer.thickness = layer_template['thickness'] 

125 material_name = layer_template['material']['name'] 

126 if material_name in self.template_materials: 

127 material = self.template_materials[material_name] 

128 else: 

129 material = self.create_material_from_template( 

130 material_template[material_name]) 

131 self.template_materials[material_name] = material 

132 material.parents.append(layer) 

133 layer.material = material 

134 layer.to_layerset.append(layer_set) 

135 layer_set.layers.append(layer) 

136 self.layer_sets_added.append(layer_set) 

137 return layer_set 

138 

139 @staticmethod 

140 def create_material_from_template(material_template: dict) -> Material: 

141 """Creates a material from template.""" 

142 material = Material() 

143 material.name = material_template['material'] 

144 material.density = ( 

145 material_template['density'], AttributeDataSource.enrichment) 

146 material.spec_heat_capacity = ( 

147 material_template['heat_capac'], AttributeDataSource.enrichment) 

148 material.thermal_conduc = ( 

149 material_template['thermal_conduc'], 

150 AttributeDataSource.enrichment) 

151 material.solar_absorp = ( 

152 material_template['solar_absorp'], AttributeDataSource.enrichment) 

153 return material 

154 

155 def get_templates(self, elements: dict) -> object: 

156 """Get templates for elements and materials. 

157 

158 Args: 

159 elements: dict[guid: element] of bim2sim elements 

160 Returns: 

161 element_templates (dict): Holds enrichment templates for each 

162 Building with layer set information and material reference for 

163 different BPSProducts 

164 material_templates (dict): Holds information about physical 

165 attributes for each material referenced in the element_templates 

166 """ 

167 buildings = filter_elements(elements, Building) 

168 element_templates = yield from self.get_templates_for_buildings( 

169 buildings, self.playground.sim_settings) 

170 if not element_templates: 

171 self.logger.warning( 

172 "Tried to run enrichment for layers structure and materials, " 

173 "but no fitting templates were found. " 

174 "Please check your settings.") 

175 return elements, 

176 material_templates = self.get_material_templates() 

177 return element_templates, material_templates 

178 

179 @staticmethod 

180 def get_material_templates(attrs: dict = None) -> dict: 

181 """get dict with the material templates and its respective 

182 attributes""" 

183 material_templates = get_material_templates() 

184 resumed = {} 

185 for k in material_templates: 

186 resumed[material_templates[k]['name']]: dict = {} 

187 if attrs is not None: 

188 for attr in attrs: 

189 if attr == 'thickness': 

190 resumed[material_templates[k]['name']][attr] = \ 

191 material_templates[k]['thickness_default'] 

192 else: 

193 resumed[material_templates[k]['name']][attr] = \ 

194 material_templates[k][attr] 

195 else: 

196 for attr in material_templates[k]: 

197 if attr == 'thickness_default': 

198 resumed[material_templates[k]['name']]['thickness'] = \ 

199 material_templates[k][attr] 

200 elif attr == 'name': 

201 resumed[material_templates[k]['name']]['material'] = \ 

202 material_templates[k][attr] 

203 elif attr == 'thickness_list': 

204 continue 

205 else: 

206 resumed[material_templates[k]['name']][attr] = \ 

207 material_templates[k][attr] 

208 return resumed 

209 

210 @dataclass 

211 class ConstructionTemplate: 

212 """Data class to hold construction template information. 

213 

214 Attributes: 

215 walls: Path to wall construction data file 

216 windows: Path to window construction data file 

217 doors: Path to door construction data file 

218 """ 

219 walls: Path 

220 windows: Path 

221 doors: Path 

222 

223 @staticmethod 

224 def determine_construction_data_file(construction_type: str, 

225 data_source: str) -> Path: 

226 """Determines the appropriate construction data file based on the data source. 

227 

228 Args: 

229 construction_type: Type of construction element ('wall', 'window', or 'door') 

230 data_source: Source of construction data (e.g., 'iwu', 'kfw', 'tabula_de') 

231 

232 Returns: 

233 Path: Path to the construction data file 

234 

235 Raises: 

236 ValueError: If the construction data source is unknown 

237 """ 

238 # Map of known data sources to their respective files 

239 source_to_file = { 

240 'iwu': 'TypeElements_IWU.json', 

241 'kfw': 'TypeElements_KFW.json', 

242 'tabula_de': 'TypeElements_TABULA_DE.json', 

243 'tabula_dk': 'TypeElements_TABULA_DK.json' 

244 } 

245 

246 # Special case for IWU windows 

247 iwu_window_types = [ 

248 'Holzfenster, zweifach', 

249 'Kunststofffenster, Isolierverglasung', 

250 'Alu- oder Stahlfenster, Isolierverglasung', 

251 'Alu- oder Stahlfenster, Waermeschutzverglasung, zweifach', 

252 'Waermeschutzverglasung, dreifach' 

253 ] 

254 

255 if construction_type == 'window' and any( 

256 type_name in data_source for type_name in iwu_window_types): 

257 return Path('TypeElements_IWU.json') 

258 

259 for source, filename in source_to_file.items(): 

260 if source in data_source.lower(): 

261 return Path(filename) 

262 

263 raise ValueError( 

264 f"Unknown {construction_type} construction class: {data_source}") 

265 

266 @staticmethod 

267 def get_element_template(element_type: str, year_of_construction: int, 

268 years_dict: dict, construction_data: str, 

269 logger: logging.Logger) -> dict: 

270 """Retrieves the template for a specific building element based on construction year. 

271 

272 Args: 

273 element_type: Type of building element 

274 year_of_construction: Year when the building was constructed 

275 years_dict: Dictionary containing templates for different year ranges 

276 construction_data: Construction data identifier 

277 logger: Logger instance for warnings 

278 

279 Returns: 

280 dict: Template for the specified building element 

281 """ 

282 # Handle single template case 

283 if len(years_dict) == 1: 

284 template_options = years_dict[list(years_dict.keys())[0]] 

285 if len(template_options) == 1: 

286 return template_options[list(template_options.keys())[0]] 

287 return template_options[construction_data] 

288 

289 # Find template for specific year range 

290 template_options = None 

291 for year_range, template in years_dict.items(): 

292 years = ast.literal_eval(year_range) 

293 if years[0] <= year_of_construction <= years[1]: 

294 template_options = template 

295 break 

296 

297 if not template_options: 

298 return None 

299 

300 if len(template_options) == 1: 

301 return template_options[list(template_options.keys())[0]] 

302 

303 # Special handling for windows 

304 if element_type == 'Window': 

305 try: 

306 return template_options[construction_data] 

307 except KeyError: 

308 # Fallback to last available window type 

309 new_construction_data = list(template_options.keys())[-1] 

310 logger.warning( 

311 f"The window_construction_data {construction_data} is not " 

312 f"available for year_of_construction {year_of_construction}. " 

313 f"Using {new_construction_data} instead.") 

314 return template_options[new_construction_data] 

315 

316 return template_options[construction_data] 

317 

318 def get_templates_for_buildings( 

319 self, buildings: List, 

320 sim_settings: 'BuildingSimSettings') -> Dict: 

321 """Generate templates for building elements based on construction year. 

322 

323 Args: 

324 buildings: List of building objects to process 

325 sim_settings: Settings object containing construction parameters 

326 

327 Returns: 

328 Dict: Mapping of buildings to their construction templates 

329 

330 Raises: 

331 ValueError: If no buildings are provided 

332 """ 

333 if not buildings: 

334 raise ValueError( 

335 "No buildings found, without a building no template can be " 

336 "assigned and enrichment can't proceed.") 

337 

338 templates = {} 

339 for building in buildings: 

340 # Handle construction year 

341 if sim_settings.year_of_construction_overwrite: 

342 building.year_of_construction = int( 

343 sim_settings.year_of_construction_overwrite) 

344 if not building.year_of_construction: 

345 year_decision = building.request('year_of_construction') 

346 yield DecisionBunch([year_decision]) 

347 

348 year_of_construction = int(building.year_of_construction.m) 

349 

350 # Get construction data files 

351 construction_files = self.ConstructionTemplate( 

352 walls=self.determine_construction_data_file( 

353 'wall', sim_settings.construction_class_walls), 

354 windows=self.determine_construction_data_file( 

355 'window', sim_settings.construction_class_windows), 

356 doors=self.determine_construction_data_file( 

357 'door', sim_settings.construction_class_doors) 

358 ) 

359 

360 # Load element templates 

361 element_templates = { 

362 'walls': get_type_building_elements(construction_files.walls), 

363 'windows': get_type_building_elements( 

364 construction_files.windows), 

365 'doors': get_type_building_elements(construction_files.doors) 

366 } 

367 

368 # Build template for current building 

369 bldg_template = {} 

370 for element_type, years_dict in element_templates[ 

371 'windows'].items(): 

372 if element_type == 'Window': 

373 bldg_template[element_type] = self.get_element_template( 

374 element_type, year_of_construction, years_dict, 

375 sim_settings.construction_class_windows, self.logger) 

376 

377 for element_type, years_dict in element_templates['doors'].items(): 

378 if element_type == 'Door': 

379 bldg_template[element_type] = self.get_element_template( 

380 element_type, year_of_construction, years_dict, 

381 sim_settings.construction_class_doors, self.logger) 

382 

383 for element_type, years_dict in element_templates['walls'].items(): 

384 if element_type not in ('Window', 'Door'): 

385 bldg_template[element_type] = self.get_element_template( 

386 element_type, year_of_construction, years_dict, 

387 sim_settings.construction_class_walls, self.logger) 

388 

389 templates[building] = bldg_template 

390 

391 return templates