degeneracy.py
7.0 kB · python · 205 lines
1import numpy as np2from scipy.sparse import coo_matrix3from scipy.sparse.csgraph import connected_components45CODE = 76BASE = 27TOL = 1e-98LEVELS = list(range(1, 9))9SWEEP = [1e-12, 1e-11, 1e-10, 1e-09, 1e-08, 1e-07, 1e-06, 1e-05]10SECOND = 1.0 - np.sqrt(30.0) / 6.011THIRD = 1.0 - 0.98833242156612NEAR = 1e-6131415def tile(code, q):16 cells = np.array([(code >> i) & 1 for i in range(q * q)], dtype=np.uint8)17 return cells.reshape(q, q)181920def fractal(code, q, level):21 unit = tile(code, q)22 out = unit23 for _ in range(1, level):24 out = np.kron(out, unit)25 return out262728def and_form(grid):29 side = grid.shape[0]30 rows = np.arange(side).reshape(-1, 1)31 cols = np.arange(side).reshape(1, -1)32 return np.array_equal(grid != 0, (rows & cols) == 0)333435def graph(grid):36 flat = grid.reshape(-1)37 index = np.full(flat.size, -1, dtype=np.int64)38 filled = np.flatnonzero(flat)39 index[filled] = np.arange(filled.size)40 index = index.reshape(grid.shape)41 pairs = []42 for axis in range(grid.ndim):43 low = np.take(index, np.arange(index.shape[axis] - 1), axis=axis)44 high = np.take(index, np.arange(1, index.shape[axis]), axis=axis)45 keep = (low >= 0) & (high >= 0)46 pairs.append(np.stack([low[keep], high[keep]], axis=1))47 return filled.size, np.concatenate(pairs)484950def one_component(nodes, edges):51 ones = np.ones(edges.shape[0])52 adj = coo_matrix((ones, (edges[:, 0], edges[:, 1])), shape=(nodes, nodes))53 count, _ = connected_components(adj, directed=False)54 return count == 1555657def laplacian(nodes, edges):58 degree = np.zeros(nodes)59 np.add.at(degree, edges[:, 0], 1.0)60 np.add.at(degree, edges[:, 1], 1.0)61 root = 1.0 / np.sqrt(degree)62 out = np.zeros((nodes, nodes))63 np.fill_diagonal(out, 1.0)64 weight = root[edges[:, 0]] * root[edges[:, 1]]65 out[edges[:, 0], edges[:, 1]] = -weight66 out[edges[:, 1], edges[:, 0]] = -weight67 return out686970def spectrum(level):71 grid = fractal(CODE, BASE, level)72 if not and_form(grid):73 raise SystemExit("kronecker fractal is not the AND set at level %d" % level)74 nodes, edges = graph(grid)75 if not one_component(nodes, edges):76 raise SystemExit("graph is disconnected at level %d" % level)77 return np.sort(np.linalg.eigvalsh(laplacian(nodes, edges)))787980def classes(mu, tol):81 starts = np.concatenate(([0], np.flatnonzero(np.diff(mu) > tol) + 1))82 ends = np.concatenate((starts[1:], [mu.size]))83 counts = ends - starts84 values = np.add.reduceat(mu, starts) / counts85 spreads = mu[ends - 1] - mu[starts]86 return values, counts, spreads878889def locate(values, target):90 at = int(np.argmin(np.abs(values - target)))91 if abs(values[at] - target) < NEAR:92 return at93 return -1949596def multiplicity(values, counts, target):97 at = locate(values, target)98 return int(counts[at]) if at >= 0 else 099100101def fib(n):102 a, b = 0, 1103 for _ in range(n):104 a, b = b, a + b105 return a106107108def main():109 print("object: design code %d, base %d, dimension 2, normalised Laplacian" % (CODE, BASE))110 print("clustering tolerance: %g" % TOL)111 print()112 print("level nodes distinct degenerate repeated mult_1 mult_second max_spread")113 table = {}114 top = None115 for level in LEVELS:116 mu = spectrum(level)117 values, counts, spreads = classes(mu, TOL)118 nodes = mu.size119 big = counts > 1120 distinct = int(values.size)121 degenerate = int(big.sum())122 repeated = float(counts[big].sum()) / nodes123 unit = multiplicity(values, counts, 1.0)124 second = multiplicity(values, counts, SECOND)125 spread = float(spreads[big].max()) if degenerate else 0.0126 print(127 "%d %d %d %d %.4f %d %d %.2e"128 % (level, nodes, distinct, degenerate, repeated, unit, second, spread)129 )130 third_at = locate(values, THIRD)131 table[level] = {132 "nodes": nodes,133 "distinct": distinct,134 "degenerate": degenerate,135 "unit": unit,136 "second": second,137 "second_value": values[locate(values, SECOND)] if second else None,138 "third": int(counts[third_at]) if third_at >= 0 else 0,139 "third_value": float(values[third_at]) if third_at >= 0 else None,140 "spread": spread,141 }142 if level == LEVELS[-1]:143 top = (mu, values, counts)144 print()145146 print("multiplicity of eigenvalue 1, levels 1 to 8")147 print(" measured: %s" % ", ".join(str(table[l]["unit"]) for l in LEVELS))148 print(" 3^(L-1): %s" % ", ".join(str(3 ** (l - 1)) for l in LEVELS))149 print()150151 print("multiplicity of the 1 -/+ sqrt(30)/6 family, levels 3 to 8")152 print(" measured: %s" % ", ".join(str(table[l]["second"]) for l in LEVELS[2:]))153 print(" 3^(L-3)+1: %s" % ", ".join(str(3 ** (l - 3) + 1) for l in LEVELS[2:]))154 worst = 0.0155 for level in LEVELS[1:]:156 worst = max(worst, abs(table[level]["second_value"] - SECOND))157 print(" 1 - sqrt(30)/6 = %.12f" % SECOND)158 print(" 1 + sqrt(30)/6 = %.12f" % (2.0 - SECOND))159 print(" worst deviation over levels 2 to 8: %.2e" % worst)160 print()161162 print("third family, levels 4 to 8")163 print(" eigenvalue sought: 1 - %.12f" % (1.0 - THIRD))164 print(" measured at level 8: 1 - %.12f" % (1.0 - table[8]["third_value"]))165 print(" measured: %s" % ", ".join(str(table[l]["third"]) for l in LEVELS[3:]))166 print(" 3^(L-4)+1: %s" % ", ".join(str(3 ** (l - 4) + 1) for l in LEVELS[3:]))167 _, values8, counts8 = top168 print(" classes at level 8 with multiplicity 82: %d" % int((counts8 == 82).sum()))169 print()170171 print("counting fits")172 print(" distinct: %s" % ", ".join(str(table[l]["distinct"]) for l in LEVELS))173 print(" 2*Fib(2L)+1: %s" % ", ".join(str(2 * fib(2 * l) + 1) for l in LEVELS))174 print(" degenerate: %s" % ", ".join(str(table[l]["degenerate"]) for l in LEVELS[1:]))175 print(" 2*Fib(2L-3)-1: %s" % ", ".join(str(2 * fib(2 * l - 3) - 1) for l in LEVELS[1:]))176 print()177178 mu8, values8, counts8 = top179 print("reading the integers off a floating-point spectrum, level 8")180 print(" widest degenerate class spans %.2e" % table[8]["spread"])181 at = locate(values8, 1.0)182 print(" nearest distinct eigenvalue below 1: %.10f" % values8[at - 1])183 print(" nearest distinct eigenvalue above 1: %.10f" % values8[at + 1])184 print(" isolation of the class at 1: %.3f below, %.3f above"185 % (1.0 - values8[at - 1], values8[at + 1] - 1.0))186 print()187188 stop = 3 ** 9189 print("why level 9 stops under this method")190 print(" nodes at level 9: %d" % stop)191 print(" dense matrix before eigensolver workspace: %.2f GB" % (stop * stop * 8 / 1e9))192 print()193194 print("tolerance sweep at level 8")195 print(" tol distinct mult_1 mult_second")196 for tol in SWEEP:197 values, counts, _ = classes(mu8, tol)198 print(199 " %.0e %d %d %d"200 % (tol, values.size, multiplicity(values, counts, 1.0),201 multiplicity(values, counts, SECOND))202 )203204205main()