Skip to content

Commit

Permalink
ALB999 IfcAlignment start station
Browse files Browse the repository at this point in the history
  • Loading branch information
aothms committed Jan 25, 2023
1 parent 0b67572 commit 08ae8d6
Show file tree
Hide file tree
Showing 13 changed files with 625 additions and 29 deletions.
29 changes: 29 additions & 0 deletions features/ALB998_alignment_start_station.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@implementer-agreement
@ALB
Feature: IfcAlignment start station

An IfcAlignment must have a start station, which is instantiated as an IfcReferent
with PredefinedType set to REFERENCEMARKER or STATION. The IfcReferent must have the
property Pset_Stationing.Station.

Scenario: An alignment must nest at least one referent

Given An IfcAlignment
Then A relationship IfcRelNests exists from IfcAlignment to IfcReferent

Scenario: At least one of these referents must have type REFERENCEMARKER or STATION

Given An IfcAlignment
And A relationship IfcRelNests from IfcAlignment to IfcReferent and following that
And Its attribute PredefinedType

Then at least "1" value must be "REFERENCEMARKER" or "STATION"

Scenario: At least one of these referents typed REFERENCEMARKER or STATION must have a value for Pset_Stationing.Station

Given An IfcAlignment
And A relationship IfcRelNests from IfcAlignment to IfcReferent and following that
And PredefinedType = "REFERENCEMARKER" or "STATION"
And Its value for property Pset_Stationing.Station

Then at least "1" value must exist
3 changes: 3 additions & 0 deletions features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
def before_feature(context, feature):
context.model = ifcopenshell.open(context.config.userdata["input"])
Scenario.continue_after_failed_step = True

def before_step(context, step):
context.step = step
160 changes: 131 additions & 29 deletions features/steps/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,44 @@ def __str__(self):
return f"The value {self.path[0]!r} on {self.path[1]} is not one of {', '.join(map(repr, self.allowed_values))}"


@dataclass
class instance_attribute_value_count_error:
paths: typing.Sequence[ifcopenshell.entity_instance]
allowed_values: typing.Sequence[typing.Any]
num_required: int

def __str__(self):
vs = "".join(f"\n * {p[0]!r} on {p[1]}" for p in self.paths)
return f"Not at least {self.num_required} instances of {', '.join(map(repr, self.allowed_values))} for values:{vs}"


@dataclass
class instance_attribute_existence_error:
path: typing.Sequence[ifcopenshell.entity_instance]

def __str__(self):
return f"The value on {self.path[1]} does not exist"


@dataclass
class instance_attribute_existence_count_error:
paths: typing.Sequence[ifcopenshell.entity_instance]
num_required: int

def __str__(self):
vs = "".join(f"\n * {p[1]}" for p in self.paths)
return f"Not at least {self.num_required} exist for instances:{vs}"


@dataclass
class missing_relationship_error:
inst: ifcopenshell.entity_instance
relationship: str

def __str__(self):
return f"Instance {fmt(self.inst)} has no relationship {self.relationship!r}"


def is_a(s):
return lambda inst: inst.is_a(s)

Expand Down Expand Up @@ -315,9 +353,9 @@ def _():

@given("{attribute} = {value}")
def step_impl(context, attribute, value):
value = ast.literal_eval(value)
value = list(map(ast.literal_eval, map(str.strip, value.split('or'))))
context.instances = list(
filter(lambda inst: getattr(inst, attribute, True) == value, context.instances)
filter(lambda inst: getattr(inst, attribute, True) in value, context.instances)
)

def map_state(values, fn):
Expand All @@ -333,6 +371,40 @@ def step_impl(context, attribute):
setattr(context, 'instances', context.instances)
setattr(context, 'attribute', attribute)


