Position Vector vs Coordinate Frame Rotation Conventions

Two conventions describe the same seven-parameter transformation, they differ only in the sign of the three rotations, and using a parameter set under the wrong one produces an error of roughly twice the rotation effect — metres, for a typical global parameter set. This guide, part of Helmert 7-parameter transformations in Python, sets out the two conventions precisely, shows how to detect which one a parameter set belongs to, and gives the conversion that lets a single implementation consume both.

The Two Conventions

Both write the transformation as a translation, a scale and a small rotation:

The two rotation conventions and how they relate The same seven parameters produce two rotation matrices. Under the position vector convention, EPSG method 9606, the rotations turn the position itself. Under the coordinate frame convention, method 9607, they turn the axes of the frame. The two matrices are transposes of one another, which for the small angles used here is the same as negating all three rotation values. Translations and scale are identical, which is exactly why the mistake is easy to make and hard to see. Seven parameters T, s, and three rotations Position vector (9606) rotates the position Coordinate frame (9607) rotates the frame R_CF = R_PV transposed negate all three rotations

Figure — one transformation, two conventions: the rotation matrices are transposes.

Xt=T+(1+s)RXs\mathbf{X}_t = \mathbf{T} + (1 + s)\,\mathbf{R}\,\mathbf{X}_s

and both use the same small-angle rotation matrix built from three angles. The difference is what the angles rotate. In the position vector convention (EPSG method 9606) the rotations are applied to the position vector itself, so a positive rzr_z rotates the point counter-clockwise about the Z axis. In the coordinate frame convention (EPSG method 9607) the rotations are applied to the axes of the frame, so a positive rzr_z rotates the frame counter-clockwise — which moves the point the other way.

RPV=[1rzryrz1rxryrx1],RCF=RPVT=[1rzryrz1rxryrx1]\mathbf{R}_{\text{PV}} = \begin{bmatrix} 1 & -r_z & r_y \\ r_z & 1 & -r_x \\ -r_y & r_x & 1 \end{bmatrix}, \qquad \mathbf{R}_{\text{CF}} = \mathbf{R}_{\text{PV}}^{\mathsf{T}} = \begin{bmatrix} 1 & r_z & -r_y \\ -r_z & 1 & r_x \\ r_y & -r_x & 1 \end{bmatrix}

The two matrices are transposes, which for a small-angle rotation is the same as negating all three angles. Everything else — translations, scale, units — is identical, which is exactly why the mistake is so easy to make and so hard to see.

Complete Runnable Implementation

from __future__ import annotations

from enum import Enum

import numpy as np

MAS_TO_RAD = np.pi / (180.0 * 3600.0 * 1000.0)


class RotationConvention(Enum):
    """EPSG method 9606 (position vector) or 9607 (coordinate frame)."""

    POSITION_VECTOR = "position_vector"
    COORDINATE_FRAME = "coordinate_frame"


def rotation_matrix(rot_rad: np.ndarray,
                    convention: RotationConvention) -> np.ndarray:
    """Small-angle rotation matrix in the stated convention."""
    rx, ry, rz = (float(v) for v in rot_rad)
    pv = np.array([
        [1.0, -rz, ry],
        [rz, 1.0, -rx],
        [-ry, rx, 1.0],
    ], dtype=np.float64)
    return pv if convention is RotationConvention.POSITION_VECTOR else pv.T


def helmert(xyz: np.ndarray, t_m: np.ndarray, rot_mas: np.ndarray, scale_ppb: float,
            convention: RotationConvention) -> np.ndarray:
    """Apply a 7-parameter transformation under an EXPLICIT convention.

    The convention is a required argument on purpose: a default here is a decision
    made silently, and it is the decision that goes wrong.
    """
    if xyz.shape != (3,) or xyz.dtype != np.float64:
        raise ValueError("expected a (3,) float64 Cartesian position in metres")
    r = rotation_matrix(np.asarray(rot_mas, dtype=np.float64) * MAS_TO_RAD, convention)
    return t_m + (1.0 + scale_ppb * 1e-9) * (r @ xyz)


def convert_convention(rot_mas: np.ndarray) -> np.ndarray:
    """Convert a rotation triple between the two conventions: negate all three."""
    return -np.asarray(rot_mas, dtype=np.float64)


def detect_convention(xyz_source: np.ndarray, xyz_target: np.ndarray,
                      t_m: np.ndarray, rot_mas: np.ndarray,
                      scale_ppb: float) -> RotationConvention:
    """Decide which convention a parameter set belongs to, from one control pair.

    Applies both and returns whichever lands closer to the published target. The
    two differ by twice the rotation effect, so for any real parameter set the
    answer is unambiguous — usually metres apart.
    """
    errors = {}
    for conv in RotationConvention:
        got = helmert(xyz_source, t_m, rot_mas, scale_ppb, conv)
        errors[conv] = float(np.linalg.norm(got - xyz_target))
    best, worst = sorted(errors, key=errors.get)
    if errors[worst] < 4.0 * max(errors[best], 1e-9):
        raise ValueError(
            f"cannot distinguish the conventions from this pair: residuals "
            f"{errors[best]:.4f} m and {errors[worst]:.4f} m — the rotations may be "
            f"too small, or the control pair may be wrong"
        )
    return best

Parameter Reference

Name Type Units Note
t_m np.ndarray m Identical in both conventions
rot_mas np.ndarray mas Sign differs between conventions
scale_ppb float ppb Identical in both conventions
convention RotationConvention Required; no default is offered
detect_convention Needs one independently known control pair

