diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index e6639a322f..3407cacfd1 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -222,7 +222,7 @@ class file: """ wrapped_data: ifcopenshell_wrapper.file - units: dict[str: ifcopenshell.entity_instance] = None + units: dict[str, entity_instance] = {} history_size: int = 64 def __init__( @@ -358,7 +358,6 @@ class file: :param args: The positional arguments of the IFC class :param kwargs: The keyword arguments of the IFC class :returns: An entity instance - :rtype: ifcopenshell.entity_instance Example: diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 1522779540..8bdb9e1423 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -413,12 +413,24 @@ def get_unit_assignment(ifc_file: ifcopenshell.file) -> Union[ifcopenshell.entit def cache_units(ifc_file: ifcopenshell.file) -> None: + """Cache the default units for performance + + Repetitively fetching project units (such as for determining the unit of a + property) can be costly. This enables a cache to make it faster. If the + project units change, you can update the cache by rerunning this function. + + :param ifc_file: The IFC file. + """ ifc_file.units = {} if assignment := get_unit_assignment(ifc_file): ifc_file.units = {u.UnitType: u for u in assignment.Units if getattr(u, "UnitType", None)} def clear_unit_cache(ifc_file: ifcopenshell.file) -> None: + """Clears the unit cache of the project + + :param ifc_file: The IFC file. + """ ifc_file.units = {} @@ -472,8 +484,11 @@ def get_property_unit( elif prop.is_a("IfcPropertySingleValue"): measure_class = prop.NominalValue.is_a() elif prop.is_a("IfcPropertyEnumeratedValue"): - if unit := prop.EnumerationReference.Unit: - return unit + if prop.EnumerationReference: + if unit := prop.EnumerationReference.Unit: + return unit + if value := next(iter(prop.EnumerationReference.EnumerationValues or ()), None): + measure_class = value.is_a() if value := next(iter(prop.EnumerationValues or ()), None): measure_class = value.is_a() elif prop.is_a("IfcPropertyListValue"): @@ -484,7 +499,7 @@ def get_property_unit( measure_class = value.is_a() if measure_class and (unit_type := get_measure_unit_type(measure_class)): - return get_project_unit(ifc_file, unit_type) + return get_project_unit(ifc_file, unit_type, use_cache=use_cache) def get_property_table_unit( @@ -513,14 +528,14 @@ def get_property_table_unit( defining_unit = unit elif value := next(iter(prop.DefiningValues or ()), None): if unit_type := get_measure_unit_type(value.is_a()): - defining_unit = get_project_unit(ifc_file, unit_type) + defining_unit = get_project_unit(ifc_file, unit_type, use_cache=use_cache) defined_unit = None if unit := prop.DefinedUnit: defined_unit = unit elif value := next(iter(prop.DefinedValues or ()), None): if unit_type := get_measure_unit_type(value.is_a()): - defining_unit = get_project_unit(ifc_file, unit_type) + defining_unit = get_project_unit(ifc_file, unit_type, use_cache=use_cache) return { "DefiningUnit": defining_unit, diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index a65a538f7e..b8a31f70c0 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -25,6 +25,133 @@ import ifcopenshell.util.unit as subject from math import pi +class TestCacheUnits(test.bootstrap.IFC4): + def test_run(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + assert self.file.units == {} + subject.cache_units(self.file) + assert self.file.units == {"LENGTHUNIT": length, "AREAUNIT": area} + + +class TestClearUnitCache(test.bootstrap.IFC4): + def test_run(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + subject.cache_units(self.file) + subject.clear_unit_cache(self.file) + assert self.file.units == {} + + +class TestGetProjectUnit(test.bootstrap.IFC4): + def test_run(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + assert subject.get_project_unit(self.file, "LENGTHUNIT") == length + assert subject.get_project_unit(self.file, "AREAUNIT") == area + + def test_using_a_cache(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + assert self.file.units == {} + assert subject.get_project_unit(self.file, "LENGTHUNIT", use_cache=True) == length + assert self.file.units == {"LENGTHUNIT": length, "AREAUNIT": area} + ifcopenshell.api.unit.assign_unit(self.file, units=[length2]) + assert subject.get_project_unit(self.file, "LENGTHUNIT", use_cache=True) == length + subject.clear_unit_cache(self.file) + assert subject.get_project_unit(self.file, "LENGTHUNIT", use_cache=True) == length2 + assert self.file.units == {"LENGTHUNIT": length2, "AREAUNIT": area} + + +class TestGetPropertyUnit(test.bootstrap.IFC4): + def test_no_unit(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + prop = self.file.createIfcQuantityLength(Name="Foo", LengthValue=42.0) + assert subject.get_property_unit(prop, self.file) is None + + def test_simple_quantity(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + prop = self.file.createIfcQuantityLength(Name="Foo", LengthValue=42.0) + assert subject.get_property_unit(prop, self.file) == length + prop.Unit = length2 + assert subject.get_property_unit(prop, self.file) == length2 + + def test_single_value(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + prop = self.file.createIfcPropertySingleValue(Name="Foo", NominalValue=self.file.createIfcLengthMeasure(42.0)) + assert subject.get_property_unit(prop, self.file) == length + prop.Unit = length2 + assert subject.get_property_unit(prop, self.file) == length2 + + def test_enumerated_value(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + prop = self.file.createIfcPropertyEnumeratedValue(Name="Foo", EnumerationValues=[self.file.createIfcLengthMeasure(42.0)]) + assert subject.get_property_unit(prop, self.file) == length + prop.EnumerationValues = [] + prop.EnumerationReference = self.file.createIfcPropertyEnumeration("Foo", [self.file.createIfcAreaMeasure(42.0)]) + assert subject.get_property_unit(prop, self.file) == area + prop.EnumerationReference = self.file.createIfcPropertyEnumeration("Foo", [self.file.createIfcAreaMeasure(42.0)], Unit=length2) + assert subject.get_property_unit(prop, self.file) == length2 + prop.EnumerationValues = [self.file.createIfcAreaMeasure(42.0)] + assert subject.get_property_unit(prop, self.file) == length2 + prop.EnumerationReference.Unit = None + assert subject.get_property_unit(prop, self.file) == area + + def test_list_value(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + prop = self.file.createIfcPropertyListValue(Name="Foo", ListValues=[self.file.createIfcLengthMeasure(42.0)]) + assert subject.get_property_unit(prop, self.file) == length + prop.Unit = length2 + assert subject.get_property_unit(prop, self.file) == length2 + prop.Unit = None + prop.ListValues = [] + assert subject.get_property_unit(prop, self.file) is None + + def test_bounded_value(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + length = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + length2 = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="CENTI") + area = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="AREAUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[length, area]) + prop = self.file.createIfcPropertyBoundedValue(Name="Foo") + assert subject.get_property_unit(prop, self.file) is None + prop.UpperBoundValue = self.file.createIfcLengthMeasure(42.0) + assert subject.get_property_unit(prop, self.file) == length + prop.UpperBoundValue = None + prop.LowerBoundValue = self.file.createIfcLengthMeasure(42.0) + assert subject.get_property_unit(prop, self.file) == length + prop.LowerBoundValue = None + prop.SetPointValue = self.file.createIfcLengthMeasure(42.0) + assert subject.get_property_unit(prop, self.file) == length + prop.Unit = length2 + assert subject.get_property_unit(prop, self.file) == length2 + + class TestConvert(test.bootstrap.IFC4): def test_run(self): assert subject.convert(1, None, "METRE", None, "METRE") == 1