Repository: claritychallenge/clarity (language: python)

Issue:
Unit tests for data module
Develop unit tests for the data module (see #80 for overview).

Specific modules within `data` are...

- [ ] `data/demo_data.py`
- [ ] `data/HOA_tools_cec2.py`
- [ ] `data/scene_builder_cec1.py`
- [ ] `data/scene_render_cec1.py`
- [ ] `data/scene_render_cec2.py`
- [ ] `data/utils.py` #88 


Discussion / hints:

Fix patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index dcfc2048b..1dfd9751b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,8 +5,15 @@
         "hasqi"
     ],
     "python.testing.pytestArgs": [
-        "tests"
+        "tests/",
+        "--cov",
+        "--cov-branch",
+        "--cov-report",
+        "html",
     ],
     "python.testing.unittestEnabled": false,
-    "python.testing.pytestEnabled": true
+    "python.testing.pytestEnabled": true,
+    "githubPullRequests.ignoredPullRequestBranches": [
+        "main"
+    ],
 }
\ No newline at end of file
diff --git a/clarity/data/HOA_tools_cec2.py b/clarity/data/HOA_tools_cec2.py
index 9ff0add33..83f61c16b 100644
--- a/clarity/data/HOA_tools_cec2.py
+++ b/clarity/data/HOA_tools_cec2.py
@@ -2,10 +2,12 @@
 from __future__ import annotations
 
 import logging
+from typing import Any
 
 import numpy as np
 from numba import njit
 from numba.typed import List as TypedList  # pylint: disable=no-name-in-module
+from numpy import ndarray
 from scipy.signal import convolve
 from scipy.spatial.transform import Rotation as R
 from scipy.special import comb
@@ -56,7 +58,7 @@ def convert_a_to_b_format(
 
 # Code for generation ambisonic rotation matrices
 @njit
-def compute_rotation_matrix(n: int, foa_rotmat: np.ndarray) -> np.ndarray:
+def compute_rotation_matrix(n: int, foa_rotmat: ndarray) -> ndarray:
     """Generate a rotation matrix to rotate HOA soundfield.
 
     Based on [1]_ and [2]_. Operates on HOA of a given order rotates by azimuth theta
@@ -92,7 +94,6 @@ def compute_rotation_matrix(n: int, foa_rotmat: np.ndarray) -> np.ndarray:
 
     sub_matrices = [np.eye(i * 2 + 1) for i in np.arange(n + 1)]
     sub_matrices[1] = foa_rotmat
-
     typed_sub_matrices = TypedList()
     for x in sub_matrices:
         typed_sub_matrices.append(x)
@@ -126,7 +127,13 @@ def centred_element(reference: np.ndarray, row: int, col: int):
 
 
 @njit
-def P(i, a, b, order, rotation_matrices):
+def P(
+    i: int,
+    a: int,
+    b: int,
+    order: int,
+    rotation_matrices: TypedList,
+) -> TypedList:
     """P function for rotation matrix calculation.
 
     Args:
@@ -165,7 +172,12 @@ def P(i, a, b, order, rotation_matrices):
 
 
 @njit
-def U(degree, n, order, rotation_matrices):
+def U(
+    degree: int,
+    n: int,
+    order: int,
+    rotation_matrices: TypedList,
+) -> TypedList:
     """U coefficient initialiser for rotation matrix calculation.
 
     Args:
@@ -182,7 +194,12 @@ def U(degree, n, order, rotation_matrices):
 
 
 @njit
-def V(degree, n, order, rotation_matrices):
+def V(
+    degree: int,
+    n: int,
+    order: int,
+    rotation_matrices: TypedList,
+) -> TypedList:
     """V coefficient initialiser for rotation matrix calculation.
 
     Args:
@@ -194,7 +211,7 @@ def V(degree, n, order, rotation_matrices):
     Returns:
         float: V value
     """
-    d = 0
+    d = 0.0
     if degree == 0:
         v_coeff = P(1, 1, n, order, rotation_matrices) + P(
             -1, -1, n, order, rotation_matrices
@@ -215,7 +232,7 @@ def V(degree, n, order, rotation_matrices):
 
 
 @njit
-def W(degree, n, order, rotation_matrices):
+def W(degree: int, n: int, order: int, rotation_matrices: TypedList) -> TypedList:
     """W coefficient initialiser for rotation matrix calculation.
 
     Args:
@@ -285,7 +302,7 @@ def compute_UVW_coefficients(degree, n, order):
 
 
 @njit
-def compute_band_rotation(order, rotation_matrices, output):
+def compute_band_rotation(order: int, rotation_matrices: TypedList, output):
     """Compute submatrix for rotation matrix.
 
     Args:
@@ -297,7 +314,6 @@ def compute_band_rotation(order, rotation_matrices, output):
         matrix: rotation submatrix
     """
     # print(f'entering band rotation with l = {el}')
-
     for row, m in enumerate(np.arange(-order, order + 1, 1)):
         for col, n in enumerate(np.arange(-order, order + 1, 1)):
             u, v, w = compute_UVW_coefficients(m, n, order)
@@ -360,7 +376,7 @@ def __init__(self, order, resolution):
             foa_rotmat = np.linalg.inv(foa_rotmat)
             self.rotmat[i, :, :] = compute_rotation_matrix(order, foa_rotmat)
 
-    def rotate(self, signal, rotation_vector):
+    def rotate(self, signal: ndarray, rotation_vector: ndarray) -> ndarray:
         """Apply rotation to HOA signals using precomputed rotation matrices.
 
         Args:
@@ -389,7 +405,11 @@ def rotate(self, signal, rotation_vector):
         return signal
 
 
-def binaural_mixdown(ambisonic_signals, hrir, hrir_metadata):
+def binaural_mixdown(
+    ambisonic_signals: ndarray,
+    hrir: dict[str, Any],
+    hrir_metadata: dict[str, Any],
+) -> ndarray:
     """Perform binaural mixdown of ambisonic signals.
 
     Args:
@@ -425,8 +445,8 @@ def binaural_mixdown(ambisonic_signals, hrir, hrir_metadata):
 
 
 def ambisonic_convolve(
-    signal: np.ndarray, hoa_impulse_responses: np.ndarray, order: int
-) -> np.ndarray:
+    signal: ndarray, hoa_impulse_responses: ndarray, order: int
+) -> ndarray:
     """Convolve HOA Impulse Responses with signals.
 
     Args:
@@ -438,6 +458,11 @@ def ambisonic_convolve(
         np.ndarray[samples, channels]: the convolved signal
     """
     n = (order + 1) ** 2
