Bursa-Wolf vs Molodensky-Badekas Transformations

Both the Bursa-Wolf and Molodensky-Badekas models carry the same seven quantities — three translations, three rotations, one scale — that drive Helmert 7-parameter transformations in Python, the parent topic under Core Transformation Fundamentals & Standards; the difference is where the rotations act. Bursa-Wolf rotates the position vector about the geocentre, while Molodensky-Badekas rotates it about a local centroid, and because the two publish different translation numbers for the very same physical transformation, using one set through the other’s formula is a silent, systematic error.

Rotation origin: Bursa-Wolf versus Molodensky-Badekas Two panels. On the left, Bursa-Wolf: the rotation axes pass through the Earth's geocentre, far from the survey region, so the rotation is applied about the origin of the geocentric frame. On the right, Molodensky-Badekas: the rotation axes pass through the centroid of the control points inside the survey region, so the rotation is applied about that local evaluation point. Bursa-Wolf geocentre O survey far from O Molodensky-Badekas survey centroid Xₘ rotate about local point

Figure — Bursa-Wolf applies the rotation about the distant geocentre; Molodensky-Badekas applies it about the local control centroid.

Why the Two Models Are Not Interchangeable

Bursa-Wolf writes the transformation as a scaled rotation of the raw geocentric vector plus a translation:

Xt=TBW+(1+ds)RXs \mathbf{X}_t = \mathbf{T}_{BW} + (1 + d_s)\,\mathbf{R}\,\mathbf{X}_s

Molodensky-Badekas first subtracts the centroid Xm\mathbf{X}_m of the control points, rotates and scales about that local point, then adds the centroid back with its own translation:

Xt=Xm+TMB+(1+ds)R(XsXm) \mathbf{X}_t = \mathbf{X}_m + \mathbf{T}_{MB} + (1 + d_s)\,\mathbf{R}\,(\mathbf{X}_s - \mathbf{X}_m)

The rotation matrix R\mathbf{R} and scale dsd_s are identical between the two forms; only the translation differs. Expanding the Molodensky-Badekas expression and matching it to Bursa-Wolf gives the exact conversion:

TBW=TMB+(I(1+ds)R)Xm \mathbf{T}_{BW} = \mathbf{T}_{MB} + \left(\mathbf{I} - (1 + d_s)\,\mathbf{R}\right)\mathbf{X}_m

The correction term (I(1+ds)R)Xm(\mathbf{I} - (1+d_s)\mathbf{R})\mathbf{X}_m is not small: with Xm\mathbf{X}_m on the order of six million metres and rotations of an arc-second, it reaches tens of metres, which is why swapping the translations between models produces a gross datum error. The reason both forms exist is conditioning: rotating about the geocentre makes the Bursa-Wolf rotation and translation parameters strongly correlated when the control sits far from the origin, whereas centring on Xm\mathbf{X}_m decorrelates them — the same numerical motivation behind centring in deriving 7-parameter Helmert from control points. Many national agencies therefore estimate and publish in Molodensky-Badekas form, quoting the centroid alongside the seven parameters.

Comparison Table

Property Bursa-Wolf Molodensky-Badekas
Rotation origin geocentre O\mathbf{O} control centroid Xm\mathbf{X}_m
Extra published quantity none centroid coordinates Xm\mathbf{X}_m
Translation meaning offset at the geocentre offset at the centroid
Rotation R\mathbf{R}, scale dsd_s shared, identical shared, identical
Parameter correlation high when survey far from O\mathbf{O} low (decorrelated by centring)
Typical use global/continental sets, EPSG 1033/1032 regional/national fits, EPSG 1034/9636
Interchangeable numbers? no — translations differ by tens of metres no — requires the conversion above

Complete Runnable Implementation

The module applies both forms and converts between their translations. Feeding a Bursa-Wolf and its equivalent Molodensky-Badekas set through their respective functions must transform any point identically.

from __future__ import annotations
from dataclasses import dataclass
import math

import numpy as np

ARCSEC_TO_RAD: float = math.pi / (180.0 * 3600.0)


@dataclass(frozen=True)
class SevenParameters:
    """Shared rotation/scale, plus a translation whose meaning depends on model."""
    tx: float           # metres
    ty: float           # metres
    tz: float           # metres
    rx: float           # arc-seconds
    ry: float           # arc-seconds
    rz: float           # arc-seconds
    ds: float           # ppm


def _rot_and_scale(p: SevenParameters) -> tuple[np.ndarray, float]:
    rx, ry, rz = (p.rx * ARCSEC_TO_RAD, p.ry * ARCSEC_TO_RAD, p.rz * ARCSEC_TO_RAD)
    rot = np.array([[1.0, -rz, ry],
                    [rz, 1.0, -rx],
                    [-ry, rx, 1.0]], dtype=np.float64)   # position-vector sense
    return rot, 1.0 + p.ds * 1.0e-6


