from svd import svd
from schur import schur
from linalg import test_matrix
#| The classical quintic, which fixes 1 and converges to the polar factor.
POLAR_A = 1.875
POLAR_B = -1.25
POLAR_C = 0.375
#| Rounds of it `polar_factor` runs: enough to have converged.
POLAR_STEPS = 32
#| Muon's, from the nanogpt speedrun. Faster off the mark, and the fixed point
#| is not 1.
MUON_A = 3.4445
MUON_B = -4.775
MUON_C = 2.0315
#| Rounds of it Muon runs, as the speedrun does.
MUON_STEPS = 5
def frob(A: Arr) {
return sqrt(sum(A * A))
}
def ns_poly(sigma, a, b, c) {
return a * sigma + b * sigma * sigma * sigma + c * sigma * sigma * sigma * sigma * sigma
}
def ns_step(X: Arr, a, b, c) {
S = X @ transpose(X)
return a * X + b * (S @ X) + c * ((S @ S) @ X)
}
def ns_normalize(A: Arr) {
f = frob(A)
if f == 0.0 {
return A
}
return (1.0 / f) * A
}
def newton_schulz(A: Arr, steps, a, b, c) {
X = ns_normalize(A)
for k to steps {
X = ns_step(X, a, b, c)
}
return X
}
def polar_factor(A: Arr) {
return newton_schulz(A, POLAR_STEPS, POLAR_A, POLAR_B, POLAR_C)
}
def muon_direction(G: Arr) {
return newton_schulz(G, MUON_STEPS, MUON_A, MUON_B, MUON_C)
}
def polar_h(A: Arr, U: Arr) {
return transpose(U) @ A
}
def singular_values(A: Arr) {
m, n = shape(A)
s = zeros(m)
svd(transpose(A), zeros(n, m), s, zeros(m, m), n, m)
return s
}
def row_orthonormality(U: Arr) {
G = U @ transpose(U)
return max(abs(G - eye(len(G))))
}
def polar_test_matrix(want: Arr, n, seed) {
m = len(want)
T = zeros(m, m)
Q1 = zeros(m, m)
T2 = zeros(n, n)
Q2 = zeros(n, n)
schur(test_matrix(m, seed), T, Q1)
schur(test_matrix(n, seed + 700.0), T2, Q2)
return (Q1 * want) @ transpose(Q2[:, 0:m])
}
test "the scalar polynomial is what the matrix iteration does to each singular value" {
# the classical one fixes 1 exactly; Muon's does not
assert_near(ns_poly(1.0, POLAR_A, POLAR_B, POLAR_C), 1.0, 1.0e-15)
assert_near(ns_poly(1.0, MUON_A, MUON_B, MUON_C), 0.701, 1.0e-12)
# and Muon starts nearly twice as fast from near zero
assert_near(ns_poly(1.0e-6, POLAR_A, POLAR_B, POLAR_C) / 1.0e-6, POLAR_A, 1.0e-9)
assert_near(ns_poly(1.0e-6, MUON_A, MUON_B, MUON_C) / 1.0e-6, MUON_A, 1.0e-9)
assert_lt(1.8, MUON_A / POLAR_A)
}
test "Muon's polynomial does not converge: it keeps bouncing inside a band" {
lo = 1.0e30
hi = 0.0
nstarts = 60
niter = 40
# the iterates up to this one are the transient
transient = 9
for a to nstarts {
sigma = 0.005 + 0.016 * a
for k to niter {
sigma = ns_poly(sigma, MUON_A, MUON_B, MUON_C)
# look only at the late iterates, once the transient is gone
if k > transient {
lo = min(lo, sigma)
hi = max(hi, sigma)
}
}
}
# measured: everything late lives in [0.682, 1.134]
assert_lt(0.68, lo)
assert_lt(hi, 1.14)
# and it is a band, not a point: forty more steps do not shrink it
assert_lt(0.4, hi - lo)
# 1 is not a fixed point, so there is nothing there to converge to
assert_lt(0.29, abs(ns_poly(1.0, MUON_A, MUON_B, MUON_C) - 1.0))
}
test "the classical iteration converges to the polar factor lib/svd computes" {
n = 7
A = polar_test_matrix([3.0, 2.0, 1.4, 0.8, 0.5], n, 31.0)
m = len(A)
U = polar_factor(A)
assert_lt(row_orthonormality(U), 1.0e-12)
# the polar factor from the decomposition: A^T = W S V^T, so A = V S W^T
# and the polar factor of A is V W^T
W = zeros(n, m)
s = zeros(m)
V = zeros(m, m)
svd(transpose(A), W, s, V, n, m)
assert_lt(max(abs(U - V @ transpose(W))), 1.0e-10)
}
test "the polar factor is a factor: A = U H with H symmetric and non-negative" {
n = 6
A = polar_test_matrix([2.5, 1.1, 0.9, 0.4], n, 77.0)
U = polar_factor(A)
H = polar_h(A, U)
# H is n by n, symmetric, and A = U H
assert_lt(max(abs(U @ H - A)), 1.0e-10)
assert_lt(max(abs(H - transpose(H))), 1.0e-10)
vals = zeros(n)
vecs = zeros(n, n)
eig_sym(H, vals, vecs)
assert_lt(-1.0e-10, vals[0])
# its nonzero eigenvalues are the singular values of A
assert_near(vals[n - 1], 2.5, 1.0e-9)
}
test "a matrix that is already orthonormal comes back unchanged" {
n = 6
T = zeros(n, n)
Q = zeros(n, n)
schur(test_matrix(n, 404.0), T, Q)
assert_lt(max(abs(polar_factor(Q) - Q)), 1.0e-11)
}
test "five Muon steps beat nine classical ones at evening out the singular values" {
A = polar_test_matrix([1.0, 0.3, 0.05, 0.005, 0.001], 8, 123.0)
m = len(A)
sv = singular_values(A)
assert_lt(500.0, sv[0] / sv[m - 1])
sv = singular_values(muon_direction(A))
muon5 = sv[0] / sv[m - 1]
sv = singular_values(newton_schulz(A, MUON_STEPS, POLAR_A, POLAR_B, POLAR_C))
slow5 = sv[0] / sv[m - 1]
sv = singular_values(newton_schulz(A, 9, POLAR_A, POLAR_B, POLAR_C))
slow9 = sv[0] / sv[m - 1]
assert_near(muon5, 2.60128, 1.0e-4)
assert_near(slow5, 45.1093, 1.0e-3)
assert_near(slow9, 3.72199, 1.0e-4)
# seventeen times tighter at the same count, and tighter at five than the
# classical polynomial gets at nine
assert_lt(15.0, slow5 / muon5)
assert_lt(muon5, slow9)
# and what comes out is not orthonormal, which it never needed to be
assert_lt(0.01, row_orthonormality(muon_direction(A)))
}
test "the polar factor is the nearest orthonormal matrix, which is what robotics wants it for" {
n = 3
# a rotation that has drifted, the way one does when it is integrated
R = zeros(n, n)
th = 0.7
R[0, 0] = cos(th)
R[0, 1] = -sin(th)
R[1, 0] = sin(th)
R[1, 1] = cos(th)
R[2, 2] = 1.0
R = R + 0.03 * test_matrix(n, 909.0)
assert_lt(0.01, row_orthonormality(R))
U = polar_factor(R)
assert_lt(row_orthonormality(U), 1.0e-12)
# and nothing orthonormal nearby is closer to R than U is
mine = sum((U - R) * (U - R))
ntrials = 40
for trial to ntrials {
T = zeros(n, n)
Q = zeros(n, n)
schur(test_matrix(n, 11.0 * trial + 3.0), T, Q)
# nudge Q toward U, then re-orthonormalize: a neighbor of U on the
# orthogonal group
N = polar_factor(U + 0.15 * Q)
assert_lt(mine, sum((N - R) * (N - R)) + 1.0e-12)
}
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors