Skip to content

Commit

Permalink
Pse001 more checks (#102)
Browse files Browse the repository at this point in the history
* Initial proposal of PSE001 rule

* Fix of IFC4x3 + test file fix

* PR comments

* Fix

* PSE001 - additional checks

* Type checking

* change type hints

* change type hints other error class

* PR comments

* Update features/steps/givens/attributes.py

Co-authored-by: Thomas Krijnen <t.krijnen@gmail.com>

* MR conflicts

---------

Co-authored-by: Geert Hesselink <geert.hess@gmail.com>
Co-authored-by: Geert Hesselink <geerthesselink@Geerts-MacBook-Air.local>
Co-authored-by: Thomas Krijnen <t.krijnen@gmail.com>
  • Loading branch information
4 people authored Jan 9, 2024
1 parent a3dadc8 commit dbab66c
Show file tree
Hide file tree
Showing 33 changed files with 2,235 additions and 0 deletions.
39 changes: 39 additions & 0 deletions features/PSE001_Ifcpropertyset-validation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@implementer-agreement
@PSE
Feature: PSE001 - IfcPropertySet validation
The rule verifies that each IfcPropertySet starting with Pset_ is defined correctly.

Scenario: Agreement on each IfcPropertySet correctly defining an applicable entity.

Given A file with Schema "IFC2X3"
And An IfcPropertySet
And Its attribute Name starts with Pset_
Then The IfcPropertySet Name attribute value must use predefined values according to the IFC2x3_definitions.csv table
And The IfcPropertySet must be assigned according to the property set definitions table IFC2x3_definitions.csv
And Each associated IfcProperty must be named according to the property set definitions table IFC2x3_definitions.csv
And Each associated IfcProperty must be of type according to the property set definitions table IFC2x3_definitions.csv
And Each associated IfcProperty value must be of data type according to the property set definitions table IFC2x3_definitions.csv


Scenario: Agreement on each IfcPropertySet correctly defining an applicable entity.

Given A file with Schema "IFC4"
And An IfcPropertySet
And Its attribute Name starts with Pset_
Then The IfcPropertySet Name attribute value must use predefined values according to the IFC4_definitions.csv table
And The IfcPropertySet must be assigned according to the property set definitions table IFC4_definitions.csv
And Each associated IfcProperty must be named according to the property set definitions table IFC4_definitions.csv
And Each associated IfcProperty must be of type according to the property set definitions table IFC4_definitions.csv
And Each associated IfcProperty value must be of data type according to the property set definitions table IFC4_definitions.csv


Scenario: Agreement on each IfcPropertySet correctly defining an applicable entity.

Given A file with Schema "IFC4X3"
And An IfcPropertySet
And Its attribute Name starts with Pset_
Then The IfcPropertySet Name attribute value must use predefined values according to the IFC4X3_definitions.csv table
And The IfcPropertySet must be assigned according to the property set definitions table IFC4X3_definitions.csv
And Each associated IfcProperty must be named according to the property set definitions table IFC4X3_definitions.csv
And Each associated IfcProperty must be of type according to the property set definitions table IFC4X3_definitions.csv
And Each associated IfcProperty value must be of data type according to the property set definitions table IFC4X3_definitions.csv
318 changes: 318 additions & 0 deletions features/resources/property_set_definitions/IFC2x3_definitions.csv

Large diffs are not rendered by default.

646 changes: 646 additions & 0 deletions features/resources/property_set_definitions/IFC4X3_definitions.csv

Large diffs are not rendered by default.

421 changes: 421 additions & 0 deletions features/resources/property_set_definitions/IFC4_definitions.csv

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions features/steps/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,45 @@ class InvalidValueError(RuleState):
def __str__(self):
return f"On instance {misc.fmt(self.inst)} the following invalid value for {self.attribute} has been found: {self.value}"

@dataclass
class InvalidPropertySetDefinition(RuleState):
inst: ifcopenshell.entity_instance
object: ifcopenshell.entity_instance
name: typing.Optional[str] = None
types: typing.Optional[typing.List] = None
template_type_enum: typing.Optional[str] = None
# https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/schema/ifckernel/lexical/ifcpropertysettemplatetypeenum.htm
ifc_property_set_template_type_enum = {"PSET_TYPEDRIVENONLY": "The property sets defined by this IfcPropertySetTemplate can only be assigned to subtypes of IfcTypeObject.",
"PSET_TYPEDRIVENOVERRIDE": "The property sets defined by this IfcPropertySetTemplate can be assigned to subtypes of IfcTypeObject and can be overridden by a property set with same name at subtypes of IfcObject.",
"PSET_OCCURRENCEDRIVEN": "The property sets defined by this IfcPropertySetTemplate can only be assigned to subtypes of IfcObject.",
"PSET_PERFORMANCEDRIVEN": "The property sets defined by this IfcPropertySetTemplate can only be assigned to IfcPerformanceHistory."}

def __str__(self):
if self.template_type_enum:
return f"The instance {misc.fmt(self.inst)} with Name attribute {self.name} is assigned to {misc.fmt(self.object)}. {self.name} is {self.template_type_enum}. {self.ifc_property_set_template_type_enum.get(self.template_type_enum)}"
if self.types:
return f"The instance {misc.fmt(self.inst)} with Name attribute {self.name} is assigned to {misc.fmt(self.object)}. It must be assigned to one of the following types instead: {self.types}"
else:
return f"The instance {misc.fmt(self.inst)} has an inappropriate Name attribute {self.name} value. Pset_ prefix is reserved for standardised values only."


@dataclass
class InvalidPropertyDefinition(RuleState):
inst: ifcopenshell.entity_instance
property: ifcopenshell.entity_instance
accepted_values: typing.Optional[typing.List] = None
accepted_type: typing.Optional[str] = None
accepted_data_type_value: typing.Optional[str] = None
value: typing.Optional[str] = None

def __str__(self):
if self.accepted_values:
return f"The instance {misc.fmt(self.inst)} has an associated property {misc.fmt(self.property)} with Name attribute equal to: '{misc.fmt(self.property.Name)}'. Expected values for {misc.fmt(self.inst.Name)} are {self.accepted_values}"
elif self.accepted_type:
return f"The instance {misc.fmt(self.inst)} has an associated property {misc.fmt(self.property)}. Expected type of this property is: {misc.fmt(self.accepted_type)}"
elif self.accepted_data_type_value:
return f"The instance {misc.fmt(self.inst)} has an associated value {misc.fmt(self.property)} with Name attribute equal to: '{misc.fmt(self.property.Name)}'. Expected data type of this value is {self.accepted_data_type_value}, but {misc.fmt(self.value)} was found."


@dataclass
class ValueCountError(RuleState):
Expand Down
13 changes: 13 additions & 0 deletions features/steps/givens/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ def step_impl(context, attribute, value):
)


