Implementing the Faddeev-LeVerrier algorithm in APL

The Faddeev-LeVerrier algorithm is one of my favourite algorithms related to linear algebra for a variety of reasons:
- It’s incredibly elegant
- It computes many things at once - the trace of a matrix, its determinant, inverse and the characteristic polynomial (and thus, its eigenvalues).
- The task of computing the characteristic polynomial usually requires the use of symbolic computation - one needs to compute $\det(A - \lambda I)$ to get a function of $\lambda$, while the Faddeev-LeVerrier algorithm can do it numerically.
Initial considerations
The objective of the Faddeev-LeVerrier algorithm is to compute coefficients of the characteristic polynomial of a square matrix.
$$ p_{A}(\lambda )\equiv \det(\lambda I_{n}-A)=\sum_{k=0}^{n}c_{k}\lambda ^{k} $$
The first observation to be made is that $c_n = 1$ and $c_0 = (-1)^n \det A$. The algorithm states:
$$ M_{0} \equiv 0 \\ M_{k} \equiv AM_{k-1}+c_{n-k+1}I \\ c_{n-k} =-{\frac {1}{k}}\mathrm {tr} (AM_{k}) \\ $$
A few observations assuming a $n$ by $n$ matrix $A$:
- $M_{n+1} = 0$.
- The trace of $A$ is equal to $-c_2$.
- $\det A$ is equal to $(-1)^n c_0$ (a consequence of the previous observations about $c$).
- $A^{-1}$ is equal to $-\frac{1}{c_0}M_k$.
An implementation
I start my implementation by asserting a few things about the environment and the input:
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
}
I’m interested only in matrices (arrays of rank 2 - 2=≢⍴⍵
) and I specifically want them square -=/⍴⍵
. If the conditions aren’t met, the algorithm returns an empty coefficient vector.
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
n←≢⍵⋄M0←⍵⋄I←n n⍴1↑⍨1+n
}
Next up, I define n
as the number of rows/columns of the input matrix, M0
as the starting matrix for the algorithm, and I
as an identity matrix of order n
. The actual recursive implementation of the algorithm will return a vector of the characteristic polynomial coefficients and the matrix corresponding to the current iteration. Since the matrix corresponding to the last iteration is simply n n⍴0
(per observation 1), I will discard it:
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
M0←⍵⋄I←n n⍴1↑⍨1+n⋄⊃ {
⍵=0:1 I
} n
}
I have also added a guard for the final iteration, which simply returns the identity matrix and 1
as the coefficient.
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
M0←⍵⋄I←n n⍴1↑⍨1+n⋄⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
} n
}
I recursively call the function asking for the pair of $M_{n-1}$ and the corresponding c
value. I also multiply matrices M0
(the input matrix) and MP
(the previous matrix), as it’ll prove itself useful in upcoming computations, first one of them being the computation of c
for the current iteration. For this purpose, I take the trace of X
and divide it by the negated iteration index. The dyadic transposition (axis rearrangement) 0 0⍉X
yields the leading diagonal of X
, while +/
sums it.
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
M0←⍵⋄I←n n⍴1↑⍨1+n⋄⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵
} n
}
I am one step away from the final version of the algorithm, the only thing left is returning the result tuple:
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
M0←⍵⋄I←n n⍴1↑⍨1+n⋄⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵⋄(cp,c)(X+I×c)
} n
}
The computation of the current M
matrix is derived verbatim from the second equation of the algorithm.
Applications
I will combine today’s post with the yesterday’s one about polynomial roots. Namely, the yesterday’s post already contains an example use of my faddeev_leverrier
function. Let’s do something different today.
With a few not so beautiful hacks on top of my existing function, I can compute all the properties of my test matrix:
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
inv←trace←⍬
M0←⍵⋄I←n n⍴1↑⍨1+n⋄cpoly←⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵
MC←X+I×c
_←{⍵=n-1:inv∘←MC⋄0}⍵
_←{⍵=1:trace∘←-c⋄0}⍵
(cp,c)MC
} n
(inv×-÷⊃⌽cpoly) trace cpoly ((⊃⌽cpoly)ׯ1*n)
}
The function will return a vector of:
- inverse matrix
- trace
- characteristic polynomial
- determinant
data
3 1 5
3 3 1
4 6 4
faddeev_leverrier data
┌─────────────────┬──┬───────────┬──┐
│ 0.15 0.65 ¯0.35│10│1 ¯10 4 ¯40│40│
│¯0.2 ¯0.2 0.3 │ │ │ │
│ 0.15 ¯0.35 0.15│ │ │ │
└─────────────────┴──┴───────────┴──┘
The results appear to match my pencil and paper calculations (not presented in this blog post - as an exercise, feel free to compute these properties of my test matrix yourself). Of course, there is a way to compute the remaining properties besides the characteristic polynomial using more idiomatic APL:
'alt'⎕cy'dfns'
demo←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
(⌹⍵) (+/0 0⍉⍵) (-alt× ⍵)
}
The results appear to be the same:
demo data
┌─────────────────┬──┬──┐
│ 0.15 0.65 ¯0.35│10│40│
│¯0.2 ¯0.2 0.3 │ │ │
│ 0.15 ¯0.35 0.15│ │ │
└─────────────────┴──┴──┘
To my surprise, though, it appears that the Faddeev-LeVerrier algorithm is faster for this particular test case! The benchmark data:
cmpx 'demo data' 'faddeev_leverrier data'
demo data → 2.7E¯5 | 0% ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
* faddeev_leverrier data → 1.5E¯5 | -45% ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
demo data
┌─────────────────┬──┬──┐
│ 0.15 0.65 ¯0.35│10│40│
│¯0.2 ¯0.2 0.3 │ │ │
│ 0.15 ¯0.35 0.15│ │ │
└─────────────────┴──┴──┘
faddeev_leverrier data
┌─────────────────┬──┬───────────┬──┐
│ 0.15 0.65 ¯0.35│10│1 ¯10 4 ¯40│40│
│¯0.2 ¯0.2 0.3 │ │ │ │
│ 0.15 ¯0.35 0.15│ │ │ │
└─────────────────┴──┴───────────┴──┘
Replacing the generalised alternant with a (hopefully) more performant determinant function yields oddly similar results:
cmpx 'demo data' 'faddeev_leverrier data'
demo data → 2.0E¯5 | 0% ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
* faddeev_leverrier data → 1.5E¯5 | -25% ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
Eigenvectors
The final part of my blog post will shed some more light on eigenvectors. Let’s compute the eigenvalues first:
50 solve faddeev_leverrier data
0J2 0J¯2 10
For simplicity, I will present the pencil and paper calculations for $\lambda = 10$ alongside the APL code. I start with computing $A - \lambda I$:
eigenvec←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
n←≢⍵⋄I←n n⍴1↑⍨1+n⋄s←⍵-⍺×I
}
$$ A - \lambda I = \begin{vmatrix} -7 & 1 & 5\\ 3 & -7 & 1\\ 4 & 6 & -6 \end{vmatrix} $$
$(A - \lambda I) \cdot v = 0$ yields a homogenous equation system which needs to be solved. A very sad property of this system is that it’s underspecified, so $x_{k-1}$ up to $n$ are all functions of $x_n$. Surprisingly, that’s not a problem, though! If we ignore the presence of $x_n$, turns out one row of the matrix is redundant. If we assume that $x_n = 1$, in our example, we solve a non-homogenous system of equations with two variables.
eigenvec←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
n←≢⍵⋄I←n n⍴1↑⍨1+n⋄s←⍵-⍺×I
N←{¯9 ¯11+.○(⊢×⎕ct<|)9 11∘.○⍵}
q←1,⍨1↑⍨1-⍨⊃⌽⍴s⋄N¨1,⍨∊⌹⍨∘-/q⊂1↓s
}
$$ x_1 = \frac{18}{23} \\ x_2 = \frac{11}{23} \\ x_3 = 1 $$
Of course, one could scale $x_3$ as they please, and modify $x_1$ and $x_2$ accordingly. I also use the same real/imaginary truncation technique as outlined in the previous post to strip (likely) unnecessary real/imaginary parts. Finally, I test my code:
data∘(eigenvec⍨)¨ 50 solve faddeev_leverrier data
┌───────────┬───────────┬───────────────────────────┐
│¯1J¯1 0J1 1│¯1J1 0J¯1 1│0.7826086957 0.4782608696 1│
└───────────┴───────────┴───────────────────────────┘
The results seem to match. Great!
Summary
I believe that today’s post was much more involved than the previous ones. The full source code follows:
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
M0←⍵⋄I←n n⍴1↑⍨1+n⋄⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵⋄(cp,c)(X+I×c)
} n
}
demo←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
(⌹⍵) (+/0 0⍉⍵) (-alt× ⍵)
}
⍝ A less general version that uses det instead.
demo←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
(⌹⍵) (+/0 0⍉⍵) (det ⍵)
}
eigenvec←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬
n←≢⍵⋄I←n n⍴1↑⍨1+n⋄s←⍵-⍺×I
N←{¯9 ¯11+.○(⊢×⎕ct<|)9 11∘.○⍵}
q←1,⍨1↑⍨1-⍨⊃⌽⍴s⋄N¨1,⍨∊⌹⍨∘-/q⊂1↓s
}
⍝ Alternatively, the "more powerful" Faddeev-LeVerrier algorithm implementation.
faddeev_leverrier←{
⎕io ⎕ml←0 1⋄(≠/⍴⍵)∨2≠≢⍴⍵:⍬⋄n←≢⍵
inv←trace←⍬
M0←⍵⋄I←n n⍴1↑⍨1+n⋄cpoly←⊃ {
⍵=0:1 I⋄(cp MP)←∇⍵-1⋄X←M0+.×MP
c←(+/0 0⍉X)÷-⍵
MC←X+I×c
_←{⍵=n-1:inv∘←MC⋄0}⍵
_←{⍵=1:trace∘←-c⋄0}⍵
(cp,c)MC
} n
(inv×-÷⊃⌽cpoly) trace cpoly ((⊃⌽cpoly)ׯ1*n)
}