From 4e1884be5d3315a5f1b7d4fd2b4d309f211d2411 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sat, 1 Feb 2025 18:19:43 +0100 Subject: [PATCH] IVS-369 SPS002 behaviour update (#344) --- .../SPS002_Correct-spatial-breakdown.feature | 2 +- features/steps/thens/relations.py | 7 +- features/steps/utils/ifc.py | 11 --- .../sps002/pass-sps002-road_facilitypart.ifc | 69 +++++++++++++++++++ 4 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 test/files/sps002/pass-sps002-road_facilitypart.ifc diff --git a/features/SPS002_Correct-spatial-breakdown.feature b/features/SPS002_Correct-spatial-breakdown.feature index 1ec375ab..9847413f 100644 --- a/features/SPS002_Correct-spatial-breakdown.feature +++ b/features/SPS002_Correct-spatial-breakdown.feature @@ -1,6 +1,6 @@ @implementer-agreement @SPS -@version2 +@version3 @E00100 Feature: SPS002 - Correct spatial breakdown The rule verifies that spatial elements are aggregated as per the Spatial Composition Table. diff --git a/features/steps/thens/relations.py b/features/steps/thens/relations.py index b944e459..28560f26 100644 --- a/features/steps/thens/relations.py +++ b/features/steps/thens/relations.py @@ -42,22 +42,23 @@ def step_impl(context, inst, relationship, table): # raise Exception(f'Entity {entity} was not found in the {table}') continue - applicable_entity = ifc.order_by_ifc_inheritance(applicable_entities, base_class_last = True)[0] - expected_relationship_objects = aggregated_table[applicable_entity] + # For all applicable entities (could be multiple e.g IfcRoad, IfcFacility) we union the allowed types + expected_relationship_objects = sorted(set(functools.reduce(operator.or_, map(set, [aggregated_table[e] for e in applicable_entities])))) try: relation = getattr(inst, stmt_to_op[relationship], True)[0] except IndexError: # no relationship found for the entity if is_required: yield ValidationOutcome(inst=inst, expected={"oneOf": expected_relationship_objects, "context": context}, severity=OutcomeSeverity.ERROR) continue + relationship_objects = getattr(relation, relationship_tbl_header, True) if not isinstance(relationship_objects, tuple): relationship_objects = (relationship_objects,) - for relationship_object in relationship_objects: is_correct = any(relationship_object.is_a(expected_relationship_object) for expected_relationship_object in expected_relationship_objects) if not is_correct: + # related object not of the correct type yield ValidationOutcome(inst=inst, expected={"oneOf": expected_relationship_objects, "context": context}, observed=relationship_object, severity=OutcomeSeverity.ERROR) diff --git a/features/steps/utils/ifc.py b/features/steps/utils/ifc.py index 5c9821e9..cc7bdbe9 100644 --- a/features/steps/utils/ifc.py +++ b/features/steps/utils/ifc.py @@ -50,17 +50,6 @@ def instance_getter(i, representation_id, representation_type, negative=False): if condition(i, representation_id, representation_type): return i -def order_by_ifc_inheritance(instances, base_class_last): - import ifcopenshell - ifc = ifcopenshell.file(schema='IFC4X3') - inheritance_nr = {} - for instance in instances: - ifc_instance = ifc.create_entity(instance) - result = sum(1 for str_instance in instances if ifc_instance.is_a(str_instance)) - inheritance_nr[instance] = result - inheritance_nr = dict(sorted(inheritance_nr.items(), key=lambda item: item[1], reverse=base_class_last)) - return list(inheritance_nr.keys()) - def recurrently_get_entity_attr(ifc_context, inst, entity_to_look_for, attr_to_get, attr_found=None): if attr_found is None: diff --git a/test/files/sps002/pass-sps002-road_facilitypart.ifc b/test/files/sps002/pass-sps002-road_facilitypart.ifc new file mode 100644 index 00000000..9e57531c --- /dev/null +++ b/test/files/sps002/pass-sps002-road_facilitypart.ifc @@ -0,0 +1,69 @@ +ISO-10303-21; +/* Testfile: pass-sps002-road_facilitypart.ifc */ +/* IFC-Requirement: sps002; Correct spatial breakdown */ +HEADER; +FILE_DESCRIPTION(('ViewDefinition [Ifc4X3NotAssigned]'),'2;1'); +FILE_NAME ('pass-sps002-road_facilitypart.ifc', '2023-03-14T15:36:48', ('redacted'), ('redacted'), 'Python+IfcOpenShell', '',''); +FILE_SCHEMA(('IFC4X3_ADD2')); +ENDSEC; +DATA; +#1= IFCAPPLICATION(#2,'1.0.0.0','redacted','redacted'); +#2= IFCORGANIZATION($,'Geometry Gym Pty Ltd',$,$,$); +#3= IFCPERSONANDORGANIZATION(#4,#5,$); +#4= IFCPERSON('redacted','redacted',$,$,$,$,$,$); +#5= IFCORGANIZATION($,'redacted',$,$,$); +#6= IFCOWNERHISTORY(#3,#1,$,.ADDED.,1418084874,$,$,1418084874); +#7= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,0.0001,#8,#10); +#8= IFCAXIS2PLACEMENT3D(#9,$,$); +#9= IFCCARTESIANPOINT((0.0,0.0,0.0)); +#10= IFCDIRECTION((0.0,1.0)); +#11= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#7,$,.MODEL_VIEW.,$); +#12= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#7,$,.MODEL_VIEW.,$); +#13= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,0.0001,#8,#14); +#14= IFCDIRECTION((0.0,1.0)); +#50= IFCBUILDING('0Cd2Mw3cP09wW6qWHK8v2f',$,'IfcBuilding',$,$,#51,$,$,.ELEMENT.,$,$,#57); +#51= IFCLOCALPLACEMENT($,#52); +#52= IFCAXIS2PLACEMENT3D(#53,$,$); +#53= IFCCARTESIANPOINT((0.0,0.0,0.0)); +#54= IFCRELCONTAINEDINSPATIALSTRUCTURE('3ffS1zvV94ExNgP6hOHMLr',$,'Building','Building Container for Elements',(#302),#50); +#57= IFCPOSTALADDRESS($,$,$,$,$,$,$,'Unknown',$,$); +#100= IFCPROJECT('3KEb34nozBu9ezspX8gM9d',#6,'IfcProject',$,$,'IfcProject','',(#13),#101); +#101= IFCUNITASSIGNMENT((#102,#103,#104)); +#102= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.); +#103= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#104= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.); +#200= IFCMATERIAL('Masonry - Brick - Brown',$,$); +#203= IFCMATERIAL('Masonry',$,$); +#206= IFCMATERIALLAYER(#200,110.0,.F.,'Finish',$,$,$); +#208= IFCMATERIALLAYER($,50.0,.T.,'Air Infiltration Barrier',$,$,$); +#210= IFCMATERIALLAYER(#203,110.0,.F.,'Core',$,$,$); +#212= IFCMATERIALLAYERSET((#206,#208,#210),'Double Brick - 270',$); +#213= IFCRELASSOCIATESMATERIAL('2D3xie$bD8YgahfQF1htfq',$,'MatAssoc','Material Associates',(#300),#212); +#300= IFCWALLTYPE('2GdZ7nhi52Geyiua9QcAH9',$,'Double Brick - 270',$,$,$,$,$,$,.NOTDEFINED.); +#301= IFCRELDEFINESBYTYPE('3Gcd0t0WTDWe18S5rhROgf',$,'Double Brick - 270',$,(#302),#300); +#302= IFCWALLSTANDARDCASE('0czCsOQ5z4dg8QGBRFInu2',$,$,$,$,#305,#320,$,$); +#303= IFCMATERIALLAYERSETUSAGE(#212,.AXIS2.,.POSITIVE.,0.0,$); +#304= IFCRELASSOCIATESMATERIAL('0PFtmoJBHDOvVAl6M9w55u',$,'MatAssoc','Material Associates',(#302),#303); +#305= IFCLOCALPLACEMENT($,#306); +#306= IFCAXIS2PLACEMENT3D(#307,#308,#309); +#307= IFCCARTESIANPOINT((0.0,0.0,0.0)); +#308= IFCDIRECTION((0.0,0.0,1.0)); +#309= IFCDIRECTION((1.0,0.0,0.0)); +#310= IFCCARTESIANPOINT((5000.0,0.0)); +#311= IFCCARTESIANPOINT((0.0,0.0)); +#312= IFCPOLYLINE((#311,#310)); +#313= IFCSHAPEREPRESENTATION(#11,'Axis','Curve2D',(#312)); +#314= IFCAXIS2PLACEMENT2D(#315,$); +#315= IFCCARTESIANPOINT((2500.0,135.0)); +#316= IFCRECTANGLEPROFILEDEF(.AREA.,'Wall Perim',#314,5000.0,270.0); +#317= IFCDIRECTION((0.0,0.0,1.0)); +#318= IFCEXTRUDEDAREASOLID(#316,$,#317,2000.0); +#319= IFCSHAPEREPRESENTATION(#12,'Body','SweptSolid',(#318)); +#320= IFCPRODUCTDEFINITIONSHAPE($,$,(#313,#319)); +#321=IFCROAD('0hb5vCxjv2ZetiycRLI_Fx',#6,'','',$,$,$,$,$,$); +#322=IFCFACILITYPARTCOMMON('0hb5vCxjv2ZetiycRLI_Fy',#6,'','',$,$,$,$,$,.NOTDEFINED.,$); +#323= IFCRELAGGREGATES('2G17HB5orFmhdpTVYmbgKX',$,'RailwayContainer','Container for Railway Parts',#321,(#322)); +#324= IFCRELAGGREGATES('2G17HB5orFmhdpTVYmbgCX',$,'RailwayContainer','Container for Railway Parts',#100,(#321,#50)); +ENDSEC; + +END-ISO-10303-21;