@given("Its attribute {attribute} {condition} with {prefix}")
def step_impl(context, attribute, condition, prefix):
assert condition in ('starts', 'does not start')

if condition == 'starts':
context.instances = list(
filter(lambda inst: hasattr(inst, attribute) and str(getattr(inst, attribute, '')).startswith(prefix), context.instances)
)
elif condition == 'does not start':
context.instances = list(
filter(lambda inst: hasattr(inst, attribute) and not str(getattr(inst, attribute)).startswith(prefix), context.instances)
)

@given('{attr} forms {closed_or_open} curve')
def step_impl(context, attr, closed_or_open):
assert closed_or_open in ('a closed', 'an open')
Expand Down
150 changes: 150 additions & 0 deletions features/steps/thens/relations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import errors as err
import json

from behave import *
from utils import ifc, misc, system
Expand Down Expand Up @@ -91,6 +92,155 @@ def step_impl(context, related, relating, other_entity, condition):
yield(err.RuleSuccessInst(True, inst))


@then('The IfcPropertySet Name attribute value must use predefined values according to the {table} table')
@then('The IfcPropertySet must be assigned according to the property set definitions table {table}')
@then('Each associated IfcProperty must be named according to the property set definitions table {table}')
@then('Each associated IfcProperty must be of type according to the property set definitions table {table}')
@then('Each associated IfcProperty value must be of data type according to the property set definitions table {table}')
@err.handle_errors
def step_impl(context, table):
if getattr(context, 'applicable', True):
tbl_path = system.get_abs_path(f"resources/property_set_definitions/{table}")
tbl = system.get_csv(tbl_path, return_type='dict')
property_set_definitons = {}
for d in tbl:
property_set_definitons[d['property_set_name']] = d

def establish_accepted_pset_values(name, property_set_definitons):
def make_obj(s):
if s:
return json.loads(s.replace("'", '"'))
else:
return ''

try:
property_set_attr = property_set_definitons[name]
except KeyError: # Pset_ not found in template
property_set_attr = ''
return property_set_attr

accepted_values = {}
accepted_values['template_type'] = property_set_attr.get('template_type', '')

accepted_values['property_names'] = []
accepted_values['property_types'] = []
accepted_values['data_types'] = []

for property_def in make_obj(property_set_attr['property_definitions']):
accepted_values['property_names'].append(property_def['property_name'])
accepted_values['property_types'].append(property_def['property_type'])
accepted_values['data_types'].append(property_def['data_type'])

accepted_values['applicable_entities'] = make_obj(property_set_attr['applicable_entities'])

accepted_values['applicable_type_values'] = property_set_attr.get('applicable_type_value', '').split(',')

return accepted_values

for inst in context.instances:
if isinstance(inst, (tuple, list)):
inst = inst[0]

name = getattr(inst, 'Name', 'Attribute not found')


if 'IfcPropertySet Name attribute value must use predefined values according' in context.step.name:
if name not in property_set_definitons.keys():
yield (err.InvalidValueError(False, inst, 'Name', name)) # A custom Pset_ prefixed attribute, e.g. Pset_Mywall

accepted_values = establish_accepted_pset_values(name, property_set_definitons)

if accepted_values: # If not it's a custom Pset_ prefixed attribute, e.g. Pset_Mywall (no need for further Pset_ checks),

