File size: 2,952 Bytes
570b87b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
"""Rotation + iterative RNN-stub motion tests."""

from __future__ import annotations

import sys
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, peak_direction
from hoa64.encode import encode_points
from hoa64.rotate import (
    rotate_matrix_order1,
    rotate_source_directions,
    rotate_yaw_pitch_roll,
)
from hoa64.rnn_stub import step_rotate, world_from_sources


def test_order1_yaw_matches_reencode():
    a = encode_points([0.0], [0.0], [1.0])
    # rotate field +90° yaw: front → left
    a_fast = rotate_matrix_order1(a, yaw=90.0)
    az, el = doa_from_intensity(a_fast)
    err = angular_error_deg(90.0, 0.0, az, el)
    assert err < 1.0, f"intensity DOA after yaw90: ({az},{el}) err={err}"


def test_source_dir_rotation_exact_reencode():
    """Ground truth: rotate source coordinates, re-encode."""
    az0, el0 = 20.0, -10.0
    a0 = encode_points([az0], [el0], [1.0])
    az1, el1 = rotate_source_directions(az0, el0, yaw=45.0)
    a1 = encode_points([float(az1)], [float(el1)], [1.0])
    # dense rotate of a0 by same yaw should ≈ a1 for low orders
    a_rot = rotate_yaw_pitch_roll(a0, yaw=45.0, max_order=3, n_azi=120, n_el=60)
    # compare order ≤ 3 channels
    nch = 16
    rel = np.linalg.norm(a_rot[:nch] - a1[:nch]) / (np.linalg.norm(a1[:nch]) + 1e-12)
    assert rel < 0.15, f"relative L2 order≤3 after rotate: {rel}"


def test_rnn_loop_keeps_tracking_after_turns():
    """Agent turns through 360° in steps; peak should stay world-stable in intensity
    when we interpret DOA in head frame correctly.

    After total yaw +90 (agent turns left), a world-front source is at head-right
    (−90) in listener frame if field is counter-rotated.
    """
    st = world_from_sources([0.0], [0.0], [1.0])
    # 6 steps of +15° agent yaw
    for _ in range(6):
        st = step_rotate(st, d_yaw=15.0, max_order=3, dense=True)
    assert abs(st.yaw - 90.0) < 1e-9
    az, el = doa_from_intensity(st.hoa)
    # front source, agent yawed +90 → source appears at az=-90 (right)
    err = angular_error_deg(-90.0, 0.0, az, el)
    assert err < 8.0, f"after +90 agent yaw, head-frame DOA=({az},{el}) err={err}"


def test_iterative_path_energy_stable_order1():
    st = world_from_sources([45.0], [0.0], [1.0])
    e0 = float(np.dot(st.hoa[:4], st.hoa[:4]))
    for _ in range(24):
        st = step_rotate(st, d_yaw=15.0, max_order=1, dense=False)
    e1 = float(np.dot(st.hoa[:4], st.hoa[:4]))
    # full 360° of order-1 rotations should preserve energy exactly
    assert abs(e1 - e0) / e0 < 1e-9


if __name__ == "__main__":
    for fn in [
        test_order1_yaw_matches_reencode,
        test_source_dir_rotation_exact_reencode,
        test_rnn_loop_keeps_tracking_after_turns,
        test_iterative_path_energy_stable_order1,
    ]:
        fn()
        print("OK", fn.__name__)