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`.
This commit is contained in:
Cyril Waechter
2020-12-31 11:39:33 +01:00
committed by GitHub
parent c3d4ce8f51
commit 9060b6ee40
8 changed files with 123 additions and 81 deletions
@@ -8,7 +8,6 @@ import os
import zipfile import zipfile
import tempfile import tempfile
import ifcopenshell import ifcopenshell
import ifcopenshell.util.pset
import ifcopenshell.util.schema import ifcopenshell.util.schema
from pathlib import Path from pathlib import Path
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
@@ -477,10 +476,10 @@ class IfcParser:
return return
qto_names = self.get_applicable_qtos(ifc_class) qto_names = self.get_applicable_qtos(ifc_class)
for name in qto_names: for name in qto_names:
if name not in ifcopenshell.util.pset.qtos: if name not in schema.ifc.psetqto.qtos:
continue continue
has_automatic_value = False has_automatic_value = False
props = ifcopenshell.util.pset.qtos[name]["HasPropertyTemplates"].keys() props = schema.ifc.psetqto.qtos[name]["HasPropertyTemplates"].keys()
guessed_values = {} guessed_values = {}
for prop_name in props: for prop_name in props:
value = self.qto_calculator.guess_quantity(prop_name, props, obj) 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"]) pset["ifc"] = self.file.create_entity("IfcMaterialProperties", **pset["attributes"])
def create_qto_properties(self, qto): 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_templated_qto_properties(qto)
return self.create_custom_qto_properties(qto) return self.create_custom_qto_properties(qto)
def create_pset_properties(self, pset): 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_templated_pset_properties(pset)
return self.create_custom_pset_properties(pset) return self.create_custom_pset_properties(pset)
@@ -1737,7 +1736,7 @@ class IfcExporter:
def create_templated_pset_properties(self, pset): def create_templated_pset_properties(self, pset):
properties = [] 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(): for name, data in templates.items():
if name not in pset["raw"]: if name not in pset["raw"]:
continue continue
@@ -1764,7 +1763,7 @@ class IfcExporter:
def create_templated_qto_properties(self, qto): def create_templated_qto_properties(self, qto):
properties = [] 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(): for name, data in templates.items():
if name not in qto["raw"]: if name not in qto["raw"]:
continue continue
@@ -3,7 +3,6 @@ import ifcopenshell.geom
import ifcopenshell.util.geolocation import ifcopenshell.util.geolocation
import ifcopenshell.util.selector import ifcopenshell.util.selector
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.pset
import bpy import bpy
import bmesh import bmesh
import os import os
@@ -22,6 +21,7 @@ from itertools import cycle
from datetime import datetime from datetime import datetime
from . import helper from . import helper
from . import ifc from . import ifc
from . import schema
class FileCopy(threading.Thread): class FileCopy(threading.Thread):
@@ -1394,8 +1394,8 @@ class IfcImporter:
def add_pset(self, pset, props): def add_pset(self, pset, props):
new_pset = props.psets.add() new_pset = props.psets.add()
new_pset.name = pset.Name new_pset.name = pset.Name
if new_pset.name in ifcopenshell.util.pset.psets: if new_pset.name in schema.ifc.psetqto.psets:
for prop_name in ifcopenshell.util.pset.psets[new_pset.name]["HasPropertyTemplates"].keys(): for prop_name in schema.ifc.psetqto.psets[new_pset.name]["HasPropertyTemplates"].keys():
prop = new_pset.properties.add() prop = new_pset.properties.add()
prop.name = prop_name prop.name = prop_name
try: try:
@@ -1421,8 +1421,8 @@ class IfcImporter:
def add_qto(self, qto, obj): def add_qto(self, qto, obj):
new_qto = obj.BIMObjectProperties.qtos.add() new_qto = obj.BIMObjectProperties.qtos.add()
new_qto.name = str(qto.Name) new_qto.name = str(qto.Name)
if new_qto.name in ifcopenshell.util.pset.qtos: if new_qto.name in schema.ifc.psetqto.qtos:
for prop_name in ifcopenshell.util.pset.qtos[new_qto.name]["HasPropertyTemplates"].keys(): for prop_name in schema.ifc.psetqto.qtos[new_qto.name]["HasPropertyTemplates"].keys():
prop = new_qto.properties.add() prop = new_qto.properties.add()
prop.name = prop_name prop.name = prop_name
for prop in qto.Quantities: for prop in qto.Quantities:
+12 -19
View File
@@ -10,7 +10,6 @@ import subprocess
import ifcopenshell import ifcopenshell
import ifcopenshell.util.selector import ifcopenshell.util.selector
import ifcopenshell.util.geolocation import ifcopenshell.util.geolocation
import ifcopenshell.util.pset
import ifcopenshell.util.schema import ifcopenshell.util.schema
import tempfile import tempfile
from . import export_ifc from . import export_ifc
@@ -588,7 +587,7 @@ class AddQto(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.applicable_qtos_cache = {} self.applicable_qtos_cache = {}
name = bpy.context.active_object.BIMObjectProperties.qto_name 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"} return {"FINISHED"}
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects:
if "/" not in obj.name or obj.BIMObjectProperties.qtos.find(name) != -1: if "/" not in obj.name or obj.BIMObjectProperties.qtos.find(name) != -1:
@@ -598,16 +597,14 @@ class AddQto(bpy.types.Operator):
continue continue
qto = obj.BIMObjectProperties.qtos.add() qto = obj.BIMObjectProperties.qtos.add()
qto.name = name 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 = qto.properties.add()
prop.name = prop_name prop.name = prop_name
return {"FINISHED"} return {"FINISHED"}
def get_applicable_qtos(self, ifc_class): def get_applicable_qtos(self, ifc_class):
if ifc_class not in self.applicable_qtos_cache: if ifc_class not in self.applicable_qtos_cache:
self.applicable_qtos_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( self.applicable_qtos_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, qto_only=True)
bpy.context.scene.BIMProperties.export_schema, ifc_class, is_qto=True
)
return self.applicable_qtos_cache[ifc_class] return self.applicable_qtos_cache[ifc_class]
@@ -618,7 +615,7 @@ class AddPset(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.applicable_psets_cache = {} self.applicable_psets_cache = {}
name = bpy.context.active_object.BIMObjectProperties.pset_name 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"} return {"FINISHED"}
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects:
if "/" not in obj.name or obj.BIMObjectProperties.psets.find(name) != -1: if "/" not in obj.name or obj.BIMObjectProperties.psets.find(name) != -1:
@@ -628,16 +625,14 @@ class AddPset(bpy.types.Operator):
continue continue
pset = obj.BIMObjectProperties.psets.add() pset = obj.BIMObjectProperties.psets.add()
pset.name = name 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 = pset.properties.add()
prop.name = prop_name prop.name = prop_name
return {"FINISHED"} return {"FINISHED"}
def get_applicable_psets(self, ifc_class): def get_applicable_psets(self, ifc_class):
if ifc_class not in self.applicable_psets_cache: if ifc_class not in self.applicable_psets_cache:
self.applicable_psets_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True)
bpy.context.scene.BIMProperties.export_schema, ifc_class, is_pset=True
)
return self.applicable_psets_cache[ifc_class] return self.applicable_psets_cache[ifc_class]
@@ -680,13 +675,13 @@ class AddMaterialPset(bpy.types.Operator):
def execute(self, context): def execute(self, context):
material = bpy.context.active_object.active_material material = bpy.context.active_object.active_material
name = material.BIMMaterialProperties.pset_name name = material.BIMMaterialProperties.pset_name
if name not in ifcopenshell.util.pset.psets: if name not in schema.ifc.psetqto.psets:
return {"FINISHED"} return {"FINISHED"}
if material.BIMMaterialProperties.psets.find(name) != -1: if material.BIMMaterialProperties.psets.find(name) != -1:
return {"FINISHED"} return {"FINISHED"}
pset = material.BIMMaterialProperties.psets.add() pset = material.BIMMaterialProperties.psets.add()
pset.name = name 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 = pset.properties.add()
prop.name = prop_name prop.name = prop_name
return {"FINISHED"} return {"FINISHED"}
@@ -2649,7 +2644,7 @@ class CopyPropertyToSelection(bpy.types.Operator):
continue continue
pset = obj.BIMObjectProperties.psets.add() pset = obj.BIMObjectProperties.psets.add()
pset.name = self.pset_name 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 = pset.properties.add()
prop.name = template_prop_name prop.name = template_prop_name
prop = pset.properties.get(self.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): def get_applicable_psets(self, ifc_class):
if ifc_class not in self.applicable_psets_cache: if ifc_class not in self.applicable_psets_cache:
self.applicable_psets_cache[ifc_class] = ifcopenshell.util.pset.get_applicable_psetqtos( self.applicable_psets_cache[ifc_class] = schema.ifc.psetqto.get_applicable_names(ifc_class, pset_only=True)
bpy.context.scene.BIMProperties.export_schema, ifc_class, is_pset=True
)
return self.applicable_psets_cache[ifc_class] return self.applicable_psets_cache[ifc_class]
@@ -3573,11 +3566,11 @@ class GuessQuantity(bpy.types.Operator):
prop.string_value = str(round(quantity, 3)) prop.string_value = str(round(quantity, 3))
def add_qto(self, obj, name): def add_qto(self, obj, name):
if name not in ifcopenshell.util.pset.qtos: if name not in schema.ifc.psetqto.qtos:
return return
qto = obj.BIMObjectProperties.qtos.add() qto = obj.BIMObjectProperties.qtos.add()
qto.name = name 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 = qto.properties.add()
prop.name = prop_name prop.name = prop_name
return qto return qto
+3 -10
View File
@@ -1,7 +1,6 @@
import json import json
import os import os
import ifcopenshell import ifcopenshell
import ifcopenshell.util.pset
from pathlib import Path from pathlib import Path
from . import export_ifc from . import export_ifc
from . import schema from . import schema
@@ -433,9 +432,7 @@ def getPsetNames(self, context):
global psetnames_enum global psetnames_enum
psetnames_enum.clear() psetnames_enum.clear()
if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: 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( pset_names = schema.ifc.psetqto.get_applicable_names(context.active_object.name.split("/")[0], pset_only=True)
bpy.context.scene.BIMProperties.export_schema, context.active_object.name.split("/")[0], is_pset=True
)
psetnames_enum.extend([(p, p, "") for p in pset_names]) psetnames_enum.extend([(p, p, "") for p in pset_names])
return psetnames_enum return psetnames_enum
@@ -443,9 +440,7 @@ def getPsetNames(self, context):
def getMaterialPsetNames(self, context): def getMaterialPsetNames(self, context):
global materialpsetnames_enum global materialpsetnames_enum
materialpsetnames_enum.clear() materialpsetnames_enum.clear()
pset_names = ifcopenshell.util.pset.get_applicable_psetqtos( pset_names = schema.ifc.psetqto.get_applicable_names("IfcMaterial", pset_only=True)
bpy.context.scene.BIMProperties.export_schema, "IfcMaterial", is_pset=True
)
materialpsetnames_enum.extend([(p, p, "") for p in pset_names]) materialpsetnames_enum.extend([(p, p, "") for p in pset_names])
return materialpsetnames_enum return materialpsetnames_enum
@@ -454,9 +449,7 @@ def getQtoNames(self, context):
global qtonames_enum global qtonames_enum
qtonames_enum.clear() qtonames_enum.clear()
if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: 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( qto_names = schema.ifc.psetqto.get_applicable_names(context.active_object.name.split("/")[0], qto_only=True)
bpy.context.scene.BIMProperties.export_schema, context.active_object.name.split("/")[0], is_qto=True
)
qtonames_enum.extend([(q, q, "") for q in qto_names]) qtonames_enum.extend([(q, q, "") for q in qto_names])
return qtonames_enum return qtonames_enum
@@ -10,8 +10,8 @@ cwd = os.path.dirname(os.path.realpath(__file__))
class IfcSchema: class IfcSchema:
def __init__(self): def __init__(self):
self.schema_dir = os.path.join(cwd, "schema") # TODO: make configurable self.schema_dir = Path(cwd).joinpath("schema") # TODO: make configurable
self.data_dir = os.path.join(cwd, "data") # TODO: make configurable self.data_dir = Path(cwd).joinpath("data") # TODO: make configurable
# TODO: Make it less troublesome # TODO: Make it less troublesome
self.products = [ self.products = [
"IfcContext", "IfcContext",
@@ -29,10 +29,10 @@ class IfcSchema:
self.elements = {} self.elements = {}
self.property_files = [] 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: for path in property_paths:
ifcopenshell.util.pset.load_property_set_template(path) self.psetqto.templates.append(ifcopenshell.open(path))
ifcopenshell.util.pset.load_property_set_template(os.path.join(self.schema_dir, "Pset_IFC4_ADD2.ifc"))
self.classification_files = {} self.classification_files = {}
self.classifications = {} self.classifications = {}
@@ -1,39 +1,97 @@
import pathlib
import re
from typing import List, Generator
import ifcopenshell import ifcopenshell
from ifcopenshell.entity_instance import entity_instance
property_set_template_files = []
psets = {}
qtos = {}
applicable_psets = {}
applicable_qtos = {}
def load_property_set_template(path): class PsetQto:
property_set_template_files.append(ifcopenshell.open(path)) templates_path = {
for prop in property_set_template_files[-1].by_type("IfcPropertySetTemplate"): "IFC4": "Pset_IFC4_ADD2.ifc",
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)
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 cache_template(self, template):
def is_a(entity, ifc_class): for prop_set in template.by_type("IfcPropertySetTemplate"):
if entity.name() == ifc_class: if prop_set.Name[0:4] == "Qto_":
return True self.qtos[prop_set.Name] = {"HasPropertyTemplates": {p.Name: p for p in prop_set.HasPropertyTemplates}}
return is_a(entity.supertype(), ifc_class) if entity.supertype() else False 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 = [] def get_applicable(
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_version) self, ifc_class="", predefined_type="", pset_only=False, qto_only=False
entity = schema.declaration_by_name(ifc_class) ) -> Generator[entity_instance, entity_instance, None]:
if is_pset: any_class = not ifc_class
search_items = applicable_psets.items() if not any_class:
elif is_qto: entity = self.schema.declaration_by_name(ifc_class)
search_items = applicable_qtos.items() for template in self.templates:
for ifc_class, pset_names in search_items: for prop_set in template.by_type("IfcPropertySetTemplate"):
if is_a(entity, ifc_class): if pset_only:
results.extend(pset_names) if prop_set.Name.startswith("Qto_"):
return results 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
+1 -2
View File
@@ -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") MESSAGE(WARNING "Unable to locate Python site-package directory, unable to install the Python wrapper")
ELSE() ELSE()
FILE(GLOB_RECURSE sourcefiles FILE(GLOB_RECURSE sourcefiles
"${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*.py" "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*"
"${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/*.bnf"
) )
FOREACH(file ${sourcefiles}) FOREACH(file ${sourcefiles})
FILE(RELATIVE_PATH relative "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/" "${file}") FILE(RELATIVE_PATH relative "${CMAKE_CURRENT_SOURCE_DIR}/../ifcopenshell-python/ifcopenshell/" "${file}")