procrustes 0.1.0

Orthogonal Procrustes and signed-permutation alignment via faer
Documentation

procrustes

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 — brute-force O(K!·K) enumeration with optimal sign per permutation. Practical up to about K ≤ 10; for K ≳ 12 decompose by hand. Use when columns are abstractly indexed (PLS components, ICA sources, eigenmaps) and you need a discrete match rather than a rotation.
    • TODO (planned, future minor version): Hungarian-algorithm O(K³) fast path to lift the K ≤ 10 ceiling. The cost matrix C[i, j] = -|⟨a[:, i], reference[:, j]⟩| is exactly a linear assignment problem after the closed-form sign reduction.

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.

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.

License

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


Paweł LenartowiczFreestyler Scientist · GitHub · ORCID