From c300c4f718f0c5d7fec9e8277f4aad9756685feb Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sat, 28 Dec 2024 15:45:57 +0100 Subject: [PATCH] GEM113 Initial commit --- ...t-be-defined-using-colinear-points.feature | 11 +++ features/steps/thens/geometry.py | 19 +++++- features/steps/utils/geometry.py | 25 ++++++- .../fail-gem113-arc_almost_colinear.ifc | 38 +++++++++++ .../files/gem113/fail-gem113-arc_colinear.ifc | 38 +++++++++++ test/files/gem113/generate.py | 67 +++++++++++++++++++ test/files/gem113/pass-gem113-arc.ifc | 38 +++++++++++ .../pass-gem113-arc_non_colinear_enough.ifc | 38 +++++++++++ 8 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 features/GEM113_Indexed-poly-curve-arcs-must-not-be-defined-using-colinear-points.feature create mode 100644 test/files/gem113/fail-gem113-arc_almost_colinear.ifc create mode 100644 test/files/gem113/fail-gem113-arc_colinear.ifc create mode 100644 test/files/gem113/generate.py create mode 100644 test/files/gem113/pass-gem113-arc.ifc create mode 100644 test/files/gem113/pass-gem113-arc_non_colinear_enough.ifc diff --git a/features/GEM113_Indexed-poly-curve-arcs-must-not-be-defined-using-colinear-points.feature b/features/GEM113_Indexed-poly-curve-arcs-must-not-be-defined-using-colinear-points.feature new file mode 100644 index 00000000..80838e91 --- /dev/null +++ b/features/GEM113_Indexed-poly-curve-arcs-must-not-be-defined-using-colinear-points.feature @@ -0,0 +1,11 @@ +@implementer-agreement +@GEM +@version1 +Feature: GEM113 - Indexed poly curve arcs must not be defined using colinear points +The rule verifies, that all the three points of any IfcArcIndex segment of an IfcIndexedPolyCurve are not colinear after taking the Precision factor into account + + @E00050 + Scenario: No poly curve arcs using colinear points + + Given An IfcIndexedPolyCurve + Then It must have no arc segments that use colinear points after taking the Precision factor into account diff --git a/features/steps/thens/geometry.py b/features/steps/thens/geometry.py index b4ff2a33..88ea2457 100644 --- a/features/steps/thens/geometry.py +++ b/features/steps/thens/geometry.py @@ -231,4 +231,21 @@ def step_impl(context, inst: ifcopenshell.entity_instance, clause: str): if clause == 'including' or (clause == 'excluding' and (i, j) != (0, len(points_coordinates) - 1)): if math.dist(points_coordinates[i], points_coordinates[j]) < precision: yield ValidationOutcome(inst=inst, observed=(points_coordinates[i], points_coordinates[j]), - severity=OutcomeSeverity.ERROR) \ No newline at end of file + severity=OutcomeSeverity.ERROR) + +@gherkin_ifc.step("It must have no arc segments that use colinear points after taking the Precision factor into account") +def step_impl(context, inst: ifcopenshell.entity_instance): + import mpmath as mp + mp.prec = 128 + + representation_context = geometry.recurrently_get_entity_attr(context, inst, 'IfcRepresentation', 'ContextOfItems') + precision = mp.mpf(geometry.get_precision_from_contexts(representation_context)) + + for seg in (inst.Segments or ()): + ps = inst.Points.CoordList + if seg.is_a('IfcArcIndex') and len(seg[0]) == 3 and all((i >= 1) and ((i - 1) < len(ps)) for i in seg[0]): + a, b, c = (ps[i-1] for i in seg[0]) + l = geometry.Line.from_points(a, c) + if l.distance(b) < precision: + yield ValidationOutcome(inst=inst, observed=str(seg), + severity=OutcomeSeverity.ERROR) diff --git a/features/steps/utils/geometry.py b/features/steps/utils/geometry.py index cb60f3f5..fe48de60 100644 --- a/features/steps/utils/geometry.py +++ b/features/steps/utils/geometry.py @@ -1,9 +1,10 @@ from dataclasses import dataclass import operator import math -from typing import Dict +from typing import Dict, Tuple import numpy as np +import mpmath as mp import ifcopenshell.entity_instance import ifcopenshell.geom as ifcos_geom @@ -306,3 +307,25 @@ def compare_with_precision(value_1: float, value_2: float, precision: float, com return value_1 < value_2 or math.isclose(value_1, value_2, rel_tol=0., abs_tol=precision) case _: raise ValueError(f"Invalid comparison operator: {comparison_operator}") + +@dataclass +class Line: + """ + Represents a line a + d*b where a is a position and b a normalized unit vector + """ + a: Tuple[mp.mpf] + b: Tuple[mp.mpf] + + def distance(self, point: Tuple[mp.mpf]) -> mp.mpf: + v = [p - ai for p, ai in zip(point, self.a)] + dot_prod = mp.fsum([x * y for x, y in zip(v, self.b)]) + proj = [dot_prod * bi for bi in self.b] + dist_vec = [vi - pi for vi, pi in zip(v, proj)] + return mp.sqrt(mp.fsum([x*x for x in dist_vec])) + + @staticmethod + def from_points(a, b): + a, b = (tuple(map(mp.mpf, p)) for p in (a,b)) + l = mp.sqrt(mp.fsum([x*x for x in b])) + b = [x / l for x in b] + return Line(a, b) diff --git a/test/files/gem113/fail-gem113-arc_almost_colinear.ifc b/test/files/gem113/fail-gem113-arc_almost_colinear.ifc new file mode 100644 index 00000000..108b6d58 --- /dev/null +++ b/test/files/gem113/fail-gem113-arc_almost_colinear.ifc @@ -0,0 +1,38 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-12-28T14:43:26',(''),(''),'IfcOpenShell-0.8.0','IfcOpenShell-0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,$,'',$,$,$,$,$); +#2=IFCORGANIZATION($,'',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'0.8.0','IfcOpenShell-0.8.0',''); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1735397006); +#6=IFCDIRECTION((1.,0.,0.)); +#7=IFCDIRECTION((0.,0.,1.)); +#8=IFCCARTESIANPOINT((0.,0.,0.)); +#9=IFCAXIS2PLACEMENT3D(#8,#7,#6); +#10=IFCDIRECTION((0.,1.)); +#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.0174532925199433),#16); +#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17); +#19=IFCUNITASSIGNMENT((#13,#14,#15,#18)); +#20=IFCPROJECT('24xp2u2Rr7uOpUKiXfPYXD',#5,'',$,$,$,$,(#11),#19); +#21=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.5,1.E-06),(1.,0.))); +#22=IFCINDEXEDPOLYCURVE(#21,(IFCARCINDEX((1,2,3)),IFCLINEINDEX((3,1))),$); +#23=IFCSHAPEREPRESENTATION(#11,'FootPrint','Curve2D',(#22)); +#24=IFCPRODUCTDEFINITIONSHAPE($,$,(#23)); +#25=IFCCARTESIANPOINT((0.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#25,$,$); +#27=IFCLOCALPLACEMENT($,#26); +#28=IFCSPACE('1vgt69T2bFTu4vLXNn24Y8',$,$,$,$,#27,#24,$,$,$,$); +#29=IFCRELAGGREGATES('30p94qT6L2YQvAFbzZIwTk',$,$,$,#20,(#28)); +ENDSEC; +END-ISO-10303-21; diff --git a/test/files/gem113/fail-gem113-arc_colinear.ifc b/test/files/gem113/fail-gem113-arc_colinear.ifc new file mode 100644 index 00000000..d412b34a --- /dev/null +++ b/test/files/gem113/fail-gem113-arc_colinear.ifc @@ -0,0 +1,38 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-12-28T14:43:26',(''),(''),'IfcOpenShell-0.8.0','IfcOpenShell-0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,$,'',$,$,$,$,$); +#2=IFCORGANIZATION($,'',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'0.8.0','IfcOpenShell-0.8.0',''); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1735397006); +#6=IFCDIRECTION((1.,0.,0.)); +#7=IFCDIRECTION((0.,0.,1.)); +#8=IFCCARTESIANPOINT((0.,0.,0.)); +#9=IFCAXIS2PLACEMENT3D(#8,#7,#6); +#10=IFCDIRECTION((0.,1.)); +#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.0174532925199433),#16); +#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17); +#19=IFCUNITASSIGNMENT((#13,#14,#15,#18)); +#20=IFCPROJECT('1dV8LfekbEiPWnicWc56Kx',#5,'',$,$,$,$,(#11),#19); +#21=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.5,0.),(1.,0.))); +#22=IFCINDEXEDPOLYCURVE(#21,(IFCARCINDEX((1,2,3)),IFCLINEINDEX((3,1))),$); +#23=IFCSHAPEREPRESENTATION(#11,'FootPrint','Curve2D',(#22)); +#24=IFCPRODUCTDEFINITIONSHAPE($,$,(#23)); +#25=IFCCARTESIANPOINT((0.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#25,$,$); +#27=IFCLOCALPLACEMENT($,#26); +#28=IFCSPACE('0djY0fT75CuflGivoTukmi',$,$,$,$,#27,#24,$,$,$,$); +#29=IFCRELAGGREGATES('3ZawqDx_j4$wdFp5nfgwyT',$,$,$,#20,(#28)); +ENDSEC; +END-ISO-10303-21; diff --git a/test/files/gem113/generate.py b/test/files/gem113/generate.py new file mode 100644 index 00000000..ecd995e2 --- /dev/null +++ b/test/files/gem113/generate.py @@ -0,0 +1,67 @@ +import ifcopenshell +import ifcopenshell.template +from math import sqrt + +sqrt2 = sqrt(2.)/2. + +pass_arc = [ + (1.0, 0.0), + (sqrt2, sqrt2), + (0.0, 1.0) +],[ + (1,2,3),(3,1) +] + +fail_arc_colinear = [ + (0.0, 0.0), + (0.5, 0.0), + (1.0, 0.0) +],[ + (1,2,3),(3,1) +] + +fail_arc_almost_colinear = [ + (0.0, 0.0), + (0.5, 1.e-6), + (1.0, 0.0) +],[ + (1,2,3),(3,1) +] + +pass_arc_non_colinear_enough = [ + (0.0, 0.0), + (0.5, 1.e-4), + (1.0, 0.0) +],[ + (1,2,3),(3,1) +] + +cases = ['pass_arc', 'fail_arc_colinear', 'fail_arc_almost_colinear', 'pass_arc_non_colinear_enough'] +for case in cases: + pf, reason = case.split('_', 1) + f = ifcopenshell.template.create() + ctx = f.by_type('IfcGeometricRepresentationContext')[0] + proj = f.by_type('IfcProject')[0] + points, edges = globals()[case] + plist = f.createIfcCartesianPointList2D(points) + def create_segment(tup): + if len(tup) == 2: + return f.createIfcLineIndex((tup)) + if len(tup) == 3: + return f.createIfcArcIndex((tup)) + segments = list(map(create_segment, edges)) if edges else edges + crv = f.createIfcIndexedPolyCurve(plist, segments, None) + rep = f.createIfcShapeRepresentation(ctx, 'FootPrint', 'Curve2D', [crv]) + pds = f.createIfcProductDefinitionShape(Representations=[rep]) + road = f.createIfcSpace( + ifcopenshell.guid.new(), + ObjectPlacement=f.createIfcLocalPlacement(RelativePlacement=f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0., 0., 0.)))), + Representation=pds + ) + f.createIfcRelAggregates( + ifcopenshell.guid.new(), + None, None, None, + proj, + [road] + ) + f.write(f'{pf}-gem113-{reason}.ifc') diff --git a/test/files/gem113/pass-gem113-arc.ifc b/test/files/gem113/pass-gem113-arc.ifc new file mode 100644 index 00000000..34c0fe1c --- /dev/null +++ b/test/files/gem113/pass-gem113-arc.ifc @@ -0,0 +1,38 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-12-28T14:43:26',(''),(''),'IfcOpenShell-0.8.0','IfcOpenShell-0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,$,'',$,$,$,$,$); +#2=IFCORGANIZATION($,'',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'0.8.0','IfcOpenShell-0.8.0',''); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1735397006); +#6=IFCDIRECTION((1.,0.,0.)); +#7=IFCDIRECTION((0.,0.,1.)); +#8=IFCCARTESIANPOINT((0.,0.,0.)); +#9=IFCAXIS2PLACEMENT3D(#8,#7,#6); +#10=IFCDIRECTION((0.,1.)); +#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.0174532925199433),#16); +#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17); +#19=IFCUNITASSIGNMENT((#13,#14,#15,#18)); +#20=IFCPROJECT('1lmEZwk0jCxu1UhV3YDklD',#5,'',$,$,$,$,(#11),#19); +#21=IFCCARTESIANPOINTLIST2D(((1.,0.),(0.707106781186548,0.707106781186548),(0.,1.))); +#22=IFCINDEXEDPOLYCURVE(#21,(IFCARCINDEX((1,2,3)),IFCLINEINDEX((3,1))),$); +#23=IFCSHAPEREPRESENTATION(#11,'FootPrint','Curve2D',(#22)); +#24=IFCPRODUCTDEFINITIONSHAPE($,$,(#23)); +#25=IFCCARTESIANPOINT((0.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#25,$,$); +#27=IFCLOCALPLACEMENT($,#26); +#28=IFCSPACE('1l6UMBjaL0Iu2SkTL0BXbm',$,$,$,$,#27,#24,$,$,$,$); +#29=IFCRELAGGREGATES('33aK_7G8XC_uTta6bAcd86',$,$,$,#20,(#28)); +ENDSEC; +END-ISO-10303-21; diff --git a/test/files/gem113/pass-gem113-arc_non_colinear_enough.ifc b/test/files/gem113/pass-gem113-arc_non_colinear_enough.ifc new file mode 100644 index 00000000..777871e2 --- /dev/null +++ b/test/files/gem113/pass-gem113-arc_non_colinear_enough.ifc @@ -0,0 +1,38 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-12-28T14:43:26',(''),(''),'IfcOpenShell-0.8.0','IfcOpenShell-0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,$,'',$,$,$,$,$); +#2=IFCORGANIZATION($,'',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'0.8.0','IfcOpenShell-0.8.0',''); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1735397006); +#6=IFCDIRECTION((1.,0.,0.)); +#7=IFCDIRECTION((0.,0.,1.)); +#8=IFCCARTESIANPOINT((0.,0.,0.)); +#9=IFCAXIS2PLACEMENT3D(#8,#7,#6); +#10=IFCDIRECTION((0.,1.)); +#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.0174532925199433),#16); +#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17); +#19=IFCUNITASSIGNMENT((#13,#14,#15,#18)); +#20=IFCPROJECT('1mJ5LZpnD8yh32dfmevuI8',#5,'',$,$,$,$,(#11),#19); +#21=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.5,0.0001),(1.,0.))); +#22=IFCINDEXEDPOLYCURVE(#21,(IFCARCINDEX((1,2,3)),IFCLINEINDEX((3,1))),$); +#23=IFCSHAPEREPRESENTATION(#11,'FootPrint','Curve2D',(#22)); +#24=IFCPRODUCTDEFINITIONSHAPE($,$,(#23)); +#25=IFCCARTESIANPOINT((0.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#25,$,$); +#27=IFCLOCALPLACEMENT($,#26); +#28=IFCSPACE('2m1MQwdaHFr9rxB4Y7O3_5',$,$,$,$,#27,#24,$,$,$,$); +#29=IFCRELAGGREGATES('0J3nG9PyPEdgpUHCZII324',$,$,$,#20,(#28)); +ENDSEC; +END-ISO-10303-21;