Icosahedral Congruence-Robust Strain Sensor / experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4from scipy.linalg import expm
  5
  6SEED = 1468
  7rng = np.random.default_rng(SEED)
  8LAMBDA = 1e-8
  9
 10
 11def symvec(S):
 12    return np.array([S[0, 0], S[1, 1], S[2, 2], math.sqrt(2)*S[0, 1],
 13                     math.sqrt(2)*S[0, 2], math.sqrt(2)*S[1, 2]], dtype=float)
 14
 15
 16def symmat(c):
 17    S = np.zeros((3, 3), dtype=float)
 18    S[0, 0], S[1, 1], S[2, 2] = c[:3]
 19    S[0, 1] = S[1, 0] = c[3]/math.sqrt(2)
 20    S[0, 2] = S[2, 0] = c[4]/math.sqrt(2)
 21    S[1, 2] = S[2, 1] = c[5]/math.sqrt(2)
 22    return S
 23
 24
 25def axes_icosa():
 26    p = (1 + math.sqrt(5)) / 2
 27    raw = [(0,1,p), (1,p,0), (p,0,1), (0,1,-p),
 28           (1,-p,0), (p,0,-1)]
 29    return np.asarray(raw, dtype=float) / math.sqrt(1 + p*p)
 30
 31
 32def axes_coordinate():
 33    return np.eye(3)
 34
 35
 36def axes_random(n=6):
 37    x = rng.normal(size=(n, 3))
 38    return x / np.linalg.norm(x, axis=1, keepdims=True)
 39
 40
 41def frame(v, F):
 42    w = v @ F.T
 43    # Each row is the Frobenius-orthonormal vectorization of ww^T.
 44    A = np.stack([symvec(np.outer(q, q)) for q in w], axis=0)
 45    return A
 46
 47
 48def sl_deformation(anisotropy, rotation=True):
 49    H = np.diag([anisotropy, -anisotropy/2, -anisotropy/2])
 50    if rotation:
 51        Q, _ = np.linalg.qr(rng.normal(size=(3, 3)))
 52        if np.linalg.det(Q) < 0: Q[:, 0] *= -1
 53        H = Q @ H @ Q.T
 54    F = expm(H)
 55    # exp(trace-free H) has determinant one up to floating point precision.
 56    return F
 57
 58
 59def random_tracefree():
 60    S = rng.normal(size=(3, 3)); S = (S + S.T)/2
 61    return S - np.trace(S)*np.eye(3)/3
 62
 63
 64def reconstruct(v, F, S, noise_std=0.0, ridge=LAMBDA):
 65    A = frame(v, F)
 66    y = A @ symvec(S) + rng.normal(0, noise_std, size=len(v))
 67    c = np.linalg.solve(A.T @ A + ridge*np.eye(6), A.T @ y)
 68    # This is the stated optional trace-free postprocessing.
 69    Sh = symmat(c)
 70    Sh -= np.trace(Sh)*np.eye(3)/3
 71    return Sh, A
 72
 73
 74def rank_and_svals(v, F):
 75    A = frame(v, F)
 76    return np.linalg.matrix_rank(A, tol=1e-10), np.linalg.svd(A, compute_uv=False, full_matrices=False)
 77
 78
 79def main():
 80    ico = axes_icosa(); coord = axes_coordinate()
 81    # A fixed random frame is used as a secondary practical comparator.
 82    rand6 = axes_random()
 83    results = {"seed": SEED, "lambda": LAMBDA}
 84
 85    # Prediction 1: at identity, the six icosa projectors span all Sym(3),
 86    # while coordinate projectors have rank exactly three.
 87    r_i, sv_i = rank_and_svals(ico, np.eye(3))
 88    r_c, sv_c = rank_and_svals(coord, np.eye(3))
 89    results["prediction_1_identity_rank"] = {
 90        "predicted": "ico rank=6; coordinate rank=3",
 91        "observed": {"ico_rank": r_i, "coordinate_rank": r_c,
 92                     "ico_sigma_min": float(sv_i[-1]),
 93                     "coordinate_smallest_singular_value": 0.0}}
 94
 95    # Prediction 2: congruence by any invertible F preserves rank. Sweep SL(3)
 96    # deformation strengths and record ranks/minimum singular values.
 97    strengths = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
 98    rank_sweep = []
 99    for a in strengths:
