procrustes 0.1.1

Orthogonal Procrustes and signed-permutation alignment via faer
Documentation

procrustes

crates.io docs.rs CI License: MIT OR Apache-2.0

Orthogonal Procrustes (Schönemann SVD) and brute-force signed-permutation alignment for Rust, built on faer.

Install

[dependencies]
procrustes = "0.1"

When to use

  • orthogonal — closed-form Schönemann SVD; O(M·K² + K³). Use whenever your alignment is a continuous rotation / reflection.
  • signed_permutation — exact discrete alignment of columns and per-column signs. Auto-routes by K: brute-force O(K!·K) enumeration for K ≤ 3 (small-K bit-parity preserved), Jonker-Volgenant linear assignment O(K³) for K ≥ 4 on the cost matrix C[i, j] = -|⟨a[:, i], reference[:, j]⟩|. Both paths return the global optimum. Use when columns are abstractly indexed (PLS components, ICA sources, eigenmaps) and you need a discrete match rather than a rotation.
  • rotation_only — orthogonal Procrustes restricted to proper rotations (det(R) = +1, R ∈ SO(K)). Same SVD path as orthogonal; flips the last column of U if the SVD-derived rotation is a reflection. Use when reflection is physically meaningless (chemistry, physics, rigid-body alignment) or sign convention must be preserved across independent calls.
  • sign_align — sign-only alignment, O(M·K) closed form. Per-column choice s[k] = sign(⟨a[:, k], reference[:, k]⟩). Use when columns are already in the same canonical order as reference and only per-column sign is arbitrary — the typical PLS bootstrap pattern. For a general column-and-sign search, use signed_permutation.
  • generalized — iterative consensus alignment of N matrices to a shared mean (GPA). See Generalised Procrustes Analysis (GPA) below.

Convention

Both functions return a transform T such that a · T ≈ reference minimizes the Frobenius norm under their respective constraints. For orthogonal, T = R is a K×K orthogonal matrix. For signed_permutation, T = P · diag(signs) where P is the permutation encoded by assigned; equivalently, column k of a · T equals signs[k] · a[:, assigned[k]]. Matches SciPy's (A @ R) - B minimization convention in scipy.linalg.orthogonal_procrustes.

faer coupling

MatRef<'_, f64> and Mat<f64> appear in the public API. The crate re-exports them as procrustes::{Mat, MatRef}, so consumers do not need a separate faer dependency. The pin is faer = "0.24" — caret-equivalent within the minor series — so patch bumps unify with downstream ^0.24 users automatically. Until faer reaches 1.0, any faer minor bump (= breaking, pre-1.0) is a procrustes major bump: a Dependabot watcher proposes the upgrade in a PR, and the bit-parity test in tests/bit_parity.rs is the tripwire that flags faer-side numerical drift even when the API still compiles.

Third-party code

The Jonker-Volgenant LAP solver in src/lap.rs is a stripped-down port of Antti/lapjv-rust v0.3.0 (MIT-licensed by Andrii Dmytrenko). See LICENSE-THIRDPARTY for the full notice.

Example

Recover a known rotation from a rotated copy of a reference matrix:

use procrustes::Mat;

let reference = Mat::<f64>::from_fn(4, 2, |i, j| match (i, j) {
    (0, 0) | (1, 1) => 1.0,
    (2, 0) | (3, 1) => 0.5,
    _ => 0.0,
});

// Apply a known 30° rotation in column space.
let theta = std::f64::consts::PI / 6.0;
let r0 = Mat::<f64>::from_fn(2, 2, |i, j| match (i, j) {
    (0, 0) | (1, 1) => theta.cos(),
    (0, 1) => -theta.sin(),
    (1, 0) => theta.sin(),
    _ => unreachable!(),
});
let a: Mat<f64> = &reference * &r0;

// Recover R = R0ᵀ.
let aln = procrustes::orthogonal(a.as_ref(), reference.as_ref(), true).unwrap();
let residual = aln.residual_frobenius(a.as_ref(), reference.as_ref());
assert!(residual < 1e-10);

For the discrete case, see signed_permutation in the API docs.

Generalised Procrustes Analysis (GPA)

generalized aligns N matrices to a common consensus via iterative inner Procrustes calls. Use it instead of fixed-reference alignment when the reference itself is a noisy estimate (e.g. PLS bootstrap CIs anchored to the original-sample fit). The inner aligner is selected via InnerAlignerOrthogonal is the morphometric default; use SignedPermutation when component order can vary across inputs (PLS bootstrap pattern).

License

Dual-licensed under MIT OR Apache-2.0 at the user's option.


Paweł LenartowiczFreestyler Scientist · GitHub · ORCID