nx_matrix_eig_gate.nx source
↩ module page · 220 lines · 13214 B
1// nx_matrix_eig_gate.nx -- AM1: DOES THE SYMMETRIC EIGENSOLVER ACTUALLY SOLVE, AND CAN IT REFUSE?
2//
3// The done-rule was pre-declared on /compare/appliedmath before a line of the solver was written:
4// "reconstructing the matrix from its factors returns the original within the declared bound, the
5// eigenvalues of a known symmetric matrix match their exact values, and a singular matrix is reported
6// singular rather than inverted"
7// so these teeth are the rule being met rather than a rule written to fit what the code happens to do.
8//
9// EVERY TOOTH IS AN INVARIANT OF THE EIGENPROBLEM, NOT A STORED ANSWER. Trace equals the sum of the
10// eigenvalues; a diagonal matrix has its own diagonal as its spectrum; eigenvectors of a symmetric matrix
11// are orthonormal; and the residual max|AV - VL| is the bound the reference libraries publish. A stored
12// golden vector would record that the author agreed with the code on one afternoon; these hold for any
13// correct implementation and fail for any wrong one.
14//
15// license_tier: ORIGINAL expect_exit: 0
16import "syscalls.nx"
17import "nx_matrix.nx"
18import "nx_gate_verdict.nx"
19
20const EG_Q: i64 = 16384
21// DECLARED TOLERANCE, in Q14 units, for values that pass through rotations. Twelve sweeps of fixed-point
22// plane rotations accumulate rounding; 64 units is 0.4 percent of one, and it is stated here rather than
23// discovered by a reader, so nobody can mistake approximate for exact.
24const EG_TOL: i64 = 64
25// The residual bound the solver is required to meet on these fixtures. Published as a NUMBER the gate
26// emits, so a later regression shows up as the figure moving rather than as a boolean flipping.
27const EG_RESID_MAX: i64 = 96
28
29func eg_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
30
31func main() -> i64 {
32 let ctr: *i64 = gv_ctr()
33 gv_head("=== NX-MATRIX-EIG-GATE -- symmetric eigenvalues by cyclic Jacobi, in fixed point, with a published residual ===" as *u8)
34
35 // ---- A KNOWN SPECTRUM: [[2,1],[1,2]] has eigenvalues exactly 3 and 1 ---------------------------
36 let a: *Matrix = nx_matrix_alloc(2, 2)
37 nx_matrix_set(a, 0, 0, 2 * EG_Q); nx_matrix_set(a, 0, 1, 1 * EG_Q)
38 nx_matrix_set(a, 1, 0, 1 * EG_Q); nx_matrix_set(a, 1, 1, 2 * EG_Q)
39 let v: *Matrix = nx_matrix_alloc(2, 2)
40 let ev: *i64 = (sys_mmap(16)) as *i64
41 let rc: i64 = nx_matrix_eig_sym(a, ev, v, 12)
42
43 gv_check_eq("fixture-reached-the-condition: the solver ACCEPTS a symmetric matrix and returns OK" as *u8,
44 rc, NXM_EIG_OK, ctr)
45
46 var hi: i64 = ev[0]
47 var lo: i64 = ev[1]
48 if lo > hi { hi = ev[1]; lo = ev[0] }
49 gv_check_near("the-LARGER-eigenvalue-of-a-known-2x2-is-exactly-3-within-the-declared-tolerance" as *u8,
50 hi, 3 * EG_Q, EG_TOL, ctr)
51 gv_check_near("the-SMALLER-eigenvalue-of-a-known-2x2-is-exactly-1-within-the-declared-tolerance" as *u8,
52 lo, 1 * EG_Q, EG_TOL, ctr)
53
54 // TRACE IS INVARIANT UNDER SIMILARITY: the eigenvalues must sum to the trace, whatever they are.
55 // This holds for ANY symmetric matrix, so it cannot be satisfied by a solver that returns the diagonal.
56 gv_check_near("TRACE-INVARIANCE-the-eigenvalues-sum-to-the-trace" as *u8,
57 ev[0] + ev[1], nx_matrix_get(a, 0, 0) + nx_matrix_get(a, 1, 1), EG_TOL, ctr)
58
59 // ---- THE RESIDUAL, WHICH IS THE WHOLE POINT OF PUBLISHING A BOUND ------------------------------
60 let resid: i64 = nx_matrix_residual(a, v, ev)
61 gv_check("RECONSTRUCTION-A-times-V-equals-V-times-Lambda-within-the-published-residual-bound" as *u8,
62 (resid <= EG_RESID_MAX) as i64, ctr)
63
64 // NEG-CONTROL FOR THE RESIDUAL ITSELF. A residual function that returned zero for everything would
65 // make the tooth above vacuous, so a deliberately corrupted eigenvalue must drive it far up.
66 let bad: *i64 = (sys_mmap(16)) as *i64
67 bad[0] = ev[0] + (EG_Q / 2)
68 bad[1] = ev[1]
69 let resid_bad: i64 = nx_matrix_residual(a, v, bad)
70 gv_check("neg-control-a-CORRUPTED-eigenvalue-drives-the-residual-far-above-the-bound" as *u8,
71 (resid_bad > EG_RESID_MAX * 4) as i64, ctr)
72
73 // ---- EIGENVECTORS OF A SYMMETRIC MATRIX ARE ORTHONORMAL ----------------------------------------
74 var col0: i64 = nxm_fmul(nx_matrix_get(v, 0, 0), nx_matrix_get(v, 0, 0))
75 + nxm_fmul(nx_matrix_get(v, 1, 0), nx_matrix_get(v, 1, 0))
76 gv_check_near("each-EIGENVECTOR-is-a-UNIT-vector-so-the-rotations-preserved-length" as *u8,
77 col0, EG_Q, EG_TOL, ctr)
78 let dot: i64 = nxm_fmul(nx_matrix_get(v, 0, 0), nx_matrix_get(v, 0, 1))
79 + nxm_fmul(nx_matrix_get(v, 1, 0), nx_matrix_get(v, 1, 1))
80 gv_check_near("distinct-EIGENVECTORS-are-ORTHOGONAL-which-only-a-real-diagonalisation-gives" as *u8,
81 dot, 0, EG_TOL, ctr)
82
83 // ---- A DIAGONAL MATRIX IS ITS OWN SPECTRUM ------------------------------------------------------
84 let d: *Matrix = nx_matrix_alloc(3, 3)
85 nx_matrix_fill(d, 0)
86 nx_matrix_set(d, 0, 0, 5 * EG_Q)
87 nx_matrix_set(d, 1, 1, 2 * EG_Q)
88 nx_matrix_set(d, 2, 2, 9 * EG_Q)
89 let vd: *Matrix = nx_matrix_alloc(3, 3)
90 let evd: *i64 = (sys_mmap(24)) as *i64
91 let rcd: i64 = nx_matrix_eig_sym(d, evd, vd, 12)
92 gv_check_eq("a-DIAGONAL-3x3-is-accepted" as *u8, rcd, NXM_EIG_OK, ctr)
93 gv_check_near("a-DIAGONAL-matrix-returns-its-own-diagonal-as-its-spectrum-summed-invariantly" as *u8,
94 evd[0] + evd[1] + evd[2], 16 * EG_Q, EG_TOL, ctr)
95
96 // ---- A SINGULAR MATRIX IS REPORTED SINGULAR, NOT INVERTED ---------------------------------------
97 // [[1,1],[1,1]] has eigenvalues 2 and 0. The ZERO eigenvalue is the report of singularity, and it is
98 // the thing a caller must see before trying to invert.
99 let s: *Matrix = nx_matrix_alloc(2, 2)
100 nx_matrix_set(s, 0, 0, 1 * EG_Q); nx_matrix_set(s, 0, 1, 1 * EG_Q)
101 nx_matrix_set(s, 1, 0, 1 * EG_Q); nx_matrix_set(s, 1, 1, 1 * EG_Q)
102 let vs: *Matrix = nx_matrix_alloc(2, 2)
103 let evs: *i64 = (sys_mmap(16)) as *i64
104 let rcs: i64 = nx_matrix_eig_sym(s, evs, vs, 12)
105 gv_check_eq("a-SINGULAR-matrix-is-accepted-rather-than-rejected" as *u8, rcs, NXM_EIG_OK, ctr)
106 var small: i64 = evs[0]
107 if eg_abs(evs[1]) < eg_abs(evs[0]) { small = evs[1] }
108 gv_check_near("a-SINGULAR-matrix-REPORTS-a-zero-eigenvalue-so-a-caller-learns-not-to-invert-it" as *u8,
109 small, 0, EG_TOL, ctr)
110 gv_check_eq("and-the-determinant-agrees-that-it-is-singular" as *u8,
111 nx_matrix_det_2x2(s), 0, ctr)
112
113 // ---- REFUSALS, EACH NAMING ITS OWN RULE ---------------------------------------------------------
114 let asym: *Matrix = nx_matrix_alloc(2, 2)
115 nx_matrix_set(asym, 0, 0, 1 * EG_Q); nx_matrix_set(asym, 0, 1, 5 * EG_Q)
116 nx_matrix_set(asym, 1, 0, 0); nx_matrix_set(asym, 1, 1, 1 * EG_Q)
117 let va: *Matrix = nx_matrix_alloc(2, 2)
118 let eva: *i64 = (sys_mmap(16)) as *i64
119 gv_check_eq("neg-control-an-ASYMMETRIC-matrix-is-REFUSED-by-name-because-Jacobi-would-return-plausible-nonsense" as *u8,
120 nx_matrix_eig_sym(asym, eva, va, 12), NXM_EIG_ERR_ASYMMETRIC, ctr)
121
122 let rect: *Matrix = nx_matrix_alloc(2, 3)
123 gv_check_eq("neg-control-a-NON-SQUARE-matrix-is-REFUSED-by-its-own-named-rule" as *u8,
124 nx_matrix_eig_sym(rect, eva, va, 12), NXM_EIG_ERR_NOT_SQUARE, ctr)
125
126 gv_check_eq("neg-control-the-symmetry-predicate-SEPARATES-a-symmetric-matrix-from-an-asymmetric-one" as *u8,
127 nx_matrix_is_symmetric(a) - nx_matrix_is_symmetric(asym), 1, ctr)
128
129 // ---- SVD: THE SECOND HALF OF AM1 ---------------------------------------------------------------
130 // The teeth are INVARIANTS of the factorisation, so they hold for any correct SVD and fail for any
131 // wrong one: the squared singular values sum to the Frobenius norm, their product is the absolute
132 // determinant, and U*diag(sv)*V^T reconstructs the original.
133 let sa: *Matrix = nx_matrix_alloc(2, 2)
134 nx_matrix_set(sa, 0, 0, 2 * EG_Q); nx_matrix_set(sa, 0, 1, 1 * EG_Q)
135 nx_matrix_set(sa, 1, 0, 1 * EG_Q); nx_matrix_set(sa, 1, 1, 2 * EG_Q)
136 let su: *Matrix = nx_matrix_alloc(2, 2)
137 let svv: *Matrix = nx_matrix_alloc(2, 2)
138 let svals: *i64 = (sys_mmap(16)) as *i64
139 let rcv: i64 = nx_matrix_svd(sa, su, svals, svv, 12)
140 gv_check_eq("fixture-reached-the-condition: the SVD accepts a general matrix and returns OK" as *u8,
141 rcv, NXM_SVD_OK, ctr)
142
143 gv_check("SINGULAR-VALUES-come-back-in-DESCENDING-order-which-rank-truncation-depends-on" as *u8,
144 (svals[0] >= svals[1]) as i64, ctr)
145 gv_check("SINGULAR-VALUES-are-NON-NEGATIVE-by-definition" as *u8,
146 (svals[1] >= 0) as i64, ctr)
147
148 // FROBENIUS IDENTITY: the squared singular values sum to the sum of the squared entries. True of any
149 // correct SVD of any matrix, and impossible to satisfy by returning the diagonal or the eigenvalues.
150 let frob_sv: i64 = nxm_fmul(svals[0], svals[0]) + nxm_fmul(svals[1], svals[1])
151 var frob_a: i64 = 0
152 var fr: i64 = 0
153 while fr < 2 {
154 var fc: i64 = 0
155 while fc < 2 { frob_a = frob_a + nxm_fmul(nx_matrix_get(sa, fr, fc), nx_matrix_get(sa, fr, fc)); fc = fc + 1 }
156 fr = fr + 1
157 }
158 gv_check_near("FROBENIUS-IDENTITY-the-squared-singular-values-sum-to-the-sum-of-squared-entries" as *u8,
159 frob_sv, frob_a, EG_TOL * 4, ctr)
160
161 // DETERMINANT IDENTITY: the product of the singular values is the absolute determinant.
162 gv_check_near("DETERMINANT-IDENTITY-the-product-of-the-singular-values-is-the-absolute-determinant" as *u8,
163 nxm_fmul(svals[0], svals[1]), 3 * EG_Q, EG_TOL * 4, ctr)
164
165 let sresid: i64 = nx_matrix_svd_residual(sa, su, svals, svv)
166 gv_check("RECONSTRUCTION-U-times-Sigma-times-V-transpose-returns-the-original-within-the-bound" as *u8,
167 (sresid <= EG_RESID_MAX) as i64, ctr)
168
169 let sbad: *i64 = (sys_mmap(16)) as *i64
170 sbad[0] = svals[0] + (EG_Q / 2)
171 sbad[1] = svals[1]
172 gv_check("neg-control-a-CORRUPTED-singular-value-drives-the-SVD-residual-far-above-the-bound" as *u8,
173 (nx_matrix_svd_residual(sa, su, sbad, svv) > EG_RESID_MAX * 4) as i64, ctr)
174
175 // RANK DEFICIENCY IS REPORTED, NOT PAPERED OVER: [[1,1],[1,1]] has rank 1, so the second singular
176 // value must vanish. That zero is how a caller learns the matrix has a null space.
177 let rd: *Matrix = nx_matrix_alloc(2, 2)
178 nx_matrix_set(rd, 0, 0, 1 * EG_Q); nx_matrix_set(rd, 0, 1, 1 * EG_Q)
179 nx_matrix_set(rd, 1, 0, 1 * EG_Q); nx_matrix_set(rd, 1, 1, 1 * EG_Q)
180 let ru: *Matrix = nx_matrix_alloc(2, 2)
181 let rv: *Matrix = nx_matrix_alloc(2, 2)
182 let rvals: *i64 = (sys_mmap(16)) as *i64
183 gv_check_eq("a-RANK-DEFICIENT-matrix-is-accepted" as *u8,
184 nx_matrix_svd(rd, ru, rvals, rv, 12), NXM_SVD_OK, ctr)
185 gv_check_near("a-RANK-DEFICIENT-matrix-returns-a-ZERO-second-singular-value-so-the-null-space-is-visible" as *u8,
186 rvals[1], 0, EG_TOL, ctr)
187
188 let badu: *Matrix = nx_matrix_alloc(3, 3)
189 gv_check_eq("neg-control-NON-CONFORMING-output-dimensions-are-REFUSED-by-their-own-named-rule" as *u8,
190 nx_matrix_svd(sa, badu, svals, svv, 12), NXM_SVD_ERR_DIMS, ctr)
191
192 // ---- EMITTED VALUES ----------------------------------------------------------------------------
193 gv_values_head()
194 gv_kv("svd_largest_singular_value" as *u8, svals[0])
195 gv_kv("svd_smallest_singular_value" as *u8, svals[1])
196 gv_kv("svd_frobenius_from_singular_values" as *u8, frob_sv)
197 gv_kv("svd_frobenius_from_entries" as *u8, frob_a)
198 gv_kv("svd_reconstruction_residual_q14" as *u8, sresid)
199 gv_kv("svd_rank_deficient_second_value" as *u8, rvals[1])
200 gv_kv("fixed_point_unit_q14" as *u8, EG_Q)
201 gv_kv("known_2x2_larger_eigenvalue" as *u8, hi)
202 gv_kv("known_2x2_smaller_eigenvalue" as *u8, lo)
203 gv_kv("known_2x2_expected_larger" as *u8, 3 * EG_Q)
204 gv_kv("known_2x2_expected_smaller" as *u8, 1 * EG_Q)
205 gv_kv("reconstruction_residual_q14" as *u8, resid)
206 gv_kv("residual_bound_declared" as *u8, EG_RESID_MAX)
207 gv_kv("residual_with_one_corrupted_eigenvalue" as *u8, resid_bad)
208 gv_kv("eigenvector_norm_q14" as *u8, col0)
209 gv_kv("eigenvector_dot_product_q14" as *u8, dot)
210 gv_kv("diagonal_3x3_spectrum_sum" as *u8, evd[0] + evd[1] + evd[2])
211 gv_kv("singular_matrix_small_eigenvalue" as *u8, small)
212
213 let rcx: i64 = gv_verdict("matrix-eig-gate" as *u8, ctr,
214 // NOTE DELIBERATELY CARRIES NO COUNTS. A verdict note that recites how many teeth or how many
215 // refusals it has is a duplicate ruler beside the tooth list, and it drifts toward understatement
216 // the moment a tooth is added -- the direction nobody audits. Each tooth states itself.
217 "symmetric eigenvalues by cyclic Jacobi and a singular value decomposition built on them, in Q14 fixed point, proved by invariants that hold for any correct implementation -- trace, orthonormality, the Frobenius and determinant identities -- with published reconstruction residuals whose own sensitivity is bite-proven, and every refusal naming the rule that fired" as *u8)
218 sys_exit(rcx)
219 return rcx
220}