Reconciling BIM Project Elevation with a National Datum

A BIM model’s Z values are measured from a project origin chosen for drafting convenience, and reconciling them with a national datum is a single additive constant — read it from IfcMapConversion.OrthogonalHeight where the model is georeferenced, and obtain it from the project setup where it is not. The arithmetic is trivial; getting the number from the right place is not. This page belongs to Vertical Datums and Height Systems.

How IFC Records the Offset

A georeferenced IFC model relates its own engineering coordinate system to a projected coordinate reference system through a map conversion. That record carries eastings and northings for the horizontal placement, a rotation expressed as a direction vector, a scale, and OrthogonalHeight for the vertical placement.

Model zero against the national datum Three levels stacked. The national vertical datum is the reference; the model origin sits a recorded distance above it; element elevations are measured from the model origin. A national-datum height is therefore the element elevation plus the recorded offset — the offset is additive, not a height in its own right. Element elevation measured from the model origin model Z Model origin sits above the datum by a recorded constant OrthogonalHeight National vertical datum the reference surface H = 0

OrthogonalHeight is an offset, not a height. It says where the model’s zero level sits in the target vertical datum, so a national-datum height is model_z + OrthogonalHeight. Reading it as the height of anything in particular collapses the model onto one level.

Where the record is absent — an IFC2X3 export, or an IFC4 model that was never georeferenced — the offset is not in the file. It is in the project setup: a survey point with a specified elevation, a stated relationship between a floor level and a datum, a note from the surveyor. That is a document lookup, not a computation, and inventing a value from a single element’s level is how a model ends up a storey out.

Production-Ready Script

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

from dataclasses import dataclass
import numpy as np
import ifcopenshell


@dataclass(frozen=True)
class VerticalReference:
    """How this model's Z relates to a national vertical datum."""
    offset_m: float
    source: str            # "IfcMapConversion" | "project setup"
    vertical_datum: str    # e.g. "ODN" — never inferred

    def to_national(self, model_z: np.ndarray) -> np.ndarray:
        return np.asarray(model_z, dtype=float) + self.offset_m


def read_vertical_reference(
    ifc_path: str, *, documented_offset_m: float | None = None,
    documented_datum: str | None = None,
) -> VerticalReference:
    """Prefer the model's own record; fall back only to a DOCUMENTED value."""
    model = ifcopenshell.open(ifc_path)
    conversions = model.by_type("IfcMapConversion")

    if conversions:
        mc = conversions[0]
        height = mc.OrthogonalHeight
        if height is None:
            raise ValueError(
                f"{ifc_path}: IfcMapConversion present but OrthogonalHeight is unset"
            )
        crs = mc.TargetCRS
        datum = getattr(crs, "VerticalDatum", None) or "declared in TargetCRS"
        return VerticalReference(float(height), "IfcMapConversion", str(datum))

    if documented_offset_m is None or documented_datum is None:
        raise ValueError(
            f"{ifc_path}: no IfcMapConversion and no documented offset supplied — "
            "obtain the project datum relationship from the project setup"
        )
    return VerticalReference(documented_offset_m, "project setup", documented_datum)


def verify_against_level(
    ref: VerticalReference, model_z: float, surveyed_H: float, tol_m: float = 0.02
) -> float:
    """Check one known point before trusting the offset for the whole model."""
    computed = float(ref.to_national(np.array([model_z]))[0])
    residual = abs(computed - surveyed_H)
    if residual > tol_m:
        raise AssertionError(
            f"vertical residual {residual:.3f} m exceeds {tol_m} m — "
            f"offset {ref.offset_m:.3f} m from {ref.source} is not consistent "
            "with the surveyed level"
        )
    return residual


if __name__ == "__main__":
    ref = read_vertical_reference("model.ifc")
    print(f"offset {ref.offset_m:.3f} m from {ref.source} ({ref.vertical_datum})")
    print("residual:", verify_against_level(ref, model_z=0.000, surveyed_H=42.310))
Where the offset comes from A three-way branch on what the model actually declares. A map conversion with an orthogonal height is authoritative. A map conversion without one carries horizontal georeferencing only and the vertical relationship must come from the project setup. A model with neither has no offset in the file at all, and inventing one from a single element level is how a model ends up a storey out. What does the model declare? Authoritative read it height set Project setup documented value height unset Project setup never from an element no conversion

Key implementation notes:

  • The function refuses to invent an offset. A model with no map conversion and no documented value raises, which is correct: there is no number to compute, only one to look up.
  • vertical_datum is carried alongside the offset. An offset without a datum name is only half a statement, and the half that is missing is the one that makes it checkable.
  • to_national operates on arrays, so the same reference applies to a single level or to every vertex in a mesh.
  • Verification is against one independently known point. That single check catches a sign error, a units error and a wrong lookup at once.

Compatibility Matrix

Component Supported range Notes
ifcopenshell >=0.7.0 by_type("IfcMapConversion") stable across this range
IFC schema IFC4, IFC4X3 IFC2X3 has no map conversion — use the documented route
numpy >=1.24 array application of the offset
Model units any declared length unit resolve units first; the offset is in the target CRS unit
Vertical datum any named datum recorded, never inferred

Fallback Strategies

1. IfcMapConversion present but OrthogonalHeight unset. The exporter wrote horizontal georeferencing only. Treat this as the no-record case and use the documented offset; do not default it to zero, which asserts that the project origin sits exactly on the national datum.

Why a stubborn 50 to 150 mm residual is usually not the datum Three plausible meanings of a floor level in a model and in a survey, with the typical difference between them. A residual in this range that will not resolve with datum adjustments is very often the two datasets describing different physical surfaces of the same floor rather than a vertical reference problem. What is called "floor level" Surface Typical difference Structural slab top concrete reference Screed top screed 50 – 80 mm above Finished floor finish 65 – 150 mm above Confirm which surface each dataset measured before adjusting a datum.

2. Model units are not metres. The offset is expressed in the target coordinate reference system’s unit, and model Z in the model’s unit. Resolve the model unit assignment and convert before adding, or the offset is added to numbers a thousand times too large.

3. Several map conversions in one file. Federated exports can carry more than one. Selecting the first is a guess. Fail on ambiguity and resolve which context applies, because the contexts may genuinely differ.

4. The verification point is not what you think. A “finished floor level” in a model may be the structural slab top, the screed top or the finish, and a surveyed level may be any of the three. A 50–150 mm residual that will not resolve is usually this rather than a datum problem.

5. The model was moved after the offset was recorded. If elements have been shifted vertically since georeferencing was set up, the recorded offset no longer describes the geometry. The verification step catches it; without that step, nothing does.

FAQ

Is OrthogonalHeight the height of the building?

No. It is the height of the model origin above the projected coordinate reference system, so it is added to each element Z rather than replacing it. Treating it as an absolute height places the whole model at that single elevation, which is a mistake that produces a geometrically intact model at completely the wrong level.

What if the model has no IfcMapConversion?

Then the model is not georeferenced and the offset does not exist in the file. It exists in the project setup — a survey point, a specified base level, a note on a drawing — and must be obtained from there. Do not infer it from a single element level; a floor slab whose top is at project 0.000 may be at any national height at all.

Is the relationship always a constant?

Vertically, yes. A project elevation and a national datum differ by a constant because both are measured along the same vertical direction; only the origin differs. That is why the vertical part of the reconciliation is a single addition, whereas the horizontal part is a full similarity transform.