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.
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
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_continuitycompares 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.
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.
Related Pages
- IFC4x3 Schema Mapping — parent reference on the infrastructure entities IFC4X3 introduced
- ifcopenshell Workflow — the geometry evaluation that alignments deliberately sit outside of
- CityGML and GML Interchange — where a sampled centreline usually ends up for city-scale analysis