Fix multiple specifications in IDS

This commit is contained in:
Dion Moult
2022-05-25 17:49:12 +10:00
parent e302fea33b
commit 0a3e5f65fa
2 changed files with 71 additions and 22 deletions
+43 -17
View File
@@ -19,18 +19,14 @@
import os import os
import re import re
import logging import logging
import operator
import numpy as np import numpy as np
import datetime import datetime
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.classification import ifcopenshell.util.classification
from bcf.v2.bcfxml import BcfXml from bcf.v2.bcfxml import BcfXml
from bcf.v2 import data as bcf from bcf.v2 import data as bcf
from xmlschema import XMLSchema from xmlschema import XMLSchema
from xmlschema import etree_tostring from xmlschema import etree_tostring
from xmlschema.validators import identities from xmlschema.validators import identities
@@ -112,10 +108,10 @@ class ids:
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd",
"info": self.info, "info": self.info,
"specifications": [], "specifications": {"specification": []},
} }
for spec in self.specifications: for spec in self.specifications:
ids_dict["specifications"].append({"specification": spec.asdict()}) # TEST! ids_dict["specifications"]["specification"].append(spec.asdict())
return ids_dict return ids_dict
def to_string(self, ids_schema=ids_schema): def to_string(self, ids_schema=ids_schema):
@@ -139,6 +135,7 @@ class ids:
:return: Result of the newly created file validation against the schema. :return: Result of the newly created file validation against the schema.
:rtype: bool :rtype: bool
""" """
ET.register_namespace("", "http://standards.buildingsmart.org/IDS")
ET.ElementTree(ids_schema.encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True) ET.ElementTree(ids_schema.encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True)
return ids_schema.is_valid(filepath) return ids_schema.is_valid(filepath)
@@ -162,6 +159,29 @@ class ids:
ids_file.specifications = [specification.parse(s) for s in ids_content["specifications"]["specification"]] ids_file.specifications = [specification.parse(s) for s in ids_content["specifications"]["specification"]]
return ids_file return ids_file
def validate2(self, ifc_file):
"""Use to validate IFC model against IDS specifications.
:param ifc_file: path to ifc file
:type ifc_file: str
:param logger: Logging object with handlers, defaults to None
:type logger: logging, optional
"""
for specification in self.specifications:
specification.applicable_entities.clear()
specification.failed_entities.clear()
specification.status = None
for element in ifc_file:
for specification in self.specifications:
if not specification.applicability(element, None):
continue
specification.applicable_entities.append(element)
if specification.requirements(element, None):
specification.status = True
else:
specification.status = False
specification.failed_entities.append(element)
def validate(self, ifc_file, logger=None): def validate(self, ifc_file, logger=None):
"""Use to validate IFC model against IDS specifications. """Use to validate IFC model against IDS specifications.
@@ -237,6 +257,10 @@ class specification:
self.description = description self.description = description
self.instructions = instructions self.instructions = instructions
self.applicable_entities = []
self.failed_entities = []
self.status = None
def asdict(self): def asdict(self):
"""Converts object to a dictionary, adding required attributes. """Converts object to a dictionary, adding required attributes.
@@ -337,7 +361,6 @@ class specification:
:rtype: [bool,bool] :rtype: [bool,bool]
""" """
if self.applicability(inst, logger): if self.applicability(inst, logger):
valid = self.requirements(inst, logger) valid = self.requirements(inst, logger)
if valid: if valid:
@@ -473,10 +496,10 @@ class facet(metaclass=meta_facet):
class entity(facet): class entity(facet):
"""The IDS entity facet currently *with* inheritance""" """The IDS entity facet currently *with* inheritance"""
parameters = ["name", "predefinedType"] parameters = ["name", "predefinedType", "instructions"]
@staticmethod @staticmethod
def create(name=None, predefinedType=None): def create(name=None, predefinedType=None, instructions=None):
"""Create an entity facet that can be added to applicability or requirements of IDS specification. """Create an entity facet that can be added to applicability or requirements of IDS specification.
:param name: IFC entity name that is required. e.g. IfcWall, defaults to None :param name: IFC entity name that is required. e.g. IfcWall, defaults to None
@@ -490,6 +513,7 @@ class entity(facet):
inst = entity() inst = entity()
inst.name = name inst.name = name
inst.predefinedType = predefinedType inst.predefinedType = predefinedType
inst.instructions = instructions
return inst return inst
def asdict(self): def asdict(self):
@@ -501,6 +525,8 @@ class entity(facet):
results = {"name": parameter_asdict(self.name)} results = {"name": parameter_asdict(self.name)}
if self.predefinedType: if self.predefinedType:
results["predefinedType"] = parameter_asdict(self.predefinedType) results["predefinedType"] = parameter_asdict(self.predefinedType)
if self.instructions:
results["@instructions"] = self.instructions
return results return results
def __call__(self, inst, logger=None): def __call__(self, inst, logger=None):
@@ -569,16 +595,16 @@ class attribute(facet):
:return: Xmlschema compliant dictionary. :return: Xmlschema compliant dictionary.
:rtype: dict :rtype: dict
""" """
fac_dict = {"name": parameter_asdict(self.name)} results = {"name": parameter_asdict(self.name)}
if self.value: if self.value:
fac_dict["value"] = parameter_asdict(self.value) results["value"] = parameter_asdict(self.value)
if self.location: if self.location:
fac_dict["@location"] = self.location results["@location"] = self.location
if self.use: if self.use:
fac_dict["@use"] = self.use results["@use"] = self.use
if self.instructions: if self.instructions:
fac_dict["@instructions"] = self.instructions results["@instructions"] = self.instructions
return fac_dict return results
def __call__(self, inst, logger=None): def __call__(self, inst, logger=None):
"""Validate an ifc instance. """Validate an ifc instance.
@@ -752,7 +778,7 @@ class partOf(facet):
:return: Xmlschema compliant dictionary. :return: Xmlschema compliant dictionary.
:rtype: dict :rtype: dict
""" """
return {"@entity": parameter_asdict(self.entity)} return {"@entity": self.entity}
def __call__(self, inst, logger=None): def __call__(self, inst, logger=None):
"""Validate an ifc instance against that partOf facet. """Validate an ifc instance against that partOf facet.
@@ -1149,7 +1175,7 @@ class restriction:
rest_dict["xs:enumeration"].append({"@value": option}) rest_dict["xs:enumeration"].append({"@value": option})
elif self.type == "bounds": elif self.type == "bounds":
for option in self.options: for option in self.options:
rest_dict["xs:" + option] = [{"@value": self.options[option], "@fixed": False}] rest_dict["xs:" + option] = [{"@value": str(self.options[option]), "@fixed": False}]
elif self.type == "pattern": elif self.type == "pattern":
if "xs:pattern" not in rest_dict: if "xs:pattern" not in rest_dict:
rest_dict["xs:pattern"] = [{"@value": self.options}] rest_dict["xs:pattern"] = [{"@value": self.options}]
+28 -5
View File
@@ -240,7 +240,7 @@ class TestIdsAuthoring(unittest.TestCase):
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd",
"info": {"title": "Untitled"}, "info": {"title": "Untitled"},
"specifications": [], "specifications": {'specification': []},
} }
def test_create_an_ids_with_all_possible_information(self): def test_create_an_ids_with_all_possible_information(self):
@@ -269,7 +269,7 @@ class TestIdsAuthoring(unittest.TestCase):
"purpose": "purpose", "purpose": "purpose",
"milestone": "milestone", "milestone": "milestone",
}, },
"specifications": [], "specifications": {'specification': []},
} }
def test_check_invalid_ids_information(self): def test_check_invalid_ids_information(self):
@@ -280,7 +280,7 @@ class TestIdsAuthoring(unittest.TestCase):
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd",
"info": {"title": "Untitled"}, "info": {"title": "Untitled"},
"specifications": [], "specifications": {'specification': []},
} }
def test_authoring_an_ids_with_no_specifications_is_invalid(self): def test_authoring_an_ids_with_no_specifications_is_invalid(self):
@@ -363,6 +363,7 @@ class TestIdsAuthoring(unittest.TestCase):
# IFC class is case insensitive. # IFC class is case insensitive.
# WRONG WRONG WRONG should be UPPERCASE # WRONG WRONG WRONG should be UPPERCASE
# But in that case why are the enumerations for things like partOf using the IFC capitalisation?
facet = ids.entity.create(name="IFCWALL") facet = ids.entity.create(name="IFCWALL")
case("Entity case 0", facet=facet, inst=ifc.createIfcWall(), expected=True) case("Entity case 0", facet=facet, inst=ifc.createIfcWall(), expected=True)
case("Entity case 1", facet=facet, inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), expected=True) case("Entity case 1", facet=facet, inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), expected=True)
@@ -1103,9 +1104,9 @@ class TestIdsAuthoring(unittest.TestCase):
def test_creating_a_partof_facet(self): def test_creating_a_partof_facet(self):
facet = ids.partOf.create() facet = ids.partOf.create()
assert facet.asdict() == {"@entity": {"simpleValue": "IfcSystem"}} assert facet.asdict() == {"@entity": "IfcSystem"}
facet = ids.partOf.create(entity="IfcGroup") facet = ids.partOf.create(entity="IfcGroup")
assert facet.asdict() == {"@entity": {"simpleValue": "IfcGroup"}} assert facet.asdict() == {"@entity": "IfcGroup"}
def test_filtering_using_a_partof_facet(self): def test_filtering_using_a_partof_facet(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -1274,6 +1275,28 @@ class TestIfcValidation(unittest.TestCase):
assert set(spec.applicable_entities) == {wall, waldo} assert set(spec.applicable_entities) == {wall, waldo}
assert spec.failed_entities == [wall] assert spec.failed_entities == [wall]
def test_creating_multiple_specifications(self):
specs = ids.ids(title="Title")
spec = ids.specification(name="Name")
spec.add_applicability(ids.entity.create(name="IfcWall"))
spec.add_requirement(ids.attribute.create(name="Name", value="Waldo"))
specs.specifications.append(spec)
spec2 = ids.specification(name="Name")
spec2.add_applicability(ids.entity.create(name="IfcWall"))
spec2.add_requirement(ids.attribute.create(name="Name", value="Waldo"))
specs.specifications.append(spec2)
model = ifcopenshell.file()
wall = model.createIfcWall()
waldo = model.createIfcWall(Name="Waldo")
specs.validate2(model)
assert spec.status == False
assert set(spec.applicable_entities) == {wall, waldo}
assert spec.failed_entities == [wall]
assert spec2.failed_entities == [wall]
def test_validate_simple(self): def test_validate_simple(self):
# Same test as in reporting... # Same test as in reporting...
ids_file = ids.ids.open(IDS_URL) ids_file = ids.ids.open(IDS_URL)