procrustes
Orthogonal Procrustes (Schönemann SVD) and brute-force signed-permutation alignment for Rust, built on faer.
Install
[]
= "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-forceO(K!·K)enumeration forK ≤ 3(small-K bit-parity preserved), Jonker-Volgenant linear assignmentO(K³)forK ≥ 4on the cost matrixC[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 asorthogonal; flips the last column ofUif 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 choices[k] = sign(⟨a[:, k], reference[:, k]⟩). Use when columns are already in the same canonical order asreferenceand only per-column sign is arbitrary — the typical PLS bootstrap pattern. For a general column-and-sign search, usesigned_permutation.generalized— iterative consensus alignment ofNmatrices 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 Mat;
let reference = from_fn;
// Apply a known 30° rotation in column space.
let theta = PI / 6.0;
let r0 = from_fn;
let a: = &reference * &r0;
// Recover R = R0ᵀ.
let aln = orthogonal.unwrap;
let residual = aln.residual_frobenius;
assert!;
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
InnerAligner — Orthogonal 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ł Lenartowicz — Freestyler Scientist · GitHub · ORCID