|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +TFT demo: invariance + φ-lock → plot + WAV |
| 4 | +- Builds SPD tensor T |
| 5 | +- Rotates: T' = R T R^T |
| 6 | +- Shows invariants unchanged |
| 7 | +- Maps eigenvalues → audio freqs, synthesizes φ-locked stereo tone |
| 8 | +- Saves: figures/phase_cube.png, figures/tensor_demo.wav |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | +import os |
| 13 | +import pathlib |
| 14 | +import numpy as np |
| 15 | +import matplotlib.pyplot as plt |
| 16 | +from scipy.io import wavfile # lightweight, part of scipy |
| 17 | + |
| 18 | +from tft.resonance import ( |
| 19 | + spd_matrix, rotation_matrix, rotate_rank2, invariants, |
| 20 | + map_to_audio, phi_lock_pair, rng |
| 21 | +) |
| 22 | + |
| 23 | +ROOT = pathlib.Path(__file__).resolve().parents[1] |
| 24 | +FIGDIR = ROOT / "figures" |
| 25 | +FIGDIR.mkdir(parents=True, exist_ok=True) |
| 26 | + |
| 27 | +def synth_stereo(freqs, sr=48_000, dur=2.0, seed=1337): |
| 28 | + """Sum of cos tones, right channel is φ=π/2 quadrature of left.""" |
| 29 | + g = rng(seed) |
| 30 | + t = np.linspace(0.0, dur, int(sr*dur), endpoint=False) |
| 31 | + # Left: sum cos(2π f t) with tiny random phases for richness (seeded) |
| 32 | + phases = g.uniform(0, 2*np.pi, size=len(freqs)) |
| 33 | + left = np.sum([np.cos(2*np.pi*f*t + p) for f, p in zip(freqs, phases)], axis=0) |
| 34 | + # Right: quadrature partner (φ-lock) using analytic signal |
| 35 | + _, right = phi_lock_pair(left) |
| 36 | + # Normalize to avoid clipping |
| 37 | + peak = max(np.max(np.abs(left)), np.max(np.abs(right)), 1e-9) |
| 38 | + left /= peak |
| 39 | + right /= peak |
| 40 | + stereo = np.stack([left, right], axis=-1).astype(np.float32) |
| 41 | + return 48_000, stereo |
| 42 | + |
| 43 | +def plot_phase_cube(freqs, savepath): |
| 44 | + """ |
| 45 | + Toy 'phase cube' visualization: |
| 46 | + - x: normalized freq index |
| 47 | + - y: frequency (Hz) normalized |
| 48 | + - z: time phase sweep (0..2π) |
| 49 | + """ |
| 50 | + n = len(freqs) |
| 51 | + idx = np.linspace(0, 1, n) |
| 52 | + f_norm = (freqs - freqs.min()) / max(freqs.ptp(), 1e-9) |
| 53 | + phi = np.linspace(0, 2*np.pi, 400) |
| 54 | + X, Z = np.meshgrid(idx, phi) |
| 55 | + Y = np.interp(X, np.linspace(0,1,n), f_norm) |
| 56 | + # Simple quadrature surface |
| 57 | + V = np.cos(2*np.pi*Y*Z) # arbitrary “vibration” field |
| 58 | + fig = plt.figure(figsize=(7, 5)) |
| 59 | + ax = fig.add_subplot(111, projection="3d") |
| 60 | + ax.plot_surface(X, Y, V, linewidth=0, antialiased=True, alpha=0.9) |
| 61 | + ax.set_xlabel("Index (space)") |
| 62 | + ax.set_ylabel("Freq (conjugate)") |
| 63 | + ax.set_zlabel("Phase (time)") |
| 64 | + ax.set_title("TFT Phase Cube (toy)") |
| 65 | + fig.tight_layout() |
| 66 | + fig.savefig(savepath, dpi=140) |
| 67 | + plt.close(fig) |
| 68 | + |
| 69 | +def main(): |
| 70 | + seed = int(os.environ.get("TFT_SEED", "1337")) |
| 71 | + dim = int(os.environ.get("TFT_DIM", "3")) |
| 72 | + |
| 73 | + T = spd_matrix(dim=dim, seed=seed) |
| 74 | + R = rotation_matrix(dim=dim, seed=seed + 1) |
| 75 | + T_rot = rotate_rank2(T, R) |
| 76 | + |
| 77 | + inv0 = invariants(T) |
| 78 | + inv1 = invariants(T_rot) |
| 79 | + |
| 80 | + print("=== TFT Invariants Demo ===") |
| 81 | + print(f"Frobenius norm : {inv0['fro']:.8f} -> {inv1['fro']:.8f}") |
| 82 | + print(f"Eigenvalues : {inv0['eigvals']} -> {inv1['eigvals']}") |
| 83 | + print("(Should match up to numeric noise.)") |
| 84 | + |
| 85 | + # Audio mapping |
| 86 | + freqs = map_to_audio(inv0["eigvals"], fmin=220.0, fmax=880.0) |
| 87 | + sr, stereo = synth_stereo(freqs, sr=48_000, dur=2.0, seed=seed) |
| 88 | + |
| 89 | + wav_out = FIGDIR / "tensor_demo.wav" |
| 90 | + wavfile.write(wav_out.as_posix(), sr, stereo) |
| 91 | + print(f"Wrote WAV: {wav_out}") |
| 92 | + |
| 93 | + png_out = FIGDIR / "phase_cube.png" |
| 94 | + plot_phase_cube(freqs, png_out.as_posix()) |
| 95 | + print(f"Wrote figure: {png_out}") |
| 96 | + |
| 97 | +if __name__ == "__main__": |
| 98 | + main() |
0 commit comments