@given('Its value for property {prop}')
def step_impl(context, prop : str):
pset_prop = prop.split('.', 1)
if len(pset_prop) == 1:
pset_prop = [None] + pset_prop
pset, prop = pset_prop
def get_property(inst):
if inst.is_a('IfcMaterialDefinition'):
raise NotImplementedError()
elif inst.is_a('IfcProfileDef'):
raise NotImplementedError()
elif inst.is_a('IfcObject'):
for r in inst.IsDefinedBy:
if r.is_a('IfcRelDefinesByProperties'):
p = r.RelatingPropertyDefinition
if p.is_a('IfcPropertySet') and pset is None or p.Name and pset.lower() == p.Name.lower():
for pp in p.HasProperties:
if pp.is_a('IfcPropertySingleValue') and pp.Name.lower() == prop.lower():
return pp.NominalValue
for r in [rel for rel in getattr(inst, 'IsTypedBy', []) + inst.IsDefinedBy if rel.is_a('IfcRelDefinesByType')]:
return get_property(r.RelatingType)
elif inst.is_a('IfcTypeObject'):
for p in inst.HasPropertySets:
if p.is_a('IfcPropertySet') and pset is None or p.Name and pset.lower() == p.Name.lower():
for pp in p.HasProperties:
if pp.is_a('IfcPropertySingleValue') and pp.Name.lower() == prop.lower():
return pp.NominalValue
context._push()
context.instances = map_state(context.instances, get_property)
setattr(context, 'instances', context.instances)
setattr(context, 'attribute', prop) #?


@given('The element has {constraint} {num:d} instance(s) of {entity}')
def step_impl(context, constraint, num, entity):
ent_attr = {'IfcShapeRepresentation':'Representations'}
Expand All @@ -357,7 +429,7 @@ def step_impl(context, relationship_type, entity):
assert relationship_type in reltype_to_extr
extr = reltype_to_extr[relationship_type]
context.instances = list(filter(lambda inst: do_try(lambda: getattr(getattr(inst,extr['attribute'])[0],extr['object_placement']).is_a(entity),False), context.instances))


@given('A file with {field} "{values}"')
def step_impl(context, field, values):
Expand All @@ -373,6 +445,7 @@ def step_impl(context, field, values):
context.applicable = getattr(context, 'applicable', True) and applicable

@given('A relationship {relationship} {dir1:from_to} {entity} {dir2:from_to} {other_entity}')
@then('A relationship {relationship} exists {dir1:from_to} {entity} {dir2:from_to} {other_entity}')
@given('A relationship {relationship} {dir1:from_to} {entity} {dir2:from_to} {other_entity} {tail:maybe_and_following_that}')
def step_impl(context, relationship, dir1, entity, dir2, other_entity, tail=0):
assert dir1 != dir2
Expand All @@ -385,28 +458,32 @@ def step_impl(context, relationship, dir1, entity, dir2, other_entity, tail=0):
related_attr_matrix = next(csv.DictReader(open(filename_related_attr_matrix)))
relating_attr_matrix = next(csv.DictReader(open(filename_relating_attr_matrix)))

for rel in relationships:
attr_to_entity = relating_attr_matrix.get(rel.is_a())
attr_to_other = related_attr_matrix.get(rel.is_a())

if dir1:
attr_to_entity, attr_to_other = attr_to_other, attr_to_entity

def make_aggregate(val):
if not isinstance(val, (list, tuple)):
val = [val]
return val

to_entity = set(make_aggregate(getattr(rel, attr_to_entity)))
to_other = set(filter(lambda i: i.is_a(other_entity), make_aggregate(getattr(rel, attr_to_other))))

if v := set(context.instances) & to_entity:
if tail:
instances.extend(to_other)
else:
instances.extend(v)

context.instances = instances
for inst in context.instances:
for rel in relationships:
attr_to_entity = relating_attr_matrix.get(rel.is_a())
attr_to_other = related_attr_matrix.get(rel.is_a())

if dir1:
attr_to_entity, attr_to_other = attr_to_other, attr_to_entity

def make_aggregate(val):
if not isinstance(val, (list, tuple)):
val = [val]
return val

to_entity = set(make_aggregate(getattr(rel, attr_to_entity)))
to_other = set(filter(lambda i: i.is_a(other_entity), make_aggregate(getattr(rel, attr_to_other))))

if v := {inst} & to_entity:
if tail:
instances.extend(to_other)
else:
instances.extend(v)

if context.step.keyword.lower() == 'then':
handle_errors(context, [missing_relationship_error(inst, relationship) for inst in context.instances if inst not in set(instances)])
else:
context.instances = instances

