Implement newly proposed IDS PartOf facet

This commit is contained in:
Dion Moult
2022-09-11 17:20:50 +10:00
parent e5e81c818e
commit 77c9bf5bb6
3 changed files with 183 additions and 75 deletions
+70 -19
View File
@@ -232,7 +232,7 @@ class Attribute(Facet):
elif isinstance(self.value, str):
cast_value = cast_to_value(self.value, value)
if isinstance(value, float) and isinstance(cast_value, float):
if value < cast_value * (1. - 1e-6) or value > cast_value * (1. + 1e-6):
if value < cast_value * (1.0 - 1e-6) or value > cast_value * (1.0 + 1e-6):
is_pass = False
reason = {"type": "VALUE", "actual": value}
break
@@ -295,28 +295,79 @@ class Classification(Facet):
class PartOf(Facet):
def __init__(self, entity="IfcSystem"):
self.parameters = ["@entity"]
self.applicability_templates = ["An element part of a {entity}"]
self.requirement_templates = ["Must be part of a {entity}"]
super().__init__(entity)
def __init__(self, entity=None, relation="IfcRelAggregates", instructions=None):
self.parameters = ["entity", "@relation", "@instructions"]
self.applicability_templates = [
"An element with an {relation} relationship with an {entity}",
"An element with an {relation} relationship",
]
self.requirement_templates = [
"An element must have an {relation} relationship with an {entity}",
"An element must have an {relation} relationship",
]
super().__init__(entity, relation, instructions)
def __call__(self, inst, logger=None):
if self.entity == "IfcElementAssembly":
is_pass = False
reason = None
if self.relation == "IfcRelAggregates":
aggregate = ifcopenshell.util.element.get_aggregate(inst)
while aggregate is not None:
if aggregate.is_a() == "IfcElementAssembly":
is_pass = True
break
aggregate = ifcopenshell.util.element.get_aggregate(aggregate)
else:
is_pass = False
is_pass = aggregate is not None
if not is_pass:
reason = {"type": "RELATION"}
if is_pass and self.entity:
is_pass = False
ancestors = []
while aggregate is not None:
ancestors.append(aggregate.is_a())
if aggregate.is_a().upper() == self.entity:
is_pass = True
break
aggregate = ifcopenshell.util.element.get_aggregate(aggregate)
if not is_pass:
reason = {"type": "ENTITY", "actual": ancestors}
elif self.relation == "IfcRelAssignsToGroup":
group = None
for rel in getattr(inst, "HasAssignments", []) or []:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a(self.entity):
is_pass = True
if rel.is_a("IfcRelAssignsToGroup"):
group = rel.RelatingGroup
break
is_pass = group is not None
if not is_pass:
reason = {"type": "NOVALUE"}
if is_pass and self.entity:
if group.is_a().upper() != self.entity:
is_pass = False
reason = {"type": "ENTITY", "actual": group.is_a().upper()}
elif self.relation == "IfcRelContainedInSpatialStructure":
container = ifcopenshell.util.element.get_container(inst)
is_pass = container is not None
if not is_pass:
reason = {"type": "RELATION"}
if is_pass and self.entity:
if container.is_a().upper() != self.entity:
is_pass = False
reason = {"type": "ENTITY", "actual": container.is_a().upper()}
elif self.relation == "IfcRelNests":
nest = self.get_nested_whole(inst)
is_pass = nest is not None
if not is_pass:
reason = {"type": "NOVALUE"}
if is_pass and self.entity:
is_pass = False
ancestors = []
while nest is not None:
ancestors.append(nest.is_a())
if nest.is_a().upper() == self.entity:
is_pass = True
break
nest = self.get_nested_whole(nest)
if not is_pass:
reason = {"type": "ENTITY", "actual": ancestors}
return PartOfResult(is_pass, reason)
return PartOfResult(is_pass, "TODO")
def get_nested_whole(self, element):
for rel in getattr(element, "Nests", []) or []:
return rel.RelatingObject
class Property(Facet):
@@ -426,7 +477,7 @@ class Property(Facet):
elif isinstance(self.value, str):
cast_value = cast_to_value(self.value, value)
if isinstance(value, float) and isinstance(cast_value, float):
if value < cast_value * (1. - 1e-6) or value > cast_value * (1. + 1e-6):
if value < cast_value * (1.0 - 1e-6) or value > cast_value * (1.0 + 1e-6):
is_pass = False
reason = {"type": "VALUE", "actual": value}
break
+18 -12
View File
@@ -56,9 +56,16 @@
<xs:element name="system" type="ids:idsValue" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="partOfType">
<xs:sequence>
<xs:element name="entity" type="ids:idsValue" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="relation" type="ids:relations" use="required"/>
</xs:complexType>
<xs:complexType name="applicabilityType">
<xs:sequence>
<xs:element name="entity" type="ids:entityType" minOccurs="0"/>
<xs:element name="partOf" type="ids:partOfType" minOccurs="0"/>
<xs:element name="classification" type="ids:classificationType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="attribute" type="ids:attributeType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="property" minOccurs="0" maxOccurs="unbounded">
@@ -115,17 +122,16 @@
</xs:element>
<xs:element name="partOf" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="entity" maxOccurs="unbounded">
<xs:complexType>
<xs:complexContent>
<xs:extension base="ids:idsValue">
<xs:attribute name="relation" type="ids:relations" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:complexContent>
<xs:extension base="ids:partOfType">
<xs:attributeGroup ref="xs:occurs"/>
<xs:attribute name="instructions" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:element name="classification" minOccurs="0" maxOccurs="unbounded">
@@ -242,4 +248,4 @@
<xs:enumeration value="IfcRelNests"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
</xs:schema>
+95 -44
View File
@@ -492,25 +492,25 @@ class TestAttribute:
run(
"Floating point numbers are compared with a 1e-6 tolerance 1/4",
facet=facet,
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. + 1e-6)),
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 + 1e-6)),
expected=True,
)
run(
"Floating point numbers are compared with a 1e-6 tolerance 2/4",
facet=facet,
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. - 1e-6)),
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 - 1e-6)),
expected=True,
)
run(
"Floating point numbers are compared with a 1e-6 tolerance 3/4",
facet=facet,
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. + 2e-6)),
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 + 2e-6)),
expected=False,
)
run(
"Floating point numbers are compared with a 1e-6 tolerance 4/4",
facet=facet,
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. - 2e-6)),
inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 - 2e-6)),
expected=False,
)
@@ -931,13 +931,21 @@ class TestProperty:
run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=element, expected=True)
facet = Property(propertySet="Foo_Bar", name="Foo", value="42.")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. + 1e-6))})
ifcopenshell.api.run(
"pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 1e-6))}
)
run("Floating point numbers are compared with a 1e-6 tolerance 1/4", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. - 1e-6))})
ifcopenshell.api.run(
"pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 - 1e-6))}
)
run("Floating point numbers are compared with a 1e-6 tolerance 2/4", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. + 2e-6))})
ifcopenshell.api.run(
"pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 2e-6))}
)
run("Floating point numbers are compared with a 1e-6 tolerance 3/4", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. - 2e-6))})
ifcopenshell.api.run(
"pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 - 2e-6))}
)
run("Floating point numbers are compared with a 1e-6 tolerance 4/4", facet=facet, inst=element, expected=False)
facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE")
@@ -1017,9 +1025,19 @@ class TestProperty:
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)})
run("Unit conversions shall take place to IDS-nominated standard units 1/2", facet=facet, inst=element, expected=False)
run(
"Unit conversions shall take place to IDS-nominated standard units 1/2",
facet=facet,
inst=element,
expected=False,
)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2000)})
run("Unit conversions shall take place to IDS-nominated standard units 2/2", facet=facet, inst=element, expected=True)
run(
"Unit conversions shall take place to IDS-nominated standard units 2/2",
facet=facet,
inst=element,
expected=True,
)
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")
@@ -1197,69 +1215,102 @@ class TestMaterial:
class TestPartOf:
def test_creating_a_partof_facet(self):
facet = PartOf()
assert facet.asdict() == {"@entity": "IfcSystem"}
facet = PartOf(entity="IfcGroup")
assert facet.asdict() == {"@entity": "IfcGroup"}
assert facet.asdict() == {"@relation": "IfcRelAggregates"}
facet = PartOf(entity="IfcGroup", relation="IfcRelAssignsToGroup", instructions="instructions")
assert facet.asdict() == {
"entity": {"simpleValue": "IfcGroup"},
"@relation": "IfcRelAssignsToGroup",
"@instructions": "instructions",
}
def test_filtering_using_a_partof_facet(self):
ifc = ifcopenshell.file()
# An IfcElementAssembly entity only passes those who are part of an assembly
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
facet = PartOf(relation="IfcRelAggregates")
run("A non aggregated element fails an aggregate relationship", facet=facet, inst=subelement, expected=False)
ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element)
facet = PartOf(entity="IfcElementAssembly")
run("", facet=facet, inst=element, expected=False)
run("", facet=facet, inst=subelement, expected=True)
run("The aggregated whole fails an aggregate relationship", facet=facet, inst=element, expected=False)
run("The aggregated part passes an aggregate relationship", facet=facet, inst=subelement, expected=True)
# An IfcElementAssembly strictly checks that the whole is an IfcElementAssembly class
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam")
ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element)
facet = PartOf(entity="IfcElementAssembly")
run("", facet=facet, inst=subelement, expected=False)
facet = PartOf(entity="IFCSLAB", relation="IfcRelAggregates")
run("An aggregate may specify the entity of the whole 1/2", facet=facet, inst=subelement, expected=True)
facet = PartOf(entity="IFCWALL", relation="IfcRelAggregates")
run("An aggregate may specify the entity of the whole 2/2", facet=facet, inst=subelement, expected=False)
# A nested subelement still passes so long as one of its parents is an IfcElementAssembly
# TODO nononono
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab")
subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam")
ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element)
ifcopenshell.api.run("aggregate.assign_object", ifc, product=subsubelement, relating_object=subelement)
facet = PartOf(entity="IfcElementAssembly")
run("", facet=facet, inst=subsubelement, expected=True)
facet = PartOf(entity="IFCELEMENTASSEMBLY", relation="IfcRelAggregates")
run("An aggregate entity may pass any ancestral whole passes", facet=facet, inst=subsubelement, expected=True)
# An IfcGroup only checks that a group is assigned without any other logic
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifcopenshell.api.run("group.add_group", ifc)
facet = PartOf(entity="IfcGroup")
run("", facet=facet, inst=element, expected=False)
facet = PartOf(relation="IfcRelAssignsToGroup")
run("A non grouped element fails a group relationship", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("group.assign_group", ifc, products=[element], group=group)
run("", facet=facet, inst=element, expected=True)
run("A grouped element passes a group relationship", facet=facet, inst=element, expected=True)
# An IfcGroup can be passed by subtypes
# TODO: wrong, subtypes should not be matched
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifc.createIfcInventory()
facet = PartOf(entity="IfcGroup")
facet = PartOf(entity="IFCGROUP", relation="IfcRelAssignsToGroup")
ifcopenshell.api.run("group.assign_group", ifc, products=[element], group=group)
run("", facet=facet, inst=element, expected=True)
run("A group entity must match exactly 1/2", facet=facet, inst=element, expected=False)
facet = PartOf(entity="IFCINVENTORY", relation="IfcRelAssignsToGroup")
run("A group entity must match exactly 2/2", facet=facet, inst=element, expected=True)
# An IfcSystem only checks that a system is assigned without any other logic
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
system = ifcopenshell.api.run("system.add_system", ifc)
facet = PartOf(entity="IfcSystem")
run("", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system)
run("", facet=facet, inst=element, expected=True)
container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace")
facet = PartOf(relation="IfcRelContainedInSpatialStructure")
run("Any contained element passes a containment relationship 1/2", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container)
run("Any contained element passes a containment relationship 2/2", facet=facet, inst=element, expected=True)
run("The container itself always fails", facet=facet, inst=container, expected=False)
# An IfcSystem allows subtypes
# TODO: wrong, subtypes should not be matched
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcDistributionSystem")
ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system)
facet = PartOf(entity="IfcSystem")
run("", facet=facet, inst=element, expected=True)
container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace")
ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container)
facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSITE")
run("The container entity must match exactly 1/2", facet=facet, inst=element, expected=False)
facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSPACE")
run("The container entity must match exactly 2/2", facet=facet, inst=element, expected=True)
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam")
ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element)
container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace")
ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container)
facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSPACE")
run("The container may be indirect", facet=facet, inst=subelement, expected=True)
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element)
facet = PartOf(relation="IfcRelNests")
run("Any nested part passes a nest relationship", facet=facet, inst=subelement, expected=True)
run("Any nested whole fails a nest relationship", facet=facet, inst=element, expected=False)
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element)
facet = PartOf(relation="IfcRelNests", entity="IFCBEAM")
run("The nest entity must match exactly 1/2", facet=facet, inst=subelement, expected=False)
facet = PartOf(relation="IfcRelNests", entity="IFCFURNITURE")
run("The nest entity must match exactly 2/2", facet=facet, inst=subelement, expected=True)
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcMechanicalFastener")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element)
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subsubelement, relating_object=subelement)
facet = PartOf(relation="IfcRelNests", entity="IFCFURNITURE")
run("Nesting may be indirect", facet=facet, inst=subsubelement, expected=True)
class TestRestriction: