diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index 3f7a9b281a..a1171c2ba8 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -23,6 +23,7 @@ import operator import numpy as np import datetime +import ifcopenshell.util.unit import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.classification @@ -824,7 +825,16 @@ class property(facet): message = "%(location)sproperty '%(name)s' in '%(propertySet)s' with a value %(value)s" @staticmethod - def create(propertySet="Property_Set", name="PropertyName", value=None, location="any", measure=None, uri=None, use=None, instructions=None): + def create( + propertySet="Property_Set", + name="PropertyName", + value=None, + location="any", + measure=None, + uri=None, + use=None, + instructions=None, + ): """Create a property facet that can be added to applicability or requirements of IDS specification. :param location: Where to check for the parameter. One of "any"|"instance"|"type", defaults to "any" @@ -914,6 +924,35 @@ class property(facet): is_pass = False break + if self.measure: + pset_entity = inst.wrapped_data.file.by_id(pset_props["id"]) + for prop_entity in pset_entity.HasProperties: + if ( + prop_entity.Name not in props[pset_name].keys() + or not prop_entity.is_a("IfcPropertySingleValue") + or prop_entity.NominalValue is None + ): + continue + + data_type = prop_entity.NominalValue.is_a().replace("Ifc", "").replace("Measure", "") + + if data_type != self.measure: + is_pass = False + break + + unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) + + props[pset_name][prop_entity.Name] = ifcopenshell.util.unit.convert( + prop_entity.NominalValue.wrappedValue, + getattr(unit, "Prefix", None), + unit.Name, + None, + ifcopenshell.util.unit.si_type_names[unit.UnitType], + ) + + if not is_pass: + break + if self.value: if any([v != self.value for v in props[pset_name].values()]): is_pass = False diff --git a/src/ifcopenshell-python/test/test_ids.py b/src/ifcopenshell-python/test/test_ids.py index 0968641b7a..bc52c5c32a 100644 --- a/src/ifcopenshell-python/test/test_ids.py +++ b/src/ifcopenshell-python/test/test_ids.py @@ -633,6 +633,13 @@ class TestIdsAuthoring(unittest.TestCase): def test_filtering_using_a_property_facet(self): ifc = ifcopenshell.file() + ifc.createIfcProject() + # Milli prefix used to check measurement conversions + lengthunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI") + areaunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="AREAUNIT", name="SQUARE_METRE", prefix="MILLI") + volumeunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="VOLUMEUNIT", name="CUBIC_METRE", prefix="MILLI") + timeunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="TIMEUNIT", name="SECOND") + ifcopenshell.api.run("unit.assign_unit", ifc, units=[lengthunit, areaunit, volumeunit, timeunit]) # A name check by itself only checks that a property is non-null and non empty string # The logic is that unfortunately most BIM users cannot differentiate between the two. @@ -719,6 +726,34 @@ class TestIdsAuthoring(unittest.TestCase): ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": False}) assert bool(facet(element)) is False + # When measure is not specified, no unit conversion is done and only primitives are checked + restriction = ids.restriction.create(options=[42.12], type="enumeration", base="decimal") + facet = ids.property.create(propertySet="Foo_Bar", name="Foobar", value=restriction) + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": 42.12}) + assert bool(facet(element)) is True + + # Measure may be used to specify an IFC data type + restriction = ids.restriction.create(options=[2], type="enumeration", base="decimal") + facet = ids.property.create(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Time") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcMassMeasure(2)}) + assert bool(facet(element)) is False + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcTimeMeasure(2)}) + assert bool(facet(element)) is True + + # Measure also implies that a unit matters, and so a conversion shall take place to SI units + restriction = ids.restriction.create(options=[2], type="enumeration", base="decimal") + facet = ids.property.create(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Length") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2)}) + assert bool(facet(element)) is False + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2000)}) + assert bool(facet(element)) is True + # Location instance only checks on the instance, even if the instance is a type. Yes, weird, I know. wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")