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
« 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
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
18class EnrichMaterial(ITask):
19 """Enriches material properties that were recognized as invalid
20 LOD.layers = Medium & Full"""
22 reads = ('elements',)
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 }
35 def __init__(self, playground: Playground):
36 super().__init__(playground)
37 self.layer_sets_added = []
38 self.template_materials = {}
40 def run(self, elements: dict):
41 """Enriches materials and layer sets of building elements.
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)
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
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.
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)
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
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
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
155 def get_templates(self, elements: dict) -> object:
156 """Get templates for elements and materials.
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
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
210 @dataclass
211 class ConstructionTemplate:
212 """Data class to hold construction template information.
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
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.
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')
232 Returns:
233 Path: Path to the construction data file
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 }
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 ]
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')
259 for source, filename in source_to_file.items():
260 if source in data_source.lower():
261 return Path(filename)
263 raise ValueError(
264 f"Unknown {construction_type} construction class: {data_source}")
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.
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
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]
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
297 if not template_options:
298 return None
300 if len(template_options) == 1:
301 return template_options[list(template_options.keys())[0]]
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]
316 return template_options[construction_data]
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.
323 Args:
324 buildings: List of building objects to process
325 sim_settings: Settings object containing construction parameters
327 Returns:
328 Dict: Mapping of buildings to their construction templates
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.")
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])
348 year_of_construction = int(building.year_of_construction.m)
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 )
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 }
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)
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)
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)
389 templates[building] = bldg_template
391 return templates