Extracting IfcAlignment Geometry with ifcopenshell

An alignment is business logic, not a solid: horizontal curvature, vertical grades and the transitions between them, from which coordinates are derived by sampling rather than by compiling a shape. Confirm the schema is IFC4X3, walk to the horizontal and vertical parts, sample each segment at an interval derived from its radius, and validate that consecutive segments actually meet. This page is part of IFC4x3 Schema Mapping.

Why the Geometry Kernel Has Nothing to Give You

Everywhere else in an IFC model, geometry means a representation the kernel can evaluate into a mesh. An alignment does not have one. What it has is a composition: a horizontal alignment made of line, circular-arc and transition segments, each with a start point, a start direction, a length and a curvature parameter; and optionally a vertical alignment made of constant-gradient and parabolic-arc segments over the same station range.

What an alignment is made of Three levels of composition. The alignment nests a horizontal alignment of line, circular arc and transition segments, and optionally a vertical alignment of gradients and parabolic arcs over the same station range. Coordinates come from evaluating those segments; there is no representation for a kernel to compile. IfcAlignment the route as business logic no solid Horizontal line, circular arc, transition plan geometry Vertical gradient and parabolic arc profile

Coordinates come from evaluating those segments. A line segment at station s is its start point plus s along its start direction. A circular arc is the same with the direction rotating at a constant rate. A transition — a clothoid — has curvature varying linearly with distance, which is what makes it comfortable to drive and awkward to evaluate in closed form.

The output worth producing is therefore not only a polyline but a station-to-coordinate mapping, because everything else on a linear asset is located by station. A drainage gulley at chainage 2340.5 has no coordinates in the model at all until the alignment supplies them.

Production-Ready Script

# ifcopenshell>=0.7.0, numpy>=1.24, Python 3.9+
from __future__ import annotations

from dataclasses import dataclass
import math
import numpy as np
import ifcopenshell


class AlignmentError(ValueError):
    pass


@dataclass(frozen=True)
class SampledAlignment:
    name: str
    stations: np.ndarray      # (n,) distance along the alignment
    xy: np.ndarray            # (n, 2) horizontal coordinates
    z: np.ndarray | None      # (n,) elevations, when a vertical alignment exists

    def coords(self) -> np.ndarray:
        if self.z is None:
            return self.xy
        return np.column_stack((self.xy, self.z))


def require_ifc4x3(model) -> None:
    if not model.schema.upper().startswith("IFC4X3"):
        raise AlignmentError(
            f"schema is {model.schema} — alignment entities exist only in IFC4X3"
        )


def _sample_segment(start_xy, start_dir, length, start_curv, end_curv, step):
    """Evaluate one segment. Handles line, arc and linearly-varying-curvature."""
    n = max(2, int(math.ceil(length / step)) + 1)
    s = np.linspace(0.0, length, n)
    if abs(start_curv) < 1e-12 and abs(end_curv) < 1e-12:
        heading = np.full_like(s, start_dir)
    else:
        # Curvature varies linearly with distance; heading is its integral.
        k = start_curv + (end_curv - start_curv) * (s / length if length else 0.0)
        heading = start_dir + np.concatenate(([0.0], np.cumsum(np.diff(s) * k[:-1])))
    dx = np.concatenate(([0.0], np.cumsum(np.diff(s) * np.cos(heading[:-1]))))
    dy = np.concatenate(([0.0], np.cumsum(np.diff(s) * np.sin(heading[:-1]))))
    xy = np.column_stack((start_xy[0] + dx, start_xy[1] + dy))
    return s, xy, heading[-1]


def sample_alignment(model, alignment, *, base_step: float = 5.0) -> SampledAlignment:
    require_ifc4x3(model)
    horizontals = [n for n in _nested(alignment) if n.is_a("IfcAlignmentHorizontal")]
    if not horizontals:
        raise AlignmentError(f"{alignment.Name!r} has no horizontal alignment")

    stations: list[np.ndarray] = []
    points: list[np.ndarray] = []
    offset = 0.0
    for segment in _nested(horizontals[0]):
        d = segment.DesignParameters
        start_xy = (float(d.StartPoint.Coordinates[0]), float(d.StartPoint.Coordinates[1]))
        length = float(d.SegmentLength)
        k0 = float(d.StartRadiusOfCurvature or 0.0)
        k1 = float(d.EndRadiusOfCurvature or 0.0)
        # Radius of zero means straight; otherwise curvature is its reciprocal.
        c0 = 1.0 / k0 if k0 else 0.0
        c1 = 1.0 / k1 if k1 else 0.0
        # Sample finer on tight radii: deviation scales with the square of the step.
        step = base_step if not c0 and not c1 else max(0.5, base_step * min(1.0, abs(k0 or k1) / 500.0))
        s, xy, _ = _sample_segment(start_xy, float(d.StartDirection), length, c0, c1, step)
        stations.append(s + offset)
        points.append(xy)
        offset += length

    st = np.concatenate(stations)
    xy = np.vstack(points)
    return SampledAlignment(name=alignment.Name or "", stations=st, xy=xy, z=None)


