Add IDS support for restrictions in attribute facets and handle inheritance

This commit is contained in:
Dion Moult
2022-05-11 13:06:15 +10:00
parent 7e09ab0ced
commit c5de1d8727
2 changed files with 141 additions and 18 deletions
+45 -16
View File
@@ -1,5 +1,5 @@
# IDS - Information Delivery Specification. # IDS - Information Delivery Specification.
# Copyright (C) 2021 Artur Tomczak <artomczak@gmail.com>, Thomas Krijnen <mail@thomaskrijnen.com> # Copyright (C) 2021 Artur Tomczak <artomczak@gmail.com>, Thomas Krijnen <mail@thomaskrijnen.com>, Dion Moult <dion@thinkmoult.com>
# #
# This file is part of IfcOpenShell. # This file is part of IfcOpenShell.
# #
@@ -211,7 +211,13 @@ class specification:
"""Represents the XML <specification> node and its two children <applicability> and <requirements>""" """Represents the XML <specification> node and its two children <applicability> and <requirements>"""
def __init__( def __init__(
self, name="Unnamed", use="required", ifcVersion=["IFC2X3", "IFC4"], identifier=None, description=None, instructions=None self,
name="Unnamed",
use="required",
ifcVersion=["IFC2X3", "IFC4"],
identifier=None,
description=None,
instructions=None,
): ):
"""Create a specification to be added in ids. """Create a specification to be added in ids.
@@ -509,19 +515,21 @@ class entity(facet):
:rtype: facet_evaluation(bool, str) :rtype: facet_evaluation(bool, str)
""" """
if isinstance(self.name, str): if isinstance(self.name, str):
is_class = inst.is_a().lower() == self.name.lower() is_pass = inst.is_a().lower() == self.name.lower()
else: else:
is_class = inst.is_a() == self.name is_pass = inst.is_a() == self.name
if self.predefinedType: if is_pass and self.predefinedType:
predefined_type = ifcopenshell.util.element.get_predefined_type(inst) predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
is_pass = predefined_type == self.predefinedType
if self.predefinedType:
self.message = "an entity name '%(name)s' of predefined type '%(predefinedType)s'" self.message = "an entity name '%(name)s' of predefined type '%(predefinedType)s'"
return facet_evaluation( return facet_evaluation(
is_class and predefined_type == self.predefinedType, is_pass, self.message % {"name": inst.is_a(), "predefinedType": predefined_type}
self.message % {"name": inst.is_a(), "predefinedType": predefined_type},
) )
else: else:
self.message = "an entity name '%(name)s'" self.message = "an entity name '%(name)s'"
return facet_evaluation(is_class, self.message % {"name": inst.is_a()}) return facet_evaluation(is_pass, self.message % {"name": inst.is_a()})
class attribute(facet): class attribute(facet):
@@ -573,7 +581,7 @@ class attribute(facet):
return fac_dict return fac_dict
def __call__(self, inst, logger=None): def __call__(self, inst, logger=None):
"""Validate an ifc instance against that entity facet. """Validate an ifc instance.
:param inst: IFC entity element :param inst: IFC entity element
:type inst: IFC entity :type inst: IFC entity
@@ -582,19 +590,40 @@ class attribute(facet):
:return: result of the validation as bool and message :return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str) :rtype: facet_evaluation(bool, str)
""" """
element_type = ifcopenshell.util.element.get_type(inst) def get_values(element, name):
if isinstance(name, str):
return [getattr(element, name, None)]
return [v for k, v in element.get_info().items() if k == name]
if self.location == "instance": if self.location == "instance":
value = getattr(inst, self.name, None) values = get_values(inst, self.name)
elif self.location == "type": elif self.location == "type":
value = getattr(element_type, self.name, None) if element_type else None element_type = ifcopenshell.util.element.get_type(inst)
values = get_values(element_type, self.name) if element_type else []
elif self.location == "any": elif self.location == "any":
value = getattr(element_type, self.name, None) if element_type else None element_type = ifcopenshell.util.element.get_type(inst)
value = getattr(inst, self.name, value)
if isinstance(self.name, str):
type_value = getattr(element_type, self.name, None) if element_type else None
occurrence_value = getattr(inst, self.name, None)
values = [occurrence_value if occurrence_value is not None else type_value]
else:
if element_type:
info = element_type.get_info()
info.update({k: v for k, v in inst.get_info().items() if v is not None})
else:
info = inst.get_info()
values = [v for k, v in info.items() if k == self.name]
is_pass = bool(values) and all([v is not None and v != "" for v in values])
if is_pass and self.value:
is_pass = all([v == self.value for v in values])
if self.value: if self.value:
self.message = "foo" self.message = "foo"
return facet_evaluation(value == self.value, f"an entity with {self.name} set to '{value}'") return facet_evaluation(is_pass, f"an entity with {self.name} set to something wrong")
else: else:
return facet_evaluation(value is not None and value != "", f"an entity with {self.name}") return facet_evaluation(is_pass, f"an entity with {self.name}")
class classification(facet): class classification(facet):
+96 -2
View File
@@ -22,8 +22,9 @@ import logging
import unittest import unittest
import tempfile import tempfile
import xmlschema import xmlschema
from bcf import bcfxml
import ifcopenshell import ifcopenshell
import ifcopenshell.api
from bcf import bcfxml
from ifcopenshell import ids from ifcopenshell import ids
@@ -275,42 +276,77 @@ class TestIdsAuthoring(unittest.TestCase):
def test_filtering_using_an_entity_facet(self): def test_filtering_using_an_entity_facet(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
# Wrong IFC classes are never matched.
facet = ids.entity.create(name="IfcRabbit")
assert bool(facet(ifc.createIfcWall())) is False
# IFC class is checked for an exact match. Subclasses should not match.
facet = ids.entity.create(name="IfcWall") facet = ids.entity.create(name="IfcWall")
assert bool(facet(ifc.createIfcWall())) is True assert bool(facet(ifc.createIfcWall())) is True
assert bool(facet(ifc.createIfcWall(PredefinedType="SOLIDWALL"))) is True assert bool(facet(ifc.createIfcWall(PredefinedType="SOLIDWALL"))) is True
assert bool(facet(ifc.createIfcSlab())) is False assert bool(facet(ifc.createIfcSlab())) is False
assert bool(facet(ifc.createIfcWallStandardCase())) is False assert bool(facet(ifc.createIfcWallStandardCase())) is False
# IFC class is case insensitive.
facet = ids.entity.create(name="IFCWALL") facet = ids.entity.create(name="IFCWALL")
assert bool(facet(ifc.createIfcWall())) is True assert bool(facet(ifc.createIfcWall())) is True
assert bool(facet(ifc.createIfcWall(PredefinedType="SOLIDWALL"))) is True assert bool(facet(ifc.createIfcWall(PredefinedType="SOLIDWALL"))) is True
assert bool(facet(ifc.createIfcSlab())) is False assert bool(facet(ifc.createIfcSlab())) is False
# Predefined types are checked from standard enumeration values
facet = ids.entity.create(name="IfcWall", predefinedType="SOLIDWALL") facet = ids.entity.create(name="IfcWall", predefinedType="SOLIDWALL")
assert bool(facet(ifc.createIfcWall())) is False assert bool(facet(ifc.createIfcWall())) is False
assert bool(facet(ifc.createIfcWall(PredefinedType="SOLIDWALL"))) is True assert bool(facet(ifc.createIfcWall(PredefinedType="SOLIDWALL"))) is True
assert bool(facet(ifc.createIfcWall(PredefinedType="PARTITIONING"))) is False assert bool(facet(ifc.createIfcWall(PredefinedType="PARTITIONING"))) is False
# Predefined types are checked from the object type field if not standard
facet = ids.entity.create(name="IfcWall", predefinedType="WALDO") facet = ids.entity.create(name="IfcWall", predefinedType="WALDO")
assert bool(facet(ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"))) is True assert bool(facet(ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"))) is True
# Predefined types are checked from the element type field if not standard for types
facet = ids.entity.create(name="IfcWallType", predefinedType="WALDO") facet = ids.entity.create(name="IfcWallType", predefinedType="WALDO")
assert bool(facet(ifc.createIfcWallType(PredefinedType="USERDEFINED", ElementType="WALDO"))) is True assert bool(facet(ifc.createIfcWallType(PredefinedType="USERDEFINED", ElementType="WALDO"))) is True
# Userdefined is not an allowed filter because userdefined implies a specified object or element type
facet = ids.entity.create(name="IfcWall", predefinedType="USERDEFINED") facet = ids.entity.create(name="IfcWall", predefinedType="USERDEFINED")
assert bool(facet(ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"))) is False assert bool(facet(ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"))) is False
# Predefined types should match inherited predefined types from the element type
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="X")
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
facet = ids.entity.create(name="IfcWall", predefinedType="X")
assert bool(facet(wall)) is True
# Predefined types should match overridden predefined types from the element type
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="X")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="NOTDEFINED")
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
facet = ids.entity.create(name="IfcWall", predefinedType="X")
assert bool(facet(wall)) is True
# Restrictions are allowed when matching the IFC class
restriction = ids.restriction.create(options=["IfcWall", "IfcSlab"], type="enumeration", base="string") restriction = ids.restriction.create(options=["IfcWall", "IfcSlab"], type="enumeration", base="string")
facet = ids.entity.create(name=restriction) facet = ids.entity.create(name=restriction)
assert bool(facet(ifc.createIfcWall())) is True assert bool(facet(ifc.createIfcWall())) is True
assert bool(facet(ifc.createIfcSlab())) is True assert bool(facet(ifc.createIfcSlab())) is True
assert bool(facet(ifc.createIfcBeam())) is False assert bool(facet(ifc.createIfcBeam())) is False
# Another example of how restrictions are allowed when matching the IFC class
restriction = ids.restriction.create(options="Ifc.*Type", type="pattern", base="string") restriction = ids.restriction.create(options="Ifc.*Type", type="pattern", base="string")
facet = ids.entity.create(name=restriction) facet = ids.entity.create(name=restriction)
assert bool(facet(ifc.createIfcWall())) is False assert bool(facet(ifc.createIfcWall())) is False
assert bool(facet(ifc.createIfcWallType())) is True assert bool(facet(ifc.createIfcWallType())) is True
# Restrictions are allowed when matching the predefined type
restriction = ids.restriction.create(options="FOO.*", type="pattern", base="string")
facet = ids.entity.create(name="IfcWall", predefinedType=restriction)
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAR")
wall2 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAZ")
assert bool(facet(wall)) is True
assert bool(facet(wall2)) is True
def test_creating_an_attribute_facet(self): def test_creating_an_attribute_facet(self):
attribute = ids.attribute.create(name="name") attribute = ids.attribute.create(name="name")
assert attribute.asdict() == {"name": {"simpleValue": "name"}, "@location": "any"} assert attribute.asdict() == {"name": {"simpleValue": "name"}, "@location": "any"}
@@ -332,32 +368,90 @@ class TestIdsAuthoring(unittest.TestCase):
def test_filtering_using_an_attribute_facet(self): def test_filtering_using_an_attribute_facet(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
# Attribute names that don't exist are never matched
facet = ids.attribute.create(name="Foobar") facet = ids.attribute.create(name="Foobar")
assert bool(facet(ifc.createIfcWall())) is False assert bool(facet(ifc.createIfcWall())) is False
# Attribute names that are either null or empty string are not matched.
# The logic is that unfortunately most BIM users cannot differentiate between the two.
facet = ids.attribute.create(name="Name") facet = ids.attribute.create(name="Name")
assert bool(facet(ifc.createIfcWall())) is False assert bool(facet(ifc.createIfcWall())) is False
assert bool(facet(ifc.createIfcWall(Name=""))) is False assert bool(facet(ifc.createIfcWall(Name=""))) is False
assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is True assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is True
# When a value is specified, the value shall match case sensitively
facet = ids.attribute.create(name="Name", value="Foobar") facet = ids.attribute.create(name="Name", value="Foobar")
assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is True assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is True
assert bool(facet(ifc.createIfcWall(Name="Foobaz"))) is False assert bool(facet(ifc.createIfcWall(Name="Foobaz"))) is False
# A value that is 0 is still considered a value, not "null-like", so it is matched
facet = ids.attribute.create(name="Eastings") facet = ids.attribute.create(name="Eastings")
assert bool(facet(ifc.createIfcMapConversion(Eastings=0))) is True assert bool(facet(ifc.createIfcMapConversion(Eastings=0))) is True
# Values are checked with strict typing. No type casting shall occur.
facet = ids.attribute.create(name="Eastings", value=42) facet = ids.attribute.create(name="Eastings", value=42)
assert bool(facet(ifc.createIfcMapConversion(Eastings=0))) is False assert bool(facet(ifc.createIfcMapConversion(Eastings=0))) is False
assert bool(facet(ifc.createIfcMapConversion(Eastings=42))) is True assert bool(facet(ifc.createIfcMapConversion(Eastings=42))) is True
facet = ids.attribute.create(name="Eastings", value="42")
assert bool(facet(ifc.createIfcMapConversion(Eastings=42))) is False
# Restrictions are allowed for the name
restriction = ids.restriction.create(options=".*Name.*", type="pattern", base="string")
facet = ids.attribute.create(name=restriction)
assert bool(facet(ifc.createIfcMaterialLayerSet(LayerSetName="Foo"))) is True
assert bool(facet(ifc.createIfcMaterialConstituentSet(Name="Foo"))) is True
# Restrictions for the name imply that multiple names may be matched. All, not any, must pass the checks.
restriction = ids.restriction.create(options=["Name", "Description"], type="enumeration", base="string")
facet = ids.attribute.create(name=restriction)
assert bool(facet(ifc.createIfcWall(Name="Foo"))) is False
assert bool(facet(ifc.createIfcWall(Name="Foo", Description="Bar"))) is True
# Restrictions are allowed for the value
restriction = ids.restriction.create(options=["Foo", "Bar"], type="enumeration", base="string") restriction = ids.restriction.create(options=["Foo", "Bar"], type="enumeration", base="string")
facet = ids.attribute.create(name="Name", value=restriction) facet = ids.attribute.create(name="Name", value=restriction)
assert bool(facet(ifc.createIfcWall(Name="Foo"))) is True assert bool(facet(ifc.createIfcWall(Name="Foo"))) is True
assert bool(facet(ifc.createIfcWall(Name="Bar"))) is True assert bool(facet(ifc.createIfcWall(Name="Bar"))) is True
assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is False assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is False
def test_classification_create(self): # Location instance only checks on the instance, this seems like intuitive behaviour
facet = ids.attribute.create(name="Name", value="Foobar", location="instance")
assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is True
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
wall_type.Name = "Foobar"
assert bool(facet(wall)) is False
# Location type only checks on the type. This seems a bit weird honestly.
facet = ids.attribute.create(name="Name", value="Foobar", location="type")
assert bool(facet(ifc.createIfcWall(Name="Foobar"))) is False
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
wall_type.Name = "Foobar"
assert bool(facet(wall)) is True
# Location any checks on attributes on the type, which may be inherited by the occurence
facet = ids.attribute.create(name="Description", value="Foobar", location="any")
assert bool(facet(ifc.createIfcWall(Description="Foobar"))) is True
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
wall_type.Description = "Foobar"
assert bool(facet(wall)) is True
# Location any checks on attributes on the type, which may be overriden by attributes on the occurence
facet = ids.attribute.create(name="Description", value="Foobar", location="any")
assert bool(facet(ifc.createIfcWall(Description="Foobar"))) is True
wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
wall_type.Description = "Foobaz"
wall.Description = "Foobar"
assert bool(facet(wall)) is True
def test_creating_a_classification_facet(self):
c = ids.classification.create(location="any", value="Test_Value", system="Test_System") c = ids.classification.create(location="any", value="Test_Value", system="Test_System")
self.assertEqual(c.location, "any") self.assertEqual(c.location, "any")
self.assertEqual(c.value, "Test_Value") self.assertEqual(c.value, "Test_Value")