HOA7 Spatial Field Decoder (hoa64 v0.5.0): 7th-order Ambisonics encode/decode, Wigner-D rotation, DOA analysis, vision fuse, diffusion conditioning
570b87b verified | """Phase 2: Wigner-D HOA rotation accuracy and speed.""" | |
| from __future__ import annotations | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from hoa64.analysis import angular_error_deg, doa_from_intensity | |
| from hoa64.encode import encode_points | |
| from hoa64.rotate import rotate_matrix_order1, rotate_source_directions, rotate_yaw_pitch_roll | |
| from hoa64.wigner import hoa_rotation_matrix, rotation_matrix_zyx, apply_hoa_rotation | |
| def test_order1_wigner_matches_cartesian(): | |
| a = encode_points([25.0], [-12.0], [1.0]) | |
| for yaw, pitch, roll in [(90, 0, 0), (0, 45, 0), (30, -20, 15), (180, 0, 0)]: | |
| a_w = rotate_yaw_pitch_roll( | |
| a, yaw=yaw, pitch=pitch, roll=roll, max_order=1, method="wigner" | |
| ) | |
| a_c = rotate_matrix_order1(a, yaw=yaw, pitch=pitch, roll=roll) | |
| np.testing.assert_allclose(a_w[:4], a_c[:4], atol=1e-9, rtol=1e-9) | |
| def test_wigner_matches_plane_wave_reencode(): | |
| """Ground truth: rotate source direction, re-encode.""" | |
| cases = [ | |
| (0.0, 0.0, 90.0, 0.0, 0.0), | |
| (40.0, -15.0, 35.0, 0.0, 0.0), | |
| (10.0, 20.0, 0.0, 40.0, 0.0), | |
| (-70.0, 5.0, 20.0, -25.0, 30.0), | |
| ] | |
| for az0, el0, yaw, pitch, roll in cases: | |
| a0 = encode_points([az0], [el0], [1.0], max_order=7) | |
| az1, el1 = rotate_source_directions(az0, el0, yaw=yaw, pitch=pitch, roll=roll) | |
| a_gt = encode_points([float(az1)], [float(el1)], [1.0], max_order=7) | |
| a_w = rotate_yaw_pitch_roll( | |
| a0, yaw=yaw, pitch=pitch, roll=roll, max_order=7, method="wigner" | |
| ) | |
| # Relative error on full 64-vector | |
| denom = np.linalg.norm(a_gt) + 1e-15 | |
| rel = np.linalg.norm(a_w[:64] - a_gt[:64]) / denom | |
| assert rel < 1e-6, ( | |
| f"rel={rel:.3e} for src=({az0},{el0}) rot=({yaw},{pitch},{roll})" | |
| ) | |
| def test_wigner_orthogonal_blocks(): | |
| R = rotation_matrix_zyx(33.0, -17.0, 8.0, degrees=True) | |
| M = hoa_rotation_matrix(R, max_order=7) | |
| # Each order block should be orthogonal (rotation) | |
| for n in range(0, 8): | |
| i0 = n * n | |
| dim = 2 * n + 1 | |
| B = M[i0 : i0 + dim, i0 : i0 + dim] | |
| I = B.T @ B | |
| np.testing.assert_allclose(I, np.eye(dim), atol=1e-8) | |
| def test_wigner_faster_than_dense(): | |
| a = encode_points([15.0], [10.0], [1.0], max_order=7) | |
| # warm-up | |
| rotate_yaw_pitch_roll(a, yaw=20.0, pitch=10.0, roll=5.0, method="wigner") | |
| rotate_yaw_pitch_roll( | |
| a, yaw=20.0, pitch=10.0, roll=5.0, method="dense", n_azi=48, n_el=24 | |
| ) | |
| t0 = time.perf_counter() | |
| for _ in range(50): | |
| rotate_yaw_pitch_roll(a, yaw=20.0, pitch=10.0, roll=5.0, method="wigner") | |
| t_w = time.perf_counter() - t0 | |
| t0 = time.perf_counter() | |
| for _ in range(5): | |
| rotate_yaw_pitch_roll( | |
| a, yaw=20.0, pitch=10.0, roll=5.0, method="dense", n_azi=48, n_el=24 | |
| ) | |
| t_d = time.perf_counter() - t0 | |
| # per-call times | |
| tw = t_w / 50 | |
| td = t_d / 5 | |
| # Wigner should be substantially faster (typically 50–1000×) | |
| assert tw < td, f"wigner {tw:.4f}s not faster than dense {td:.4f}s" | |
| print(f" timing: wigner={tw*1e3:.3f} ms/call dense={td*1e3:.3f} ms/call speedup={td/tw:.0f}x") | |
| def test_stream_rotation_CT(): | |
| # (C,T) path | |
| from hoa64.encode import encode_plane_waves | |
| t = np.linspace(0, 1, 32, endpoint=False) | |
| sig = np.sin(2 * np.pi * 3 * t)[None, :] | |
| hoa = encode_plane_waves([0.0], [0.0], sig, max_order=3) | |
| out = rotate_yaw_pitch_roll(hoa, yaw=90.0, max_order=3, method="wigner") | |
| assert out.shape[0] >= 16 | |
| # DOA of first frame energy via products | |
| W, Y, Z, X = out[0], out[1], out[2], out[3] | |
| I = np.array([np.mean(W * X), np.mean(W * Y), np.mean(W * Z)]) | |
| n = np.linalg.norm(I) | |
| assert n > 1e-9 | |
| if __name__ == "__main__": | |
| test_order1_wigner_matches_cartesian() | |
| print("OK test_order1_wigner_matches_cartesian") | |
| test_wigner_matches_plane_wave_reencode() | |
| print("OK test_wigner_matches_plane_wave_reencode") | |
| test_wigner_orthogonal_blocks() | |
| print("OK test_wigner_orthogonal_blocks") | |
| test_wigner_faster_than_dense() | |
| print("OK test_wigner_faster_than_dense") | |
| test_stream_rotation_CT() | |
| print("OK test_stream_rotation_CT") | |