BIM2SIM / bim2sim

A python tool to create simulation models for different domains based on BIM IFC models.
https://bim2sim.github.io/bim2sim/
GNU Lesser General Public License v3.0
42 stars 7 forks source link

reimplement material enrichment and not only material overwrite #676

Open DaJansenGit opened 2 days ago

DaJansenGit commented 2 days ago

sim_setting.layers_and_materials = LOD.full is currently not supported anymore

DaJansenGit commented 2 days ago

Here is the old relevant code:

EnrichMaterial Task:

import ast
import re

from bim2sim.kernel.decision import ListDecision, DecisionBunch
from bim2sim.elements.base_elements import Material
from bim2sim.elements.bps_elements import Layer, LayerSet, Building

from bim2sim.tasks.base import ITask
from bim2sim.utilities.common_functions import get_material_templates, \
    translate_deep, filter_elements, get_type_building_elements

class EnrichMaterial(ITask):
    """Enriches material properties that were recognized as invalid
    LOD.layers = Medium & Full"""

    reads = ('elements', 'invalid',)
    touches = ('elements',)

    def __init__(self, playground):
        super().__init__(playground)
        self.enriched_elements = {}
        self.template_layer_set = {}
        self.template_materials = {}

    def run(self, elements: dict, invalid: dict):
        buildings = filter_elements(elements, Building)
        templates = yield from self.get_templates_for_buildings(
            buildings, self.playground.sim_settings)
        if not templates:
            self.logger.warning(
                "Tried to run enrichment for layers structure and materials, "
                "but no fitting templates were found. "
                "Please check your settings.")
            return elements,
        resumed = self.get_resumed_material_templates()
        for invalid_inst in invalid.values():
            yield from self.enrich_invalid_element(invalid_inst, resumed,
                                                   templates)
        self.logger.info("enriched %d invalid materials",
                         len(self.enriched_elements))
        elements = self.update_elements(elements, self.enriched_elements)
        # TODO currently, old materials and layser sets still exist, clean this
        return elements,

    def get_templates_for_buildings(
            self, buildings, sim_settings):
        """get templates for building"""
        templates = {}
        construction_type = sim_settings.construction_class_walls
        windows_construction_type = sim_settings.construction_class_windows
        if not buildings:
            raise ValueError(
                "No buildings found, without a building no template can be"
                " assigned and enrichment can't proceed.")
        for building in buildings:
            if sim_settings.year_of_construction_overwrite:
                building.year_of_construction = \
                    int(sim_settings.year_of_construction_overwrite)
            if not building.year_of_construction:
                year_decision = building.request('year_of_construction')
                yield DecisionBunch([year_decision])
            year_of_construction = int(building.year_of_construction.m)
            templates[building] = self.get_template_for_year(
                year_of_construction, construction_type,
                windows_construction_type)
        return templates

    def get_template_for_year(self, year_of_construction, construction_type,
                              windows_construction_type):
        element_templates = get_type_building_elements()
        bldg_template = {}
        for element_type, years_dict in element_templates.items():
            if len(years_dict) == 1:
                template_options = years_dict[list(years_dict.keys())[0]]
            else:
                template_options = None
                for i, template in years_dict.items():
                    years = ast.literal_eval(i)
                    if years[0] <= year_of_construction <= years[1]:
                        template_options = element_templates[element_type][i]
                        break
            if len(template_options) == 1:
                bldg_template[element_type] = \
                    template_options[list(template_options.keys())[0]]
            else:
                if element_type == 'Window':
                    try:
                        bldg_template[element_type] = \
                            template_options[windows_construction_type]
                    except KeyError:
                        # select last available window construction type if
                        # the selected/default window type is not available
                        # for the given year. The last construction type is
                        # selected, since the first construction type may be a
                        # single pane wood frame window and should not be
                        # used as new default construction.
                        new_window_construction_type = \
                            list(template_options.keys())[-1]
                        self.logger.warning(
                            "The window_construction_type %s is not available "
                            "for year_of_construction %i. Using the "
                            "window_construction_type %s instead.",
                            windows_construction_type, year_of_construction,
                            new_window_construction_type)
                        bldg_template[element_type] = \
                            template_options[new_window_construction_type]
                else:
                    bldg_template[element_type] = \
                        template_options[construction_type]
        return bldg_template

    def enrich_invalid_element(self, invalid_element, resumed, templates):
        """enrich invalid element"""
        # TODO when material lod = low --> don't enrich layerssets, just create
        #  fresh ones from template. Maybe don't even create materials and
        #  layersets from IFC in the first place
        #
        if type(invalid_element) is Layer:
            enriched_element = yield from self.enrich_layer(
                invalid_element, resumed, templates)
            self.enriched_elements[invalid_element.guid] = enriched_element

        elif type(invalid_element) is LayerSet:
            enriched_element = self.enrich_layer_set(invalid_element, resumed,
                                                      templates)
            self.enriched_elements[invalid_element.guid] = enriched_element
        else:
            self.enrich_element(invalid_element, resumed, templates)

    def enrich_layer(self, invalid_layer, resumed, templates):
        """enrich layer"""
        invalid_layer_sets = [layer_set for layer_set in
                              invalid_layer.to_layerset]
        type_invalid_elements = self.get_invalid_elements_type(
            invalid_layer_sets)
        if len(type_invalid_elements) == 1:
            specific_element_template = templates[list(
                templates.keys())[0]][type_invalid_elements[0]]
            resumed_names = list(set(
                layer['material']['name'] for layer in
                specific_element_template['layer'].values()))
        else:
            resumed_names = list(resumed.keys())
        layer = Layer()
        layer.thickness = invalid_layer.thickness
        material_name = invalid_layer.material.name
        if material_name in self.template_materials:
            material = self.template_materials[material_name]
        else:
            specific_template = yield from self.get_material_template(
                material_name, resumed_names, resumed)
            material = self.create_material_from_template(specific_template)
            self.template_materials[material_name] = material
        material.parents.append(layer)
        layer.material = material
        for layer_set in invalid_layer_sets:
            layer.to_layerset.append(layer_set)
            layer_set.layers[layer_set.layers.index(invalid_layer)] = layer
        return layer

    @staticmethod
    def get_invalid_elements_type(layer_sets):
        """get invalid elements"""
        invalid_elements = []
        for layer_set in layer_sets:
            for parent in layer_set.parents:
                element_type = type(parent).__name__
                if element_type not in invalid_elements:
                    invalid_elements.append(element_type)
        return invalid_elements

    @classmethod
    def get_material_template(cls, material_name: str, resumed_names: list,
                              resumed: dict) -> [list, str]:
        """get list of matching materials
        if material has no matches, more common name necessary"""
        material = re.sub(r'[^\w]*?[0-9]', '', material_name)
        material_options = cls.get_matches_list(
            material, resumed_names) if material_name else []
        if len(material_options) == 1:
            selected_material = material_options[0]
        else:
            selected_material = yield from cls.material_search(material_options,
                                                               material_name)
        return resumed[selected_material]

    def enrich_layer_set(self, invalid_element, resumed, templates):
        """enrich layer set"""
        type_invalid_elements = self.get_invalid_elements_type(
            [invalid_element])[0]
        layer_set, add_enrichment = self.layer_set_search(
            type_invalid_elements, templates, resumed)
        for parent in invalid_element.parents:
            layer_set.parents.append(parent)
            parent.layerset = layer_set
            self.additional_element_enrichment(parent,
                                                add_enrichment)
        return layer_set

    def enrich_element(self, invalid_element, resumed, templates):
        """enrich element"""
        type_invalid_element = type(invalid_element).__name__
        # Handle disaggregated classes
        if "Disaggregated" in type_invalid_element:
            type_invalid_element = type_invalid_element.replace(
                "Disaggregated", "")
        if type_invalid_element == "InnerFloor":
            type_invalid_element = "Floor"
        layer_set, add_enrichment = self.layer_set_search(type_invalid_element,
                                                          templates, resumed)

        layer_set.parents.append(invalid_element)
        invalid_element.layerset = layer_set
        self.additional_element_enrichment(invalid_element, add_enrichment)
        # return element

    def layer_set_search(self, type_invalid_element, templates, resumed):
        """Search for layer set.

        Args:
            type_invalid_element:
            templates:
            resumed:

        Returns:
            layer_set: bim2sim LayerSet instance
            ele_enrichment_info (dict): additional enrichment information that
             needs to be attached to the element and not the LayerSet
        """

        if type_invalid_element in self.template_layer_set:
            layer_set, ele_enrichment_info = self.template_layer_set[
                type_invalid_element].values()
        else:
            specific_template = templates[
                list(templates.keys())[0]][type_invalid_element]
            ele_enrichment_info = {key: info for key, info in
                              specific_template.items()
                              if type(info) not in [list, dict]}
            layer_set = self.create_layer_set_from_template(resumed,
                                                            specific_template)
            self.template_layer_set[type_invalid_element] = {
                'layer_set': layer_set,
                'add_enrichment': ele_enrichment_info}
        return layer_set, ele_enrichment_info

    @staticmethod
    def additional_element_enrichment(invalid_element, add_enrichment):
        # TODO what does this do?
        for key in add_enrichment:
            if hasattr(invalid_element, key):
                setattr(invalid_element, key, add_enrichment[key])

    def create_layer_set_from_template(self, resumed, template):
        """create layer set from template"""
        layer_set = LayerSet()
        for layer_template in template['layer'].values():
            layer = Layer()
            layer.thickness = layer_template['thickness']
            material_name = layer_template['material']['name']
            if material_name in self.template_materials:
                material = self.template_materials[material_name]
            else:
                material = self.create_material_from_template(
                    resumed[material_name])
                self.template_materials[material_name] = material
            material.parents.append(layer)
            layer.material = material
            layer.to_layerset.append(layer_set)
            layer_set.layers.append(layer)

        return layer_set

    @staticmethod
    def create_material_from_template(material_template):
        material = Material()
        material.name = material_template['material']
        material.density = material_template['density']
        material.spec_heat_capacity = material_template['heat_capac']
        material.thermal_conduc = material_template['thermal_conduc']
        material.solar_absorp = material_template['solar_absorp']
        return material

    def update_elements(self, elements, enriched_elements):
        # add new created materials to elements
        for mat in self.template_materials.values():
            elements[mat.guid] = mat
        for guid, new_element in enriched_elements.items():
            old_element = elements[guid]
            if type(old_element) is Layer:
                old_material = old_element.material
                if old_material.guid in elements:
                    del elements[old_material.guid]
                new_material = new_element.material
                elements[new_material.guid] = new_material
            if type(old_element) is LayerSet:
                for old_layer in old_element.layers:
                    old_material = old_layer.material
                    if old_material.guid in elements:
                        del elements[old_material.guid]
                    if old_layer.guid in elements:
                        del elements[old_layer.guid]
                for new_layer in new_element.layers:
                    new_material = new_layer.material
                    elements[new_material.guid] = new_material
                    elements[new_layer.guid] = new_layer
            if guid in elements:
                del elements[guid]
            elements[new_element.guid] = new_element
        return elements

    @staticmethod
    def get_resumed_material_templates(attrs: dict = None) -> dict:
        """get dict with the material templates and its respective attributes"""
        material_templates = get_material_templates()
        resumed = {}
        for k in material_templates:
            resumed[material_templates[k]['name']] = {}
            if attrs is not None:
                for attr in attrs:
                    if attr == 'thickness':
                        resumed[material_templates[k]['name']][attr] = \
                            material_templates[k]['thickness_default']
                    else:
                        resumed[material_templates[k]['name']][attr] = \
                            material_templates[k][attr]
            else:
                for attr in material_templates[k]:
                    if attr == 'thickness_default':
                        resumed[material_templates[k]['name']]['thickness'] = \
                            material_templates[k][attr]
                    elif attr == 'name':
                        resumed[material_templates[k]['name']]['material'] = \
                            material_templates[k][attr]
                    elif attr == 'thickness_list':
                        continue
                    else:
                        resumed[material_templates[k]['name']][attr] = \
                            material_templates[k][attr]
        return resumed

    @staticmethod
    def get_matches_list(search_words: str, search_list: list) -> list:
        """get patterns for a material name in both english and original language,
        and get afterwards the related elements from list"""

        material_ref = []

        if type(search_words) is str:
            pattern_material = search_words.split()
            translated = translate_deep(search_words)
            if translated:
                pattern_material.extend(translated.split())
            for i in pattern_material:
                material_ref.append(
                    re.compile('(.*?)%s' % i, flags=re.IGNORECASE))

        material_options = []
        for ref in material_ref:
            for mat in search_list:
                if ref.match(mat):
                    if mat not in material_options:
                        material_options.append(mat)
        if len(material_options) == 0:
            return search_list
        return material_options

    @staticmethod
    def material_search(material_options: list, material_input: str):
        material_selection = ListDecision(
            "Multiple possibilities found for material \"%s\"\n"
            "Enter name from given list" % material_input,
            choices=list(material_options),
            global_key='_Material_%s_search' % material_input,
            allow_skip=True,
            live_search=True)
        yield DecisionBunch([material_selection])
        return material_selection.value