@then('There must be {constraint} {num:d} instance(s) of {entity}')
def step_impl(context, constraint, num, entity):
Expand Down Expand Up @@ -459,13 +536,18 @@ def convert_values(values, context):
converted_values.append(value)
return converted_values

@then("The value must be {constraint}")
@then("The values must be {constraint}")
def step_impl(context, constraint):
@then("The value must {constraint}")
@then("The values must {constraint}")
@then('At least "{num:d}" value must {constraint}')
@then('At least "{num:d}" values must {constraint}')
def step_impl(context, constraint, num=None):
errors = []

within_model = getattr(context, 'within_model', False)

if constraint.startswith('be '):
constraint = constraint[3:]

if getattr(context, 'applicable', True):

stack_tree = list(filter(None, list(map(lambda layer: layer.get('instances'), context._stack))))
Expand All @@ -488,14 +570,34 @@ def step_impl(context, constraint):
if (msg.values and msg.relating):
errors.append(msg)

elif constraint == 'exist':
if stack_tree:
num_valid = 0
for i in range(len(stack_tree[0])):
path = [l[i] for l in stack_tree]
if path[0] is None:
if num is None:
errors.append(instance_attribute_existence_error(path))
else:
num_valid += 1
if num is not None and num_valid < num:
paths = [[l[i] for l in stack_tree] for i in range(len(stack_tree[0]))]
errors.append(instance_attribute_existence_count_error(paths, num))
else:
values = list(map(lambda s: s.strip('"'), constraint.split(' or ')))

if stack_tree:
num_valid = 0
for i in range(len(stack_tree[0])):
path = [l[i] for l in stack_tree]
if path[0] not in values:
errors.append(instance_attribute_value_error(path, values))
if num is None:
errors.append(instance_attribute_value_error(path, values))
else:
num_valid += 1
if num is not None and num_valid < num:
paths = [[l[i] for l in stack_tree] for i in range(len(stack_tree[0]))]
errors.append(instance_attribute_value_count_error(paths, values, num))

handle_errors(context, errors)