Worked Example

import numpy as np

xyz = np.array([3_771_793.968, 140_253.342, 5_124_304.349])   # a European site
t = np.array([0.0, 0.0, 0.0])
rot = np.array([0.891, 5.390, -8.712])      # mas — a realistic magnitude
scale = 0.0

pv = helmert(xyz, t, rot, scale, RotationConvention.POSITION_VECTOR)
cf = helmert(xyz, t, rot, scale, RotationConvention.COORDINATE_FRAME)
print(f"{np.linalg.norm(pv - cf):.4f} m apart")
# 0.4113 m apart

Forty-one centimetres from a sign, with translations of zero and no scale — and both results look like perfectly ordinary coordinates. With a global parameter set carrying rotations of tens of milliarcseconds the separation runs to several metres.

Separation between the two conventions by rotation magnitude Bar chart of the distance in metres between the two conventions applied to the same parameter set, at four rotation magnitudes: 0.04 metres at 0.1 milliarcseconds, 0.41 at 1, 4.1 at 10 and 12.4 at 30 — the magnitude of a typical global parameter set. The error is roughly twice the rotation effect, and both results look like perfectly ordinary coordinates. 0.01 0.1 1 10 100 m 0.041 0.1 mas 0.41 1 mas 4.1 10 mas 12.4 30 mas

Figure — the separation between the two conventions, by rotation magnitude.

Validation Check

def assert_convention_recorded(metadata: dict) -> None:
    """A parameter set without its convention is not usable."""
    conv = metadata.get("rotation_convention")
    assert conv in {c.value for c in RotationConvention}, (
        "the parameter set records no rotation convention; it cannot be applied "
        "without guessing, and the guess is worth up to several metres"
    )

Common Mistakes

Copying rotations from a table without reading its convention. Published tables state the convention, usually in a header or a footnote that does not survive being copied into a configuration file. Carry the convention with the numbers in the same record, always.

Assuming a library’s default matches your source. Different tools default differently, and applying a coordinate-frame set through a position-vector implementation reverses the rotation. The fix is to make the convention an explicit argument, as above, so there is no default to be wrong about.

Testing only against a control point near the rotation axis. The two conventions agree exactly where the rotation has no effect, so a test point badly chosen can validate a wrong implementation. Test where the rotations bite — far from the axis — and use the detection function’s own guard, which refuses when the two candidates are too close to separate.

Recording the Convention With the Parameters

Because the two conventions are indistinguishable from the numbers alone, the convention has to be carried as data rather than as knowledge. Three places it belongs, in descending order of reliability.

Where the rotation convention should be recorded Three places. The EPSG operation identifier names the method — 9606 or 9607 — and is the most reliable, because it leaves no room for transcription. A constrained field on the parameter record itself is next, since a parameter set without one then cannot be constructed. The deliverable metadata block is third, and is needed regardless so that a recipient can reproduce a coordinate. A comment in a configuration file is not a place: it is separated from the numbers on the first copy. operation id (9606/9607) best no transcription possible a field on the parameter record good cannot construct without it the deliverable metadata needed anyway so a recipient can reproduce a comment in a config file not a place lost on the first copy

Figure — three places the convention can live, in descending order of reliability.

In the operation identifier. A registered EPSG operation names its method — 9606 or 9607 — so recording the operation code records the convention exactly, with no room for transcription error. This is the best answer whenever the operation is registered.

In the parameter record itself. Where the parameters are local or unregistered, the structure that holds them should have a convention field with a constrained set of values, so that a set without one cannot be constructed. The enumeration in the implementation above exists for that reason.

In the deliverable’s metadata block. Even when the pipeline is internally consistent, a recipient who wants to reproduce a coordinate needs the convention, and it is not implied by anything else they receive.

What does not work is a comment in a configuration file, or a note in a project document. Both are separated from the numbers on the first copy-paste, and the resulting error is a metre or more with no diagnostic signature beyond a failed comparison against control.

Frequently Asked Questions

Which convention does EPSG use?

Both, as two distinct methods: 9606 is position vector and 9607 is coordinate frame, and a registered operation names which one it uses. That is the authoritative answer for any operation with an EPSG code, and it is another reason to record the operation identifier rather than only the seven numbers.

Is one convention more correct than the other?

No — they are two descriptions of the same physical relationship, and each is standard in a different community. Geodesy and the IERS conventions favour position vector; some national and photogrammetric practice favours coordinate frame. What matters is that the convention travels with the parameters.

Can I detect the convention from the parameters alone?

Not reliably. Nothing in the magnitudes distinguishes them, and the sign pattern is not a tell because either convention can produce any sign pattern. You need one independently known control pair, which is what the detection function above uses — and if you have no control pair, you have no way to check anything else about the parameter set either.

Does the convention affect the derived uncertainty?

Not its magnitude: the rotation covariance transforms the same way under either convention, so the propagated uncertainty is identical. It affects the correlations between the rotation and translation parameters by a sign, which matters if you are converting a published covariance rather than a published parameter set — a detail covered in propagating covariance through a Helmert transformation.

Does the convention matter for a three-parameter transformation?

No — with no rotations there is nothing to disagree about, and the two conventions coincide exactly. That is worth knowing because it explains why a pipeline can work for years on translation-only shifts and break the first time somebody supplies a seven-parameter set: the code never had a convention, and the default it acquired was whichever the implementer assumed.

The summary, if only one line survives: the two conventions differ by the sign of all three rotations, the difference is metres at the Earth’s surface, and nothing in the numbers reveals which one a parameter set belongs to.