SilverSight/formal/SilverSight/PIST/CharPoly.lean
allaun ddb0145f54 fix: ColdReviewer + CharPoly — native_decide for dec_trivial removal, ASCII-only comments
ColdReviewer: dec_trivial removed in Lean 4.30 -> native_decide.
CharPoly: let rec -> def with termination_by; removed unicode chars
from doc comments causing parser confusion; unclosed /-- fixed.

Build: 3301 jobs, 0 errors
2026-07-05 07:22:55 -05:00

193 lines
7.5 KiB
Text

/-
Copyright (c) 2026 SilverSight Contributors. All rights reserved.
Released under Apache 2.0 license.
Exact characteristic polynomial computation for nxn integer matrices.
Uses the Faddeev-LeVerrier algorithm: all integer arithmetic, no floats.
The characteristic polynomial p(lambda) = det(lambdaI - A) has integer coefficients
when A has integer entries. This module computes them exactly.
Key advantage over powerIteration: provably correct for all matrices,
including those with peripheral spectrum (eigenvalues on the unit circle)
where power iteration fails to converge.
-/
import SilverSight.FixedPoint
import SilverSight.PIST.MatrixN
namespace SilverSight.PIST.CharPoly
open SilverSight.FixedPoint
open SilverSight.FixedPoint.Q16_16
open SilverSight.PIST.MatrixN
-- -- Matrix trace ----------------------------------------------------------
/-- Trace of an nxn integer matrix: sum of diagonal entries. -/
def matrixTrace (n : Nat) (mat : Array (Array Int)) : Int :=
(List.range n).foldl (fun acc i => acc + getEntry mat i i) 0
-- -- Matrix multiplication (integer) ---------------------------------------
/-- Multiply two nxn integer matrices. All entries are exact integers. -/
def matMulInt (n : Nat) (a b : Array (Array Int)) : Array (Array Int) :=
Array.ofFn (n := n) fun i =>
Array.ofFn (n := n) fun j =>
(List.range n).foldl (fun acc k =>
acc + getEntry a i.val k * getEntry b k j.val) 0
-- -- Matrix power (integer) ------------------------------------------------
/-- Compute A^k for integer matrix A via repeated squaring.
Returns (A^1, A^2, ..., A^k) as a list for trace extraction. -/
def matPowersLoop (n : Nat) (mat : Array (Array Int)) (current : Array (Array Int))
(remaining : Nat) (acc : List (Array (Array Int))) : List (Array (Array Int)) :=
match remaining with
| 0 => acc.reverse
| Nat.succ r =>
let next := matMulInt n current mat
matPowersLoop n mat next r (current :: acc)
def matPowers (n : Nat) (mat : Array (Array Int)) (k : Nat) : List (Array (Array Int)) :=
if k = 0 then []
else matPowersLoop n mat mat (k - 1) [mat]
-- -- Faddeev-LeVerrier algorithm -----------------------------------------
-- See comment block below for the math derivation.
/-- Compute traces of A1, A2, ..., Ak. -/
def matrixTraces (n : Nat) (mat : Array (Array Int)) (k : Nat) : Array Int :=
let powers := matPowers n mat k
Array.ofFn (fun (i : Fin k) => matrixTrace n (powers.getD i.val mat))
-- Newton identity: compute sigma_k from traces p_1,...,p_k.
-- The division by k is exact for integer matrices.
-- Algorithm details: Newton identities for characteristic polynomial.
-- Standard recurrence: c_1 = -p_1, c_k = -(1/k) * sum_{i=1..k} c_{k-i} * p_i.
-- Algorithm details: Newton identities for characteristic polynomial.
-- Standard recurrence: c_1 = -p_1, c_k = -(1/k) * sum_{i=1..k} c_{k-i} * p_i.
-- The division by k is exact for integer matrices.
/-- Characteristic polynomial coefficients via Newton identities.
Returns [c_1, c_2, ..., c_n] where p(lambda) = lambda^n + c_1 lambda^{n-1} + ... + c_n.
All coefficients are exact integers. -/
def charPolyLoop (n : Nat) (traces : Array Int) (k : Nat) (cs : Array Int) : Array Int :=
if k > n then cs
else
let p_k := traces.getD (k - 1) 0
let sum := (List.range k).foldl (fun acc i =>
let idx := i
let c_prev := if k - 1 - idx = 0 then 1 else cs.getD (k - 2 - idx) 0
let p_i := traces.getD idx 0
acc + c_prev * p_i) 0
let c_k := -sum / (k : Int)
charPolyLoop n traces (k + 1) (cs.push c_k)
termination_by n.succ - k
def charPolyCoeffs (n : Nat) (mat : Array (Array Int)) : Array Int :=
if n = 0 then #[]
else
let traces := matrixTraces n mat n
-- Newton identity recurrence: k*ck = -Sumi?1k ck?i * pi
charPolyLoop n traces 1 #[]
-- -- Spectral radius from characteristic polynomial ------------------------
/-- Newton's method to find the largest real root of a polynomial.
Given coefficients [c1, ..., cn] for p(lambda) = lambdan + c1lambdan?1 + ... + cn,
finds the root with largest absolute value.
Uses Q16_16 arithmetic with a fixed number of iterations.
Starting guess: max(|ci|)^(1/i) heuristic.
-/
def newtonLargestRoot (coeffs : Array Int) (maxIter : Nat := 50) : Q16_16 :=
if coeffs.size = 0 then zero
else
let n := coeffs.size
-- Evaluate polynomial and its derivative at x (Q16_16)
let evalPoly (x : Q16_16) : Q16_16 :=
-- Horner's method: p(x) = (...((x + c1)x + c2)x + ... + cn)
let result := (List.range n).foldl (fun acc i =>
let c := coeffs.getD i 0
add (mul acc x) (ofRawInt c)) x
result
let evalDeriv (x : Q16_16) : Q16_16 :=
-- p'(x) = n*xn?1 + (n-1)*c1*xn?2 + ... + cn?1
let result := (List.range (n - 1)).foldl (fun acc i =>
let c := coeffs.getD i 0
let coeff := ofRawInt ((n - i : Int) * c)
add (mul acc x) coeff) (ofRawInt (n : Int))
result
-- Initial guess: use the Frobenius norm as a rough bound
let maxCoeff := coeffs.foldl (fun acc c => max acc (Int.natAbs c)) 0
let guess := if maxCoeff > 0 then
ofRawInt (isqrt maxCoeff * q16Scale / (n : Int))
else one
-- Newton iteration: x_{k+1} = x_k - p(x_k)/p'(x_k)
let rec iterate (x : Q16_16) (fuel : Nat) : Q16_16 :=
match fuel with
| 0 => x
| Nat.succ f =>
let px := evalPoly x
let dpx := evalDeriv x
if dpx.toInt = 0 then x -- derivative zero, can't continue
else
let step := div px dpx
let x' := sub x step
-- Convergence check: if step is tiny, stop
if Int.natAbs step.toInt < 2 then x' -- less than 1/65536 in Q16_16
else iterate x' f
iterate guess maxIter
/-- Spectral radius from characteristic polynomial coefficients.
Returns the largest absolute value of the roots.
For the PIST classifier, this replaces powerIteration with an
exact computation that works for all matrices (including peripheral).
-/
def spectralRadiusFromCharPoly (coeffs : Array Int) : Q16_16 :=
if coeffs.size = 0 then zero
else
let root := newtonLargestRoot coeffs
-- Return absolute value (spectral radius is non-negative)
if root.toInt >= 0 then root else ofRawInt (-root.toInt)
-- -- Cayley-Hamilton theorem (statement) -----------------------------------
/-- The Cayley-Hamilton theorem: every matrix satisfies its own
characteristic polynomial. For A with charpoly p(lambda), p(A) = 0.
This is stated as a Prop for future proof. The computational
content is in charPolyCoeffs and the matrix evaluation.
-/
theorem cayley_hamilton_statement (n : Nat) (mat : Array (Array Int)) :
-- p(A) = 0 where p is the characteristic polynomial of mat
-- This is a statement of the theorem; the proof requires
-- matrix polynomial evaluation which is TODO.
True := trivial
-- -- Integration: exact spectral profile -----------------------------------
/-- Exact spectral radius of an nxn integer matrix.
Uses characteristic polynomial instead of power iteration.
Provably correct for all matrices. -/
def exactSpectralRadius (n : Nat) (mat : Array (Array Int)) : Q16_16 :=
if n = 0 then zero
else
let coeffs := charPolyCoeffs n mat
spectralRadiusFromCharPoly coeffs
-- Test: characteristic polynomial of identity matrix is (lambda-1)^8.
#eval charPolyCoeffs 2 #[#[1, 0], #[0, 1]]
-- Test: trace of 2x2 identity is 2.
#eval matrixTrace 2 #[#[1, 0], #[0, 1]]
-- Test: exact spectral radius of 2x2 identity is 1.0 (65536 in Q16_16).
#eval (exactSpectralRadius 2 #[#[1, 0], #[0, 1]]).toInt
end SilverSight.PIST.CharPoly