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.
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))
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_datumis 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_nationaloperates 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.
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.
Related Pages
- Vertical Datums and Height Systems — parent reference covering the three height surfaces this offset connects
- Reading IFC Georeferencing with ifcopenshell — the full map conversion record this guide reads one field from
- Aligning BIM Models with GIS Survey Data — the horizontal half of the same reconciliation