+    if n > hoa_impulse_responses.shape[1]:
+        raise ValueError(
+            f"Number of channels in impulse response ({hoa_impulse_responses.shape[1]})"
+            f" must be >= number of channels required for order {order} ({n})"
+        )
     return np.array(
         [
             convolve(impulse_response, signal)
@@ -446,7 +471,7 @@ def ambisonic_convolve(
     ).T
 
 
-def compute_rms(input_signal: np.ndarray, axis: int = 0) -> float:
+def compute_rms(input_signal: ndarray, axis: int = 0) -> ndarray:
     """Compute rms values along a given axis.
     Args:
         input_signal (np.ndarray): Input signal
@@ -458,7 +483,7 @@ def compute_rms(input_signal: np.ndarray, axis: int = 0) -> float:
     return np.sqrt(np.mean(input_signal**2, axis=axis))
 
 
-def equalise_rms_levels(inputs: list[np.ndarray]) -> list[np.ndarray]:
+def equalise_rms_levels(inputs: list[ndarray]) -> list[ndarray]:
     """Equalise RMS levels.
 
     Args:
@@ -473,7 +498,7 @@ def equalise_rms_levels(inputs: list[np.ndarray]) -> list[np.ndarray]:
     return outputs
 
 
-def dB_to_gain(x: float) -> float:
+def dB_to_gain(x: int | float) -> float:
     """Convert dB to gain.
 
     Args:
@@ -486,8 +511,8 @@ def dB_to_gain(x: float) -> float:
 
 
 def smoothstep(
-    x: np.ndarray, x_min: float = 0, x_max: float = 1, N: int = 1
-) -> np.ndarray:
+    x: ndarray, x_min: float = 0.0, x_max: float = 1.0, N: int = 1
+) -> ndarray:
     """Apply the smoothstep function.
 
     Args:
@@ -512,7 +537,7 @@ def smoothstep(
 
 def rotation_control_vector(
     array_length: int, start_index: int, end_index: int, smoothness: int = 1
-) -> np.ndarray:
+) -> ndarray:
     """Generate mapped rotation control vector for values of theta.
 
     Args:
@@ -541,7 +566,7 @@ def compute_rotation_vector(
     signal_length: int,
     start_idx: int,
     end_idx: int,
-) -> np.ndarray:
+) -> ndarray:
     """Compute the rotation vector.
 
     Args:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 3f149c48d..9149d521f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,6 @@
 """Fixtures for testing."""
+from __future__ import annotations
+
 from pathlib import Path
 
 import numpy as np
@@ -12,12 +14,32 @@
 
 
 @pytest.fixture
-def random_matrix() -> np.ndarray:
-    """Generate a random matrix for use in tests."""
-    return np.asarray(rng.random((100, 100)))
+def make_random_matrix():
+    """Generate a random matrix for use in tests.
+
+    The fixture returns a function that can be called to generate a random matrix
+    >>> def my_test(random_matrix):
+    >>>     matrix = random_matrix(seed=1234)
+    or use the global seed
+    >>>     matrix = random_matrix()
+    """
+
+    def _random_matrix(seed: int | None = None, size=(100, 100)) -> np.ndarray:
+        if seed is not None:
+            # Seed is supplied so use a generator with that seed...
+            rng_to_use = np.random.default_rng(seed)
+        else:
+            # ... else use the global generator
+            rng_to_use = rng
+        return np.asarray(rng_to_use.random(size))
+
+    return _random_matrix
 
 
 def pytest_configure() -> None:
-    """Configure custom variables for pytest"""
+    """Configure custom variables for pytest.
+
+    **NB**: pytest automatically calls this hook when the conftest is loaded.
+    """
     pytest.abs_tolerance = 1e-7
     pytest.rel_tolerance = 1e-7
diff --git a/tests/data/test_HOA_tools_cec2.py b/tests/data/test_HOA_tools_cec2.py
index cbe7a132e..cb5c88c92 100644
--- a/tests/data/test_HOA_tools_cec2.py
+++ b/tests/data/test_HOA_tools_cec2.py
@@ -1,50 +1,140 @@
 """Tests for the data.HOA_tools_cec2 module."""
 import numpy as np
 import pytest
+from numba.typed import List as TypedList  # pylint: disable=no-name-in-module
 
 # HOARotator,; ambisonic_convolve,; binaural_mixdown,;;;
 # compute_band_rotation,;; compute_rotation_matrix,;  dot,; dot,
 from clarity.data.HOA_tools_cec2 import (
+    HOARotator,
     P,
     U,
     V,
     W,
+    ambisonic_convolve,
+    binaural_mixdown,
     centred_element,
+    compute_band_rotation,
     compute_rms,
+    compute_rotation_matrix,
     compute_rotation_vector,
     compute_UVW_coefficients,
+    convert_a_to_b_format,
     dB_to_gain,
+    dot,
     equalise_rms_levels,
     rotation_control_vector,
     smoothstep,
 )
 
 
+def test_convert_a_to_b_format() -> None:
+    """Test for convert_a_to_b_format() function."""
+    np.random.seed(1234)
+    signal = np.random.random((4, 100))
+    result = convert_a_to_b_format(
+        signal[0, :], signal[1, :], signal[2, :], signal[3, :]
+    )
+    assert result.shape == (4, 100)
+    assert np.sum(np.abs(signal)) == pytest.approx(208.78666127520782)
+
+
+def test_convert_a_to_b_format_index_error() -> None:
+    """Test convert_a_to_b_format() raises IndexError if inputs not same length."""
+    with pytest.raises(ValueError):
+        convert_a_to_b_format(np.zeros(100), np.zeros(100), np.zeros(100), np.zeros(80))
+
+
 def test_compute_rotation_matrix() -> None:
     """Test for compute_rotation_matrix() function."""
-    assert True
+    order = 2
+    rot_mat_dim = (order + 1) ** 2
+
+    # Test for identity matrix
+    rot_mat = compute_rotation_matrix(order, np.eye(3))
+    assert rot_mat == pytest.approx(np.eye(rot_mat_dim))
+
+    # Test for 90 degree rotation about x-axis
+    rot_mat = compute_rotation_matrix(
+        2, np.array([[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]])
+    )
+    assert rot_mat.shape == (rot_mat_dim, rot_mat_dim)
+    # For this matrix, all values should be either 0, 1 or -1
+    assert np.all(
+        np.isclose(rot_mat, 0.0) + np.isclose(rot_mat, 1.0) + np.isclose(rot_mat, -1.0)
+    )
+
+
+def test_hoa_rotator_construction() -> None:
+    """Test for HOARotator class construction."""
+
+    # Testing 2nd order matrix with a 90 degree rotation step
+    hoa_rotator = HOARotator(2, 90)
+    rot_mat = hoa_rotator.rotmat
+    assert rot_mat.shape == (4, 9, 9)
+    rot90 = np.array([[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]])
+    assert rot_mat[1, :, :] == pytest.approx(compute_rotation_matrix(2, rot90))
+    # For multiples of 90 degrees all entries should be either 0, 1 or -1
+    assert np.all(
+        np.isclose(rot_mat, 0.0) + np.isclose(rot_mat, 1.0) + np.isclose(rot_mat, -1.0)
+    )
+
+
+def test_hoa_rotate_no_rotation() -> None:
+    """Test the HOARotator rotate method"""
+
+    # 1st order ambinsonic signal, ie 4 channels
+    np.random.seed(1234)
+    signal = np.random.random((100, 4))
+    # equal length signal specify rotation for each sample
+    rotations = np.zeros(100)
+    # Apply the rotation
+    hoa_rotator = HOARotator(1, 90)
+    result = hoa_rotator.rotate(signal, rotations)
+    assert result.shape == (100, 4)
+    assert result == pytest.approx(signal)
 
 
 @pytest.mark.parametrize(
-    "row, col, expected",
+    "seed, expected",
     [
-        (1, 1, 0.8840922122960315),
-        (45, 54, 0.2378095805520779),
-        (18, 91, 0.39897595320190293),
+        (1234, 34.9827354617758),
+        (1235, 37.63083581519224),
+        (1236, 35.83739821236641),
+    ],
+)
+def test_hoa_rotate(seed: int, expected: float) -> None:
+    """Test the HOARotator rotate method"""
+
+    # 1st order ambinsonic signal, ie 4 channels
+    np.random.seed(seed)
+    signal = np.random.random((20, 4))
+    rotations = np.random.random(20)
+    hoa_rotator = HOARotator(1, 90)
+    result = hoa_rotator.rotate(signal, rotations)
+    assert np.sum(np.abs(result)) == pytest.approx(expected)
+
+
+@pytest.mark.parametrize(
+    "seed, row, col, expected",
+    [
+        (1234, 1, 1, 0.740996817336437),
+        (1235, 45, 24, 0.2617612929455504),
+        (1236, 18, 47, 0.2706397841499083),
     ],
 )
 def test_centred_element(
-    random_matrix: np.ndarray, row: int, col: int, expected: float
+    make_random_matrix, seed: int, row: int, col: int, expected: float
 ) -> None:
     """Test for centered_element() function."""
-    assert centred_element(random_matrix, row, col) == pytest.approx(
-        expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
-    )
+    random_matrix = make_random_matrix(seed)
+    assert centred_element(random_matrix, row, col) == pytest.approx(expected)
 
 
-def test_centred_element_index_error(random_matrix: np.ndarray) -> None:
+def test_centred_element_index_error(make_random_matrix) -> None:
     """Test that centered_element() raises an IndexError if centering is outside
     of matrix dimensions."""
+    random_matrix = make_random_matrix()
     with pytest.raises(IndexError):
         centred_element(random_matrix, row=101, col=2)
     with pytest.raises(IndexError):
@@ -52,95 +142,110 @@ def test_centred_element_index_error(random_matrix: np.ndarray) -> None:
 
 
 @pytest.mark.parametrize(
-    "i, a, b, order, expected",
+    "i, a, b, order, seed, expected",
     [
-        (1, 1, 2, 2, -0.3475073926260553),
-        (1, 1, 2, -2, 0.6814353682034401),
-        (1, 1, 2, 2, -0.7185822065660934),
+        (1, 1, 2, 2, 1234, -0.20056163713681976),
+        (1, 1, 2, -2, 1235, 0.1767992365183524),
+        (1, 1, 2, 2, 1236, -0.5552636713315029),
     ],
 )
 def test_P(
-    i: int, a: int, b: int, order: int, random_matrix: np.ndarray, expected: float
+    i: int, a: int, b: int, order: int, make_random_matrix, seed: int, expected: float
 ) -> None:
     """Test for P() function."""
+    r1_matrix = make_random_matrix(seed)
+    r2_matrix = make_random_matrix(seed + 10)  # Different seed to get different matrix
+    r3_matrix = make_random_matrix(seed + 20)  # ditto
     assert P(
         i,
         a,
         b,
         order,
-        rotation_matrices=[random_matrix, random_matrix, random_matrix],
-    ) == pytest.approx(expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance)
+        rotation_matrices=TypedList([r1_matrix, r2_matrix, r3_matrix]),
+    ) == pytest.approx(expected)
 
 
-def test_P_index_error(random_matrix: np.ndarray) -> None:
+def test_P_index_error(make_random_matrix) -> None:
     """Test P() raises IndexError if invalid indices to r are provided."""
+    random_matrix = make_random_matrix()
+    rotation_matrices = TypedList([random_matrix])
     with pytest.raises(IndexError):
-        assert P(i=1, a=5, b=1, order=1, rotation_matrices=[random_matrix])
+        assert P(i=1, a=5, b=1, order=1, rotation_matrices=rotation_matrices)
     with pytest.raises(IndexError):
-        assert P(i=1, a=5, b=1, order=-1, rotation_matrices=[random_matrix])
+        assert P(i=1, a=5, b=1, order=-1, rotation_matrices=rotation_matrices)
     with pytest.raises(IndexError):
-        assert P(i=1, a=5, b=4, order=3, rotation_matrices=[random_matrix])
+        assert P(i=1, a=5, b=4, order=3, rotation_matrices=rotation_matrices)
 
 
 @pytest.mark.parametrize(
-    "degree, n, order, expected",
+    "degree, n, order, seed, expected",
     [
-        (1, 2, 2, 0.8066335578037782),
-        (1, 2, -2, 0.647932971103906),
-        (1, 2, 2, 0.504340149676519),
+        (1, 2, 2, 2234, -0.08645029499482301),
+        (1, 2, -2, 2235, 0.1550159595919902),
+        (1, 2, 2, 2236, -0.6545704498040456),
     ],
 )
 def test_U(
-    degree: int, n: int, order: int, random_matrix: np.ndarray, expected: float
+    degree: int, n: int, order: int, make_random_matrix, seed: int, expected: float
 ) -> None:
     """Test for U function."""
+    r1_matrix = make_random_matrix(seed)
+    r2_matrix = make_random_matrix(seed + 10)  # Different seed to get different matrix
+    r3_matrix = make_random_matrix(seed + 20)  # ditto
     assert U(
         degree,
         n,
         order,
-        rotation_matrices=[random_matrix, random_matrix, random_matrix],
-    ) == pytest.approx(expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance)
+        rotation_matrices=TypedList([r1_matrix, r2_matrix, r3_matrix]),
+    ) == pytest.approx(expected)
 
 
 @pytest.mark.parametrize(
-    "degree, n, order, expected",
+    "degree, n, order, seed, expected",
     [
-        (0, 2, 2, -0.02607000059684761),
-        (1, 2, 2, -0.8041211269599031),
-        (1, 2, -2, 0.889239093568916),
-        (-1, 2, -2, 0.5668224726503112),
+        (0, 2, 2, 3234, 0.6475456062624654),
+        (1, 2, 2, 3235, -0.009790830648337613),
+        (1, 2, -2, 3236, 1.3180460765445767),
+        (-1, 2, -2, 3237, 0.44136069630527924),
     ],
 )
 def test_V(
-    degree: int, n: int, order: int, random_matrix: np.ndarray, expected: float
+    degree: int, n: int, order: int, make_random_matrix, seed: int, expected: float
 ) -> None:
     """Test for V() function."""