if 'IfcPropertySet must be assigned according to the property set definitions table' in context.step.name:
try:
relations = inst.PropertyDefinitionOf # IFC2x3 https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/HTML/ifckernel/lexical/ifcpropertysetdefinition.htm
except AttributeError: # IFC4-CHANGE Inverse attribute renamed from PropertyDefinitionOf with upward compatibility for file-based exchange.
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPropertySet.htm
relations = inst.DefinesOccurrence
except IndexError: # IfcPropertySet not assigned to IfcObjects
relations = []

for relation in relations:
related_objects = relation.RelatedObjects
for obj in related_objects:

if accepted_values['template_type'] and accepted_values['template_type'] in ['PSET_TYPEDRIVENONLY']:
yield (err.InvalidPropertySetDefinition(False, inst=inst, object=obj, name=name, template_type_enum=accepted_values['template_type']))

correct = [obj.is_a(accepted_object) for accepted_object in accepted_values['applicable_entities']]
if not any(correct):
yield (err.InvalidPropertySetDefinition(False, inst, obj, name, accepted_values['applicable_entities']))
elif context.error_on_passed_rule:
yield (err.RuleSuccessInst(True, inst))

related_objects = inst.DefinesType
for obj in related_objects:
if accepted_values['template_type'] and accepted_values['template_type'] in ['PSET_OCCURRENCEDRIVEN', 'PSET_PERFORMANCEDRIVEN']:
yield (err.InvalidPropertySetDefinition(False, inst=inst, object=obj, name=name, template_type_enum=accepted_values['template_type']))

correct = [obj.is_a(accepted_object) for accepted_object in accepted_values['applicable_type_values']]
if not any(correct):
yield (err.InvalidPropertySetDefinition(False, inst, obj, name, accepted_values['applicable_type_values']))
elif context.error_on_passed_rule:
yield (err.RuleSuccessInst(True, inst))

if 'Each associated IfcProperty must be named according to the property set definitions table' in context.step.name:
properties = inst.HasProperties

for property in properties:
if property.Name not in accepted_values['property_names']:
yield (err.InvalidPropertyDefinition(False, inst=inst, property=property, accepted_values=accepted_values['property_names']))

if context.error_on_passed_rule and all([property.Name in accepted_values['property_names'] for property in properties]):
yield (err.RuleSuccessInst(True, inst))


if 'Each associated IfcProperty must be of type according to the property set definitions table' in context.step.name:
accepted_property_name_type_map = {}
for accepted_property_name, accepted_property_type in zip(accepted_values['property_names'], accepted_values['property_types']):
accepted_property_name_type_map[accepted_property_name] = accepted_property_type

properties = inst.HasProperties
for property in properties:

try:
accepted_property_type = accepted_property_name_type_map[property.Name]
except KeyError: # Custom property name, not matching the Pset_ expected property. Error found in previous step, no need to check more.
break

if not property.is_a(accepted_property_type):
yield (err.InvalidPropertyDefinition(False, inst=inst, property=property, accepted_type=accepted_property_type))
elif context.error_on_passed_rule:
yield (err.RuleSuccessInst(True, inst))

if 'Each associated IfcProperty value must be of data type according to the property set definitions table' in context.step.name:
accepted_property_name_datatype_map = {}
for accepted_property_name, accepted_data_type in zip(accepted_values['property_names'], accepted_values['data_types']):
accepted_property_name_datatype_map[accepted_property_name] = accepted_data_type

properties = inst.HasProperties
for property in properties:
try:
accepted_data_type = accepted_property_name_datatype_map[property.Name]
except KeyError: # Custom property name, not matching the Pset_ expected property. Error found in previous step, no need to check more.
break

if property.is_a('IfcPropertySingleValue'):
values = property.NominalValue
if not values.is_a(accepted_data_type['instance']):
yield (err.InvalidPropertyDefinition(False, inst=inst, property=property, accepted_data_type_value=accepted_data_type['instance'], value=values))

elif property.is_a('IfcPropertyEnumeratedValue'):
values = property.EnumerationValues
for value in values:
if not value.wrappedValue in accepted_data_type['values']:
yield (err.InvalidPropertyDefinition(False, inst=inst, property=property, accepted_data_type_value=accepted_data_type['values'], value=value.wrappedValue))
else:
continue

if not values:
yield (err.InvalidPropertyDefinition(False, inst=inst, property=property, accepted_data_type=accepted_data_type, value=values))

@then('It {decision} be {relationship:aggregated_or_contained_or_positioned} {preposition} {other_entity} {condition}')
@err.handle_errors
Expand Down
6 changes: 6 additions & 0 deletions features/steps/utils/ifc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def get_precision_from_contexts(entity_contexts, func_to_return=max, default_pre
return default_precision
return func_to_return(precisions)

def get_relation(instance, attrs : list):
relations = (
getattr(instance, attr, [None])[0]
for attr in attrs
)
return next((rel for rel in relations if rel is not None), None) # always len == 1

def get_mvd(ifc_file):
try:
Expand Down
Loading

0 comments on commit dbab66c

Please sign in to comment.