nodal.py
6.5 kB · python · 144 lines
1import sys2import time3import numpy as np4from scipy.sparse import coo_matrix5from scipy.sparse.csgraph import connected_components67ZERO = 1e-98MULT = 1e-89SHOW = 1210BINS = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])1112def carpet(level):13 tile = np.ones((3, 3), dtype=np.uint8)14 tile[1, 1] = 015 grid = np.ones((1, 1), dtype=np.uint8)16 for _ in range(level):17 grid = np.kron(grid, tile)18 assert int(grid.sum()) == 8 ** level19 return grid2021def edges(grid):22 index = -np.ones(grid.shape, dtype=np.int64)23 filled = np.argwhere(grid == 1)24 index[filled[:, 0], filled[:, 1]] = np.arange(len(filled))25 right = (grid[:, :-1] == 1) & (grid[:, 1:] == 1)26 down = (grid[:-1, :] == 1) & (grid[1:, :] == 1)27 a = np.concatenate([index[:, :-1][right], index[:-1, :][down]])28 b = np.concatenate([index[:, 1:][right], index[1:, :][down]])29 return len(filled), a, b3031def laplacian(n, a, b):32 lap = np.zeros((n, n))33 lap[a, b] = -1.034 lap[b, a] = -1.035 lap[np.arange(n), np.arange(n)] = -lap.sum(axis=1)36 return lap3738def nodal(n, a, b, v):39 sign = np.where(v > ZERO, 1, np.where(v < -ZERO, -1, 0))40 keep = (sign[a] == sign[b]) & (sign[a] != 0)41 graph = coo_matrix((np.ones(int(keep.sum())), (a[keep], b[keep])), shape=(n, n))42 parts, _ = connected_components(graph, directed=False)43 zeros = int((sign == 0).sum())44 return parts - zeros, zeros4546def clusters(values):47 n = len(values)48 last = np.zeros(n, dtype=np.int64)49 mult = np.zeros(n, dtype=np.int64)50 start = 051 for i in range(1, n + 1):52 if i == n or values[i] - values[i - 1] > MULT:53 last[start:i] = i54 mult[start:i] = i - start55 start = i56 return last, mult5758def report(name, n, a, b, values, vectors):59 k = np.arange(1, n + 1)60 nu = np.zeros(n, dtype=np.int64)61 zeros = np.zeros(n, dtype=np.int64)62 for i in range(n):63 nu[i], zeros[i] = nodal(n, a, b, vectors[:, i])64 last, mult = clusters(values)65 ratio = nu / k66 hist = np.histogram(np.minimum(ratio, 1.0), bins=BINS)[0]67 violations = nu > k68 degenerate_k = int((mult > 1).sum())69 degenerate_classes = len(set(last[mult > 1].tolist()))70 classes = len(set(last.tolist()))71 tail = ratio[1:]72 print(f"{name}: {n} nodes, {len(a)} edges, lambda_2 = {values[1]:.10f}, lambda_max = {values[-1]:.10f}")73 print(f" nu_k, k = 1..{SHOW}: {nu[:SHOW].tolist()}")74 print(f" r_k, k = 1..{SHOW}: {mult[:SHOW].tolist()}")75 print(f" zero cells, k = 1..{SHOW}: {zeros[:SHOW].tolist()}")76 print(f" max nu_k = {nu.max()} at k = {int(k[nu.argmax()])}; over k >= 2 max nu_k/k = {tail.max():.6f} at k = {int(tail.argmax()) + 2}, mean nu_k/k = {tail.mean():.6f}")77 print(f" equality nu_k = k at k = {k[nu == k][:SHOW].tolist()}, {int((nu == k).sum())} times")78 print(f" Courant nu_k <= k: {int((~violations).sum())} of {n}, fraction {(~violations).mean():.6f}; violations {int(violations.sum())}, of which at degenerate k {int((mult[violations] > 1).sum())}")79 print(f" DGLS nu_k <= k + r_k - 1: {int((nu <= last).sum())} of {n}, fraction {(nu <= last).mean():.6f}")80 print(f" eigenvalue classes {classes}, degenerate classes {degenerate_classes}, degenerate indices {degenerate_k} of {n}, fraction {degenerate_k / n:.6f}, max multiplicity {int(mult.max())}")81 print(f" eigenvectors with a zero cell: {int((zeros > 0).sum())}")82 top = vectors[:, -1]83 sign = np.sign(top) * (np.abs(top) > ZERO)84 print(f" top eigenvector: zero cells {int(zeros[-1])}, min |v| = {np.abs(top).min():.3e}, edges joining two nonzero cells of one sign: {int(((sign[a] == sign[b]) & (sign[a] != 0)).sum())}")85 print(f" nu_k/k in [0,0.2) [0.2,0.4) [0.4,0.6) [0.6,0.8) [0.8,1]: {hist.tolist()}, above 1: {int((ratio > 1).sum())}")86 return nu, mult, last8788def mixing(n, a, b, vectors, mult):89 found = []90 for i in np.flatnonzero(mult == 2)[::2]:91 u, v = vectors[:, i], vectors[:, i + 1]92 counts = [nodal(n, a, b, w)[0] for w in (u, v, (u + v) / np.sqrt(2), (u - v) / np.sqrt(2))]93 if len(set(counts)) > 1:94 found.append((i + 1, counts))95 print(f" double eigenvalues where the two returned vectors, their sum and their difference disagree in nu: {len(found)} of {int((mult == 2).sum()) // 2}; first at k = {found[0][0] if found else None}, counts {found[0][1] if found else None}")9697def separable(side):98 i = np.arange(side)99 lam = 2.0 - 2.0 * np.cos(np.pi * np.arange(side) / side)100 cos = np.cos(np.pi * np.outer(np.arange(side), i + 0.5) / side)101 items = sorted(((lam[p] + lam[q], p, q) for p in range(side) for q in range(side)))102 vectors = np.empty((side * side, side * side))103 expect = np.empty(side * side, dtype=np.int64)104 for col, (_, p, q) in enumerate(items):105 vectors[:, col] = np.outer(cos[p], cos[q]).ravel()106 expect[col] = (p + 1) * (q + 1)107 return np.array([t[0] for t in items]), vectors, expect108109def run_carpet(level):110 t = time.time()111 n, a, b = edges(carpet(level))112 values, vectors = np.linalg.eigh(laplacian(n, a, b))113 assert connected_components(coo_matrix((np.ones(len(a)), (a, b)), shape=(n, n)), directed=False)[0] == 1114 nu, mult, last = report(f"carpet L = {level}", n, a, b, values, vectors)115 mixing(n, a, b, vectors, mult)116 print(f" {time.time() - t:.1f} s")117 print()118119def run_grid(side):120 t = time.time()121 n, a, b = edges(np.ones((side, side), dtype=np.uint8))122 values, vectors = np.linalg.eigh(laplacian(n, a, b))123 nu, mult, last = report(f"grid {side} x {side}, numpy basis", n, a, b, values, vectors)124 mixing(n, a, b, vectors, mult)125 svalues, svectors, expect = separable(side)126 assert np.abs(svalues - values).max() < 1e-9127 snu, smult, slast = report(f"grid {side} x {side}, separable basis", n, a, b, svalues, svectors)128 print(f" separable nu_k equals (p + 1)(q + 1) for all k: {bool((snu == expect).all())}")129 print(f" {time.time() - t:.1f} s")130 print()131132def main():133 t = time.time()134 print(f"domain: carpet L = 3, 4 on 4-neighbour adjacency, combinatorial Laplacian D - A, numpy.linalg.eigh; zero tolerance {ZERO:g}, multiplicity tolerance {MULT:g}; controls grid 22 x 22 and 64 x 64")135 print("nu_k counts strong nodal domains of the k-th returned eigenvector; on a degenerate eigenvalue the count depends on the basis, so every nu_k below is a fact about numpy's returned basis only")136 print()137 for level in (3, 4):138 run_carpet(level)139 for side in (22, 64):140 run_grid(side)141 print(f"total {time.time() - t:.1f} s")142143if __name__ == "__main__":144 main()