flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mg_precond_analyze.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""WO-M — spectrum of the densely assembled multigrid preconditioner (float vs double storage).
3
4Reads the matrices written by `mg_dense_precond` and answers, for each configuration, the ONE
5question WO-M was opened on: is the V-cycle preconditioner M indefinite because the coarse operator
6is a bad approximation (VOF_PLAN S3, coefficient-aware coarsening), or because the operator is
7STORED in float (mac_cutcell_mg.hpp MReal) and float rounding breaks A*1 = 0 at eps_f32 x contrast?
8
9Both builds run the identical code path on the identical problem; only MReal differs.
10
11Method note. sym(M) is singular BY CONSTRUCTION: M's inputs are mean-removed and its output is
12mean-removed at the end of the level-0 V-cycle, so the constant vector is an exact null direction.
13An UNPIVOTED LDL^T on such a matrix produces one pivot at round-off whose SIGN is noise — which is
14why WO-H's "1 negative pivot (-1.1e-12)" at ratio 1e2 must not be read as indefiniteness. This
15script therefore reports both:
16 * the raw unpivoted LDL^T pivots, for direct comparability with the WO-H table, and
17 * the eigenvalues of sym(M) deflated onto the mean-free subspace (P sym(M) P, P = I - 11^T/n),
18 where a negative eigenvalue that is not at round-off is unambiguous indefiniteness.
19
20Usage: python mg_precond_analyze.py <dir-with-*.bin> [more dirs...]
21"""
22import glob
23import os
24import re
25import sys
26
27import numpy as np
28
29MAGIC = 0x50434D47444E5331
30
31
32def load(path):
33 with open(path, "rb") as f:
34 hdr = np.fromfile(f, dtype=np.int64, count=3)
35 if hdr[0] != MAGIC:
36 raise ValueError(f"{path}: bad magic {hdr[0]:x}")
37 rows, cols = int(hdr[1]), int(hdr[2])
38 a = np.fromfile(f, dtype=np.float64, count=rows * cols)
39 return a.reshape(rows, cols)
40
41
43 """Unpivoted LDL^T pivots of a symmetric matrix (the WO-H instrument, reproduced)."""
44 n = S.shape[0]
45 A = S.astype(np.float64).copy()
46 d = np.zeros(n)
47 L = np.eye(n)
48 for j in range(n):
49 d[j] = A[j, j] - (L[j, :j] ** 2 * d[:j]).sum()
50 if abs(d[j]) < 1e-300:
51 continue
52 for i in range(j + 1, n):
53 L[i, j] = (A[i, j] - (L[i, :j] * L[j, :j] * d[:j]).sum()) / d[j]
54 return d
55
56
58 """kappa(A) of the FINE operator on the mean-free subspace, and the attainable-accuracy floor it
59 implies. Classical result (Greenbaum; Higham NLA ch.19): a CG-type iteration cannot drive the
60 TRUE relative residual below O(eps * kappa(A)) whatever the preconditioner, because round-off in
61 the recurrence enters at that level. So this column says what a solve on this operator could
62 ever reach in each storage precision — the ceiling any precision policy is measured against, and
63 the reason a fixed rtol of 1e-8 stops being meaningful once kappa exceeds ~1e8 in fp64."""
64 A = load(path)
65 n = A.shape[0]
66 S = 0.5 * (A + A.T)
67 one = np.ones(n) / np.sqrt(n)
68 e1 = np.zeros(n)
69 e1[0] = 1.0
70 w = one - e1
71 nw = np.linalg.norm(w)
72 H = np.eye(n) - 2.0 * np.outer(w, w) / (nw * nw) if nw > 1e-14 else np.eye(n)
73 Q = H[:, 1:]
74 ev = np.abs(np.linalg.eigvalsh(Q.T @ S @ Q))
75 kappa = float(ev.max() / max(ev.min(), 1e-300))
76 return dict(kappa=kappa, floor_f32=2.0 ** -24 * kappa, floor_f64=2.0 ** -53 * kappa)
77
78
79def analyse(path):
80 M = load(path)
81 n = M.shape[0]
82 S = 0.5 * (M + M.T)
83 fnorm = np.linalg.norm(M)
84 skew = np.linalg.norm(M - M.T) / (fnorm + 1e-300)
85
86 d = ldl_pivots(S) if n <= 1024 else np.array([np.nan]) # the O(n^3) python LDL is 8^3-sized
87 scale = np.max(np.abs(d))
88 # a pivot below 1e-10 of the largest is at round-off: the constant null direction, sign is noise
89 real_neg = np.sum((d < 0) & (np.abs(d) > 1e-10 * scale))
90 any_neg = int(np.sum(d < 0))
91 min_piv = float(d.min())
92
93 # Restrict sym(M) to the mean-free subspace and take its true spectrum there. Build an
94 # orthonormal basis Q of the complement of the constant (Householder) and diagonalise Q^T S Q —
95 # NOT "deflate and drop the smallest eigenvalue", which at high contrast would discard a genuine
96 # negative instead of the deflated zero.
97 one = np.ones(n) / np.sqrt(n)
98 e1 = np.zeros(n)
99 e1[0] = 1.0
100 w = one - e1
101 nw = np.linalg.norm(w)
102 H = np.eye(n) - 2.0 * np.outer(w, w) / (nw * nw) if nw > 1e-14 else np.eye(n)
103 Q = H[:, 1:] # columns span {1}^perp
104 ev = np.linalg.eigvalsh(Q.T @ S @ Q)
105 lmax = ev.max()
106 neg_ev = int(np.sum(ev < -1e-10 * lmax))
107 return dict(
108 n=n,
109 skew=skew,
110 any_neg=any_neg,
111 real_neg=int(real_neg),
112 min_piv=min_piv,
113 lmin=float(ev.min()),
114 lmax=float(lmax),
115 neg_ev=neg_ev,
116 )
117
118
119def main(dirs):
120 files = []
121 for d in dirs:
122 files += sorted(glob.glob(os.path.join(d, "M_*.bin")))
123 if not files:
124 print("no M_*.bin found")
125 return 1
126 rows = []
127 for p in files:
128 m = re.match(r"M_(\w+)_(\w+)_n(\d+)_L(\d+)_r([0-9e+.]+)\.bin", os.path.basename(p))
129 if not m:
130 continue
131 prec, geom, nn, lv, ratio = m.groups()
132 r = analyse(p)
133 ap = os.path.join(os.path.dirname(p), "A" + os.path.basename(p)[1:])
134 r.update(analyse_operator(ap) if os.path.exists(ap) else
135 dict(kappa=float("nan"), floor_f32=float("nan"), floor_f64=float("nan")))
136 r.update(prec=prec, geom=geom, grid=int(nn), levels=int(lv), ratio=float(ratio))
137 rows.append(r)
138
139 rows.sort(key=lambda r: (r["geom"], r["ratio"], r["prec"]))
140 print()
141 print(
142 f"{'geom':<9}{'ratio':>8} {'prec':<7}{'skew':>10}"
143 f"{'LDL neg':>9}{'(raw)':>7}{'min pivot':>12}"
144 f"{'lambda_min':>13}{'lambda_max':>12}{'neg eig':>9}"
145 f"{'kappa(A)':>11}{'floor f32':>11}{'floor f64':>11}"
146 )
147 print("-" * 129)
148 for r in rows:
149 print(
150 f"{r['geom']:<9}{r['ratio']:>8.0e} {r['prec']:<7}{r['skew']:>10.2e}"
151 f"{r['real_neg']:>9}{r['any_neg']:>7}{r['min_piv']:>12.2e}"
152 f"{r['lmin']:>13.3e}{r['lmax']:>12.3e}{r['neg_ev']:>9}"
153 f"{r['kappa']:>11.2e}{r['floor_f32']:>11.2e}{r['floor_f64']:>11.2e}"
154 )
155 print()
156 print("LDL neg = pivots below -1e-10*max|pivot| (real indefiniteness);")
157 print("(raw) = every negative pivot including the round-off/null-direction sign noise;")
158 print("neg eig = negative eigenvalues of sym(M) on the mean-free subspace (the verdict column);")
159 print("kappa(A), floor = the FINE operator's condition number on the mean-free subspace and the")
160 print(" O(eps*kappa) attainable relative residual it implies in each storage precision.")
161 return 0
162
163
164if __name__ == "__main__":
165 sys.exit(main(sys.argv[1:] or ["."]))
int main()
Definition dbg_scene.cpp:13