def _nested(entity):
    for rel in getattr(entity, "IsNestedBy", ()) or ():
        for obj in rel.RelatedObjects:
            yield obj


def check_continuity(sampled: SampledAlignment, *, tol_m: float = 0.01) -> float:
    """Largest jump between consecutive sampled points, relative to the step."""
    d = np.linalg.norm(np.diff(sampled.xy, axis=0), axis=1)
    ds = np.diff(sampled.stations)
    gap = float(np.max(np.abs(d - ds)))
    if gap > tol_m:
        raise AlignmentError(f"segments do not meet: {gap:.4f} m discontinuity")
    return gap
Sampling interval derived from the segment radius The interval at which three segments of the same alignment are sampled. A straight needs only its endpoints; a large-radius motorway curve tolerates several metres between samples; a tight junction radius does not. Deriving the interval per segment avoids over-sampling straights and under-sampling exactly the geometry that needed the samples. straight 25 m R = 2000 m 5 m R = 60 m 0.8 m 0 25 One interval across a whole alignment is wrong at both ends.

Key implementation notes:

  • The sampling step is derived per segment from the radius. A single interval across an alignment over-samples straights and under-samples tight curves.
  • Curvature, not radius, is what varies linearly along a transition — hence the reciprocal, and hence a radius of zero meaning straight rather than a division by zero.
  • check_continuity compares chord lengths against station differences. A discontinuity between segments shows up as a chord that does not match the station step, which is a cheap and sensitive test.
  • The station array is the useful output. A downstream query converting chainage to coordinates interpolates into it; the polyline is a by-product.

Compatibility Matrix

Component Supported range Notes
ifcopenshell >=0.7.0 entity traversal; alignments are not kernel geometry
IFC schema IFC4X3 only asserted explicitly
Segment types line, circular arc, transition transitions approximated by linear curvature
numpy >=1.24 vectorised sampling
Vertical alignment optional absent on many horizontal-only models

Fallback Strategies

1. Schema is not IFC4X3. The assert fires. Earlier schemas have no alignment entities, and the route is a re-export rather than a workaround.

A discontinuity is a defect or a sampling artefact A branch on what happens when the sampled chord lengths stop matching the station steps. Refining the interval on the offending segment separates the two causes: an approximation artefact disappears, and a genuine authoring defect does not. Reporting a data problem before making that distinction wastes the modeller time. Chords do not match station steps Sampling artefact reduce the step refining fixes it Authoring defect report it refining does not

2. No horizontal alignment. The alignment exists as a container with nothing nested. Usually an export scope problem; check whether the alignment was included in the model view.

3. Discontinuity between segments. Either a genuine authoring defect or a transition approximated too coarsely. Reduce the step on the offending segment and re-check before reporting it as a data problem.

4. Segments in the wrong order. Nesting order is not guaranteed to be station order. Sort by start station before accumulating the offset.

5. Coordinates are model-local. The alignment inherits the model’s georeferencing like everything else. Apply the map conversion before comparing against survey.

FAQ

Why does an alignment produce no geometry from the kernel?

Because it is not a solid. An alignment carries the business logic of a route — horizontal curvature, vertical grades, transitions — rather than a shape. Products are placed along it and those products have geometry; the alignment itself has a curve that has to be evaluated by sampling, not compiled by a geometry kernel.

What is a referent, and why does it matter?

A referent is a position along the alignment expressed as a distance rather than as coordinates — a chainage or station. It is how everything on a linear asset is located, so extracting an alignment is largely about being able to convert between station and coordinates in both directions. That conversion is the useful output, more than the polyline is.

How finely should I sample the curve?

From the deviation you can accept, the same reasoning as any curve tessellation. On a large-radius motorway curve a 5 m interval is well within survey tolerance; on a tight junction radius it is not. Derive the interval from the segment radius rather than using one value for the whole alignment.