Expand Down
39 changes: 39 additions & 0 deletions test/files/alb998/fail-alb998-empty.ifc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2023-01-25T20:20:04',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4X3'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1674678004);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('00cRpFO2f3FxgjXjMzm3vv',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('1zUuyQbLfDbxR6f$oF1sfR',#5,$,$,$,$,$,$,$,$,$,$,$,$);
#22=IFCALIGNMENT('0JfAp8n7bFdvNAW8I5UqI6',#5,'A1',$,$,$,$,$);
#23=IFCRAILWAY('3644pu4an2hQaRIoe0I6dt',#5,$,$,$,$,$,$,$,$);
#24=IFCRELREFERENCEDINSPATIALSTRUCTURE('3YZ0mLRLDElQAfK$8616VM',#5,$,$,(#22),#23);
#25=IFCRELCONTAINEDINSPATIALSTRUCTURE('0tmzk_xCD5CwG$m9r$mXQ_',#5,$,$,(#22,#23),#21);
#26=IFCALIGNMENTHORIZONTAL('27HNSyZKT89xhGhMA5WDiZ',#5,'AH',$,$,$,$);
#27=IFCRELNESTS('2KHlM$n3nD_BAkHDzU24qe',#5,$,$,#22,(#26));
#28=IFCALIGNMENTHORIZONTALSEGMENT('3TJRtfW596MexFyXSIbp1_',$,$,$,$,$,$,$,.LINE.);
#29=IFCALIGNMENTSEGMENT('0OC1bclHb7ROaKFGxm8uVW',$,$,$,$,$,$,#28);
#30=IFCRELNESTS('03xkjQKgj2Z94KWWVol9Gp',#5,$,$,#26,(#29));
ENDSEC;
END-ISO-10303-21;
43 changes: 43 additions & 0 deletions test/files/alb998/fail-alb998-kilopoint-with-property.ifc
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2023-01-25T20:20:04',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4X3'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1674678004);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('3LWZx1I3f5aepCnIFvecRV',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3qHnlNLF17a8mKUnuHY__2',#5,$,$,$,$,$,$,$,$,$,$,$,$);
#22=IFCALIGNMENT('2vANnRNi5868oU0c$8NvoX',#5,'A1',$,$,$,$,$);
#23=IFCRAILWAY('1qVbJoOrP7p868CM0JWoVm',#5,$,$,$,$,$,$,$,$);
#24=IFCRELREFERENCEDINSPATIALSTRUCTURE('2zvrL8x85ElgIR163Co$bP',#5,$,$,(#22),#23);
#25=IFCRELCONTAINEDINSPATIALSTRUCTURE('0V0KtgWSL8MetojGUQJRC2',#5,$,$,(#22,#23),#21);
#26=IFCALIGNMENTHORIZONTAL('3q1bQ9ubj7uByHKoHKFzzw',#5,'AH',$,$,$,$);
#27=IFCREFERENT('2NhM4Ei$L7ZhRLWOhB4XHl',#5,$,$,$,$,$,.KILOPOINT.);
#28=IFCPROPERTYSINGLEVALUE('Station',$,IFCLENGTHMEASURE(0.),$);
#29=IFCPROPERTYSET('09L_wN$wHEohuUnql4wOtc',#5,'Pset_Stationing',$,(#28));
#30=IFCRELDEFINESBYPROPERTIES('1$PVzhGlL9Qx1jwHGpxY_3',#5,$,$,(#27),#29);
#31=IFCRELNESTS('2JLhdO0dHEM9$WvgMMls0z',#5,$,$,#22,(#26,#27));
#32=IFCALIGNMENTHORIZONTALSEGMENT('0CWnlAYSL0wfkRV4FR6P2r',$,$,$,$,$,$,$,.LINE.);
#33=IFCALIGNMENTSEGMENT('2QOevn7hL68BGSM1s$qkZe',$,$,$,$,$,$,#32);
#34=IFCRELNESTS('0NOFC8F8vDaQMJYkr4ZTAf',#5,$,$,#26,(#33));
ENDSEC;
END-ISO-10303-21;
40 changes: 40 additions & 0 deletions test/files/alb998/fail-alb998-nil-without-property.ifc
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2023-01-25T20:20:04',(''),(''),'IfcOpenShell-0.7.0','IfcOpenShell-0.7.0','');
FILE_SCHEMA(('IFC4X3'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'0.7.0','IfcOpenShell-0.7.0','');
#5=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,#3,#4,1674678004);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.,0.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('2Gp$NOukTCEP5g0hq_82um',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('0E0sNm6zj1bQ1G7XY3rWxO',#5,$,$,$,$,$,$,$,$,$,$,$,$);
#22=IFCALIGNMENT('2LUYFgK0D0AvoJGLSbwJ_H',#5,'A1',$,$,$,$,$);
#23=IFCRAILWAY('1SFW5RrBH7v8V0PuZ019u3',#5,$,$,$,$,$,$,$,$);
#24=IFCRELREFERENCEDINSPATIALSTRUCTURE('1EbN1jdd9ANxznnvFtKTk2',#5,$,$,(#22),#23);
#25=IFCRELCONTAINEDINSPATIALSTRUCTURE('3AgUEWuTz4aRaPvc0DRz7X',#5,$,$,(#22,#23),#21);
#26=IFCALIGNMENTHORIZONTAL('2A9krZ6sz2tOHWUlch5JaK',#5,'AH',$,$,$,$);
#27=IFCREFERENT('2BQcBJq6P59g$WDX0zg5CZ',#5,$,$,$,$,$,$);
#28=IFCRELNESTS('2zEG6PXH539AGRAM4EVn0E',#5,$,$,#22,(#26,#27));
#29=IFCALIGNMENTHORIZONTALSEGMENT('1qSMDkCen0U80e541pFaXA',$,$,$,$,$,$,$,.LINE.);
#30=IFCALIGNMENTSEGMENT('3UJlmJ2_n8bg8aLEinRHci',$,$,$,$,$,$,#29);
#31=IFCRELNESTS('09NVDl08z47wxnwa9paDYJ',#5,$,$,#26,(#30));
ENDSEC;
END-ISO-10303-21;

0 comments on commit 08ae8d6

Please sign in to comment.