Calculating cant alignments in IFC

Posted on 2024-06-14 in how-to • 2 min read

Rule ALA003 of the buildingSMART validation service looks at individual alignment segments to confirm that the same geometry type is used in the business logic and in the geometric representation.

During development, there was a question raised regarding the proper entity type to be used in the representation segment that corresponds to a linear transition of cant in the business logic. The ensuing discussion in the implementer’s forum in IFC4.x-IF#145 lead to considerable confusion on my part.

Therefore I decided to develop this notebook to do some calculations for the model in question.

In [1]:
import os

import numpy as np

import ifcopenshell
import ifcopenshell.geom as geom

s = geom.settings()

IN_PATH = os.path.join("..", "assets", "models", "alignment_validation")
FILE_NAME = "ACCA_sleepers-linear-placement-cant-implicit.ifc"
IN_FILE = os.path.join(IN_PATH, FILE_NAME)

model = ifcopenshell.open(IN_FILE)
In [2]:
# per inspection, this model contains only 1 alignment
align = model.by_type("IfcAlignment")[0]
align
Out[2]:
#2497=IfcAlignment('2rc47I60124e8RihHvjEbb',#1,'Test Alignment',$,$,#2746,#2749,$)
In [3]:
prod_rep = align.Representation
prod_rep
Out[3]:
#2749=IfcProductDefinitionShape($,$,(#2750,#2751))
In [4]:
shape_reps = prod_rep.Representations
shape_reps
Out[4]:
(#2750=IfcShapeRepresentation(#2752,'Axis','Curve3D',(#2668)),
 #2751=IfcShapeRepresentation(#2752,'FootPrint','Curve2D',(#2572)))
In [5]:
seg_ref_curve = shape_reps[0].Items[0]
seg_ref_curve
Out[5]:
#2668=IfcSegmentedReferenceCurve((#2669,#2681,#2695,#2709,#2727),.F.,#2630,#2723)
In [6]:
grad_curve = seg_ref_curve.BaseCurve
grad_curve
Out[6]:
#2630=IfcGradientCurve((#2631,#2650),.F.,#2572,#2647)
In [7]:
comp_curve = grad_curve.BaseCurve
comp_curve
Out[7]:
#2572=IfcCompositeCurve((#2573,#2586,#2598,#2611),.F.)
In [8]:
# traverse the alignment cant layout

for _ in align.IsNestedBy[0].RelatedObjects:
    if _.is_a() == "IfcAlignmentCant":
        cant_layout = _

cant_layout
Out[8]:
#2541=IfcAlignmentCant('2uexyjTX5CfOJI4h9D7ngN',#1,'CANT',$,$,#2743,$,1.5)
In [9]:
for seg in cant_layout.IsNestedBy[0].RelatedObjects:
    dp = seg.DesignParameters
    print(dp)
#2551=IfcAlignmentCantSegment($,$,0.,400.,0.,0.,0.,0.,.CONSTANTCANT.)
#2557=IfcAlignmentCantSegment($,$,400.,49.999993741124,0.,1.,0.,0.,.LINEARTRANSITION.)
#2563=IfcAlignmentCantSegment($,$,449.999993741124,100.000006258876,1.,1.,0.,0.,.CONSTANTCANT.)
#2569=IfcAlignmentCantSegment($,$,550.,400.,1.,0.,0.,0.,.LINEARTRANSITION.)
#2737=IfcAlignmentCantSegment($,$,950.,0.,0.,0.,0.,0.,.LINEARTRANSITION.)

From inspection, we see that the initial transition in the cant layout, #2557, is a linear transition from 0.0 to 1.0 m of cant from distance 400.0 to 450.0.

Therefore, create a function to calculate the cant values at 5 m intervals along this transition. We’re helped by the fact that there is no change in the vertical alignment along this portion of the alignment, meaning that the calculated elevations are 0.0. Therefore, the z coordinate of the segmented reference curve corresponds to 1/2 the total cant.

The low rail is the axis of rotation and the total cant is the distance the high rail is elevated above its normal position. Therefore at the centerline of rail, where the alignment is located, the z coordinate is 1/2 the total cant amount.

Said another way:

segmented_ref_curve_z = gradient_curve_z + 0.5 * total_cant

if gradient_curve_z == 0:
    segmented_ref_curve_z = 0.5 * total_cant
In [10]:
def evaluate_u(curve: ifcopenshell.entity_instance, dist_along: float) -> np.ndarray:
    pwf = ifcopenshell.ifcopenshell_wrapper.map_shape(s, curve.wrapped_data)
    t = pwf.evaluate(dist_along)
    ar = np.array(t)
    x = ar[0][3]
    y = ar[1][3]
    z = ar[2][3]
    return np.array([dist_along, x, y, z], dtype=np.float64)
In [11]:
# set up the distances to be evaluated
distances = np.linspace(start=400.0, stop=450.0, num=11, endpoint=True, dtype=np.float64)

coords = np.array([evaluate_u(seg_ref_curve, d) for d in distances])

This is obviously far from a correctly pythonic use of numpy, but it gets the job done.

In [12]:
# slice a 2d array of [distance_along, total cant]

cant_coords = np.array([coords[:, 1], 2 * coords[:, 3]], dtype=np.float64).T
cant_coords
Out[12]:
array([[4.00000000e+02, 0.00000000e+00],
       [4.05000000e+02, 9.99950129e-02],
       [4.10000000e+02, 1.99990026e-01],
       [4.14999997e+02, 2.99985039e-01],
       [4.19999986e+02, 3.99980052e-01],
       [4.24999957e+02, 4.99975064e-01],
       [4.29999892e+02, 5.99970077e-01],
       [4.34999766e+02, 6.99965090e-01],
       [4.39999545e+02, 7.99960103e-01],
       [4.44999180e+02, 8.99955116e-01],
       [4.49998611e+02, 9.99950129e-01]])

Conclusion

It still may not make sense to me, but the IfcOpenShell implementation clearly calculates the correct values of cant for this example model. Therefore I’m happy to stand corrected, with thanks to RickBrice and peterrdf for their patience and explanation.