mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
More aggressive refactoring for IfcTester to break up the monolithic codebase
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,19 @@
|
||||
# IfcTester - IDS based model auditing
|
||||
# Copyright (C) 2022 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/>.
|
||||
|
||||
from .ids import open
|
||||
|
||||
@@ -0,0 +1,653 @@
|
||||
# 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 re
|
||||
import builtins
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.classification
|
||||
from xmlschema.validators import identities
|
||||
|
||||
|
||||
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 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 filter(self, ifc_file, elements):
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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"
|
||||
)
|
||||
+55
-682
@@ -17,36 +17,34 @@
|
||||
# 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
|
||||
from .facet import Entity, Attribute, Classification, Property, PartOf, Material, Restriction
|
||||
|
||||
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
schema = XMLSchema(os.path.join(cwd, "ids.xsd"))
|
||||
schema = None
|
||||
|
||||
|
||||
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)
|
||||
def open(filepath, validate=False):
|
||||
if validate:
|
||||
get_schema().validate(filepath)
|
||||
return Ids().parse(
|
||||
schema.decode(filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"})
|
||||
get_schema().decode(
|
||||
filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_schema():
|
||||
global schema
|
||||
if schema is None:
|
||||
schema = XMLSchema(os.path.join(cwd, "ids.xsd"))
|
||||
return schema
|
||||
|
||||
|
||||
class Ids:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -109,57 +107,17 @@ class Ids:
|
||||
|
||||
def to_string(self):
|
||||
ns = {"": "http://standards.buildingsmart.org/IDS"}
|
||||
return etree_tostring(schema.encode(self.asdict()), namespaces=ns)
|
||||
return etree_tostring(get_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)
|
||||
ET.ElementTree(get_schema().encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True)
|
||||
return get_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
|
||||
specification.reset_status()
|
||||
specification.validate(ifc_file)
|
||||
|
||||
|
||||
class Specification:
|
||||
@@ -231,628 +189,43 @@ class Specification:
|
||||
results.append(facet)
|
||||
return results
|
||||
|
||||
def reset_status(self):
|
||||
self.applicable_entities.clear()
|
||||
self.failed_entities = set()
|
||||
for facet in self.requirements:
|
||||
facet.failed_entities.clear()
|
||||
self.status = None
|
||||
|
||||
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 validate(self, ifc_file):
|
||||
if ifc_file.schema not in self.ifcVersion:
|
||||
return
|
||||
|
||||
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
|
||||
elements = []
|
||||
for facet in self.applicability:
|
||||
elements = facet.filter(ifc_file, elements)
|
||||
|
||||
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:
|
||||
for element in elements:
|
||||
is_applicable = True
|
||||
for facet in self.applicability:
|
||||
if isinstance(facet, Entity):
|
||||
continue
|
||||
if not bool(facet(element)):
|
||||
is_applicable = False
|
||||
break
|
||||
if not is_applicable:
|
||||
continue
|
||||
self.applicable_entities.append(element)
|
||||
for facet in self.requirements:
|
||||
result = facet(element)
|
||||
if not bool(result):
|
||||
self.failed_entities.add(element)
|
||||
facet.failed_entities.append(element)
|
||||
facet.failed_reasons.append(str(result))
|
||||
|
||||
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"
|
||||
)
|
||||
self.status = True
|
||||
if self.failed_entities:
|
||||
self.status = False
|
||||
elif self.minOccurs != 0 and not self.applicable_entities:
|
||||
self.status = False
|
||||
elif len(self.applicable_entities) > (self.maxOccurs or 1):
|
||||
self.status = False
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
|
||||
# IfcTester - IDS based model auditing
|
||||
# Copyright (C) 2021-2022 Thomas Krijnen <thomas@aecgeeks.com>, Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
# This file is part of IfcTester.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# 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.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
import pytest
|
||||
import functools
|
||||
import itertools
|
||||
import ifcopenshell
|
||||
import test_ids
|
||||
import test_facet
|
||||
from xml.dom.minidom import parseString
|
||||
from ifcopenshell import ids, template, validate
|
||||
from ifctester import ids
|
||||
from ifcopenshell import validate
|
||||
|
||||
outdir = "build"
|
||||
|
||||
@@ -42,8 +42,7 @@ class DocGenerator:
|
||||
|
||||
f = inst.wrapped_data.file
|
||||
|
||||
# Validate the file created and loop over the issues, fixing them
|
||||
# one by one.
|
||||
# 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:
|
||||
@@ -63,18 +62,18 @@ class DocGenerator:
|
||||
basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower())
|
||||
|
||||
# Write IFC to disk
|
||||
f.write(os.path.join(outdir, f"{basename}.ifc"))
|
||||
f.write(os.path.join(outdir, "files", 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 = ids.Ids(title=name)
|
||||
spec = ids.Specification(name=name)
|
||||
spec.applicability.append(ids.Entity(name=inst.is_a()))
|
||||
spec.requirements.append(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:
|
||||
with open(os.path.join(outdir, "files", f"{basename}.ids"), "w", encoding="utf-8") as ids_file:
|
||||
ids_file.write(specs.to_string())
|
||||
|
||||
xml_text = "\n".join(
|
||||
@@ -97,13 +96,12 @@ class DocGenerator:
|
||||
self.facet = facet
|
||||
|
||||
|
||||
test_ids.run = DocGenerator()
|
||||
test_ids.set_facet = test_ids.run.set_facet
|
||||
test_facet.run = DocGenerator()
|
||||
test_facet.set_facet = test_facet.run.set_facet
|
||||
|
||||
suite = unittest.TestLoader().discover(".", pattern="test_ids.py")
|
||||
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
pytest.main(["-p", "no:pytest-blender"])
|
||||
|
||||
for facet, testcases in test_ids.run.testcases.items():
|
||||
for facet, testcases in test_facet.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")
|
||||
@@ -124,6 +122,6 @@ for facet, testcases in test_ids.run.testcases.items():
|
||||
write("~~~")
|
||||
write()
|
||||
write(
|
||||
f"[Sample IDS]({testcase['basename']}.ids) - [Sample IFC: {testcase['id']}]({testcase['basename']}.ifc)"
|
||||
f"[Sample IDS](files/{testcase['basename']}.ids) - [Sample IFC: {testcase['id']}](files/{testcase['basename']}.ifc)"
|
||||
)
|
||||
write()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+45
-1262
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user