From 9060b6ee40f8cd7a87b58a3747e40b8b282ce800 Mon Sep 17 00:00:00 2001 From: Cyril Waechter Date: Thu, 31 Dec 2020 11:39:33 +0100 Subject: [PATCH] Rework python pset module (#1221) New features ============ * Handle all syntax for ApplicableEntity as defined in IFC documentation. (drawback it increase request time by ~2/3) * Allow to get applicable psets directly from template definition without caching. This allow more general purpose usages. (drawback it increase request time by ~2.4) To notice / discuss =================== * `ifcwrap/CmakeLists` was installing only `.py` and `.bnf` files from `ifcopenshell-python`. Why ? It now copy all files. Is it an issue ? * property template file is now stored in ifcopenshell python module to allow usage from every softwares. It was before only available for blenderbim addon. * psets and qtos are now optionnally cached at object instead of module level. This allow to work with multiple schema in the same python interpreter. (eg. might help conversion between schemas) * in blenderbim addon psetqto are now stored in `schema.psetqto`. --- .../blenderbim/bim/export_ifc.py | 13 +- .../blenderbim/bim/import_ifc.py | 10 +- .../blenderbim/bim/operator.py | 31 ++--- src/ifcblenderexport/blenderbim/bim/prop.py | 13 +- src/ifcblenderexport/blenderbim/bim/schema.py | 10 +- .../ifcopenshell/util/pset.py | 124 +++++++++++++----- .../util}/schema/Pset_IFC4_ADD2.ifc | 0 src/ifcwrap/CMakeLists.txt | 3 +- 8 files changed, 123 insertions(+), 81 deletions(-) rename src/{ifcblenderexport/blenderbim/bim => ifcopenshell-python/ifcopenshell/util}/schema/Pset_IFC4_ADD2.ifc (100%) diff --git a/src/ifcblenderexport/blenderbim/bim/export_ifc.py b/src/ifcblenderexport/blenderbim/bim/export_ifc.py index 935ce350e7..0e755ec314 100644 --- a/src/ifcblenderexport/blenderbim/bim/export_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/export_ifc.py @@ -8,7 +8,6 @@ import os import zipfile import tempfile import ifcopenshell -import ifcopenshell.util.pset import ifcopenshell.util.schema from pathlib import Path from mathutils import Vector, Matrix @@ -477,10 +476,10 @@ class IfcParser: return qto_names = self.get_applicable_qtos(ifc_class) for name in qto_names: - if name not in ifcopenshell.util.pset.qtos: + if name not in schema.ifc.psetqto.qtos: continue has_automatic_value = False - props = ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys() + props = schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys() guessed_values = {} for prop_name in props: value = self.qto_calculator.guess_quantity(prop_name, props, obj) @@ -1699,12 +1698,12 @@ class IfcExporter: pset["ifc"] = self.file.create_entity("IfcMaterialProperties", **pset["attributes"]) def create_qto_properties(self, qto): - if qto["attributes"]["Name"] in ifcopenshell.util.pset.qtos: + if qto["attributes"]["Name"] in schema.ifc.psetqto.qtos: return self.create_templated_qto_properties(qto) return self.create_custom_qto_properties(qto) def create_pset_properties(self, pset): - if pset["attributes"]["Name"] in ifcopenshell.util.pset.psets: + if pset["attributes"]["Name"] in schema.ifc.psetqto.psets: return self.create_templated_pset_properties(pset) return self.create_custom_pset_properties(pset) @@ -1737,7 +1736,7 @@ class IfcExporter: def create_templated_pset_properties(self, pset): properties = [] - templates = ifcopenshell.util.pset.psets[pset["attributes"]["Name"]]["HasPropertyTemplates"] + templates = schema.ifc.psetqto.psets[pset["attributes"]["Name"]]["HasPropertyTemplates"] for name, data in templates.items(): if name not in pset["raw"]: continue @@ -1764,7 +1763,7 @@ class IfcExporter: def create_templated_qto_properties(self, qto): properties = [] - templates = ifcopenshell.util.pset.qtos[qto["attributes"]["Name"]]["HasPropertyTemplates"] + templates = schema.ifc.psetqto.qtos[qto["attributes"]["Name"]]["HasPropertyTemplates"] for name, data in templates.items(): if name not in qto["raw"]: continue diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index f59a7401b3..bbe4fa7d41 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -3,7 +3,6 @@ import ifcopenshell.geom import ifcopenshell.util.geolocation import ifcopenshell.util.selector import ifcopenshell.util.element -import ifcopenshell.util.pset import bpy import bmesh import os @@ -22,6 +21,7 @@ from itertools import cycle from datetime import datetime from . import helper from . import ifc +from . import schema class FileCopy(threading.Thread): @@ -1394,8 +1394,8 @@ class IfcImporter: def add_pset(self, pset, props): new_pset = props.psets.add() new_pset.name = pset.Name - if new_pset.name in ifcopenshell.util.pset.psets: - for prop_name in ifcopenshell.util.pset.psets[new_pset.name]["HasPropertyTemplates"].keys(): + if new_pset.name in schema.ifc.psetqto.psets: + for prop_name in schema.ifc.psetqto.psets[new_pset.name]["HasPropertyTemplates"].keys(): prop = new_pset.properties.add() prop.name = prop_name try: @@ -1421,8 +1421,8 @@ class IfcImporter: def add_qto(self, qto, obj): new_qto = obj.BIMObjectProperties.qtos.add() new_qto.name = str(qto.Name) - if new_qto.name in ifcopenshell.util.pset.qtos: - for prop_name in ifcopenshell.util.pset.qtos[new_qto.name]["HasPropertyTemplates"].keys(): + if new_qto.name in schema.ifc.psetqto.qtos: + for prop_name in schema.ifc.psetqto.qtos[new_qto.name]["HasPropertyTemplates"].keys(): prop = new_qto.properties.add() prop.name = prop_name for prop in qto.Quantities: diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index 102b525102..a9ee97db61 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -10,7 +10,6 @@ import subprocess import ifcopenshell import ifcopenshell.util.selector import ifcopenshell.util.geolocation -import ifcopenshell.util.pset import ifcopenshell.util.schema import tempfile from . import export_ifc @@ -588,7 +587,7 @@ class AddQto(bpy.types.Operator): def execute(self, context): self.applicable_qtos_cache = {} name = bpy.context.active_object.BIMObjectProperties.qto_name - if name not in ifcopenshell.util.pset.qtos: + if name not in schema.ifc.psetqto.qtos: return {"FINISHED"} for obj in bpy.context.selected_objects: if "/" not in obj.name or obj.BIMObjectProperties.qtos.find(name) != -1: @@ -598,16 +597,14 @@ class AddQto(bpy.types.Operator): continue qto = obj.BIMObjectProperties.qtos.add() qto.name = name - for prop_name in ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys(): prop = qto.properties.add() prop.name = prop_name return {"FINISHED"} def get_applicable_qtos(self, ifc_class): if ifc_class not in self.applicable_qtos_cache: - self.applicable_qtos_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, ifc_class, is_qto=True - ) + self.applicable_qtos_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, qto_only=True) return self.applicable_qtos_cache[ifc_class] @@ -618,7 +615,7 @@ class AddPset(bpy.types.Operator): def execute(self, context): self.applicable_psets_cache = {} name = bpy.context.active_object.BIMObjectProperties.pset_name - if name not in ifcopenshell.util.pset.psets: + if name not in schema.ifc.psetqto.psets: return {"FINISHED"} for obj in bpy.context.selected_objects: if "/" not in obj.name or obj.BIMObjectProperties.psets.find(name) != -1: @@ -628,16 +625,14 @@ class AddPset(bpy.types.Operator): continue pset = obj.BIMObjectProperties.psets.add() pset.name = name - for prop_name in ifcopenshell.util.pset.psets[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.psets[name]["HasPropertyTemplates"].keys(): prop = pset.properties.add() prop.name = prop_name return {"FINISHED"} def get_applicable_psets(self, ifc_class): if ifc_class not in self.applicable_psets_cache: - self.applicable_psets_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, ifc_class, is_pset=True - ) + self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True) return self.applicable_psets_cache[ifc_class] @@ -680,13 +675,13 @@ class AddMaterialPset(bpy.types.Operator): def execute(self, context): material = bpy.context.active_object.active_material name = material.BIMMaterialProperties.pset_name - if name not in ifcopenshell.util.pset.psets: + if name not in schema.ifc.psetqto.psets: return {"FINISHED"} if material.BIMMaterialProperties.psets.find(name) != -1: return {"FINISHED"} pset = material.BIMMaterialProperties.psets.add() pset.name = name - for prop_name in ifcopenshell.util.pset.psets[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.psets[name]["HasPropertyTemplates"].keys(): prop = pset.properties.add() prop.name = prop_name return {"FINISHED"} @@ -2649,7 +2644,7 @@ class CopyPropertyToSelection(bpy.types.Operator): continue pset = obj.BIMObjectProperties.psets.add() pset.name = self.pset_name - for template_prop_name in ifcopenshell.util.pset.psets[self.pset_name]["HasPropertyTemplates"].keys(): + for template_prop_name in schema.ifc.psetqto.psets[self.pset_name]["HasPropertyTemplates"].keys(): prop = pset.properties.add() prop.name = template_prop_name prop = pset.properties.get(self.prop_name) @@ -2659,9 +2654,7 @@ class CopyPropertyToSelection(bpy.types.Operator): def get_applicable_psets(self, ifc_class): if ifc_class not in self.applicable_psets_cache: - self.applicable_psets_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, ifc_class, is_pset=True - ) + self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True) return self.applicable_psets_cache[ifc_class] @@ -3573,11 +3566,11 @@ class GuessQuantity(bpy.types.Operator): prop.string_value = str(round(quantity, 3)) def add_qto(self, obj, name): - if name not in ifcopenshell.util.pset.qtos: + if name not in schema.ifc.psetqto.qtos: return qto = obj.BIMObjectProperties.qtos.add() qto.name = name - for prop_name in ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys(): + for prop_name in schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys(): prop = qto.properties.add() prop.name = prop_name return qto diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index b80e6a159d..5da7176506 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -1,7 +1,6 @@ import json import os import ifcopenshell -import ifcopenshell.util.pset from pathlib import Path from . import export_ifc from . import schema @@ -433,9 +432,7 @@ def getPsetNames(self, context): global psetnames_enum psetnames_enum.clear() if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: - pset_names = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, context.active_object.name.split("/")[0], is_pset=True - ) + pset_names = schema.ifc.psetqto.get_applicable_names(context.active_object.name.split("/")[0], pset_only=True) psetnames_enum.extend([(p, p, "") for p in pset_names]) return psetnames_enum @@ -443,9 +440,7 @@ def getPsetNames(self, context): def getMaterialPsetNames(self, context): global materialpsetnames_enum materialpsetnames_enum.clear() - pset_names = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, "IfcMaterial", is_pset=True - ) + pset_names = schema.ifc.psetqto.get_applicable_names("IfcMaterial", pset_only=True) materialpsetnames_enum.extend([(p, p, "") for p in pset_names]) return materialpsetnames_enum @@ -454,9 +449,7 @@ def getQtoNames(self, context): global qtonames_enum qtonames_enum.clear() if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: - qto_names = ifcopenshell.util.pset.get_applicable_psetqtos( - bpy.context.scene.BIMProperties.export_schema, context.active_object.name.split("/")[0], is_qto=True - ) + qto_names = schema.ifc.psetqto.get_applicable_names(context.active_object.name.split("/")[0], qto_only=True) qtonames_enum.extend([(q, q, "") for q in qto_names]) return qtonames_enum diff --git a/src/ifcblenderexport/blenderbim/bim/schema.py b/src/ifcblenderexport/blenderbim/bim/schema.py index 958567ac09..1cc4d26453 100644 --- a/src/ifcblenderexport/blenderbim/bim/schema.py +++ b/src/ifcblenderexport/blenderbim/bim/schema.py @@ -10,8 +10,8 @@ cwd = os.path.dirname(os.path.realpath(__file__)) class IfcSchema: def __init__(self): - self.schema_dir = os.path.join(cwd, "schema") # TODO: make configurable - self.data_dir = os.path.join(cwd, "data") # TODO: make configurable + self.schema_dir = Path(cwd).joinpath("schema") # TODO: make configurable + self.data_dir = Path(cwd).joinpath("data") # TODO: make configurable # TODO: Make it less troublesome self.products = [ "IfcContext", @@ -29,10 +29,10 @@ class IfcSchema: self.elements = {} self.property_files = [] - property_paths = Path(os.path.join(self.data_dir, "pset")).glob("*.ifc") + property_paths = self.data_dir.joinpath("pset").glob("*.ifc") + self.psetqto = ifcopenshell.util.pset.PsetQto("IFC4", use_cache=True) for path in property_paths: - ifcopenshell.util.pset.load_property_set_template(path) - ifcopenshell.util.pset.load_property_set_template(os.path.join(self.schema_dir, "Pset_IFC4_ADD2.ifc")) + self.psetqto.templates.append(ifcopenshell.open(path)) self.classification_files = {} self.classifications = {} diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index 5e5a541528..d724287f51 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -1,39 +1,97 @@ +import pathlib +import re +from typing import List, Generator + import ifcopenshell - -property_set_template_files = [] -psets = {} -qtos = {} -applicable_psets = {} -applicable_qtos = {} +from ifcopenshell.entity_instance import entity_instance -def load_property_set_template(path): - property_set_template_files.append(ifcopenshell.open(path)) - for prop in property_set_template_files[-1].by_type("IfcPropertySetTemplate"): - if prop.Name[0:4] == "Qto_": - qtos[prop.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop.HasPropertyTemplates}} - entity = prop.ApplicableEntity if prop.ApplicableEntity else "IfcRoot" - applicable_qtos.setdefault(entity, []).append(prop.Name) - else: - psets[prop.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop.HasPropertyTemplates}} - entity = prop.ApplicableEntity if prop.ApplicableEntity else "IfcRoot" - applicable_psets.setdefault(entity, []).append(prop.Name) +class PsetQto: + templates_path = { + "IFC4": "Pset_IFC4_ADD2.ifc", + } + def __init__(self, schema: str, templates=None, use_cache=False) -> None: + self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) + if not templates: + folder_path = pathlib.Path(__file__).parent.absolute() + path = folder_path.joinpath("schema", self.templates_path[schema]) + templates = [ifcopenshell.open(path)] + self.templates = templates + # Caching reduce request time. For 100 get_applicable_names requests ~3.6 s -> ~2 s + self.use_cache = use_cache + self.psets = {} + self.qtos = {} + self.applicable_psets = {} + self.applicable_qtos = {} + if use_cache: + for template in templates: + self.cache_template(template) -def get_applicable_psetqtos(schema_version, ifc_class, is_pset=False, is_qto=False): - def is_a(entity, ifc_class): - if entity.name() == ifc_class: - return True - return is_a(entity.supertype(), ifc_class) if entity.supertype() else False + def cache_template(self, template): + for prop_set in template.by_type("IfcPropertySetTemplate"): + if prop_set.Name[0:4] == "Qto_": + self.qtos[prop_set.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop_set.HasPropertyTemplates}} + entity = prop_set.ApplicableEntity if prop_set.ApplicableEntity else "IfcRoot" + self.applicable_qtos.setdefault(entity, []).append(prop_set.Name) + else: + self.psets[prop_set.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop_set.HasPropertyTemplates}} + entity = prop_set.ApplicableEntity if prop_set.ApplicableEntity else "IfcRoot" + self.applicable_psets.setdefault(entity, []).append(prop_set.Name) - results = [] - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_version) - entity = schema.declaration_by_name(ifc_class) - if is_pset: - search_items = applicable_psets.items() - elif is_qto: - search_items = applicable_qtos.items() - for ifc_class, pset_names in search_items: - if is_a(entity, ifc_class): - results.extend(pset_names) - return results + def get_applicable( + self, ifc_class="", predefined_type="", pset_only=False, qto_only=False + ) -> Generator[entity_instance, entity_instance, None]: + any_class = not ifc_class + if not any_class: + entity = self.schema.declaration_by_name(ifc_class) + for template in self.templates: + for prop_set in template.by_type("IfcPropertySetTemplate"): + if pset_only: + if prop_set.Name.startswith("Qto_"): + continue + if qto_only: + if not prop_set.Name.startswith("Qto_"): + continue + if any_class or self.is_applicable(entity, prop_set.ApplicableEntity or "IfcRoot", predefined_type): + yield prop_set + + def get_applicable_names(self, ifc_class: str, predefined_type="", pset_only=False, qto_only=False) -> List[str]: + """Return names instead of objects for other use eg. enum""" + if self.use_cache: + results = [] + entity = self.schema.declaration_by_name(ifc_class) + if not qto_only: + for applicable_class, pset_names in self.applicable_psets.items(): + if self.is_applicable(entity, applicable_class): + results.extend(pset_names) + if not pset_only: + for applicable_class, pset_names in self.applicable_qtos.items(): + if self.is_applicable(entity, applicable_class): + results.extend(pset_names) + return results + + return [prop_set.Name for prop_set in self.get_applicable(ifc_class, predefined_type, pset_only, qto_only)] + + def is_applicable(self, entity: entity_instance, applicables: str, predefined_type=""): + """applicables can have multiple possible patterns : + IfcBoilerType (IfcClass) + IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE) + IfcBoilerType[PerformanceHistory] (IfcClass[PerformanceHistory]) + IfcBoilerType/STEAM[PerformanceHistory] (IfcClass/PREDEFINEDTYPE[PerformanceHistory]) + """ + for applicable in applicables.split(","): + match = re.match(r"(\w+)(\[\w+\])*/*(\w+)*(\[\w+\])*", applicable) + if not match: + continue + # Uncomment if usage found + # applicable_perf_history = match.group(2) or match.group(4) + if predefined_type and predefined_type != match.group(3): + continue + + applicable_class = match.group(1) + if entity.name() == applicable_class: + return True + if entity.supertype(): + return self.is_applicable(entity.supertype(), applicable_class) + return False diff --git a/src/ifcblenderexport/blenderbim/bim/schema/Pset_IFC4_ADD2.ifc b/src/ifcopenshell-python/ifcopenshell/util/schema/Pset_IFC4_ADD2.ifc similarity index 100% rename from src/ifcblenderexport/blenderbim/bim/schema/Pset_IFC4_ADD2.ifc rename to src/ifcopenshell-python/ifcopenshell/util/schema/Pset_IFC4_ADD2.ifc diff --git a/src/ifcwrap/CMakeLists.txt b/src/ifcwrap/CMakeLists.txt index 86ad0c1ace..0e8a695656 100644 --- a/src/ifcwrap/CMakeLists.txt +++ b/src/ifcwrap/CMakeLists.txt @@ -84,8 +84,7 @@ IF(PYTHONINTERP_FOUND AND NOT "${PYTHON_EXECUTABLE}" STREQUAL "") MESSAGE(WARNING "Unable to locate Python site-package directory, unable to install the Python wrapper") ELSE() FILE(GLOB_RECURSE sourcefiles - "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*.py" - "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*.bnf" + "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*" ) FOREACH(file ${sourcefiles}) FILE(RELATIVE_PATH relative "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/" "${file}")