+    r1_matrix = make_random_matrix(seed)
+    r2_matrix = make_random_matrix(seed + 10)  # Different seed to get different matrix
+    r3_matrix = make_random_matrix(seed + 20)  # ditto
     assert V(
         degree,
         n,
         order,
-        rotation_matrices=[random_matrix, random_matrix, random_matrix],
-    ) == pytest.approx(expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance)
+        rotation_matrices=TypedList([r1_matrix, r2_matrix, r3_matrix]),
+    ) == pytest.approx(expected)
 
 
 @pytest.mark.parametrize(
-    "degree, n, order, expected",
+    "degree, n, order, seed, expected",
     [
-        (0, 2, 2, 0.0),
-        (1, 2, 2, -0.18322112315448064),
-        (-1, 2, 2, -0.3501066433176404),
+        (0, 2, 2, None, 0.0),  # No seed, as should result in 0.0 for all matrices
+        (1, 2, 2, 4235, 0.6708751988390329),
+        (-1, 2, 2, 4236, 0.38743574623364374),
     ],
 )
 def test_W(
-    degree: int, n: int, order: int, random_matrix: np.ndarray, expected: float
+    degree: int, n: int, order: int, make_random_matrix, seed: int, expected: float
 ) -> None:
     """Test for W() function."""
