Feature faster unit method ---> Main (#5257)

* feature_faster_unit_method > main: added constants

* feature_faster_unit_method > main: extended `file` class to dynamically save unit information

* feature_faster_unit_method > main: refactored `get_property_unit` method

1. split out case that returns the wrong type (dictionary of units) into its own method

2. cleaned up (but preserved) logic

3. refactored common part of all cases (the method which prioritises unit then value-entity then measure_class)

---------

Co-authored-by: raj-open <raj-open@users.noreply.github.com>
Co-authored-by: Dion Moult <dion@thinkmoult.com>
This commit is contained in:
Raj
2025-01-26 09:04:49 +01:00
committed by GitHub
parent 3b1e893d34
commit 124c02897c
3 changed files with 240 additions and 47 deletions
+72 -1
View File
@@ -24,10 +24,18 @@ import zipfile
import functools import functools
import ifcopenshell import ifcopenshell
from pathlib import Path from pathlib import Path
from typing import Optional, Any, Union, Callable, Generator, Literal, TYPE_CHECKING from typing import Any
from typing import Callable
from typing import Generator
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from . import ifcopenshell_wrapper from . import ifcopenshell_wrapper
from .entity_instance import entity_instance from .entity_instance import entity_instance
# from .util.constants import IFC_TYPES
from .util.constants import IFC_UNIT_ASSIGNMENT
from .util.unit import get_measure_unit_type
if TYPE_CHECKING: if TYPE_CHECKING:
import ifcopenshell.util.schema import ifcopenshell.util.schema
@@ -217,6 +225,7 @@ class file:
""" """
wrapped_data: ifcopenshell_wrapper.file wrapped_data: ifcopenshell_wrapper.file
_units: file_units | None = None
history_size: int = 64 history_size: int = 64
def __init__( def __init__(
@@ -456,6 +465,28 @@ class file:
version.append(int(number.group(1)) if number else 0) version.append(int(number.group(1)) if number else 0)
return tuple(version) return tuple(version)
@property
def units(self) -> file_units:
"""
A class which dynamically stores unit information
and provides methods to compute unit-entities associated to types.
"""
if self._units is None:
self._units = file_units(self)
return self._units
def unit_by_measure_class(
self,
measure_class: str | None = None,
/,
) -> ifcopenshell.entity_instance | None:
"""
Helper method to obtain unit-entity either directly
or indirectly via measure class.
"""
if measure_class is not None:
return self.units.by_measure_type(measure_class)
def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]: def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]:
if attr[0:6] == "create": if attr[0:6] == "create":
return functools.partial(self.create_entity, attr[6:]) return functools.partial(self.create_entity, attr[6:])
@@ -695,3 +726,43 @@ class file:
def to_string(self) -> str: def to_string(self) -> str:
return self.wrapped_data.to_string() return self.wrapped_data.to_string()
class file_units:
"""
A class which provides methods to compute unit-entities associated to types.
"""
_unit_assignment: dict[str, entity_instance] = {}
def __init__(self, ifc_file: file):
self._load_unit_assignment(ifc_file)
def _load_unit_assignment(self, ifc_file: file, /):
"""
A mapping as dictionary which associates
to basic UnitTypes a unit-entity in the IFC file.
"""
entities = self.by_type(IFC_UNIT_ASSIGNMENT)
base = next(iter(entities), None)
units = getattr(base, "Units", None) or ()
units_and_types = [
(u, getattr(u, "UnitType", None))
for u in units
if isinstance(u, entity_instance)
]
self._unit_assignment = {t: u for u, t in units_and_types if isinstance(t, str)}
return
def by_type(self, t: str, /) -> entity_instance | None:
"""
Returns the unit entity associated to a basic type
"""
return self._unit_assignment.get(t, None)
def by_measure_type(self, t: str, /) -> entity_instance | None:
"""
Returns the unit entity associated to a measure type
"""
t = get_measure_unit_type(t)
return self.by_type(t)
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------
# IMPORTS
# ----------------------------------------------------------------
from typing import Literal
# ----------------------------------------------------------------
# EXPORTS
# ----------------------------------------------------------------
__all__ = [
"IFC_APPLICATION",
"IFC_GRID_AXIS",
"IFC_PRODUCT",
"IFC_PROJECT",
"IFC_PROPERTY_SINGLE_VALUE",
"IFC_QUANTITY_AREA",
"IFC_QUANTITY_COUNT",
"IFC_QUANTITY_LENGTH",
"IFC_QUANTITY_TIME",
"IFC_QUANTITY_VOLUME",
"IFC_QUANTITY_WEIGHT",
"IFC_SI_UNIT",
"IFC_TYPES",
"IFC_UNIT_ASSIGNMENT",
]
# ----------------------------------------------------------------
# CONSTANTS
# ----------------------------------------------------------------
# TODO: complete this
IFC_APPLICATION = "IfcApplication"
IFC_GRID_AXIS = "IfcGridAxis"
IFC_PRODUCT = "IfcProduct"
IFC_PROJECT = "IfcProject"
IFC_PROPERTY_SINGLE_VALUE = "IfcPropertySingleValue"
IFC_QUANTITY_AREA = "IfcQuantityArea"
IFC_QUANTITY_COUNT = "IfcQuantityCount"
IFC_QUANTITY_LENGTH = "IfcQuantityLength"
IFC_QUANTITY_TIME = "IfcQuantityTime"
IFC_QUANTITY_VOLUME = "IfcQuantityVolume"
IFC_QUANTITY_WEIGHT = "IfcQuantityWeight"
IFC_SI_UNIT = "IfcSIUnit"
IFC_UNIT_ASSIGNMENT = "IfcUnitAssignment"
# TODO: complete this
IFC_TYPES = Literal[
"IfcApplication",
"IfcGridAxis",
"IfcProduct",
"IfcProject",
"IfcPropertySingleValue",
"IfcQuantityArea",
"IfcQuantityCount",
"IfcQuantityLength",
"IfcQuantityTime",
"IfcQuantityVolume",
"IfcQuantityWeight",
"IfcSIUnit",
"IfcUnitAssignment",
]
+102 -45
View File
@@ -18,7 +18,12 @@
from fractions import Fraction from fractions import Fraction
from math import pi from math import pi
from typing import Iterable, Any, Union, Literal, Optional from typing import Any
from typing import Dict
from typing import Iterable
from typing import Literal
from typing import Optional
from typing import Union
import ifcopenshell import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
@@ -423,7 +428,8 @@ def get_project_unit(ifc_file: ifcopenshell.file, unit_type: str) -> Union[ifcop
def get_property_unit( def get_property_unit(
prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file prop: ifcopenshell.entity_instance,
ifc_file: ifcopenshell.file
) -> Union[ifcopenshell.entity_instance, None]: ) -> Union[ifcopenshell.entity_instance, None]:
"""Gets the unit definition of a property or quantity """Gets the unit definition of a property or quantity
@@ -439,56 +445,83 @@ def get_property_unit(
:return: The IFC unit entity, or nothing if there is no default project :return: The IFC unit entity, or nothing if there is no default project
unit defined. unit defined.
""" """
unit = getattr(prop, "Unit", None) unit = prop.Unit
if unit: if isinstance(unit, ifcopenshell.entity_instance):
return unit return unit
unit_assignment = get_unit_assignment(ifc_file)
if not unit_assignment: value = None
return
entity = prop.wrapped_data.declaration().as_entity()
measure_class = None measure_class = None
# DEV-NOTE: Using .is_a() is wrong, as it tells us nothing about super class membership
if prop.is_a("IfcPhysicalSimpleQuantity"): if prop.is_a("IfcPhysicalSimpleQuantity"):
# get underlying object
entity = prop.wrapped_data.declaration().as_entity()
# extract measure class
measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name() measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name()
elif prop.is_a("IfcPropertySingleValue") and prop.NominalValue:
measure_class = prop.NominalValue.is_a() elif prop.is_a("IfcPropertySingleValue"):
value = prop.NominalValue
elif prop.is_a("IfcPropertyEnumeratedValue"): elif prop.is_a("IfcPropertyEnumeratedValue"):
if prop.EnumerationReference: unit = prop.EnumerationReference.Unit
unit = getattr(prop.EnumerationReference, "Unit", None) value = next(iter(prop.EnumerationValues or ()), None)
if unit:
return unit elif prop.is_a("IfcPropertyListValue"):
if prop.EnumerationValues: value = next(iter(prop.ListValues or ()), None)
measure_class = prop.EnumerationValues[0].is_a()
elif prop.is_a("IfcPropertyListValue") and prop.ListValues:
measure_class = prop.ListValues[0].is_a()
elif prop.is_a("IfcPropertyBoundedValue"): elif prop.is_a("IfcPropertyBoundedValue"):
if prop.UpperBoundValue: value = prop.UpperBoundValue or prop.LowerBoundValue or prop.SetPointValue
measure_class = prop.UpperBoundValue.is_a()
elif prop.LowerBoundValue: unit = _auxiliary_method_compute_unit(
measure_class = prop.LowerBoundValue.is_a() ifc_file,
elif prop.SetPointValue: unit=unit,
measure_class = prop.SetPointValue.is_a() value=value,
elif prop.is_a("IfcPropertyTableValue"): measure_class=measure_class,
table_units = {} )
for attribute in ["Defining", "Defined"]: return unit
if getattr(prop, f"{attribute}Unit"):
table_units[f"{attribute}Unit"] = getattr(prop, f"{attribute}Unit")
elif getattr(prop, f"{attribute}Values"): def get_property_table_unit(
measure_class = getattr(prop, f"{attribute}Values")[0].is_a() prop: ifcopenshell.entity_instance,
unit_type = get_measure_unit_type(measure_class) ifc_file: ifcopenshell.file
units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type] ) -> Dict[str, Union[ifcopenshell.entity_instance, None]]:
if units: """
table_units[f"{attribute}Unit"] = units[0] Gets the unit definition of a property table
Properties and quantities in psets and qtos can be associated with a unit.
This unit may be defined at the property itself explicitly, or if not
specified, fallback to the project default.
:param prop: The property instance. You can fetch this via the instance ID
if doing :func:`ifcopenshell.util.element.get_psets` with
``verbose=True``.
:param ifc_file: The IFC file being used. This is necessary to check
default project units.
:return: A dictionary containing IFC unit entity by keyword.
If a unit-entity is missing,
the value associated to the key is `null`.
"""
if prop.is_a("IfcPropertyTableValue"):
unit = prop.DefiningUnit
value = next(iter(prop.DefiningValues or ()), None)
unit_defining = _auxiliary_method_compute_unit(ifc_file, unit=unit, value=value)
unit = prop.DefinedUnit
value = next(iter(prop.DefinedValues or ()), None)
unit_defined = _auxiliary_method_compute_unit(ifc_file, unit=unit, value=value)
units = {
"DefiningUnit": unit_defining,
"DefinedUnit": unit_defined,
}
# currently no other case
else: else:
table_units[f"{attribute}Unit"] = None units = {}
else:
table_units[f"{attribute}Unit"] = None return units
return table_units
if measure_class is None:
return
unit_type = get_measure_unit_type(measure_class)
units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type]
if units:
return units[0]
def get_unit_measure_class(unit_type: str) -> MEASURE_CLASS: def get_unit_measure_class(unit_type: str) -> MEASURE_CLASS:
@@ -843,3 +876,27 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "
ifcopenshell.util.element.remove_deep2(file_patched, old_length) ifcopenshell.util.element.remove_deep2(file_patched, old_length)
return file_patched return file_patched
# ----------------------------------------------------------------
# AUXILIARY METHODS
# ----------------------------------------------------------------
def _auxiliary_method_compute_unit(
ifc_file: ifcopenshell.file,
/,
*,
unit: ifcopenshell.entity_instance | None = None,
value: ifcopenshell.entity_instance | None = None,
measure_class: str | None = None,
) -> ifcopenshell.entity_instance | None:
"""
Helper method to obtain unit-entity either directly
or indirectly via measure class.
"""
if isinstance(unit, ifcopenshell.entity_instance):
return unit
if isinstance(value, ifcopenshell.entity_instance):
measure_class = measure_class or value.is_a()
return ifc_file.unit_by_measure_class(measure_class)