210 lines
6.1 KiB
Python
210 lines
6.1 KiB
Python
|
|
"""
|
|||
|
|
Unit tests for ria_toolkit_oss.utils.array_conversion.
|
|||
|
|
|
|||
|
|
Covers:
|
|||
|
|
- is_1xn / is_2xn classification
|
|||
|
|
- convert_to_1xn / convert_to_2xn conversion
|
|||
|
|
- Round-trip invariance
|
|||
|
|
- Error paths for invalid inputs
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from ria_toolkit_oss.utils.array_conversion import (
|
|||
|
|
convert_to_1xn,
|
|||
|
|
convert_to_2xn,
|
|||
|
|
is_1xn,
|
|||
|
|
is_2xn,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Fixtures
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
COMPLEX_1XN = np.array([[1 + 2j, 3 + 4j, 5 + 6j]], dtype=np.complex128) # shape (1, 3)
|
|||
|
|
REAL_2XN = np.array([[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]], dtype=np.float64) # shape (2, 3)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# is_1xn
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_true_for_complex_1xn():
|
|||
|
|
assert is_1xn(COMPLEX_1XN) is True
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_false_for_real_2xn():
|
|||
|
|
assert is_1xn(REAL_2XN) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_false_for_1d_complex():
|
|||
|
|
arr = np.array([1 + 2j, 3 + 4j]) # 1-D
|
|||
|
|
assert is_1xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_false_for_3d():
|
|||
|
|
arr = np.ones((1, 3, 3), dtype=np.complex128)
|
|||
|
|
assert is_1xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_false_for_real_1xn():
|
|||
|
|
arr = np.array([[1.0, 2.0, 3.0]]) # real 1×N
|
|||
|
|
assert is_1xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_false_for_complex_2xn():
|
|||
|
|
arr = np.array([[1 + 2j, 3 + 4j], [5 + 6j, 7 + 8j]]) # complex 2×N
|
|||
|
|
assert is_1xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_1xn_single_sample():
|
|||
|
|
arr = np.array([[1 + 0j]]) # shape (1, 1)
|
|||
|
|
assert is_1xn(arr) is True
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# is_2xn
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_2xn_true_for_real_2xn():
|
|||
|
|
assert is_2xn(REAL_2XN) is True
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_2xn_false_for_complex_1xn():
|
|||
|
|
assert is_2xn(COMPLEX_1XN) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_2xn_false_for_1d():
|
|||
|
|
arr = np.array([1.0, 2.0, 3.0])
|
|||
|
|
assert is_2xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_2xn_false_for_3xn():
|
|||
|
|
arr = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) # shape (3, 2)
|
|||
|
|
assert is_2xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_2xn_false_for_complex_2xn():
|
|||
|
|
arr = np.array([[1 + 2j, 3 + 4j], [5 + 6j, 7 + 8j]]) # complex 2×N
|
|||
|
|
assert is_2xn(arr) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_is_2xn_single_column():
|
|||
|
|
arr = np.array([[1.0], [2.0]]) # shape (2, 1)
|
|||
|
|
assert is_2xn(arr) is True
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# convert_to_2xn
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_2xn_from_1xn_shape():
|
|||
|
|
result = convert_to_2xn(COMPLEX_1XN)
|
|||
|
|
assert result.shape == (2, COMPLEX_1XN.shape[1])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_2xn_from_1xn_values():
|
|||
|
|
"""First row is real, second row is imaginary."""
|
|||
|
|
result = convert_to_2xn(COMPLEX_1XN)
|
|||
|
|
assert np.array_equal(result[0], COMPLEX_1XN[0].real)
|
|||
|
|
assert np.array_equal(result[1], COMPLEX_1XN[0].imag)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_2xn_from_1xn_is_real():
|
|||
|
|
result = convert_to_2xn(COMPLEX_1XN)
|
|||
|
|
assert not np.iscomplexobj(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_2xn_from_2xn_is_copy():
|
|||
|
|
"""Already-2xN input returns a copy (not the same object)."""
|
|||
|
|
result = convert_to_2xn(REAL_2XN)
|
|||
|
|
assert np.array_equal(result, REAL_2XN)
|
|||
|
|
assert result is not REAL_2XN
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_2xn_invalid_raises():
|
|||
|
|
"""1-D array is neither 1xN nor 2xN — must raise ValueError."""
|
|||
|
|
arr = np.array([1.0, 2.0, 3.0])
|
|||
|
|
with pytest.raises(ValueError):
|
|||
|
|
convert_to_2xn(arr)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_2xn_invalid_complex_2xn_raises():
|
|||
|
|
"""Complex 2×N is not a recognised format — must raise ValueError."""
|
|||
|
|
arr = np.array([[1 + 2j, 3 + 4j], [5 + 6j, 7 + 8j]])
|
|||
|
|
with pytest.raises(ValueError):
|
|||
|
|
convert_to_2xn(arr)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# convert_to_1xn
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_1xn_from_2xn_shape():
|
|||
|
|
result = convert_to_1xn(REAL_2XN)
|
|||
|
|
assert result.shape == (1, REAL_2XN.shape[1])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_1xn_from_2xn_values():
|
|||
|
|
"""Real part from row 0, imaginary from row 1."""
|
|||
|
|
result = convert_to_1xn(REAL_2XN)
|
|||
|
|
assert np.array_equal(result[0].real, REAL_2XN[0])
|
|||
|
|
assert np.array_equal(result[0].imag, REAL_2XN[1])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_1xn_from_2xn_is_complex():
|
|||
|
|
result = convert_to_1xn(REAL_2XN)
|
|||
|
|
assert np.iscomplexobj(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_1xn_from_1xn_is_copy():
|
|||
|
|
"""Already-1xN input returns a copy (not the same object)."""
|
|||
|
|
result = convert_to_1xn(COMPLEX_1XN)
|
|||
|
|
assert np.array_equal(result, COMPLEX_1XN)
|
|||
|
|
assert result is not COMPLEX_1XN
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_1xn_invalid_raises():
|
|||
|
|
"""1-D array is neither 1xN nor 2xN — must raise ValueError."""
|
|||
|
|
arr = np.array([1.0, 2.0, 3.0])
|
|||
|
|
with pytest.raises(ValueError):
|
|||
|
|
convert_to_1xn(arr)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_convert_to_1xn_invalid_3xn_raises():
|
|||
|
|
"""3×N array is not a recognised format — must raise ValueError."""
|
|||
|
|
arr = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
|
|||
|
|
with pytest.raises(ValueError):
|
|||
|
|
convert_to_1xn(arr)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Round-trip invariance
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_roundtrip_1xn_to_2xn_to_1xn():
|
|||
|
|
"""1xN → 2xN → 1xN should recover the original values."""
|
|||
|
|
intermediate = convert_to_2xn(COMPLEX_1XN)
|
|||
|
|
recovered = convert_to_1xn(intermediate)
|
|||
|
|
assert np.allclose(recovered, COMPLEX_1XN)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_roundtrip_2xn_to_1xn_to_2xn():
|
|||
|
|
"""2xN → 1xN → 2xN should recover the original values."""
|
|||
|
|
intermediate = convert_to_1xn(REAL_2XN)
|
|||
|
|
recovered = convert_to_2xn(intermediate)
|
|||
|
|
assert np.allclose(recovered, REAL_2XN)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_roundtrip_preserves_precision():
|
|||
|
|
"""Values survive a double conversion with full float64 precision."""
|
|||
|
|
data = np.array([[1.23456789 + 9.87654321j, -0.1 - 0.2j]], dtype=np.complex128)
|
|||
|
|
recovered = convert_to_1xn(convert_to_2xn(data))
|
|||
|
|
assert np.allclose(recovered, data, atol=1e-14)
|