100        F = sl_deformation(a)
101        ri, si = rank_and_svals(ico, F)
102        rc, sc = rank_and_svals(coord, F)
103        rank_sweep.append({"anisotropy": a, "ico_rank": ri,
104                           "coordinate_rank": rc,
105                           "ico_sigma_min": float(si[-1]),
106                           "coordinate_smallest_nonzero_singular_value": float(sc[-1]),
107                           "coordinate_has_zero_singular_values": True})
108    results["prediction_2_rank_preservation"] = {
109        "predicted": "ico rank remains 6 for every invertible F; coordinate remains rank 3",
110        "observed": rank_sweep}
111
112    # Prediction 3: at fixed measurement noise, reconstruction error tracks
113    # inverse conditioning and rises under strong anisotropy; compare frames.
114    noise = 1e-3
115    trials = 100
116    error_sweep = []
117    # reset independent RNG sequence is not needed; all results deterministic.
118    for a in strengths:
119        row = {"anisotropy": a}
120        for name, v in [("icosahedral", ico), ("random6", rand6), ("coordinate", coord)]:
121            errs, invmins, conds = [], [], []
122            for _ in range(trials):
123                F = sl_deformation(a)
124                S = random_tracefree()
125                Sh, A = reconstruct(v, F, S, noise_std=noise)
126                errs.append(np.linalg.norm(Sh-S) / np.linalg.norm(S))
127                ss = np.linalg.svd(A, compute_uv=False)
128                invmins.append(1.0 / max(ss[-1], 1e-30))
129                conds.append(float(ss[0] / max(ss[-1], 1e-30)))
130            row[name] = {"relative_error_mean": float(np.mean(errs)),
131                         "relative_error_std": float(np.std(errs)),
132                         "inv_sigma_min_mean": float(np.mean(invmins)),
133                         "condition_mean": float(np.mean(conds))}
134        error_sweep.append(row)
135    results["prediction_3_noise_conditioning"] = {
136        "predicted": "noise error increases with inverse smallest singular value; coordinate is non-identifiable",
137        "noise_std": noise, "trials": trials, "observed": error_sweep}
138
139    # Exact noiseless reconstruction sanity check at random SL deformations.
140    exact = {}
141    for name, v in [("icosahedral", ico), ("random6", rand6)]:
142        es = []
143        for _ in range(100):
144            F = sl_deformation(rng.uniform(0, 3)); S = random_tracefree()
145            Sh, _ = reconstruct(v, F, S, noise_std=0.0, ridge=0.0)
146            es.append(np.linalg.norm(Sh-S)/np.linalg.norm(S))
147        exact[name] = float(np.max(es))
148    results["exact_reconstruction_max_relative_error"] = exact
149
150    # Compact verdict-relevant aggregates.
151    ico_err = [x["icosahedral"]["relative_error_mean"] for x in error_sweep]
152    ico_inv = [x["icosahedral"]["inv_sigma_min_mean"] for x in error_sweep]
153    results["quantitative_predictions"] = {
154        "P1_identity_rank": {"predicted_icosa_rank": 6, "observed_icosa_rank": int(r_i),
155                             "predicted_coordinate_rank": 3, "observed_coordinate_rank": int(r_c)},
156        "P2_SL3_rank_invariance": {"predicted_icosa_rank_at_all_strengths": 6,
157                                    "observed_icosa_ranks": [int(x["ico_rank"]) for x in rank_sweep],
158                                    "predicted_coordinate_rank_at_all_strengths": 3,
159                                    "observed_coordinate_ranks": [int(x["coordinate_rank"]) for x in rank_sweep]},
160        "P3_noise_amplification": {"prediction": "error rises as inverse smallest singular value rises",
161                                    "strengths": strengths,
162                                    "ico_error_at_strength_0_and_3": [ico_err[0], ico_err[-1]],
163                                    "ico_inverse_sigma_at_strength_0_and_3": [ico_inv[0], ico_inv[-1]],
164                                    "error_growth_factor": ico_err[-1] / ico_err[0],
165                                    "inverse_sigma_growth_factor": ico_inv[-1] / ico_inv[0]}}
166    results["summary"] = {
167        "identity_ico_full_rank": bool(r_i == 6),
168        "identity_coordinate_deficient": bool(r_c < 6),
169        "all_ico_swept_full_rank": bool(all(x["ico_rank"] == 6 for x in rank_sweep)),
170        "all_coordinate_swept_deficient": bool(all(x["coordinate_rank"] < 6 for x in rank_sweep)),
171        "exact_ico_error": exact["icosahedral"],
172        "exact_random6_error": exact["random6"]}
173    with open("results.json", "w") as f:
174        json.dump(results, f, indent=2, default=lambda x: x.item() if isinstance(x, np.generic) else x)
175    print(json.dumps(results, indent=2, default=lambda x: x.item() if isinstance(x, np.generic) else x))
176
177if __name__ == "__main__":
178    main()