File size: 2,772 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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
"""Encode / decode / DOA hypothesis 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,
    field_energy,
    peak_direction,
)
from hoa64.decode import beamform, decode_directions
from hoa64.encode import encode_plane_waves, encode_points, mix
from hoa64.basis import sh_sn3d


def test_encode_matches_basis():
    a = encode_points([45.0], [10.0], [1.0])
    y = sh_sn3d(45.0, 10.0)
    np.testing.assert_allclose(a, y, atol=1e-12)


def test_beamform_peaks_at_source():
    a = encode_points([30.0], [-15.0], [1.0])
    on = float(beamform(a, 30.0, -15.0))
    off = float(beamform(a, -150.0, 40.0))
    assert on > off
    assert on > 0.5  # SN3D self-inner-product roughly order-dependent


def test_intensity_doa_cardinal():
    for az, el in [(0, 0), (90, 0), (-90, 0), (0, 45)]:
        a = encode_points([az], [el], [1.0])
        az_h, el_h = doa_from_intensity(a)
        err = angular_error_deg(az, el, az_h, el_h)
        assert err < 2.0, f"src=({az},{el}) hat=({az_h},{el_h}) err={err}"


def test_peak_direction_near_source():
    a = encode_points([120.0], [25.0], [1.0])
    paz, pel, pval = peak_direction(a, n_azi=120, n_el=60)
    err = angular_error_deg(120.0, 25.0, paz, pel)
    assert err < 5.0, f"peak=({paz},{pel}) err={err}"
    assert pval > 0


def test_superposition_linearity():
    a1 = encode_points([0], [0], [1.0])
    a2 = encode_points([90], [0], [0.5])
    m = mix(a1, a2)
    np.testing.assert_allclose(m, a1 + a2)
    assert field_energy(m) > field_energy(a1)


def test_plane_wave_time_series():
    t = np.linspace(0, 1, 100, endpoint=False)
    sig = np.sin(2 * np.pi * 5 * t)[None, :]  # (1, T)
    a = encode_plane_waves([0.0], [0.0], sig)
    assert a.shape == (64, 100)
    # W channel tracks the signal
    np.testing.assert_allclose(a[0], sig[0], atol=1e-12)
    # X (front) also tracks for front source
    np.testing.assert_allclose(a[3], sig[0], atol=1e-12)


def test_two_source_separation_energy():
    a = mix(
        encode_points([0], [0], [1.0]),
        encode_points([180], [0], [1.0]),
    )
    front = float(beamform(a, 0, 0))
    back = float(beamform(a, 180, 0))
    side = float(beamform(a, 90, 0))
    assert front > side
    assert back > side


if __name__ == "__main__":
    for fn in [
        test_encode_matches_basis,
        test_beamform_peaks_at_source,
        test_intensity_doa_cardinal,
        test_peak_direction_near_source,
        test_superposition_linearity,
        test_plane_wave_time_series,
        test_two_source_separation_energy,
    ]:
        fn()
        print("OK", fn.__name__)