def bursa_wolf(xyz: np.ndarray, p: SevenParameters) -> np.ndarray:
    """Rotate/scale about the geocentre, then translate (EPSG method 1033)."""
    pts = np.atleast_2d(np.asarray(xyz, dtype=np.float64))
    rot, scale = _rot_and_scale(p)
    out = scale * (pts @ rot.T) + np.array([p.tx, p.ty, p.tz])
    return out[0] if np.ndim(xyz) == 1 else out


def molodensky_badekas(xyz: np.ndarray, p: SevenParameters,
                       centroid: np.ndarray) -> np.ndarray:
    """Rotate/scale about the control centroid, then translate (EPSG method 1034)."""
    pts = np.atleast_2d(np.asarray(xyz, dtype=np.float64))
    xm = np.asarray(centroid, dtype=np.float64)
    rot, scale = _rot_and_scale(p)
    out = xm + np.array([p.tx, p.ty, p.tz]) + scale * ((pts - xm) @ rot.T)
    return out[0] if np.ndim(xyz) == 1 else out


def mb_translation_to_bw(p: SevenParameters, centroid: np.ndarray) -> np.ndarray:
    """Convert a Molodensky-Badekas translation to the Bursa-Wolf translation:
    T_BW = T_MB + (I - (1+ds)R) Xm."""
    rot, scale = _rot_and_scale(p)
    xm = np.asarray(centroid, dtype=np.float64)
    return np.array([p.tx, p.ty, p.tz]) + (np.eye(3) - scale * rot) @ xm

Parameter Reference

Name Type Units Valid range Notes
tx, ty, tz float metres published translation; meaning differs by model
rx, ry, rz float arc-seconds typ. ±5″ shared rotations, position-vector convention
ds float ppm typ. ±50 ppm shared scale
centroid np.ndarray metres (3,) ECEF control centroid; mandatory for Molodensky-Badekas
xyz np.ndarray metres (3,) or (n,3) geocentric ECEF to transform
return np.ndarray metres finite target-datum ECEF

Worked Example

Define a Bursa-Wolf set, choose a centroid, derive the matching Molodensky-Badekas translation, and confirm both transform a point to the same place.

bw = SevenParameters(tx=-446.448, ty=125.157, tz=-542.060,
                     rx=-0.1502, ry=-0.2470, rz=-0.8421, ds=20.4894)
xm = np.array([3874938.0, -114020.0, 5047165.0])       # a British control centroid

rot, scale = _rot_and_scale(bw)
t_mb = np.array([bw.tx, bw.ty, bw.tz]) - (np.eye(3) - scale * rot) @ xm
mb = SevenParameters(*t_mb, bw.rx, bw.ry, bw.rz, bw.ds)

point = np.array([3888891.410, -129007.815, 5036943.920])
print(np.round(bursa_wolf(point, bw), 4))
print(np.round(molodensky_badekas(point, mb, xm), 4))
# both: [ 3888518.0845 -128897.5105  5036509.8149 ]

Both models land the point on the identical target coordinate, yet the Molodensky-Badekas translation t_mb differs from the Bursa-Wolf [-446.448, 125.157, -542.060] by tens of metres — the visible proof that the two translation vectors are not interchangeable.

Validation Check

out_bw = bursa_wolf(point, bw)
out_mb = molodensky_badekas(point, mb, xm)
assert np.allclose(out_bw, out_mb, atol=1e-3), "models disagree — check centroid"
recovered = mb_translation_to_bw(mb, xm)
assert np.allclose(recovered, [bw.tx, bw.ty, bw.tz], atol=1e-6), "round-trip failed"

Common Mistakes

Applying Bursa-Wolf translations through the Molodensky-Badekas formula
The translation numbers are not the same quantity. A Bursa-Wolf translation is the offset at the geocentre; a Molodensky-Badekas translation is the offset at the centroid. Substituting one into the other's equation shifts every result by the tens-of-metres correction term (I - (1+ds)R)·Xm. Convert first with the explicit formula.
Using the wrong centroid, or omitting it entirely
Molodensky-Badekas is undefined without the exact centroid the parameters were estimated against; it is a published quantity, not something you may choose freely. Using a nearby-but-different centroid, or defaulting it to the geocentre, reintroduces the very error the model was designed to avoid.
Flipping the sign of the rotations during conversion
The rotations and scale are shared and unchanged between the two models — only the translation is recomputed. If a conversion also appears to require a rotation sign change, the two sets were published in different rotation conventions (position-vector versus coordinate-frame), a separate issue handled in the parent Helmert page. Do not fold a convention flip into the Bursa-Wolf to Molodensky-Badekas conversion.