VerifyLayersMaterialsTask

from bim2sim.elements.base_elements import Material
from bim2sim.elements.bps_elements import BPSProductWithLayers, LayerSet, Layer
from bim2sim.elements.mapping.units import ureg
from bim2sim.tasks.base import ITask
from bim2sim.utilities.common_functions import all_subclasses, filter_elements
from bim2sim.utilities.types import LOD

class VerifyLayersMaterials(ITask):
    """Verifies if layers and materials and their properties are meaningful."""

    reads = ('elements',)
    touches = ('invalid',)

    def __init__(self, playground):
        super().__init__(playground)
        self.invalid = []

    def run(self, elements: dict):
        self.logger.info("setting verifications")
        # TODO rework how invalids are assigned and use disaggregations instead
        #  elements if existing
        if self.playground.sim_settings.layers_and_materials is not LOD.low:
            materials = filter_elements(elements, Material)
            self.invalid.extend(self.materials_verification(materials))
            layers = filter_elements(elements, Layer)
            self.invalid.extend(self.layers_verification(layers))
            layer_sets = filter_elements(elements, LayerSet)
            self.invalid.extend(self.layer_sets_verification(layer_sets))
            self.invalid.extend(
                self.elements_with_layers_verification(elements))
            self.logger.warning("Found %d invalid elements", len(self.invalid))
        else:
            self.invalid.extend(
                self.elements_with_layers_verification(elements,
                                                        lod_low=True))
        self.invalid = {inv.guid: inv for inv in self.invalid}
        return self.invalid,

    def materials_verification(self, materials):
        """checks validity of the material property values"""
        invalid_layers = []
        for material in materials:
            invalid = False
            for attr in material.attributes:
                value = getattr(material, attr)
                if not self.value_verification(attr, value):
                    invalid = True
                    break
            if invalid:
                for layer in material.parents:
                    if layer not in invalid_layers:
                        invalid_layers.append(layer)
        sorted_layers = list(sorted(invalid_layers,
                                    key=lambda layer_e: layer_e.material.name))
        return sorted_layers

    def layers_verification(self, layers):
        """checks validity of the layer property values"""
        invalid_layers = []
        for layer in layers:
            if layer.guid not in self.invalid:
                invalid = True
                if layer.material:
                    if layer.thickness is not None:
                        invalid = False
                if invalid:
                    invalid_layers.append(layer)
        sorted_layers = list(sorted(invalid_layers,
                                    key=lambda layer_e: layer_e.material.name))
        return sorted_layers

    @staticmethod
    def layer_sets_verification(layer_sets):
        """checks validity of the layer set property values"""
        invalid_layer_sets = []
        for layer_set in layer_sets:
            invalid = True
            if len(layer_set.layers):
                if layer_set.thickness is not None:
                    invalid = False
            if invalid:
                invalid_layer_sets.append(layer_set)
        sorted_layer_sets = list(sorted(invalid_layer_sets,
                                        key=lambda layer_set_e:
                                        layer_set_e.name))
        return sorted_layer_sets

    @staticmethod
    def elements_with_layers_verification(elements, lod_low=False):
        invalid_elements = []
        layer_classes = list(all_subclasses(BPSProductWithLayers))
        for inst in elements.values():
            if type(inst) in layer_classes:
                if not lod_low:
                    invalid = False
                    if not inst.layerset and not inst.material_set:
                        invalid = True
                    if invalid:
                        invalid_elements.append(inst)
                else:
                    invalid_elements.append(inst)
        return invalid_elements

    @staticmethod
    def value_verification(attr: str, value: ureg.Quantity):
        """checks validity of the properties if they are on the blacklist"""
        blacklist = ['density', 'spec_heat_capacity', 'thermal_conduc']
        if (value is None or value <= 0) and attr in blacklist:
            return False
        return True