+    r1_matrix = make_random_matrix(seed)
+    r2_matrix = make_random_matrix(seed + 10 if seed is not None else None)
+    r3_matrix = make_random_matrix(seed + 10 if seed is not None else None)  # ditto
+
     assert W(
         degree,
         n,
         order,
-        rotation_matrices=[random_matrix, random_matrix, random_matrix],
-    ) == pytest.approx(expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance)
+        rotation_matrices=TypedList([r1_matrix, r2_matrix, r3_matrix]),
+    ) == pytest.approx(expected)
 
 
 @pytest.mark.parametrize(
@@ -157,9 +262,7 @@ def test_compute_UVW_coefficients(
     degree: int, n: int, order: int, expected: float
 ) -> None:
     """Test for computer_UVW_coefficients() function."""
-    assert compute_UVW_coefficients(degree, n, order) == pytest.approx(
-        expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
-    )
+    assert compute_UVW_coefficients(degree, n, order) == pytest.approx(expected)
 
 
 def test_compute_UVW_coefficients_zero_division_error() -> None:
@@ -168,90 +271,133 @@ def test_compute_UVW_coefficients_zero_division_error() -> None:
         compute_UVW_coefficients(degree=-1, n=2, order=-2)
 
 
-# TODO : Not yet working, I don't understand how to get `output` passed in correctly
-# @pytest.mark.parametrize(
-#     "el, output, expected",
-#     [
-#         (2, np.asarray([0, 0]), np.asarray([[1, 2], [3, 4]])),
-#     ],
-# )
-# def test_compute_band_rotation(el: int, output: np.ndarray,
-#   random_matrix: np.ndarray, expected: np.ndarray) -> None:
-#     """Test for compute_band_rotation() function."""
-#     np.testing.assert_array_equal(compute_band_rotation(el,
-# [random_matrix, random_matrix], output), expected)
-
-
-# TODO : Not working yet, results in a typing error???
-# @pytest.mark.parametrize(
-#     "A, B, expected",
-#     [
-#         (np.asarray([[1, 2], [3, 4]]), np.asarray([[4, 3], [2, 1]]),
-#               np.asarray([[1, 2], [3, 4]])),
-#         (np.asarray([[5, 6], [7, 8]]), np.asarray([[8, 7], [6, 5]]),
-#               np.asarray([[1, 2], [3, 4]])),
-#     ],
-# )
-# def test_dot(A: np.ndarray, B: np.ndarray, expected: np.ndarray) -> None:
-#     """Test for dot() function."""
-#     np.testing.assert_array_equal(dot(A, B), expected)
-
-
-def test_HOARotator() -> None:
-    """Test for test_HOARotator class."""
-    assert True
+def test_compute_band_rotation() -> None:
+    """Test for compute_band_rotation() function."""
+
+    # This is not a very useful test as it is just one special case
+    # but this function is tested implicitly in other tests
+    order = 2
+    rot_mats = TypedList([np.eye(1), np.eye(3), np.eye(5)])
+    output = np.eye(9)
+    new_output, new_rotation_matrices = compute_band_rotation(order, rot_mats, output)
+    # output and rotation matrices should be unchanged
+    assert new_output.shape == (9, 9)
+    assert new_output == pytest.approx(np.eye(9))
+    for new_rot_mat, expected_rot_mat in zip(new_rotation_matrices, rot_mats):
+        assert new_rot_mat == pytest.approx(expected_rot_mat)
+
+
+@pytest.mark.parametrize(
+    "A, B, expected",
+    [
+        (
+            np.asarray([[1.0, 2.0], [3.0, 4.0]]),
+            np.asarray([[4.0, 3.0], [2.0, 1.0]]),
+            np.asarray([[8.0, 5.0], [20.0, 13.0]]),
+        ),
+        (
+            np.asarray([[5.0, 6.0], [7.0, 8.0]]),
+            np.asarray([[8.0, 7.0], [6.0, 5.0]]),
+            np.asarray([[76.0, 65.0], [104.0, 89.0]]),
+        ),
+    ],
+)
+def test_dot(A: np.ndarray, B: np.ndarray, expected: np.ndarray) -> None:
+    """Test for dot() function."""
+    result = dot(A, B)
+    assert result == pytest.approx(expected)
 
 
 # TODO : need examples of hrir_metadata dictionary to be able to write a tests for this
 def test_binaural_mixdown() -> None:
     """Test for binaural_mixdown() function."""
-    assert True
+    np.random.seed(1234)
+    hrir = {"M_data": np.random.random((100, 6, 2))}  # 6 random hrir filters
+    hrir_filters = [0, 2, 3, 5]  # 4 channels selected of possible 6
+    n_hrir_filters = len(hrir_filters)
+    hrir_metadata = {
+        "selected_channels": hrir_filters,
+        "matrix": np.eye(n_hrir_filters),  # no rotation
+    }
+    signals = np.random.random((100, n_hrir_filters))
+
+    result = binaural_mixdown(signals, hrir, hrir_metadata)
+    assert result.shape == (199, 2)
+    assert np.sum(np.abs(result)) == pytest.approx(19647.32682819124)
 
 
 def test_ambisonic_convolve() -> None:
     """Test for ambisonic_convolve() function."""
-    assert True
+    np.random.seed(1234)
+    signal = np.random.randn(100)
+    hoa_irs = np.random.random((100, 64))
+    order = 2
+    result = ambisonic_convolve(signal, hoa_irs, order=order)
+    # channel
+    assert result.shape == (199, (order + 1) ** 2)
+    assert np.sum(np.abs(result)) == pytest.approx(3359.04412270433)
+
+
+def test_ambisonic_convolve_error() -> None:
+    """Test for ambisonic_convolve() function."""
+    signal = np.random.randn(100)
+    hoa_irs = np.random.random((100, 64))
+    order = 10  # too high for number of hoa_ir channels
+    with pytest.raises(ValueError):
+        ambisonic_convolve(signal, hoa_irs, order=order)
 
 
 @pytest.mark.parametrize(
     "array, axis, expected",
     [
         (
-            np.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
+            np.asarray([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]),
             0,
             [
-                np.sqrt(np.mean([1**2, 4**2, 7**2])),
-                np.sqrt(np.mean([2**2, 5**2, 8**2])),
-                np.sqrt(np.mean([3**2, 6**2, 9**2])),
+                np.sqrt(np.mean([1.0**2, 4.0**2, 7.0**2])),
+                np.sqrt(np.mean([2.0**2, 5.0**2, 8.0**2])),
+                np.sqrt(np.mean([3.0**2, 6.0**2, 9.0**2])),
             ],
         ),
         (
-            np.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
+            np.asarray([[1.0, 2.0, 3.0], [4, 5.0, 6.0], [7.0, 8.0, 9.0]]),
             1,
             [
-                np.sqrt(np.mean([1**2, 2**2, 3**2])),
-                np.sqrt(np.mean([4**2, 5**2, 6**2])),
-                np.sqrt(np.mean([7**2, 8**2, 9**2])),
+                np.sqrt(np.mean([1.0**2, 2.0**2, 3.0**2])),
+                np.sqrt(np.mean([4.0**2, 5.0**2, 6.0**2])),
+                np.sqrt(np.mean([7.0**2, 8.0**2, 9.0**2])),
             ],
         ),
     ],
 )
 def test_compute_rms(array: np.ndarray, axis: int, expected: float) -> None:
     """Test for compute_rms() function along both axes."""
-    np.testing.assert_array_equal(compute_rms(input_signal=array, axis=axis), expected)
+    result = compute_rms(input_signal=array, axis=axis)
+    assert result == pytest.approx(expected)
 
 
-def test_equalise_rms_levels(random_matrix: np.ndarray) -> None:
+def test_equalise_rms_levels() -> None:
     """Test for equalise_rms_levels() function."""
-    assert equalise_rms_levels(inputs=[random_matrix, random_matrix])
+    np.random.seed(1234)
+    signal_1 = np.random.random((100, 2)) * 100
+    signal_2 = np.random.random((100, 2)) * 200
+    signal_3 = np.random.random((100, 2)) * 200
+    results = equalise_rms_levels(inputs=[signal_1, signal_2, signal_3])
+    # Check all have same level after equalisation
+    rms_1 = compute_rms(results[0], axis=0)
+    rms_2 = compute_rms(results[1], axis=0)
+    rms_3 = compute_rms(results[2], axis=0)
+    assert rms_1[0] == pytest.approx(rms_3[0])
+    assert rms_2[0] == pytest.approx(rms_3[0])
 
 
 @pytest.mark.parametrize(
     "db, gain", [(10, 3.1622776601683795), (20, 10.0), (0.045, 1.0051942600951387)]
 )
 def test_dB_to_gain(db: float, gain: float) -> None:
-    """Test for XX function."""
-    assert dB_to_gain(db) == gain
+    """Test for test_dB_to_gain function."""
+    result = dB_to_gain(db)
+    assert result == pytest.approx(gain)
 
 
 @pytest.mark.parametrize(
@@ -259,29 +405,29 @@ def test_dB_to_gain(db: float, gain: float) -> None:
     [
         (
             np.asarray([100, 200, 300, 400]),
-            10,
-            300,
+            10.0,
+            300.0,
             1,
-            np.asarray([0.229161, 0.725286, 1.0, 1.0]),
+            np.asarray([0.22916068719504698, 0.725286, 1.0, 1.0]),
         ),
         (
             np.asarray([10, 200, 300, 4000]),
-            10,
-            300,
+            10.0,
+            300.0,
             1,
             np.asarray([0.0, 0.725286, 1.0, 1.0]),
         ),
         (
             np.asarray([10, -200, 300, 4000]),
-            -200,
-            300,
+            -200.0,
+            300.0,
             1,
             np.asarray([0.381024, 0.0, 1.0, 1.0]),
         ),
         (
             np.asarray([10, -50, 300, 4000]),
-            -200,
-            300,
+            -200.0,
+            300.0,
             1,
             np.asarray([0.381024, 0.216, 1.0, 1.0]),
         ),
@@ -291,7 +437,8 @@ def test_smoothstep(
     x: np.ndarray, x_min: float, x_max: float, N: int, expected
 ) -> None:
     """Test for smoothstep() function."""
-    np.testing.assert_array_almost_equal(smoothstep(x, x_min, x_max, N), expected)
+    result = smoothstep(x, x_min, x_max, N)
+    assert result == pytest.approx(expected)
 
 
 @pytest.mark.parametrize(
@@ -310,9 +457,9 @@ def test_rotation_control_vector(
     array_length: int, start_idx: int, end_idx: int, expected: np.ndarray
 ) -> None:
     """Test for rotation_control_vector() function."""
-    np.testing.assert_array_equal(
-        rotation_control_vector(array_length, start_idx, end_idx), expected
-    )
+
+    result = rotation_control_vector(array_length, start_idx, end_idx)
+    assert result == pytest.approx(expected)
 
 
 @pytest.mark.parametrize(
diff --git a/tests/regression/test_data_HOA_tools_cec2.py b/tests/regression/test_data_HOA_tools_cec2.py
index c9a90f56d..57ff9c2bf 100644
--- a/tests/regression/test_data_HOA_tools_cec2.py
+++ b/tests/regression/test_data_HOA_tools_cec2.py
@@ -1,5 +1,5 @@
 import numpy as np
-from numba.typed import List  # pylint: disable=no-name-in-module
+from numba.typed import List as TypedList  # pylint: disable=no-name-in-module
 from scipy.spatial.transform import Rotation as R
 
 from clarity.data import HOA_tools_cec2 as hoa
@@ -24,7 +24,7 @@ def test_P(regtest):
     theta = 45
     order = 2
     foa = R.from_euler("y", theta, degrees=True).as_matrix()
-    rotmats = [foa, hoa.compute_rotation_matrix(order, foa)]
+    rotmats = TypedList([foa, hoa.compute_rotation_matrix(order, foa)])
     p = hoa.P.py_func(0, 1, 0, order, rotmats)
     regtest.write(f"P value {p:0.3f}\n")
 
@@ -35,7 +35,7 @@ def test_U(regtest):
     theta = 45
     order = 2
     foa = R.from_euler("y", theta, degrees=True).as_matrix()
-    rotmats = List([foa, hoa.compute_rotation_matrix(order, foa)])
+    rotmats = TypedList([foa, hoa.compute_rotation_matrix(order, foa)])
     u = hoa.U.py_func(m, n, order, rotmats)
     regtest.write(f"U value {u:0.3f}\n")
 
@@ -45,7 +45,7 @@ def test_V(regtest):
     theta = 45
     order = 2
     foa = R.from_euler("y", theta, degrees=True).as_matrix()
-    rotmats = List([foa, hoa.compute_rotation_matrix(order, foa)])
+    rotmats = TypedList([foa, hoa.compute_rotation_matrix(order, foa)])
     for i in range(3):
         v = hoa.V.py_func(i - 1, n, order, rotmats)
         regtest.write(f"V[{i}] value {v:0.3f}\n")
@@ -56,7 +56,7 @@ def test_W(regtest):
     theta = 45
     order = 2
     foa = R.from_euler("y", theta, degrees=True).as_matrix()
-    rotmats = List([foa, hoa.compute_rotation_matrix(order, foa)])
+    rotmats = TypedList([foa, hoa.compute_rotation_matrix(order, foa)])
     for i in range(3):
         w = hoa.W.py_func(i - 1, n, order, rotmats)
         regtest.write(f"W[{i}] value {w:0.3f}\n")
@@ -90,6 +90,6 @@ def test_compute_band_rotation(regtest):
     if n > 1:
         for i in np.arange(2, n + 1):
             rot_mat, typed_sub_matrices = hoa.compute_band_rotation.py_func(
-                i, typed_sub_matrices, rot_mat
+                i, TypedList(typed_sub_matrices), rot_mat
             )
     regtest.write(f"Band rotations {rot_mat}, {sub_matrices}\n")