mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-10-03 13:28:19 +00:00
Refactor IDS into IfcTester with less magic and less code
* String building code all deleted and replaced with more maintainable code * Obscure features like metaclasses deleted * Regular constructors used instead of factory create and parse methods * Use Python class naming convention * Use polymorphism more (e.g. asdict, init) for facet classes * No more boolean any/and as users may want to test everything and not fail early * Error state is stored instead of streamed to logger so it can be tested and more flexible formatting
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# ifctester
|
||||||
|
|
||||||
|
Author, test, and see reports from IDS audits on the command line, as a webapp, or as a library.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# IfcTester - IDS based model auditing
|
||||||
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcTester.
|
||||||
|
#
|
||||||
|
# IfcTester is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcTester is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from . import ids
|
||||||
|
from . import reporter
|
||||||
|
import ifcopenshell
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Uses an IDS to audit an IFC")
|
||||||
|
parser.add_argument("ids", type=str, help="Path to an IDS")
|
||||||
|
parser.add_argument("ifc", type=str, help="Path to an IFC")
|
||||||
|
parser.add_argument(
|
||||||
|
"-r", "--reporter", type=str, help="The reporting method to view audit results", default="Console"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
specs = ids.open(args.ids)
|
||||||
|
ifc = ifcopenshell.open(args.ifc)
|
||||||
|
print("Finished loading:", time.time() - start)
|
||||||
|
start = time.time()
|
||||||
|
specs.validate(ifc)
|
||||||
|
print("Finished validating:", time.time() - start)
|
||||||
|
start = time.time()
|
||||||
|
reporter.Console(specs).report()
|
||||||
@@ -0,0 +1,858 @@
|
|||||||
|
# IfcTester - IDS based model auditing
|
||||||
|
# Copyright (C) 2021 Artur Tomczak <artomczak@gmail.com>, Thomas Krijnen <mail@thomaskrijnen.com>, Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcTester.
|
||||||
|
#
|
||||||
|
# IfcTester is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcTester is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
import builtins
|
||||||
|
import ifcopenshell.util.unit
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.util.placement
|
||||||
|
import ifcopenshell.util.classification
|
||||||
|
from xmlschema import XMLSchema
|
||||||
|
from xmlschema import etree_tostring
|
||||||
|
from xmlschema.validators import identities
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
schema = XMLSchema(os.path.join(cwd, "ids.xsd"))
|
||||||
|
|
||||||
|
|
||||||
|
def open(filepath):
|
||||||
|
"""Use to open ids.xml files
|
||||||
|
|
||||||
|
:param filepath: ids file path
|
||||||
|
:type filepath: str
|
||||||
|
:return: IDS file as a python object
|
||||||
|
"""
|
||||||
|
# schema.validate(filepath)
|
||||||
|
return Ids().parse(
|
||||||
|
schema.decode(filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Ids:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
title="Untitled",
|
||||||
|
copyright=None,
|
||||||
|
version=None,
|
||||||
|
description=None,
|
||||||
|
author=None,
|
||||||
|
date=None,
|
||||||
|
purpose=None,
|
||||||
|
milestone=None,
|
||||||
|
):
|
||||||
|
self.specifications = []
|
||||||
|
self.info = {}
|
||||||
|
self.info["title"] = title or "Untitled"
|
||||||
|
if copyright:
|
||||||
|
self.info["copyright"] = copyright
|
||||||
|
if version:
|
||||||
|
self.info["version"] = version
|
||||||
|
if description:
|
||||||
|
self.info["description"] = description
|
||||||
|
if author and "@" in author:
|
||||||
|
self.info["author"] = author
|
||||||
|
if date:
|
||||||
|
try:
|
||||||
|
self.info["date"] = datetime.date.fromisoformat(date).isoformat()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if purpose:
|
||||||
|
self.info["purpose"] = purpose
|
||||||
|
if milestone:
|
||||||
|
self.info["milestone"] = milestone
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
ids_dict = {
|
||||||
|
"@xmlns": "http://standards.buildingsmart.org/IDS",
|
||||||
|
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
||||||
|
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||||||
|
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd",
|
||||||
|
"info": self.info,
|
||||||
|
"specifications": {"specification": []},
|
||||||
|
}
|
||||||
|
for spec in self.specifications:
|
||||||
|
ids_dict["specifications"]["specification"].append(spec.asdict())
|
||||||
|
return ids_dict
|
||||||
|
|
||||||
|
def parse(self, data):
|
||||||
|
for attribute in ["title", "copyright", "version", "description", "author"]:
|
||||||
|
value = data["info"].get(attribute)
|
||||||
|
if value:
|
||||||
|
self.info[attribute] = value
|
||||||
|
xml_specs = data["specifications"]["specification"]
|
||||||
|
if not isinstance(xml_specs, list):
|
||||||
|
xml_specs = [xml_specs]
|
||||||
|
for xml_spec in xml_specs:
|
||||||
|
spec = Specification()
|
||||||
|
spec.parse(xml_spec)
|
||||||
|
self.specifications.append(spec)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to_string(self):
|
||||||
|
ns = {"": "http://standards.buildingsmart.org/IDS"}
|
||||||
|
return etree_tostring(schema.encode(self.asdict()), namespaces=ns)
|
||||||
|
|
||||||
|
def to_xml(self, filepath="output.xml"):
|
||||||
|
ET.register_namespace("", "http://standards.buildingsmart.org/IDS")
|
||||||
|
ET.ElementTree(schema.encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True)
|
||||||
|
return schema.is_valid(filepath)
|
||||||
|
|
||||||
|
def validate(self, ifc_file):
|
||||||
|
for specification in self.specifications:
|
||||||
|
specification.applicable_entities.clear()
|
||||||
|
specification.failed_entities = set()
|
||||||
|
for facet in specification.requirements:
|
||||||
|
facet.failed_entities.clear()
|
||||||
|
specification.status = None
|
||||||
|
|
||||||
|
filtered_elements = {}
|
||||||
|
|
||||||
|
for i, specification in enumerate(self.specifications):
|
||||||
|
if ifc_file.schema not in specification.ifcVersion:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
for facet in specification.applicability:
|
||||||
|
elements = facet.filter(ifc_file, elements)
|
||||||
|
|
||||||
|
for element in elements:
|
||||||
|
is_applicable = True
|
||||||
|
for facet in specification.applicability:
|
||||||
|
if isinstance(facet, Entity):
|
||||||
|
continue
|
||||||
|
if not bool(facet(element)):
|
||||||
|
is_applicable = False
|
||||||
|
break
|
||||||
|
if not is_applicable:
|
||||||
|
continue
|
||||||
|
specification.applicable_entities.append(element)
|
||||||
|
for facet in specification.requirements:
|
||||||
|
result = facet(element)
|
||||||
|
if not bool(result):
|
||||||
|
specification.failed_entities.add(element)
|
||||||
|
facet.failed_entities.append(element)
|
||||||
|
facet.failed_reasons.append(str(result))
|
||||||
|
|
||||||
|
for specification in self.specifications:
|
||||||
|
specification.status = True
|
||||||
|
if specification.failed_entities:
|
||||||
|
specification.status = False
|
||||||
|
elif specification.minOccurs != 0 and not specification.applicable_entities:
|
||||||
|
specification.status = False
|
||||||
|
elif len(specification.applicable_entities) > (specification.maxOccurs or 1):
|
||||||
|
specification.status = False
|
||||||
|
|
||||||
|
|
||||||
|
class Specification:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name="Unnamed",
|
||||||
|
minOccurs=None,
|
||||||
|
maxOccurs=None,
|
||||||
|
ifcVersion=["IFC2X3", "IFC4"],
|
||||||
|
identifier=None,
|
||||||
|
description=None,
|
||||||
|
instructions=None,
|
||||||
|
):
|
||||||
|
self.name = name or "Unnamed"
|
||||||
|
self.applicability = []
|
||||||
|
self.requirements = []
|
||||||
|
self.minOccurs = minOccurs
|
||||||
|
self.maxOccurs = maxOccurs
|
||||||
|
self.ifcVersion = ifcVersion
|
||||||
|
self.identifier = identifier
|
||||||
|
self.description = description
|
||||||
|
self.instructions = instructions
|
||||||
|
|
||||||
|
self.applicable_entities = []
|
||||||
|
self.status = None
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
results = {
|
||||||
|
"@name": self.name,
|
||||||
|
"@ifcVersion": self.ifcVersion,
|
||||||
|
"applicability": {},
|
||||||
|
"requirements": {},
|
||||||
|
}
|
||||||
|
for attribute in ["identifier", "description", "instructions", "minOccurs", "maxOccurs"]:
|
||||||
|
value = getattr(self, attribute)
|
||||||
|
if value:
|
||||||
|
results[f"@{attribute}"] = value
|
||||||
|
for clause_type in ["applicability", "requirements"]:
|
||||||
|
clause = getattr(self, clause_type)
|
||||||
|
if not clause:
|
||||||
|
continue
|
||||||
|
for facet in clause:
|
||||||
|
facet_type = type(facet).__name__
|
||||||
|
facet_type = facet_type[0].lower() + facet_type[1:]
|
||||||
|
if facet_type in results[clause_type]:
|
||||||
|
results[clause_type][facet_type].append(facet.asdict())
|
||||||
|
else:
|
||||||
|
results[clause_type][facet_type] = [facet.asdict()]
|
||||||
|
return results
|
||||||
|
|
||||||
|
def parse(self, ids_dict):
|
||||||
|
self.name = ids_dict.get("@name", "")
|
||||||
|
self.minOccurs = ids_dict["@minOccurs"]
|
||||||
|
self.maxOccurs = ids_dict["@maxOccurs"]
|
||||||
|
self.ifcVersion = ids_dict["@ifcVersion"]
|
||||||
|
self.applicability = self.parse_clause(ids_dict["applicability"])
|
||||||
|
self.requirements = self.parse_clause(ids_dict["requirements"])
|
||||||
|
return self
|
||||||
|
|
||||||
|
def parse_clause(self, clause):
|
||||||
|
results = []
|
||||||
|
for name, facets in clause.items():
|
||||||
|
if name not in ["entity", "attribute", "classification", "partOf", "property", "material"]:
|
||||||
|
continue
|
||||||
|
if not isinstance(facets, list):
|
||||||
|
facets = [facets]
|
||||||
|
for facet_xml in facets:
|
||||||
|
facet = globals()[name.capitalize()]().parse(facet_xml)
|
||||||
|
results.append(facet)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class Facet:
|
||||||
|
def __init__(self, *parameters):
|
||||||
|
self.failed_entities = []
|
||||||
|
self.failed_reasons = []
|
||||||
|
for i, name in enumerate(self.parameters):
|
||||||
|
setattr(self, name.replace("@", ""), parameters[i])
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
results = {}
|
||||||
|
for name in self.parameters:
|
||||||
|
value = getattr(self, name.replace("@", ""))
|
||||||
|
if value is not None:
|
||||||
|
results[name] = value if "@" in name else self.to_ids_value(value)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def parse(self, xml):
|
||||||
|
for name, value in xml.items():
|
||||||
|
name = name.replace("@", "")
|
||||||
|
if isinstance(value, dict) and "simpleValue" in value.keys():
|
||||||
|
setattr(self, name, value["simpleValue"])
|
||||||
|
elif isinstance(value, dict) and "restriction" in value.keys():
|
||||||
|
setattr(self, name, Restriction().parse(value["restriction"][0]))
|
||||||
|
# TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]]
|
||||||
|
else:
|
||||||
|
setattr(self, name, value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def filter(self, ifc_file, elements):
|
||||||
|
return [e for e in elements if self(e)]
|
||||||
|
|
||||||
|
def to_string(self, clause_type):
|
||||||
|
if clause_type == "applicability":
|
||||||
|
templates = self.applicability_templates
|
||||||
|
elif clause_type == "requirement":
|
||||||
|
templates = self.requirement_templates
|
||||||
|
|
||||||
|
for template in templates:
|
||||||
|
for key in self.parameters:
|
||||||
|
key = key.replace("@", "")
|
||||||
|
value = getattr(self, key)
|
||||||
|
key_variable = "{" + key + "}"
|
||||||
|
if value is not None and key_variable in template:
|
||||||
|
template = template.replace(key_variable, str(value))
|
||||||
|
if "{" not in template:
|
||||||
|
return template
|
||||||
|
|
||||||
|
def to_ids_value(self, parameter):
|
||||||
|
if isinstance(parameter, str):
|
||||||
|
parameter_dict = {"simpleValue": parameter}
|
||||||
|
elif isinstance(parameter, Restriction):
|
||||||
|
parameter_dict = {"xs:restriction": [parameter.asdict()]}
|
||||||
|
elif isinstance(parameter, list):
|
||||||
|
restrictions = {"@base": "xs:" + parameter[0].base}
|
||||||
|
for p in parameter:
|
||||||
|
x = p.asdict()
|
||||||
|
restrictions[list(x)[1]] = x[list(x)[1]]
|
||||||
|
parameter_dict = {"xs:restriction": [restrictions]}
|
||||||
|
else:
|
||||||
|
raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'")
|
||||||
|
return parameter_dict
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(Facet):
|
||||||
|
def __init__(self, name="IFCWALL", predefinedType=None, instructions=None):
|
||||||
|
self.parameters = ["name", "predefinedType", "@instructions"]
|
||||||
|
self.applicability_templates = [
|
||||||
|
"All {name} data of type {predefinedType}",
|
||||||
|
"All {name} data",
|
||||||
|
]
|
||||||
|
self.requirement_templates = [
|
||||||
|
"Shall be {name} data of type {predefinedType}",
|
||||||
|
"Shall be {name} data",
|
||||||
|
]
|
||||||
|
super().__init__(name, predefinedType, instructions)
|
||||||
|
|
||||||
|
def filter(self, ifc_file, elements):
|
||||||
|
if isinstance(self.name, str):
|
||||||
|
results = ifc_file.by_type(self.name, include_subtypes=False)
|
||||||
|
else:
|
||||||
|
results = []
|
||||||
|
ifc_classes = [t for t in ifc_file.wrapped_data.types() if t.upper() == self.name]
|
||||||
|
[results.append(ifc_file.by_type(ifc_class, include_subtypes=False)) for ifc_class in ifc_classes]
|
||||||
|
if self.predefinedType:
|
||||||
|
return [r for r in results if self(r)]
|
||||||
|
return results
|
||||||
|
|
||||||
|
def __call__(self, inst, logger=None):
|
||||||
|
is_pass = inst.is_a().upper() == self.name
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "NAME", "actual": inst.is_a().upper()}
|
||||||
|
|
||||||
|
if is_pass and self.predefinedType:
|
||||||
|
predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
|
||||||
|
is_pass = predefined_type == self.predefinedType
|
||||||
|
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type}
|
||||||
|
|
||||||
|
return EntityResult(is_pass, reason)
|
||||||
|
|
||||||
|
|
||||||
|
class Attribute(Facet):
|
||||||
|
def __init__(self, name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None):
|
||||||
|
self.parameters = ["name", "value", "@minOccurs", "@maxOccurs", "@instructions"]
|
||||||
|
self.applicability_templates = [
|
||||||
|
"Data where the {name} is {value}",
|
||||||
|
"Data where the {name} is provided",
|
||||||
|
]
|
||||||
|
self.requirement_templates = [
|
||||||
|
"The {name} shall be {value}",
|
||||||
|
"The {name} shall be provided",
|
||||||
|
]
|
||||||
|
super().__init__(name, value, minOccurs, maxOccurs, instructions)
|
||||||
|
|
||||||
|
def __call__(self, inst, logger=None):
|
||||||
|
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]
|
||||||
|
|
||||||
|
element_type = ifcopenshell.util.element.get_type(inst)
|
||||||
|
|
||||||
|
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)
|
||||||
|
names = [self.name]
|
||||||
|
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()
|
||||||
|
names = []
|
||||||
|
values = []
|
||||||
|
for k, v in info.items():
|
||||||
|
if k == self.name:
|
||||||
|
names.append(k)
|
||||||
|
values.append(v)
|
||||||
|
|
||||||
|
is_pass = bool(values)
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "NOVALUE"}
|
||||||
|
|
||||||
|
if is_pass:
|
||||||
|
for i, value in enumerate(values):
|
||||||
|
if value is None:
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "FALSEY", "actual": value}
|
||||||
|
elif value == "":
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "FALSEY", "actual": value}
|
||||||
|
elif value == tuple():
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "FALSEY", "actual": value}
|
||||||
|
else:
|
||||||
|
argument_index = inst.wrapped_data.get_argument_index(names[i])
|
||||||
|
try:
|
||||||
|
attribute_type = inst.attribute_type(argument_index)
|
||||||
|
if attribute_type == "LOGICAL" and value == "UNKNOWN":
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "FALSEY", "actual": value}
|
||||||
|
except:
|
||||||
|
if names[i] in inst.wrapped_data.get_inverse_attribute_names():
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "INVALID"}
|
||||||
|
if not is_pass:
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_pass and self.value:
|
||||||
|
for value in values:
|
||||||
|
if isinstance(value, ifcopenshell.entity_instance):
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "VALUE", "actual": value}
|
||||||
|
break
|
||||||
|
elif isinstance(self.value, str) and isinstance(value, str):
|
||||||
|
if value != self.value:
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "VALUE", "actual": value}
|
||||||
|
break
|
||||||
|
elif isinstance(self.value, str):
|
||||||
|
cast_value = cast_to_value(self.value, value)
|
||||||
|
if value != cast_value:
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "VALUE", "actual": value}
|
||||||
|
break
|
||||||
|
elif value != self.value:
|
||||||
|
is_pass = False
|
||||||
|
reason = {"type": "VALUE", "actual": value}
|
||||||
|
break
|
||||||
|
|
||||||
|
return AttributeResult(is_pass, reason)
|
||||||
|
|
||||||
|
|
||||||
|
class Classification(Facet):
|
||||||
|
def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None):
|
||||||
|
self.parameters = ["value", "system", "@uri", "@minOccurs", "@maxOccurs", "@instructions"]
|
||||||
|
self.applicability_templates = [
|
||||||
|
"Data having a {system} reference of {value}",
|
||||||
|
"Data classified using {system}",
|
||||||
|
"Data classified as {value}",
|
||||||
|
]
|
||||||
|
self.requirement_templates = [
|
||||||
|
"Shall have a {system} reference of {value}",
|
||||||
|
"Shall be classified using {system}",
|
||||||
|
"Shall be classified as {value}",
|
||||||
|
]
|
||||||
|
super().__init__(value, system, uri, minOccurs, maxOccurs, instructions)
|
||||||
|
|
||||||
|
def __call__(self, inst, logger=None):
|
||||||
|
leaf_references = ifcopenshell.util.classification.get_references(inst)
|
||||||
|
|
||||||
|
references = leaf_references.copy()
|
||||||
|
for leaf_reference in leaf_references:
|
||||||
|
references.update(ifcopenshell.util.classification.get_inherited_references(leaf_reference))
|
||||||
|
|
||||||
|
is_pass = bool(references)
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "NOVALUE"}
|
||||||
|
|
||||||
|
if is_pass and self.value:
|
||||||
|
values = [getattr(r, "Identification", getattr(r, "ItemReference", None)) for r in references]
|
||||||
|
is_pass = any([self.value == v for v in values])
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "VALUE", "actual": values}
|
||||||
|
|
||||||
|
if is_pass and self.system:
|
||||||
|
systems = [ifcopenshell.util.classification.get_classification(r).Name for r in references]
|
||||||
|
is_pass = any([self.system == s for s in systems])
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "SYSTEM", "actual": systems}
|
||||||
|
|
||||||
|
return ClassificationResult(is_pass, reason)
|
||||||
|
|
||||||
|
|
||||||
|
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 __call__(self, inst, logger=None):
|
||||||
|
if self.entity == "IfcElementAssembly":
|
||||||
|
is_pass = False
|
||||||
|
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
|
||||||
|
for rel in getattr(inst, "HasAssignments", []) or []:
|
||||||
|
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a(self.entity):
|
||||||
|
is_pass = True
|
||||||
|
|
||||||
|
return PartOfResult(is_pass, "TODO")
|
||||||
|
|
||||||
|
|
||||||
|
class Property(Facet):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
propertySet="Property_Set",
|
||||||
|
name="PropertyName",
|
||||||
|
value=None,
|
||||||
|
measure=None,
|
||||||
|
uri=None,
|
||||||
|
minOccurs=None,
|
||||||
|
maxOccurs=None,
|
||||||
|
instructions=None,
|
||||||
|
):
|
||||||
|
self.parameters = [
|
||||||
|
"propertySet",
|
||||||
|
"name",
|
||||||
|
"value",
|
||||||
|
"@measure",
|
||||||
|
"@uri",
|
||||||
|
"@minOccurs",
|
||||||
|
"@maxOccurs",
|
||||||
|
"@instructions",
|
||||||
|
]
|
||||||
|
self.applicability_templates = [
|
||||||
|
"Elements with {name} data of {value} in the dataset {propertySet}",
|
||||||
|
"Elements with {name} data in the dataset {propertySet}",
|
||||||
|
]
|
||||||
|
self.requirement_templates = [
|
||||||
|
"{name} data shall be {value} and in the dataset {propertySet}",
|
||||||
|
"{name} data shall be provided in the dataset {propertySet}",
|
||||||
|
]
|
||||||
|
super().__init__(propertySet, name, value, measure, uri, minOccurs, maxOccurs, instructions)
|
||||||
|
|
||||||
|
def __call__(self, inst, logger=None):
|
||||||
|
all_psets = ifcopenshell.util.element.get_psets(inst)
|
||||||
|
|
||||||
|
if isinstance(self.propertySet, str):
|
||||||
|
pset = all_psets.get(self.propertySet, None)
|
||||||
|
psets = {self.propertySet: pset} if pset else {}
|
||||||
|
else:
|
||||||
|
psets = {k: v for k, v in all_psets.items() if k == self.propertySet}
|
||||||
|
|
||||||
|
is_pass = bool(psets)
|
||||||
|
|
||||||
|
if is_pass:
|
||||||
|
props = {}
|
||||||
|
for pset_name, pset_props in psets.items():
|
||||||
|
props[pset_name] = {}
|
||||||
|
if isinstance(self.name, str):
|
||||||
|
prop = pset_props.get(self.name)
|
||||||
|
if prop:
|
||||||
|
props[pset_name][self.name] = prop
|
||||||
|
else:
|
||||||
|
props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name}
|
||||||
|
|
||||||
|
if not bool(props[pset_name]):
|
||||||
|
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
|
||||||
|
break
|
||||||
|
return PropertyResult(is_pass, "todo")
|
||||||
|
|
||||||
|
|
||||||
|
class Material(Facet):
|
||||||
|
def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None):
|
||||||
|
self.parameters = ["value", "@uri", "@minOccurs", "@maxOccurs", "@instructions"]
|
||||||
|
self.applicability_templates = [
|
||||||
|
"All data with a {value} material",
|
||||||
|
"All data with a material",
|
||||||
|
]
|
||||||
|
self.requirement_templates = [
|
||||||
|
"Shall shall have a material of {value}",
|
||||||
|
"Shall have a material",
|
||||||
|
]
|
||||||
|
super().__init__(value, uri, minOccurs, maxOccurs, instructions)
|
||||||
|
|
||||||
|
def __call__(self, inst, logger=None):
|
||||||
|
material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True)
|
||||||
|
|
||||||
|
is_pass = material is not None
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "NOVALUE"}
|
||||||
|
|
||||||
|
if is_pass and self.value:
|
||||||
|
if material.is_a("IfcMaterial"):
|
||||||
|
values = {material.Name, getattr(material, "Category")}
|
||||||
|
elif material.is_a("IfcMaterialList"):
|
||||||
|
values = set()
|
||||||
|
for mat in material.Materials or []:
|
||||||
|
values.update([mat.Name, getattr(mat, "Category")])
|
||||||
|
elif material.is_a("IfcMaterialLayerSet"):
|
||||||
|
values = {material.LayerSetName}
|
||||||
|
for item in material.MaterialLayers or []:
|
||||||
|
values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")])
|
||||||
|
elif material.is_a("IfcMaterialProfileSet"):
|
||||||
|
values = {material.Name}
|
||||||
|
for item in material.MaterialProfiles or []:
|
||||||
|
values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")])
|
||||||
|
elif material.is_a("IfcMaterialConstituentSet"):
|
||||||
|
values = {material.Name}
|
||||||
|
for item in material.MaterialConstituents or []:
|
||||||
|
values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")])
|
||||||
|
|
||||||
|
is_pass = False
|
||||||
|
for value in values:
|
||||||
|
if value == self.value:
|
||||||
|
is_pass = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_pass:
|
||||||
|
reason = {"type": "VALUE", "actual": values}
|
||||||
|
|
||||||
|
return MaterialResult(is_pass, reason)
|
||||||
|
|
||||||
|
|
||||||
|
def cast_to_value(from_value, to_value):
|
||||||
|
try:
|
||||||
|
target_type = type(to_value).__name__
|
||||||
|
if target_type == "int":
|
||||||
|
# Casting str -> float -> int means that notation like '1e3' is preserved
|
||||||
|
return int(float(from_value))
|
||||||
|
elif target_type == "bool":
|
||||||
|
if from_value == "TRUE":
|
||||||
|
return True
|
||||||
|
elif from_value == "FALSE":
|
||||||
|
return False
|
||||||
|
return builtins.__dict__[target_type](from_value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Restriction:
|
||||||
|
def __init__(self, options="", type="pattern", base="string"):
|
||||||
|
if type in ["enumeration", "pattern", "bounds"]:
|
||||||
|
self.type = type
|
||||||
|
self.base = base
|
||||||
|
self.options = options
|
||||||
|
if (
|
||||||
|
(type == "enumeration" and isinstance(options, list))
|
||||||
|
or (type == "bounds" and isinstance(options, dict))
|
||||||
|
or (type == "pattern" and isinstance(options, str))
|
||||||
|
):
|
||||||
|
self.options = options
|
||||||
|
else:
|
||||||
|
raise Exception("Options were not properly defined.")
|
||||||
|
|
||||||
|
def parse(self, ids_dict):
|
||||||
|
if ids_dict:
|
||||||
|
try:
|
||||||
|
self.base = ids_dict["@base"][3:]
|
||||||
|
except KeyError:
|
||||||
|
self.base = "String"
|
||||||
|
|
||||||
|
for n in ids_dict:
|
||||||
|
if n == "enumeration":
|
||||||
|
self.type = "enumeration"
|
||||||
|
self.options = []
|
||||||
|
for x in ids_dict[n]:
|
||||||
|
self.options.append(x["@value"])
|
||||||
|
elif n[-7:] == "clusive":
|
||||||
|
self.type = "bounds"
|
||||||
|
self.options = {}
|
||||||
|
self.options.append({n: ids_dict[n]["@value"]})
|
||||||
|
elif n[-5:] == "ength":
|
||||||
|
self.type = "length"
|
||||||
|
if n[3:6] == "min":
|
||||||
|
self.options.append(">=")
|
||||||
|
elif n[3:6] == "max":
|
||||||
|
self.options.append("<=")
|
||||||
|
else:
|
||||||
|
self.options.append("==")
|
||||||
|
self.options[-1] += str(ids_dict[n]["@value"])
|
||||||
|
elif n == "pattern":
|
||||||
|
self.type = "pattern"
|
||||||
|
self.options = ids_dict[n]["@value"]
|
||||||
|
# TODO add fractionDigits
|
||||||
|
# TODO add totalDigits
|
||||||
|
# TODO add whiteSpace
|
||||||
|
elif n == "@base":
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print("Error! Restriction not implemented")
|
||||||
|
return self
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
rest_dict = {"@base": "xs:" + self.base}
|
||||||
|
if self.type == "enumeration":
|
||||||
|
for option in self.options:
|
||||||
|
if "xs:enumeration" not in rest_dict:
|
||||||
|
rest_dict["xs:enumeration"] = [{"@value": option}]
|
||||||
|
else:
|
||||||
|
rest_dict["xs:enumeration"].append({"@value": option})
|
||||||
|
elif self.type == "bounds":
|
||||||
|
for option in self.options:
|
||||||
|
rest_dict["xs:" + option] = [{"@value": str(self.options[option]), "@fixed": False}]
|
||||||
|
elif self.type == "pattern":
|
||||||
|
if "xs:pattern" not in rest_dict:
|
||||||
|
rest_dict["xs:pattern"] = [{"@value": self.options}]
|
||||||
|
else:
|
||||||
|
rest_dict["xs:pattern"].append({"@value": self.options})
|
||||||
|
return rest_dict
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
result = False
|
||||||
|
if self and (other or other == 0):
|
||||||
|
if self.type == "enumeration" and self.base == "bool":
|
||||||
|
self.options = [x.lower() for x in self.options]
|
||||||
|
result = str(other).lower() in self.options
|
||||||
|
elif self.type == "enumeration":
|
||||||
|
result = other in [cast_to_value(o, other) for o in self.options]
|
||||||
|
elif self.type == "bounds":
|
||||||
|
result = True
|
||||||
|
for sign in self.options.keys():
|
||||||
|
if sign == "minInclusive" and other < self.options[sign]:
|
||||||
|
result = False
|
||||||
|
elif sign == "maxInclusive" and other > self.options[sign]:
|
||||||
|
result = False
|
||||||
|
elif sign == "minExclusive" and other <= self.options[sign]:
|
||||||
|
result = False
|
||||||
|
elif sign == "maxExclusive" and other >= self.options[sign]:
|
||||||
|
result = False
|
||||||
|
elif self.type == "length":
|
||||||
|
for op in self.options:
|
||||||
|
if eval(str(len(other)) + op): # TODO eval not safe?
|
||||||
|
result = True
|
||||||
|
elif self.type == "pattern":
|
||||||
|
if isinstance(self.options, list):
|
||||||
|
# TODO handle case with multiple pattern options
|
||||||
|
translated_pattern = identities.translate_pattern(self.options[0])
|
||||||
|
else:
|
||||||
|
translated_pattern = identities.translate_pattern(self.options)
|
||||||
|
regex_pattern = re.compile(translated_pattern)
|
||||||
|
if regex_pattern.fullmatch(other) is not None:
|
||||||
|
result = True
|
||||||
|
# TODO add fractionDigits
|
||||||
|
# TODO add totalDigits
|
||||||
|
# TODO add whiteSpace
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.type == "enumeration":
|
||||||
|
msg = "one of '%s'" % "' or '".join(self.options)
|
||||||
|
elif self.type == "bounds":
|
||||||
|
bounds = {
|
||||||
|
"minInclusive": "larger or equal ",
|
||||||
|
"maxInclusive": "smaller or equal ",
|
||||||
|
"minExclusive": "larger than ",
|
||||||
|
"maxExclusive": "smaller than ",
|
||||||
|
}
|
||||||
|
msg = "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options])
|
||||||
|
elif self.type == "length":
|
||||||
|
msg = "with %s letters" % " and ".join(self.options)
|
||||||
|
elif self.type == "pattern":
|
||||||
|
msg = "with pattern '%s'" % self.options
|
||||||
|
# TODO add fractionDigits
|
||||||
|
# TODO add totalDigits
|
||||||
|
# TODO add whiteSpace
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class Result:
|
||||||
|
def __init__(self, is_pass, reason=None):
|
||||||
|
self.is_pass = is_pass
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.is_pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "" if self.is_pass else self.to_string()
|
||||||
|
|
||||||
|
def to_string(self):
|
||||||
|
return str(self.reason) or "The requirements were not met for some inexplicable reason. Good luck!"
|
||||||
|
|
||||||
|
|
||||||
|
class EntityResult(Result):
|
||||||
|
def to_string(self):
|
||||||
|
if self.reason["type"] == "NAME":
|
||||||
|
return f"The entity class \"{self.reason['actual']}\" does not meet the required IFC class"
|
||||||
|
elif self.reason["type"] == "PREDEFINEDTYPE":
|
||||||
|
return f"The predefined type \"{str(self.reason['actual'])}\" does not meet the required type"
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeResult(Result):
|
||||||
|
def to_string(self):
|
||||||
|
if self.reason["type"] == "NOVALUE":
|
||||||
|
return "The required attribute did not exist"
|
||||||
|
elif self.reason["type"] == "FALSEY":
|
||||||
|
return f"The attribute value \"{str(self.reason['actual'])}\" is empty"
|
||||||
|
elif self.reason["type"] == "INVALID":
|
||||||
|
return f"An invalid attribute name was specified in the IDS"
|
||||||
|
elif self.reason["type"] == "VALUE":
|
||||||
|
return f"The attribute value \"{str(self.reason['actual'])}\" does not match the requirement"
|
||||||
|
|
||||||
|
|
||||||
|
class ClassificationResult(Result):
|
||||||
|
def to_string(self):
|
||||||
|
if self.reason["type"] == "NOVALUE":
|
||||||
|
return "The entity has no classification"
|
||||||
|
elif self.reason["type"] == "VALUE":
|
||||||
|
return f"The found references \"{str(self.reason['actual'])}\" do not match the requirements"
|
||||||
|
elif self.reason["type"] == "VALUE":
|
||||||
|
return f"The references \"{str(self.reason['actual'])}\" do not match the requirements"
|
||||||
|
elif self.reason["type"] == "system":
|
||||||
|
return f"The systems \"{str(self.reason['actual'])}\" do not match the requirements"
|
||||||
|
|
||||||
|
|
||||||
|
class PartOfResult(Result):
|
||||||
|
def to_string(self):
|
||||||
|
return "TODO"
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyResult(Result):
|
||||||
|
def to_string(self):
|
||||||
|
return "TODO"
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialResult(Result):
|
||||||
|
def to_string(self):
|
||||||
|
if self.reason["type"] == "NOVALUE":
|
||||||
|
return "The entity has no material"
|
||||||
|
elif self.reason["type"] == "VALUE":
|
||||||
|
return (
|
||||||
|
f"The material names and categories of \"{str(self.reason['actual'])}\" does not match the requirement"
|
||||||
|
)
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
<!-- edited with XMLSpy v2022 (x64) (http://www.altova.com) by Leon van Berlo (Overleaf Investments B.V.) -->
|
||||||
|
<!-- May 24, 2022 - DRAFT -->
|
||||||
|
<xs:schema xmlns:ids="http://standards.buildingsmart.org/IDS" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" targetNamespace="http://standards.buildingsmart.org/IDS" elementFormDefault="qualified" attributeFormDefault="unqualified" version="0.6.1">
|
||||||
|
<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/>
|
||||||
|
<xs:import namespace="http://www.w3.org/2001/XMLSchema" schemaLocation="https://www.w3.org/2001/XMLSchema.xsd"/>
|
||||||
|
<xs:import namespace="http://www.w3.org/2001/XMLSchema-instance" schemaLocation="http://www.w3.org/2001/XMLSchema-instance"/>
|
||||||
|
<xs:element name="ids">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="info">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="title" type="xs:string"/>
|
||||||
|
<xs:element name="copyright" type="xs:string" minOccurs="0"/>
|
||||||
|
<xs:element name="version" type="xs:string" minOccurs="0"/>
|
||||||
|
<xs:element name="description" type="xs:string" minOccurs="0"/>
|
||||||
|
<xs:element name="author" minOccurs="0">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="[^@]+@[^\.]+\..+"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="date" type="xs:date" minOccurs="0"/>
|
||||||
|
<xs:element name="purpose" type="xs:string" minOccurs="0"/>
|
||||||
|
<xs:element name="milestone" type="xs:string" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="specifications">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:specificationsType"/>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:complexType name="entityType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="name" type="ids:idsValue"/>
|
||||||
|
<xs:element name="predefinedType" type="ids:idsValue" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="idsValue">
|
||||||
|
<xs:choice minOccurs="1">
|
||||||
|
<!-- place for potential additional rules for idsValue -->
|
||||||
|
<xs:element name="simpleValue" type="xs:string" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element ref="xs:restriction" minOccurs="1" maxOccurs="unbounded"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="classificationType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
|
||||||
|
<xs:element name="system" type="ids:idsValue" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="applicabilityType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="entity" type="ids:entityType" 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">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:propertyType"/>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="material" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:materialType"/>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="propertyType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="propertySet" type="ids:idsValue"/>
|
||||||
|
<xs:element name="name" type="ids:idsValue"/>
|
||||||
|
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="measure">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>See the documentation and default units of these measures on https://github.com/buildingSMART/IDS/blob/master/Documentation/Physical_Quantities_and_Units.md</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="String"/>
|
||||||
|
<xs:enumeration value="Number"/>
|
||||||
|
<xs:enumeration value="AmountOfSubstance"/>
|
||||||
|
<xs:enumeration value="AreaDensity"/>
|
||||||
|
<xs:enumeration value="Area"/>
|
||||||
|
<xs:enumeration value="DynamicViscosity"/>
|
||||||
|
<xs:enumeration value="ElectricCapacitance"/>
|
||||||
|
<xs:enumeration value="ElectricCharge"/>
|
||||||
|
<xs:enumeration value="ElectricConductance"/>
|
||||||
|
<xs:enumeration value="ElectricCurrent"/>
|
||||||
|
<xs:enumeration value="ElectricResistance"/>
|
||||||
|
<xs:enumeration value="ElectricVoltage"/>
|
||||||
|
<xs:enumeration value="Energy"/>
|
||||||
|
<xs:enumeration value="Force"/>
|
||||||
|
<xs:enumeration value="Frequency"/>
|
||||||
|
<xs:enumeration value="HeatFluxDensity"/>
|
||||||
|
<xs:enumeration value="Heating"/>
|
||||||
|
<xs:enumeration value="Illuminance"/>
|
||||||
|
<xs:enumeration value="IonConcentration"/>
|
||||||
|
<xs:enumeration value="IsoThermalMoistureCapacity"/>
|
||||||
|
<xs:enumeration value="Length"/>
|
||||||
|
<xs:enumeration value="Speed"/>
|
||||||
|
<xs:enumeration value="LuminousFlux"/>
|
||||||
|
<xs:enumeration value="LuminousIntensity"/>
|
||||||
|
<xs:enumeration value="MassDensity"/>
|
||||||
|
<xs:enumeration value="MassFlowRate"/>
|
||||||
|
<xs:enumeration value="Mass"/>
|
||||||
|
<xs:enumeration value="MassPerLength"/>
|
||||||
|
<xs:enumeration value="ModulusOfElasticity"/>
|
||||||
|
<xs:enumeration value="MoistureDiffusivity"/>
|
||||||
|
<xs:enumeration value="MolecularWeight"/>
|
||||||
|
<xs:enumeration value="MomentOfInertia"/>
|
||||||
|
<xs:enumeration value="PH"/>
|
||||||
|
<xs:enumeration value="PlanarForce"/>
|
||||||
|
<xs:enumeration value="Angle"/>
|
||||||
|
<xs:enumeration value="PlaneAngle"/>
|
||||||
|
<xs:enumeration value="Power"/>
|
||||||
|
<xs:enumeration value="Pressure"/>
|
||||||
|
<xs:enumeration value="RadioActivity"/>
|
||||||
|
<xs:enumeration value="Ratio"/>
|
||||||
|
<xs:enumeration value="RotationalFrequency"/>
|
||||||
|
<xs:enumeration value="SectionModulus"/>
|
||||||
|
<xs:enumeration value="SoundPower"/>
|
||||||
|
<xs:enumeration value="SoundPressure"/>
|
||||||
|
<xs:enumeration value="SpecificHeatCapacity"/>
|
||||||
|
<xs:enumeration value="TemperatureRateOfChange"/>
|
||||||
|
<xs:enumeration value="ThermalConductivity"/>
|
||||||
|
<xs:enumeration value="Temperature"/>
|
||||||
|
<xs:enumeration value="Time"/>
|
||||||
|
<xs:enumeration value="Torque"/>
|
||||||
|
<xs:enumeration value="VaporPermeability"/>
|
||||||
|
<xs:enumeration value="Volume"/>
|
||||||
|
<xs:enumeration value="VolumetricFlowRate"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="attributeType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="name" type="ids:idsValue"/>
|
||||||
|
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="materialType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="value" type="ids:idsValue" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="requirementsType">
|
||||||
|
<xs:sequence maxOccurs="unbounded">
|
||||||
|
<xs:element name="entity" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Make sure 'Name' value of requirements entity is the same as the 'applicability' node, or a wildcard (inclusive pattern).</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:entityType"/>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="partOf" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<!-- Proposed definition
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="entity" type="ids:idsValue"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="relationship" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="IfcRelAggregates"/>
|
||||||
|
<xs:enumeration value="IfcRelAssignsToGroup"/>
|
||||||
|
<xs:enumeration value="IfcRelContainedInSpatialStructure"/>
|
||||||
|
<xs:enumeration value="IfcRelNests"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
-->
|
||||||
|
<!-- Current definition -->
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="entity" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="IfcElementAssembly"/>
|
||||||
|
<xs:enumeration value="IfcGroup"/>
|
||||||
|
<xs:enumeration value="IfcSystem"/>
|
||||||
|
<xs:enumeration value="IfcBuildingSystem"/>
|
||||||
|
<xs:enumeration value="IfcBuiltSystem"/>
|
||||||
|
<xs:enumeration value="IfcDistributionSystem"/>
|
||||||
|
<xs:enumeration value="IfcZone"/>
|
||||||
|
<xs:enumeration value="IfcAsset"/>
|
||||||
|
<xs:enumeration value="IfcInventory"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="classification" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:classificationType">
|
||||||
|
<xs:attribute name="uri" type="xs:anyURI" use="optional"/>
|
||||||
|
<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="attribute" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:attributeType">
|
||||||
|
<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="property" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:propertyType">
|
||||||
|
<xs:attribute name="uri" type="xs:anyURI" use="optional"/>
|
||||||
|
<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="material" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="ids:materialType">
|
||||||
|
<xs:attribute name="uri" type="xs:anyURI" use="optional"/>
|
||||||
|
<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:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="specificationType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="applicability" type="ids:applicabilityType"/>
|
||||||
|
<xs:element name="requirements" type="ids:requirementsType"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||||
|
<xs:attributeGroup ref="xs:occurs"/>
|
||||||
|
<xs:attribute name="ifcVersion" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:list>
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="IFC2X3"/>
|
||||||
|
<xs:enumeration value="IFC4"/>
|
||||||
|
<xs:enumeration value="IFC4X3"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:list>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="identifier" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Author of the IDS can provide an identifier to the IDS. Beware: this cannot be enforced/assumed as (global) unique.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||||
|
<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:complexType>
|
||||||
|
<xs:complexType name="specificationsType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="specification" type="ids:specificationType" minOccurs="1" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:schema>
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# IfcTester - IDS based model auditing
|
||||||
|
# Copyright (C) 2021 Artur Tomczak <artomczak@gmail.com>, Thomas Krijnen <mail@thomaskrijnen.com>, Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcTester.
|
||||||
|
#
|
||||||
|
# IfcTester is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcTester is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import numpy as np
|
||||||
|
import ifcopenshell.util.placement
|
||||||
|
from bcf.v2.bcfxml import BcfXml
|
||||||
|
from bcf.v2 import data as bcf
|
||||||
|
|
||||||
|
|
||||||
|
class Console:
|
||||||
|
def __init__(self, ids, use_colour=True):
|
||||||
|
self.ids = ids
|
||||||
|
self.use_colour = use_colour
|
||||||
|
self.colours = {
|
||||||
|
"red": "\033[1;31m",
|
||||||
|
"blue": "\033[1;34m",
|
||||||
|
"cyan": "\033[1;36m",
|
||||||
|
"green": "\033[0;32m",
|
||||||
|
"yellow": "\033[0;33m",
|
||||||
|
"purple": "\033[0;95m",
|
||||||
|
"grey": "\033[0;90m",
|
||||||
|
"reset": "\033[0;0m",
|
||||||
|
"bold": "\033[;1m",
|
||||||
|
"reverse": "\033[;7m",
|
||||||
|
}
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
self.set_style("bold", "blue")
|
||||||
|
print(self.ids.info.get("title", "Untitled IDS"))
|
||||||
|
for specification in self.ids.specifications:
|
||||||
|
self.report_specification(specification)
|
||||||
|
self.set_style("reset")
|
||||||
|
|
||||||
|
def report_specification(self, specification):
|
||||||
|
if specification.status is True:
|
||||||
|
self.set_style("bold", "green")
|
||||||
|
print("[PASS] ", end="")
|
||||||
|
elif specification.status is False:
|
||||||
|
self.set_style("bold", "red")
|
||||||
|
print("[FAIL] ", end="")
|
||||||
|
elif specification.status is None:
|
||||||
|
self.set_style("bold", "yellow")
|
||||||
|
print("[UNTESTED] ", end="")
|
||||||
|
|
||||||
|
self.set_style("bold")
|
||||||
|
total = len(specification.applicable_entities)
|
||||||
|
total_successes = total - len(specification.failed_entities)
|
||||||
|
print(f"({total_successes}/{total}) ", end="")
|
||||||
|
|
||||||
|
if specification.minOccurs != 0:
|
||||||
|
print(f"*", end="")
|
||||||
|
|
||||||
|
print(specification.name)
|
||||||
|
|
||||||
|
self.set_style("cyan")
|
||||||
|
print(" " * 4 + "Applies to:")
|
||||||
|
self.set_style("reset")
|
||||||
|
|
||||||
|
for applicability in specification.applicability:
|
||||||
|
print(" " * 8 + applicability.to_string("applicability"))
|
||||||
|
|
||||||
|
|
||||||
|
if not total and specification.status is False:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.set_style("cyan")
|
||||||
|
print(" " * 4 + "Requirements:")
|
||||||
|
self.set_style("reset")
|
||||||
|
|
||||||
|
for requirement in specification.requirements:
|
||||||
|
self.set_style("reset")
|
||||||
|
self.set_style("red") if requirement.failed_entities else self.set_style("green")
|
||||||
|
print(" " * 8 + requirement.to_string("requirement"))
|
||||||
|
self.set_style("reset")
|
||||||
|
for i, element in enumerate(requirement.failed_entities[0:10]):
|
||||||
|
print(" " * 12, end="")
|
||||||
|
self.report_reason(requirement.failed_reasons[i], element)
|
||||||
|
if len(requirement.failed_entities) > 10:
|
||||||
|
print(" " * 12 + f"... {len(requirement.failed_entities)} in total ...")
|
||||||
|
self.set_style("reset")
|
||||||
|
|
||||||
|
def report_reason(self, reason, element):
|
||||||
|
is_bold = False
|
||||||
|
for substring in reason.split("\""):
|
||||||
|
if is_bold:
|
||||||
|
self.set_style("purple")
|
||||||
|
else:
|
||||||
|
self.set_style("reset")
|
||||||
|
print(substring, end="")
|
||||||
|
is_bold = not is_bold
|
||||||
|
self.set_style("grey")
|
||||||
|
print(" - " + str(element))
|
||||||
|
self.set_style("reset")
|
||||||
|
|
||||||
|
def set_style(self, *colours):
|
||||||
|
if self.use_colour:
|
||||||
|
sys.stdout.write("".join([self.colours[c] for c in colours]))
|
||||||
|
|
||||||
|
|
||||||
|
class JsonReporter:
|
||||||
|
def __init__(self, specifications):
|
||||||
|
self.specifications = specifications
|
||||||
|
self.results = []
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
for specification in self.specifications:
|
||||||
|
self.results.append(self.report_specification(specification))
|
||||||
|
return self.results
|
||||||
|
|
||||||
|
def report_specification(self, specification):
|
||||||
|
return {"status": specification.status}
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleHandler(logging.StreamHandler):
|
||||||
|
"""Logging handler listing all cases in python list."""
|
||||||
|
|
||||||
|
def __init__(self, report_valid=False):
|
||||||
|
"""Logging handler listing all cases in python list.
|
||||||
|
|
||||||
|
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
|
||||||
|
:type report_valid: bool, optional
|
||||||
|
"""
|
||||||
|
logging.StreamHandler.__init__(self)
|
||||||
|
self.statements = []
|
||||||
|
if report_valid:
|
||||||
|
self.setLevel(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
self.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
def emit(self, mymsg):
|
||||||
|
"""Triggered on each use of logging with the Simple handler enabled.
|
||||||
|
|
||||||
|
:param log_content: default logger message
|
||||||
|
:type log_content: string|dict
|
||||||
|
"""
|
||||||
|
self.statements.append(mymsg.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class CsvHandler(logging.StreamHandler):
|
||||||
|
"""Logging handler listing all cases in csv file."""
|
||||||
|
|
||||||
|
def __init__(self, filepath="./Report.csv", report_valid=False):
|
||||||
|
"""Logging handler listing all cases in csv file.
|
||||||
|
|
||||||
|
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
|
||||||
|
:type report_valid: bool, optional
|
||||||
|
"""
|
||||||
|
import csv
|
||||||
|
|
||||||
|
logging.StreamHandler.__init__(self)
|
||||||
|
if report_valid:
|
||||||
|
self.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
self.setLevel(logging.ERROR)
|
||||||
|
self.file = open(filepath, "w", encoding="UTF8", newline="")
|
||||||
|
self.csvwriter = csv.writer(self.file)
|
||||||
|
self.csvwriter.writerow(["guid", "result", "sentence"]) # header
|
||||||
|
|
||||||
|
def emit(self, mymsg):
|
||||||
|
"""Triggered on each use of logging with the Simple handler enabled.
|
||||||
|
|
||||||
|
:param log_content: default logger message
|
||||||
|
:type log_content: string|dict
|
||||||
|
"""
|
||||||
|
# BUG bytes-like object is required, not 'str'
|
||||||
|
self.csvwriter.writerow(mymsg.msg)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.file.close()
|
||||||
|
|
||||||
|
|
||||||
|
class BcfHandler(logging.StreamHandler):
|
||||||
|
"""Logging handler for creation of BCF report files.
|
||||||
|
|
||||||
|
:param project_name: defaults to "IDS Project"
|
||||||
|
:type project_name: str, optional
|
||||||
|
:param author: Email of the person creating the BCF report, defaults to "your@email.com"
|
||||||
|
:type author: str, optional
|
||||||
|
:param filepath: Path to save the BCF report, defaults to None
|
||||||
|
:type filepath: str, optional
|
||||||
|
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
|
||||||
|
:type report_valid: bool, optional
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
bcf_handler = BcfHandler(
|
||||||
|
project_name="Default IDS Project",
|
||||||
|
author="your@email.com",
|
||||||
|
filepath="example.bcf",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("IDS_Logger")
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
logger.addHandler(bcf_handler)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, project_name="IDS Project", author="your@email.com", filepath=None, report_valid=False):
|
||||||
|
|
||||||
|
logging.StreamHandler.__init__(self)
|
||||||
|
if report_valid:
|
||||||
|
self.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
self.setLevel(logging.ERROR)
|
||||||
|
self.bcf = BcfXml()
|
||||||
|
self.bcf.author = author
|
||||||
|
self.bcf.new_project()
|
||||||
|
self.bcf.project.name = project_name
|
||||||
|
self.filepath = filepath
|
||||||
|
self.bcf.edit_project()
|
||||||
|
|
||||||
|
def emit(self, log_content):
|
||||||
|
"""Triggered on each use of logging with the BCF handler enabled.
|
||||||
|
|
||||||
|
:param log_content: default logger message
|
||||||
|
:type log_content: string|dict
|
||||||
|
"""
|
||||||
|
topic = bcf.Topic()
|
||||||
|
topic.title = log_content.msg["sentence"].split(".\n")[1]
|
||||||
|
topic.description = log_content.msg["sentence"].split(".\n")[0]
|
||||||
|
self.bcf.add_topic(topic)
|
||||||
|
# try: # Add viewpoint and link to ifc object
|
||||||
|
viewpoint = bcf.Viewpoint()
|
||||||
|
viewpoint.perspective_camera = bcf.PerspectiveCamera()
|
||||||
|
ifc_elem = log_content.msg["ifc_element"]
|
||||||
|
# ifc_elem = ifc_file.by_guid(log_content.msg["guid"])
|
||||||
|
target_position = np.array(ifcopenshell.util.placement.get_local_placement(ifc_elem.ObjectPlacement))
|
||||||
|
target_position = target_position[:, 3][0:3]
|
||||||
|
camera_position = target_position + np.array((5, 5, 5))
|
||||||
|
viewpoint.perspective_camera.camera_view_point.x = camera_position[0]
|
||||||
|
viewpoint.perspective_camera.camera_view_point.y = camera_position[1]
|
||||||
|
viewpoint.perspective_camera.camera_view_point.z = camera_position[2]
|
||||||
|
camera_direction = camera_position - target_position
|
||||||
|
camera_direction = camera_direction / np.linalg.norm(camera_direction)
|
||||||
|
camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction)
|
||||||
|
camera_right = camera_right / np.linalg.norm(camera_right)
|
||||||
|
camera_up = np.cross(camera_direction, camera_right)
|
||||||
|
camera_up = camera_up / np.linalg.norm(camera_up)
|
||||||
|
rotation_transform = np.zeros((4, 4))
|
||||||
|
rotation_transform[0, :3] = camera_right
|
||||||
|
rotation_transform[1, :3] = camera_up
|
||||||
|
rotation_transform[2, :3] = camera_direction
|
||||||
|
rotation_transform[-1, -1] = 1
|
||||||
|
translation_transform = np.eye(4)
|
||||||
|
translation_transform[:3, -1] = -camera_position
|
||||||
|
look_at_transform = np.matmul(rotation_transform, translation_transform)
|
||||||
|
mat = np.linalg.inv(look_at_transform)
|
||||||
|
viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1
|
||||||
|
viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1
|
||||||
|
viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1
|
||||||
|
viewpoint.perspective_camera.camera_up_vector.x = mat[0][1]
|
||||||
|
viewpoint.perspective_camera.camera_up_vector.y = mat[1][1]
|
||||||
|
viewpoint.perspective_camera.camera_up_vector.z = mat[2][1]
|
||||||
|
viewpoint.components = bcf.Components()
|
||||||
|
c = bcf.Component()
|
||||||
|
c.ifc_guid = log_content.msg["guid"]
|
||||||
|
viewpoint.components.selection.append(c)
|
||||||
|
viewpoint.components.visibility = bcf.ComponentVisibility()
|
||||||
|
viewpoint.components.visibility.default_visibility = True
|
||||||
|
viewpoint.snapshot = None
|
||||||
|
self.bcf.add_viewpoint(topic, viewpoint)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""Saves the BCF report to file. Triggered at the end of the validation process."""
|
||||||
|
if not self.filepath:
|
||||||
|
self.filepath = os.getcwd() + r"\IDS_report.bcf"
|
||||||
|
if not (self.filepath.endswith(".bcf") or self.filepath.endswith(".bcfzip")):
|
||||||
|
self.filepath = self.filepath + r"\IDS_report.bcf"
|
||||||
|
self.bcf.save_project(self.filepath)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# IfcOpenShell - IFC toolkit and geometry engine
|
||||||
|
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcOpenShell.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
import functools
|
||||||
|
import itertools
|
||||||
|
import ifcopenshell
|
||||||
|
import test_ids
|
||||||
|
from xml.dom.minidom import parseString
|
||||||
|
from ifcopenshell import ids, template, validate
|
||||||
|
|
||||||
|
outdir = "build"
|
||||||
|
|
||||||
|
|
||||||
|
class DocGenerator:
|
||||||
|
def __init__(self):
|
||||||
|
self.facet = None
|
||||||
|
self.testcases = {}
|
||||||
|
|
||||||
|
def __call__(self, name, *, facet, inst, expected):
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
|
||||||
|
result = "pass" if expected is True else "fail"
|
||||||
|
|
||||||
|
f = inst.wrapped_data.file
|
||||||
|
|
||||||
|
# Validate the file created and loop over the issues, fixing them
|
||||||
|
# one by one.
|
||||||
|
l = validate.json_logger()
|
||||||
|
validate.validate(f, l)
|
||||||
|
for issue in l.statements:
|
||||||
|
if "GlobalId" in issue["message"]:
|
||||||
|
issue["instance"].GlobalId = ifcopenshell.guid.new()
|
||||||
|
elif "PredefinedType" in issue["message"]:
|
||||||
|
ty = re.findall("\\(.+?\\)", issue["message"])[0][1:-1].split(", ")[0]
|
||||||
|
issue["instance"].PredefinedType = ty
|
||||||
|
elif "IfcMaterialList" in issue["message"]:
|
||||||
|
issue["instance"].Materials = [f.createIfcMaterial("Concrete", None, "CONCRETE")]
|
||||||
|
else:
|
||||||
|
raise Exception("About to emit invalid example data:", issue)
|
||||||
|
|
||||||
|
# ifc_text = "\n".join([f"{e} /* Testcase */" if e == inst else str(e) for e in f])
|
||||||
|
lines = f.wrapped_data.to_string().split("\n")[7:-3]
|
||||||
|
ifc_text = "\n".join([f"{l} /* Testcase */" if f"#{inst.id()}=" in l else l for l in lines])
|
||||||
|
basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower())
|
||||||
|
|
||||||
|
# Write IFC to disk
|
||||||
|
f.write(os.path.join(outdir, f"{basename}.ifc"))
|
||||||
|
|
||||||
|
# Create an IDS with the applicability selecting exactly
|
||||||
|
# the entity type passed to us in `inst`.
|
||||||
|
specs = ids.ids(title=name)
|
||||||
|
spec = ids.specification(name=name)
|
||||||
|
spec.add_applicability(ids.entity.create(name=inst.is_a()))
|
||||||
|
spec.add_requirement(facet)
|
||||||
|
specs.specifications.append(spec)
|
||||||
|
|
||||||
|
# Write IDS to disk
|
||||||
|
with open(os.path.join(outdir, f"{basename}.ids"), "w", encoding="utf-8") as ids_file:
|
||||||
|
ids_file.write(specs.to_string())
|
||||||
|
|
||||||
|
xml_text = "\n".join(
|
||||||
|
l
|
||||||
|
for l in parseString(specs.to_string())
|
||||||
|
.getElementsByTagName("requirements")[0]
|
||||||
|
.childNodes[1]
|
||||||
|
.toprettyxml()
|
||||||
|
.split("\n")
|
||||||
|
if l.strip()
|
||||||
|
).replace("\t", " ")
|
||||||
|
|
||||||
|
self.testcases.setdefault(self.facet, []).append(
|
||||||
|
{"name": name, "ids": xml_text, "ifc": ifc_text, "basename": basename, "result": result, "id": inst.id()}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert bool(facet(inst)) is expected
|
||||||
|
|
||||||
|
def set_facet(self, facet):
|
||||||
|
self.facet = facet
|
||||||
|
|
||||||
|
|
||||||
|
test_ids.run = DocGenerator()
|
||||||
|
test_ids.set_facet = test_ids.run.set_facet
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().discover(".", pattern="test_ids.py")
|
||||||
|
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
||||||
|
|
||||||
|
for facet, testcases in test_ids.run.testcases.items():
|
||||||
|
with open(os.path.join(outdir, f"testcases-{facet}.md"), "w") as f:
|
||||||
|
write = functools.partial(print, file=f)
|
||||||
|
write(f"# {facet.capitalize()} testcases")
|
||||||
|
write()
|
||||||
|
write(
|
||||||
|
"These testcases are designed to help describe behaviour in edge cases and ambiguities. All valid IDS implementations must demonstrate identical behaviour to these test cases."
|
||||||
|
)
|
||||||
|
write()
|
||||||
|
for testcase in testcases:
|
||||||
|
write(f"## [{testcase['result'].upper()}] {testcase['name']}")
|
||||||
|
write()
|
||||||
|
write("~~~xml")
|
||||||
|
write(testcase["ids"])
|
||||||
|
write("~~~")
|
||||||
|
write()
|
||||||
|
write("~~~lua")
|
||||||
|
write(testcase["ifc"])
|
||||||
|
write("~~~")
|
||||||
|
write()
|
||||||
|
write(
|
||||||
|
f"[Sample IDS]({testcase['basename']}.ids) - [Sample IFC: {testcase['id']}]({testcase['basename']}.ifc)"
|
||||||
|
)
|
||||||
|
write()
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user