diff --git a/.github/workflows/continuous-integration-quality-unit-tests.yml b/.github/workflows/continuous-integration-quality-unit-tests.yml index f456019b86..f4c9e6c556 100644 --- a/.github/workflows/continuous-integration-quality-unit-tests.yml +++ b/.github/workflows/continuous-integration-quality-unit-tests.yml @@ -47,6 +47,11 @@ jobs: uv sync --all-extras uv run python -c "import imageio;imageio.plugins.freeimage.download()" shell: bash + - name: Install Array API Backends + if: matrix.os == 'macOS-latest' && matrix.python-version == '3.13' + run: | + uv pip install jax jaxlib torch + shell: bash - name: Prek (All Files) if: matrix.os == 'macOS-latest' run: | diff --git a/colour/__init__.py b/colour/__init__.py index 779bb58798..6d1603cc39 100644 --- a/colour/__init__.py +++ b/colour/__init__.py @@ -190,6 +190,7 @@ TVS_ILLUMINANTS_HUNTERLAB, WHITENESS_METHODS, YELLOWNESS_METHODS, + CIE_illuminant_D_series, MultiSpectralDistributions, SpectralDistribution, SpectralShape, @@ -203,8 +204,11 @@ luminous_efficacy, luminous_efficiency, luminous_flux, + msds_blackbody, + msds_CIE_illuminant_D_series, msds_constant, msds_ones, + msds_rayleigh_jeans, msds_to_XYZ, msds_zeros, sd_blackbody, @@ -615,6 +619,7 @@ "TVS_ILLUMINANTS_HUNTERLAB", "WHITENESS_METHODS", "YELLOWNESS_METHODS", + "CIE_illuminant_D_series", "MultiSpectralDistributions", "SpectralDistribution", "SpectralShape", @@ -628,8 +633,11 @@ "luminous_efficacy", "luminous_efficiency", "luminous_flux", + "msds_blackbody", + "msds_CIE_illuminant_D_series", "msds_constant", "msds_ones", + "msds_rayleigh_jeans", "msds_to_XYZ", "msds_zeros", "sd_blackbody", diff --git a/colour/adaptation/cie1994.py b/colour/adaptation/cie1994.py index 956e6b5e7d..9adadf5bda 100644 --- a/colour/adaptation/cie1994.py +++ b/colour/adaptation/cie1994.py @@ -34,12 +34,14 @@ Range100, ) from colour.utilities import ( + array_namespace, as_float_array, from_range_100, to_domain_100, tsplit, tstack, usage_warning, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -151,10 +153,14 @@ def chromatic_adaptation_CIE1994( XYZ_1 = to_domain_100(XYZ_1) Y_o = as_float_array(to_domain_100(Y_o)) - E_o1 = as_float_array(E_o1) - E_o2 = as_float_array(E_o2) - if np.any(Y_o < 18) or np.any(Y_o > 100): + xp = array_namespace(XYZ_1, Y_o, E_o1, E_o2) + + Y_o = xp_as_float_array(Y_o, xp=xp, like=XYZ_1) + E_o1 = xp_as_float_array(E_o1, xp=xp, like=XYZ_1) + E_o2 = xp_as_float_array(E_o2, xp=xp, like=XYZ_1) + + if xp.any(Y_o < 18) or xp.any(Y_o > 100): usage_warning( '"Y_o" luminance factor must be in [18, 100] domain, ' "unpredictable results may occur!" @@ -295,10 +301,13 @@ def effective_adapting_responses( """ xez = as_float_array(xez) - Y_o = as_float_array(Y_o) - E_o = as_float_array(E_o) - return ((Y_o[..., None] * E_o[..., None]) / (100 * np.pi)) * xez + xp = array_namespace(xez, Y_o, E_o) + + Y_o = xp_as_float_array(Y_o, xp=xp, like=xez) + E_o = xp_as_float_array(E_o, xp=xp, like=xez) + + return ((Y_o[..., None] * E_o[..., None]) / (100 * xp.pi)) * xez @typing.overload @@ -447,12 +456,15 @@ def K_coefficient( np.float64(1.0) """ + xp = array_namespace(xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, n) + + Y_o = xp_as_float_array(Y_o, xp=xp, like=xez_1) + n = xp_as_float_array(n, xp=xp, like=xez_1) + xi_1, eta_1, _zeta_1 = tsplit(xez_1) xi_2, eta_2, _zeta_2 = tsplit(xez_2) bR_o1, bG_o1, _bB_o1 = tsplit(bRGB_o1) bR_o2, bG_o2, _bB_o2 = tsplit(bRGB_o2) - Y_o = as_float_array(Y_o) - n = as_float_array(n) return ( spow((Y_o * xi_1 + n) / (20 * xi_1 + n), (2 / 3) * bR_o1) @@ -523,14 +535,17 @@ def corresponding_colour( array([23.1636901..., 20.0211948..., 16.2001664...]) """ + xp = array_namespace(RGB_1, xez_1, xez_2, bRGB_o1, bRGB_o2, Y_o, K, n) + + Y_o = xp_as_float_array(Y_o, xp=xp, like=RGB_1) + K = xp_as_float_array(K, xp=xp, like=RGB_1) + n = xp_as_float_array(n, xp=xp, like=RGB_1) + R_1, G_1, B_1 = tsplit(RGB_1) xi_1, eta_1, zeta_1 = tsplit(xez_1) xi_2, eta_2, zeta_2 = tsplit(xez_2) bR_o1, bG_o1, bB_o1 = tsplit(bRGB_o1) bR_o2, bG_o2, bB_o2 = tsplit(bRGB_o2) - Y_o = as_float_array(Y_o) - K = as_float_array(K) - n = as_float_array(n) def RGB_c( x_1: NDArrayFloat, diff --git a/colour/adaptation/cmccat2000.py b/colour/adaptation/cmccat2000.py index 0a1d70aca7..1c12bfdfc8 100644 --- a/colour/adaptation/cmccat2000.py +++ b/colour/adaptation/cmccat2000.py @@ -43,10 +43,11 @@ from colour.utilities import ( CanonicalMapping, MixinDataclassIterable, - as_float_array, + array_namespace, from_range_100, to_domain_100, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -193,20 +194,23 @@ def chromatic_adaptation_forward_CMCCAT2000( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) XYZ_wr = to_domain_100(XYZ_wr) - L_A1 = as_float_array(L_A1) - L_A2 = as_float_array(L_A2) + + xp = array_namespace(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2) + + L_A1 = xp_as_float_array(L_A1, xp=xp, like=XYZ) + L_A2 = xp_as_float_array(L_A2, xp=xp, like=XYZ) RGB = vecmul(CAT_CMCCAT2000, XYZ) RGB_w = vecmul(CAT_CMCCAT2000, XYZ_w) RGB_wr = vecmul(CAT_CMCCAT2000, XYZ_wr) D = surround.F * ( - 0.08 * np.log10(0.5 * (L_A1 + L_A2)) + 0.08 * xp.log10(0.5 * (L_A1 + L_A2)) + 0.76 - 0.45 * (L_A1 - L_A2) / (L_A1 + L_A2) ) - D = np.clip(D, 0, 1) + D = xp.clip(D, 0, 1) a = D * XYZ_w[..., 1] / XYZ_wr[..., 1] RGB_c = RGB * (a[..., None] * (RGB_wr / RGB_w) + 1 - D[..., None]) @@ -288,20 +292,23 @@ def chromatic_adaptation_inverse_CMCCAT2000( XYZ_c = to_domain_100(XYZ_c) XYZ_w = to_domain_100(XYZ_w) XYZ_wr = to_domain_100(XYZ_wr) - L_A1 = as_float_array(L_A1) - L_A2 = as_float_array(L_A2) + + xp = array_namespace(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2) + + L_A1 = xp_as_float_array(L_A1, xp=xp, like=XYZ_c) + L_A2 = xp_as_float_array(L_A2, xp=xp, like=XYZ_c) RGB_c = vecmul(CAT_CMCCAT2000, XYZ_c) RGB_w = vecmul(CAT_CMCCAT2000, XYZ_w) RGB_wr = vecmul(CAT_CMCCAT2000, XYZ_wr) D = surround.F * ( - 0.08 * np.log10(0.5 * (L_A1 + L_A2)) + 0.08 * xp.log10(0.5 * (L_A1 + L_A2)) + 0.76 - 0.45 * (L_A1 - L_A2) / (L_A1 + L_A2) ) - D = np.clip(D, 0, 1) + D = xp.clip(D, 0, 1) a = D * XYZ_w[..., 1] / XYZ_wr[..., 1] RGB = RGB_c / (a[..., None] * (RGB_wr / RGB_w) + 1 - D[..., None]) diff --git a/colour/adaptation/datasets/cat.py b/colour/adaptation/datasets/cat.py index fcfd2fb26f..31f79235bb 100644 --- a/colour/adaptation/datasets/cat.py +++ b/colour/adaptation/datasets/cat.py @@ -276,7 +276,7 @@ :cite:`Nayatani1995a` """ -CAT_XYZ_SCALING: NDArrayFloat = np.reshape(np.array(np.identity(3)), (3, 3)) +CAT_XYZ_SCALING: NDArrayFloat = np.reshape(np.array(np.eye(3)), (3, 3)) """ *XYZ Scaling* chromatic adaptation transform. diff --git a/colour/adaptation/fairchild1990.py b/colour/adaptation/fairchild1990.py index cdde6b6af1..9d2718e866 100644 --- a/colour/adaptation/fairchild1990.py +++ b/colour/adaptation/fairchild1990.py @@ -29,12 +29,12 @@ Range100, ) from colour.utilities import ( + array_namespace, as_float_array, from_range_100, ones, - row_as_diagonal, to_domain_100, - tstack, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -134,7 +134,10 @@ def chromatic_adaptation_Fairchild1990( XYZ_1 = to_domain_100(XYZ_1) XYZ_n = to_domain_100(XYZ_n) XYZ_r = to_domain_100(XYZ_r) - Y_n = as_float_array(Y_n) + + xp = array_namespace(XYZ_1, XYZ_n, XYZ_r, Y_n) + + Y_n = xp_as_float_array(Y_n, xp=xp, like=XYZ_1) LMS_1 = XYZ_to_RGB_Fairchild1990(XYZ_1) LMS_n = XYZ_to_RGB_Fairchild1990(XYZ_n) @@ -145,18 +148,14 @@ def chromatic_adaptation_Fairchild1990( a_LMS_1 = p_LMS / LMS_n a_LMS_2 = p_LMS / LMS_r - A_1 = row_as_diagonal(a_LMS_1) - A_2 = row_as_diagonal(a_LMS_2) - - LMSp_1 = vecmul(A_1, LMS_1) - - c = 0.219 - 0.0784 * np.log10(Y_n) - C = row_as_diagonal(tstack([c, c, c])) + # Diagonal matrix products collapse to elementwise scaling; their inverses + # are reciprocals. Avoids materialising ``(N, 3, 3)`` tensors at scale. + # The ``c`` matrix in *Fairchild (1990)* Eq. 21-22 cancels exactly between + # the forward ``LMS_a = c * LMSp_1`` and inverse ``LMSp_2 = LMS_a / c`` + # steps, so the intermediate is elided. + LMSp_1 = a_LMS_1 * LMS_1 - LMS_a = vecmul(C, LMSp_1) - LMSp_2 = vecmul(np.linalg.inv(C), LMS_a) - - LMS_c = vecmul(np.linalg.inv(A_2), LMSp_2) + LMS_c = LMSp_1 / a_LMS_2 XYZ_c = RGB_to_XYZ_Fairchild1990(LMS_c) return from_range_100(XYZ_c) @@ -249,21 +248,25 @@ def degrees_of_adaptation( LMS = as_float_array(LMS) + xp = array_namespace(LMS, Y_n, v) + if discount_illuminant: - return ones(LMS.shape) + return xp_as_float_array(ones(LMS.shape), xp=xp, like=LMS) - Y_n = as_float_array(Y_n) - v = as_float_array(v) + Y_n = xp_as_float_array(Y_n, xp=xp, like=LMS) + v = xp_as_float_array(v, xp=xp, like=LMS) # E illuminant. LMS_E = vecmul(CAT_VON_KRIES, ones(LMS.shape)) + LMS_E = xp_as_float_array(LMS_E, xp=xp, like=LMS) + Ye_n = spow(Y_n, v) def m_E(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat: """Compute the :math:`m_E` term.""" - return (3 * x / y) / np.sum(x / y, axis=-1)[..., None] + return (3 * x / y) / xp.sum(x / y, axis=-1)[..., None] def P_c(x: NDArrayFloat) -> NDArrayFloat: """Compute the :math:`P_L`, :math:`P_M` or :math:`P_S` terms.""" diff --git a/colour/adaptation/fairchild2020.py b/colour/adaptation/fairchild2020.py index de5b29960a..c77cbb5de1 100644 --- a/colour/adaptation/fairchild2020.py +++ b/colour/adaptation/fairchild2020.py @@ -38,6 +38,7 @@ from colour.utilities import ( CanonicalMapping, MixinDataclassIterable, + array_namespace, as_float_array, from_range_1, get_domain_range_scale, @@ -45,6 +46,7 @@ row_as_diagonal, to_domain_1, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -214,13 +216,15 @@ def matrix_chromatic_adaptation_vk20( XYZ_r = as_float_array(XYZ_r) XYZ_p = as_float_array(XYZ_p) + xp = array_namespace(XYZ_n, XYZ_r, XYZ_p) + transform = validate_method( transform, tuple(CHROMATIC_ADAPTATION_TRANSFORMS), '"{0}" chromatic adaptation transform is invalid, it must be one of {1}!', ) - M = CHROMATIC_ADAPTATION_TRANSFORMS[transform] + M = xp_as_float_array(CHROMATIC_ADAPTATION_TRANSFORMS[transform], xp=xp, like=XYZ_n) D_n, D_r, D_p = coefficients.values @@ -231,9 +235,9 @@ def matrix_chromatic_adaptation_vk20( with sdiv_mode(): D = row_as_diagonal(sdiv(1, (D_n * LMS_n + D_r * LMS_r + D_p * LMS_p))) - M_CAT = np.matmul(np.linalg.inv(M), D) + M_CAT = xp.matmul(xp.linalg.inv(M), D) - return np.matmul(M_CAT, M) + return xp.matmul(M_CAT, M) def chromatic_adaptation_vK20( @@ -344,6 +348,10 @@ def chromatic_adaptation_vK20( ) ) + xp = array_namespace(XYZ, XYZ_p, XYZ_n, XYZ_r) + + XYZ_r = xp_as_float_array(XYZ_r, xp=xp, like=XYZ) + M_CAT = matrix_chromatic_adaptation_vk20( XYZ_p, XYZ_n, XYZ_r, transform, coefficients ) diff --git a/colour/adaptation/li2025.py b/colour/adaptation/li2025.py index fc547c88bf..d3cf1b0796 100644 --- a/colour/adaptation/li2025.py +++ b/colour/adaptation/li2025.py @@ -27,8 +27,12 @@ from colour.hints import ArrayLike, Domain100, NDArrayFloat, Range100 from colour.utilities import ( - as_float_array, + array_namespace, + from_range_100, ones, + to_domain_100, + xp_as_float_array, + xp_atleast_1d, ) __author__ = "Colour Developers" @@ -120,11 +124,14 @@ def chromatic_adaptation_Li2025( array([40.0072581..., 43.7014895..., 21.3290293...]) """ - XYZ_s = as_float_array(XYZ_s) - XYZ_ws = as_float_array(XYZ_ws) - XYZ_wd = as_float_array(XYZ_wd) - L_A = as_float_array(L_A) - F_surround = as_float_array(F_surround) + XYZ_s = to_domain_100(XYZ_s) + XYZ_ws = to_domain_100(XYZ_ws) + XYZ_wd = to_domain_100(XYZ_wd) + + xp = array_namespace(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround) + + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ_s) + F_surround = xp_as_float_array(F_surround, xp=xp, like=XYZ_s) LMS_s = vecmul(CAT_CAT16, XYZ_s) LMS_w_s = vecmul(CAT_CAT16, XYZ_ws) @@ -132,18 +139,16 @@ def chromatic_adaptation_Li2025( Y_w_s = XYZ_ws[..., 1] if XYZ_ws.ndim > 1 else XYZ_ws[1] Y_w_d = XYZ_wd[..., 1] if XYZ_wd.ndim > 1 else XYZ_wd[1] - if discount_illuminant: - D = ones(L_A.shape) + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ_s) else: - D = F_surround * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92)) - D = np.clip(D, 0, 1) - - D = np.atleast_1d(D)[..., None] if LMS_s.ndim > 1 else D + D = F_surround * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) + D = xp.clip(D, 0, 1) + D = xp_atleast_1d(D, xp=xp)[..., None] if LMS_s.ndim > 1 else D with sdiv_mode(): Y_ratio = sdiv(Y_w_s, Y_w_d) Y_ratio = Y_ratio[..., None] if LMS_s.ndim > 1 else Y_ratio LMS_a = LMS_s * (D * Y_ratio * sdiv(LMS_w_d, LMS_w_s) + (1 - D)) - return vecmul(CAT_CAT16_INVERSE, LMS_a) + return from_range_100(vecmul(CAT_CAT16_INVERSE, LMS_a)) diff --git a/colour/adaptation/tests/test__init__.py b/colour/adaptation/tests/test__init__.py index dc81ecbefc..d7155fcf3c 100644 --- a/colour/adaptation/tests/test__init__.py +++ b/colour/adaptation/tests/test__init__.py @@ -2,11 +2,19 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType from colour.adaptation import chromatic_adaptation from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale +from colour.utilities import ( + as_ndarray, + domain_range_scale, + xp_as_array, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -26,21 +34,21 @@ class TestChromaticAdaptation: tests methods. """ - def test_chromatic_adaptation(self) -> None: + def test_chromatic_adaptation(self, xp: ModuleType) -> None: """Test :func:`colour.adaptation.chromatic_adaptation` definition.""" - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) - np.testing.assert_allclose( + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_w = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_wr = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + xp_assert_close( chromatic_adaptation(XYZ, XYZ_w, XYZ_wr), - np.array([0.21638819, 0.12570000, 0.03847494]), + [0.21638819, 0.12570000, 0.03847494], atol=TOLERANCE_ABSOLUTE_TESTS, ) Y_o = 0.2 E_o = 1000 - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation( XYZ, XYZ_w, @@ -50,64 +58,69 @@ def test_chromatic_adaptation(self) -> None: E_o1=E_o, E_o2=E_o, ), - np.array([0.21347453, 0.12252986, 0.03347887]), + [0.21347453, 0.12252986, 0.03347887], atol=TOLERANCE_ABSOLUTE_TESTS, ) L_A = 200 - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation( XYZ, XYZ_w, XYZ_wr, method="CMCCAT2000", L_A1=L_A, L_A2=L_A ), - np.array([0.21498829, 0.12474711, 0.03910138]), + [0.21498829, 0.12474711, 0.03910138], atol=TOLERANCE_ABSOLUTE_TESTS, ) Y_n = 200 - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation(XYZ, XYZ_w, XYZ_wr, method="Fairchild 1990", Y_n=Y_n), - np.array([0.21394049, 0.12262315, 0.03891917]), + [0.21394049, 0.12262315, 0.03891917], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation( XYZ, XYZ_w, XYZ_wr, method="Li 2025", L_A=100, F_surround=1 ), - np.array([0.21166965, 0.12234633, 0.03888754]), + [0.21166965, 0.12234633, 0.03888754], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation(XYZ, XYZ_w, XYZ_wr, method="Zhai 2018", L_A=100), - np.array([0.21638819, 0.1257, 0.03847494]), + [0.21638819, 0.1257, 0.03847494], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_wo = np.array([1.0, 1.0, 1.0]) - np.testing.assert_allclose( + XYZ_wo = xp_as_array([1.0, 1.0, 1.0], xp=xp) + xp_assert_close( chromatic_adaptation( - XYZ, XYZ_w, XYZ_wr, method="Zhai 2018", L_A=100, XYZ_wo=XYZ_wo + XYZ, + XYZ_w, + XYZ_wr, + method="Zhai 2018", + L_A=100, + XYZ_wo=XYZ_wo, ), - np.array([0.21638819, 0.1257, 0.03847494]), + [0.21638819, 0.1257, 0.03847494], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation(XYZ, XYZ_w, XYZ_wr, method="vK20"), - np.array([0.21468842, 0.12456164, 0.04662558]), + [0.21468842, 0.12456164, 0.04662558], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation(self) -> None: + def test_domain_range_scale_chromatic_adaptation(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.chromatic_adaptation` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_w = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_wr = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) Y_o = 0.2 E_o = 1000 L_A = 200 @@ -115,17 +128,19 @@ def test_domain_range_scale_chromatic_adaptation(self) -> None: m = ("Von Kries", "CIE 1994", "CMCCAT2000", "Fairchild 1990") v = [ - chromatic_adaptation( - XYZ, - XYZ_w, - XYZ_wr, - method=method, - Y_o=Y_o, - E_o1=E_o, - E_o2=E_o, - L_A1=L_A, - L_A2=L_A, - Y_n=Y_n, + as_ndarray( + chromatic_adaptation( + XYZ, + XYZ_w, + XYZ_wr, + method=method, + Y_o=Y_o, + E_o1=E_o, + E_o2=E_o, + L_A1=L_A, + L_A2=L_A, + Y_n=Y_n, + ) ) for method in m ] @@ -134,7 +149,7 @@ def test_domain_range_scale_chromatic_adaptation(self) -> None: for method, value in zip(m, v, strict=True): for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation( XYZ * factor, XYZ_w * factor, diff --git a/colour/adaptation/tests/test_cie1994.py b/colour/adaptation/tests/test_cie1994.py index 7d68411962..741660af36 100644 --- a/colour/adaptation/tests/test_cie1994.py +++ b/colour/adaptation/tests/test_cie1994.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.adaptation import chromatic_adaptation_CIE1994 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,115 +40,121 @@ class TestChromaticAdaptationCIE1994: definition unit tests methods. """ - def test_chromatic_adaptation_CIE1994(self) -> None: + def test_chromatic_adaptation_CIE1994(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.cie1994.chromatic_adaptation_CIE1994` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CIE1994( - XYZ_1=np.array([28.00, 21.26, 5.27]), - xy_o1=np.array([0.44760, 0.40740]), - xy_o2=np.array([0.31270, 0.32900]), + XYZ_1=xp_as_array([28.00, 21.26, 5.27], xp=xp), + xy_o1=xp_as_array([0.44760, 0.40740], xp=xp), + xy_o2=xp_as_array([0.31270, 0.32900], xp=xp), Y_o=20, E_o1=1000, E_o2=1000, ), - np.array([24.03379521, 21.15621214, 17.64301199]), + [24.03379521, 21.15621214, 17.64301199], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CIE1994( - XYZ_1=np.array([21.77, 19.18, 16.73]), - xy_o1=np.array([0.31270, 0.32900]), - xy_o2=np.array([0.31270, 0.32900]), + XYZ_1=xp_as_array([21.77, 19.18, 16.73], xp=xp), + xy_o1=xp_as_array([0.31270, 0.32900], xp=xp), + xy_o2=xp_as_array([0.31270, 0.32900], xp=xp), Y_o=50, E_o1=100, E_o2=1000, ), - np.array([21.12891746, 19.42980532, 19.49577765]), + [21.12891746, 19.42980532, 19.49577765], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CIE1994( - XYZ_1=np.array([0.07818780, 0.06157201, 0.28099326]) * 100, - xy_o1=np.array([0.31270, 0.32900]), - xy_o2=np.array([0.37208, 0.37529]), + XYZ_1=xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, + xy_o1=xp_as_array([0.31270, 0.32900], xp=xp), + xy_o2=xp_as_array([0.37208, 0.37529], xp=xp), Y_o=20, E_o1=100, E_o2=1000, ), - np.array([9.14287406, 9.35843355, 15.95753504]), + [9.14287406, 9.35843355, 15.95753504], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_CIE1994(self) -> None: + def test_n_dimensional_chromatic_adaptation_CIE1994(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.cie1994.chromatic_adaptation_CIE1994` definition n-dimensional arrays support. """ - XYZ_1 = np.array([28.00, 21.26, 5.27]) - xy_o1 = np.array([0.44760, 0.40740]) - xy_o2 = np.array([0.31270, 0.32900]) + XYZ_1 = xp_as_array([28.00, 21.26, 5.27], xp=xp) + xy_o1 = xp_as_array([0.44760, 0.40740], xp=xp) + xy_o2 = xp_as_array([0.31270, 0.32900], xp=xp) Y_o = 20 E_o1 = 1000 E_o2 = 1000 - XYZ_2 = chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) + XYZ_2 = as_ndarray( + chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) + ) - XYZ_1 = np.tile(XYZ_1, (6, 1)) - XYZ_2 = np.tile(XYZ_2, (6, 1)) - np.testing.assert_allclose( + XYZ_1 = xp.tile(xp_as_array(XYZ_1, xp=xp), (6, 1)) + XYZ_2 = xp.tile(xp_as_array(XYZ_2, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2), XYZ_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy_o1 = np.tile(xy_o1, (6, 1)) - xy_o2 = np.tile(xy_o2, (6, 1)) - Y_o = np.tile(Y_o, 6) - E_o1 = np.tile(E_o1, 6) - E_o2 = np.tile(E_o2, 6) - np.testing.assert_allclose( + xy_o1 = xp.tile(xp_as_array(xy_o1, xp=xp), (6, 1)) + xy_o2 = xp.tile(xp_as_array(xy_o2, xp=xp), (6, 1)) + Y_o = xp.tile(xp_as_array(Y_o, xp=xp), (6,)) + E_o1 = xp.tile(xp_as_array(E_o1, xp=xp), (6,)) + E_o2 = xp.tile(xp_as_array(E_o2, xp=xp), (6,)) + xp_assert_close( chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2), XYZ_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_1 = np.reshape(XYZ_1, (2, 3, 3)) - xy_o1 = np.reshape(xy_o1, (2, 3, 2)) - xy_o2 = np.reshape(xy_o2, (2, 3, 2)) - Y_o = np.reshape(Y_o, (2, 3)) - E_o1 = np.reshape(E_o1, (2, 3)) - E_o2 = np.reshape(E_o2, (2, 3)) - XYZ_2 = np.reshape(XYZ_2, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_1 = xp_reshape(xp_as_array(XYZ_1, xp=xp), (2, 3, 3), xp=xp) + xy_o1 = xp_reshape(xp_as_array(xy_o1, xp=xp), (2, 3, 2), xp=xp) + xy_o2 = xp_reshape(xp_as_array(xy_o2, xp=xp), (2, 3, 2), xp=xp) + Y_o = xp_reshape(xp_as_array(Y_o, xp=xp), (2, 3), xp=xp) + E_o1 = xp_reshape(xp_as_array(E_o1, xp=xp), (2, 3), xp=xp) + E_o2 = xp_reshape(xp_as_array(E_o2, xp=xp), (2, 3), xp=xp) + XYZ_2 = xp_reshape(xp_as_array(XYZ_2, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2), XYZ_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_CIE1994(self) -> None: + def test_domain_range_scale_chromatic_adaptation_CIE1994( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.cie1994.chromatic_adaptation_CIE1994` definition domain and range scale support. """ - XYZ_1 = np.array([28.00, 21.26, 5.27]) - xy_o1 = np.array([0.44760, 0.40740]) - xy_o2 = np.array([0.31270, 0.32900]) + XYZ_1 = xp_as_array([28.00, 21.26, 5.27], xp=xp) + xy_o1 = xp_as_array([0.44760, 0.40740], xp=xp) + xy_o2 = xp_as_array([0.31270, 0.32900], xp=xp) Y_o = 20 E_o1 = 1000 E_o2 = 1000 - XYZ_2 = chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) + XYZ_2 = as_ndarray( + chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) + ) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CIE1994( XYZ_1 * factor, xy_o1, xy_o2, Y_o * factor, E_o1, E_o2 ), diff --git a/colour/adaptation/tests/test_cmccat2000.py b/colour/adaptation/tests/test_cmccat2000.py index f74122c31d..8cabb44e0f 100644 --- a/colour/adaptation/tests/test_cmccat2000.py +++ b/colour/adaptation/tests/test_cmccat2000.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -12,7 +17,14 @@ chromatic_adaptation_inverse_CMCCAT2000, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -34,110 +46,118 @@ class TestChromaticAdaptationForwardCMCCAT2000: chromatic_adaptation_forward_CMCCAT2000` definition unit tests methods. """ - def test_chromatic_adaptation_forward_CMCCAT2000(self) -> None: + def test_chromatic_adaptation_forward_CMCCAT2000(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_forward_CMCCAT2000` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000( - np.array([22.48, 22.74, 8.54]), - np.array([111.15, 100.00, 35.20]), - np.array([94.81, 100.00, 107.30]), + xp_as_array([22.48, 22.74, 8.54], xp=xp), + xp_as_array([111.15, 100.00, 35.20], xp=xp), + xp_as_array([94.81, 100.00, 107.30], xp=xp), 200, 200, ), - np.array([19.52698326, 23.06833960, 24.97175229]), + [19.52698326, 23.06833960, 24.97175229], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000( - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([1.09846607, 1.00000000, 0.35582280]) * 100, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp) * 100, 100, 100, ), - np.array([17.90511171, 22.75299363, 3.79837384]), + [17.90511171, 22.75299363, 3.79837384], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000( - np.array([0.07818780, 0.06157201, 0.28099326]) * 100, - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([0.99144661, 1.00000000, 0.67315942]) * 100, + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp) * 100, 100, 100, ), - np.array([6.76564344, 5.86585763, 18.40577315]), + [6.76564344, 5.86585763, 18.40577315], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_forward_CMCCAT2000(self) -> None: + def test_n_dimensional_chromatic_adaptation_forward_CMCCAT2000( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_forward_CMCCAT2000` definition n-dimensional arrays support. """ - XYZ = np.array([22.48, 22.74, 8.54]) - XYZ_w = np.array([111.15, 100.00, 35.20]) - XYZ_wr = np.array([94.81, 100.00, 107.30]) + XYZ = xp_as_array([22.48, 22.74, 8.54], xp=xp) + XYZ_w = xp_as_array([111.15, 100.00, 35.20], xp=xp) + XYZ_wr = xp_as_array([94.81, 100.00, 107.30], xp=xp) L_A1 = 200 L_A2 = 200 - XYZ_c = chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2) + XYZ_c = as_ndarray( + chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2) + ) - XYZ = np.tile(XYZ, (6, 1)) - XYZ_c = np.tile(XYZ_c, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + XYZ_c = xp.tile(xp_as_array(XYZ_c, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2), XYZ_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - XYZ_wr = np.tile(XYZ_wr, (6, 1)) - L_A1 = np.tile(L_A1, 6) - L_A2 = np.tile(L_A2, 6) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + XYZ_wr = xp.tile(xp_as_array(XYZ_wr, xp=xp), (6, 1)) + L_A1 = xp.tile(xp_as_array(L_A1, xp=xp), (6,)) + L_A2 = xp.tile(xp_as_array(L_A2, xp=xp), (6,)) + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2), XYZ_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ_wr = np.reshape(XYZ_wr, (2, 3, 3)) - L_A1 = np.reshape(L_A1, (2, 3)) - L_A2 = np.reshape(L_A2, (2, 3)) - XYZ_c = np.reshape(XYZ_c, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ_wr = xp_reshape(xp_as_array(XYZ_wr, xp=xp), (2, 3, 3), xp=xp) + L_A1 = xp_reshape(xp_as_array(L_A1, xp=xp), (2, 3), xp=xp) + L_A2 = xp_reshape(xp_as_array(L_A2, xp=xp), (2, 3), xp=xp) + XYZ_c = xp_reshape(xp_as_array(XYZ_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2), XYZ_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_CMCCAT2000(self) -> None: + def test_domain_range_scale_chromatic_adaptation_CMCCAT2000( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_forward_CMCCAT2000` definition domain and range scale support. """ - XYZ = np.array([22.48, 22.74, 8.54]) - XYZ_w = np.array([111.15, 100.00, 35.20]) - XYZ_wr = np.array([94.81, 100.00, 107.30]) + XYZ = xp_as_array([22.48, 22.74, 8.54], xp=xp) + XYZ_w = xp_as_array([111.15, 100.00, 35.20], xp=xp) + XYZ_wr = xp_as_array([94.81, 100.00, 107.30], xp=xp) L_A1 = 200 L_A2 = 200 - XYZ_c = chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2) + XYZ_c = as_ndarray( + chromatic_adaptation_forward_CMCCAT2000(XYZ, XYZ_w, XYZ_wr, L_A1, L_A2) + ) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_forward_CMCCAT2000( XYZ * factor, XYZ_w * factor, @@ -169,110 +189,118 @@ class TestChromaticAdaptationInverseCMCCAT2000: chromatic_adaptation_inverse_CMCCAT2000` definition unit tests methods. """ - def test_chromatic_adaptation_inverse_CMCCAT2000(self) -> None: + def test_chromatic_adaptation_inverse_CMCCAT2000(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_inverse_CMCCAT2000` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000( - np.array([19.52698326, 23.06833960, 24.97175229]), - np.array([111.15, 100.00, 35.20]), - np.array([94.81, 100.00, 107.30]), + xp_as_array([19.52698326, 23.06833960, 24.97175229], xp=xp), + xp_as_array([111.15, 100.00, 35.20], xp=xp), + xp_as_array([94.81, 100.00, 107.30], xp=xp), 200, 200, ), - np.array([22.48, 22.74, 8.54]), + [22.48, 22.74, 8.54], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000( - np.array([17.90511171, 22.75299363, 3.79837384]), - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([1.09846607, 1.00000000, 0.35582280]) * 100, + xp_as_array([17.90511171, 22.75299363, 3.79837384], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp) * 100, 100, 100, ), - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000( - np.array([6.76564344, 5.86585763, 18.40577315]), - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([0.99144661, 1.00000000, 0.67315942]) * 100, + xp_as_array([6.76564344, 5.86585763, 18.40577315], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp) * 100, 100, 100, ), - np.array([0.07818780, 0.06157201, 0.28099326]) * 100, + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_inverse_CMCCAT2000(self) -> None: + def test_n_dimensional_chromatic_adaptation_inverse_CMCCAT2000( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_inverse_CMCCAT2000` definition n-dimensional arrays support. """ - XYZ_c = np.array([19.52698326, 23.06833960, 24.97175229]) - XYZ_w = np.array([111.15, 100.00, 35.20]) - XYZ_wr = np.array([94.81, 100.00, 107.30]) + XYZ_c = xp_as_array([19.52698326, 23.06833960, 24.97175229], xp=xp) + XYZ_w = xp_as_array([111.15, 100.00, 35.20], xp=xp) + XYZ_wr = xp_as_array([94.81, 100.00, 107.30], xp=xp) L_A1 = 200 L_A2 = 200 - XYZ = chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2) + XYZ = as_ndarray( + chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2) + ) - XYZ_c = np.tile(XYZ_c, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ_c = xp.tile(xp_as_array(XYZ_c, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - XYZ_wr = np.tile(XYZ_wr, (6, 1)) - L_A1 = np.tile(L_A1, 6) - L_A2 = np.tile(L_A2, 6) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + XYZ_wr = xp.tile(xp_as_array(XYZ_wr, xp=xp), (6, 1)) + L_A1 = xp.tile(xp_as_array(L_A1, xp=xp), (6,)) + L_A2 = xp.tile(xp_as_array(L_A2, xp=xp), (6,)) + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_c = np.reshape(XYZ_c, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ_wr = np.reshape(XYZ_wr, (2, 3, 3)) - L_A1 = np.reshape(L_A1, (2, 3)) - L_A2 = np.reshape(L_A2, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_c = xp_reshape(xp_as_array(XYZ_c, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ_wr = xp_reshape(xp_as_array(XYZ_wr, xp=xp), (2, 3, 3), xp=xp) + L_A1 = xp_reshape(xp_as_array(L_A1, xp=xp), (2, 3), xp=xp) + L_A2 = xp_reshape(xp_as_array(L_A2, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_CMCCAT2000(self) -> None: + def test_domain_range_scale_chromatic_adaptation_CMCCAT2000( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_inverse_CMCCAT2000` definition domain and range scale support. """ - XYZ_c = np.array([19.52698326, 23.06833960, 24.97175229]) - XYZ_w = np.array([111.15, 100.00, 35.20]) - XYZ_wr = np.array([94.81, 100.00, 107.30]) + XYZ_c = xp_as_array([19.52698326, 23.06833960, 24.97175229], xp=xp) + XYZ_w = xp_as_array([111.15, 100.00, 35.20], xp=xp) + XYZ_wr = xp_as_array([94.81, 100.00, 107.30], xp=xp) L_A1 = 200 L_A2 = 200 - XYZ = chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2) + XYZ = as_ndarray( + chromatic_adaptation_inverse_CMCCAT2000(XYZ_c, XYZ_w, XYZ_wr, L_A1, L_A2) + ) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_inverse_CMCCAT2000( XYZ_c * factor, XYZ_w * factor, @@ -304,46 +332,46 @@ class TestChromaticAdaptationCMCCAT2000: chromatic_adaptation_CMCCAT2000` wrapper definition unit tests methods. """ - def test_chromatic_adaptation_CMCCAT2000(self) -> None: + def test_chromatic_adaptation_CMCCAT2000(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.cmccat2000.\ chromatic_adaptation_CMCCAT2000` wrapper definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CMCCAT2000( - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([1.09846607, 1.00000000, 0.35582280]) * 100, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp) * 100, 100, 100, ), - np.array([17.90511171, 22.75299363, 3.79837384]), + [17.90511171, 22.75299363, 3.79837384], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CMCCAT2000( - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([1.09846607, 1.00000000, 0.35582280]) * 100, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp) * 100, 100, 100, direction="Forward", ), - np.array([17.90511171, 22.75299363, 3.79837384]), + [17.90511171, 22.75299363, 3.79837384], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_CMCCAT2000( - np.array([17.90511171, 22.75299363, 3.79837384]), - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([1.09846607, 1.00000000, 0.35582280]) * 100, + xp_as_array([17.90511171, 22.75299363, 3.79837384], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp) * 100, 100, 100, direction="Inverse", ), - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/adaptation/tests/test_fairchild1990.py b/colour/adaptation/tests/test_fairchild1990.py index ad7b5ac872..84557a07b5 100644 --- a/colour/adaptation/tests/test_fairchild1990.py +++ b/colour/adaptation/tests/test_fairchild1990.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import contextlib from itertools import product @@ -10,7 +15,14 @@ from colour.adaptation import chromatic_adaptation_Fairchild1990 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -30,114 +42,118 @@ class TestChromaticAdaptationFairchild1990: chromatic_adaptation_Fairchild1990` definition unit tests methods. """ - def test_chromatic_adaptation_Fairchild1990(self) -> None: + def test_chromatic_adaptation_Fairchild1990(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.fairchild1990.\ chromatic_adaptation_Fairchild1990` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Fairchild1990( - np.array([19.53, 23.07, 24.97]), - np.array([111.15, 100.00, 35.20]), - np.array([94.81, 100.00, 107.30]), + xp_as_array([19.53, 23.07, 24.97], xp=xp), + xp_as_array([111.15, 100.00, 35.20], xp=xp), + xp_as_array([94.81, 100.00, 107.30], xp=xp), 200, ), - np.array([23.32526349, 23.32455819, 76.11593750]), + [23.32526349, 23.32455819, 76.11593750], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Fairchild1990( - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([1.09846607, 1.00000000, 0.35582280]) * 100, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp) * 100, 200, ), - np.array([19.28089326, 22.91583715, 3.42923503]), + [19.28089326, 22.91583715, 3.42923503], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Fairchild1990( - np.array([0.07818780, 0.06157201, 0.28099326]) * 100, - np.array([0.95045593, 1.00000000, 1.08905775]) * 100, - np.array([0.99144661, 1.00000000, 0.67315942]) * 100, + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) * 100, + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp) * 100, 200, ), - np.array([6.35093475, 6.13061347, 17.36852430]), + [6.35093475, 6.13061347, 17.36852430], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test with discount_illuminant=True - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Fairchild1990( - np.array([19.53, 23.07, 24.97]), - np.array([111.15, 100.00, 35.20]), - np.array([94.81, 100.00, 107.30]), + xp_as_array([19.53, 23.07, 24.97], xp=xp), + xp_as_array([111.15, 100.00, 35.20], xp=xp), + xp_as_array([94.81, 100.00, 107.30], xp=xp), 200, discount_illuminant=True, ), - np.array([23.32526349, 23.32455819, 76.11593750]), + [23.32526349, 23.32455819, 76.11593750], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_Fairchild1990(self) -> None: + def test_n_dimensional_chromatic_adaptation_Fairchild1990( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.fairchild1990.\ chromatic_adaptation_Fairchild1990` definition n-dimensional arrays support. """ - XYZ_1 = np.array([19.53, 23.07, 24.97]) - XYZ_n = np.array([111.15, 100.00, 35.20]) - XYZ_r = np.array([94.81, 100.00, 107.30]) + XYZ_1 = xp_as_array([19.53, 23.07, 24.97], xp=xp) + XYZ_n = xp_as_array([111.15, 100.00, 35.20], xp=xp) + XYZ_r = xp_as_array([94.81, 100.00, 107.30], xp=xp) Y_n = 200 - XYZ_c = chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n) + XYZ_c = as_ndarray(chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n)) - XYZ_1 = np.tile(XYZ_1, (6, 1)) - XYZ_c = np.tile(XYZ_c, (6, 1)) - np.testing.assert_allclose( + XYZ_1 = xp.tile(xp_as_array(XYZ_1, xp=xp), (6, 1)) + XYZ_c = xp.tile(xp_as_array(XYZ_c, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n), XYZ_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_n = np.tile(XYZ_n, (6, 1)) - XYZ_r = np.tile(XYZ_r, (6, 1)) - Y_n = np.tile(Y_n, 6) - np.testing.assert_allclose( + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + XYZ_r = xp.tile(xp_as_array(XYZ_r, xp=xp), (6, 1)) + Y_n = xp.tile(xp_as_array(Y_n, xp=xp), (6,)) + xp_assert_close( chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n), XYZ_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_1 = np.reshape(XYZ_1, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - XYZ_r = np.reshape(XYZ_r, (2, 3, 3)) - Y_n = np.reshape(Y_n, (2, 3)) - XYZ_c = np.reshape(XYZ_c, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_1 = xp_reshape(xp_as_array(XYZ_1, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + XYZ_r = xp_reshape(xp_as_array(XYZ_r, xp=xp), (2, 3, 3), xp=xp) + Y_n = xp_reshape(xp_as_array(Y_n, xp=xp), (2, 3), xp=xp) + XYZ_c = xp_reshape(xp_as_array(XYZ_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n), XYZ_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_Fairchild1990(self) -> None: + def test_domain_range_scale_chromatic_adaptation_Fairchild1990( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.fairchild1990.\ chromatic_adaptation_Fairchild1990` definition domain and range scale support. """ - XYZ_1 = np.array([19.53, 23.07, 24.97]) - XYZ_n = np.array([111.15, 100.00, 35.20]) - XYZ_r = np.array([94.81, 100.00, 107.30]) + XYZ_1 = xp_as_array([19.53, 23.07, 24.97], xp=xp) + XYZ_n = xp_as_array([111.15, 100.00, 35.20], xp=xp) + XYZ_r = xp_as_array([94.81, 100.00, 107.30], xp=xp) Y_n = 200 - XYZ_c = chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n) + XYZ_c = as_ndarray(chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r, Y_n)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Fairchild1990( XYZ_1 * factor, XYZ_n * factor, XYZ_r * factor, Y_n ), diff --git a/colour/adaptation/tests/test_fairchild2020.py b/colour/adaptation/tests/test_fairchild2020.py index c753b21c50..588b9975f2 100644 --- a/colour/adaptation/tests/test_fairchild2020.py +++ b/colour/adaptation/tests/test_fairchild2020.py @@ -4,7 +4,11 @@ from __future__ import annotations -import unittest +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -15,7 +19,14 @@ ) from colour.adaptation.fairchild2020 import CONDITIONS_DEGREE_OF_ADAPTATION_VK20 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -30,146 +41,154 @@ ] -class TestMatrixChromaticAdaptationVonKries(unittest.TestCase): +class TestMatrixChromaticAdaptationVonKries: """ Define :func:`colour.adaptation.fairchild2020.\ matrix_chromatic_adaptation_vk20` definition unit tests methods. """ - def test_matrix_chromatic_adaptation_vk20(self) -> None: + def test_matrix_chromatic_adaptation_vk20(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.fairchild2020.\ matrix_chromatic_adaptation_vk20` definition. """ - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - ), - np.array( - [ - [1.02791390, 0.02913712, -0.02279407], - [0.02070284, 0.99005317, -0.00921435], - [-0.00063759, -0.00115773, 0.91296320], - ] + xp_assert_close( + as_ndarray( + matrix_chromatic_adaptation_vk20( + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + ) ), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [ + [1.02791390, 0.02913712, -0.02279407], + [0.02070284, 0.99005317, -0.00921435], + [-0.00063759, -0.00115773, 0.91296320], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([1.09846607, 1.00000000, 0.35582280]), - ), - np.array( - [ - [0.94760338, -0.05816939, 0.06647414], - [-0.04151006, 1.02361127, 0.02667016], - [0.00163074, 0.00391656, 1.29341031], - ] + xp_assert_close( + as_ndarray( + matrix_chromatic_adaptation_vk20( + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp), + ) ), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [ + [0.94760338, -0.05816939, 0.06647414], + [-0.04151006, 1.02361127, 0.02667016], + [0.00163074, 0.00391656, 1.29341031], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - transform="XYZ Scaling", - ), - np.array( - [ - [1.03217229, 0.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.91134516], - ] + xp_assert_close( + as_ndarray( + matrix_chromatic_adaptation_vk20( + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + transform="XYZ Scaling", + ) ), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [ + [1.03217229, 0.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.91134516], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - transform="Bradford", - ), - np.array( - [ - [1.03672305, 0.01955802, -0.02193210], - [0.02763218, 0.98222961, -0.00824197], - [-0.00295083, 0.00406903, 0.91024305], - ] + xp_assert_close( + as_ndarray( + matrix_chromatic_adaptation_vk20( + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + transform="Bradford", + ) ), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [ + [1.03672305, 0.01955802, -0.02193210], + [0.02763218, 0.98222961, -0.00824197], + [-0.00295083, 0.00406903, 0.91024305], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - transform="Von Kries", - coefficients=CONDITIONS_DEGREE_OF_ADAPTATION_VK20["Simple Von Kries"], - ), - np.array( - [ - [0.98446157, -0.05474538, 0.06773143], - [-0.00601339, 1.00479590, 0.00121235], - [0.00000000, 0.00000000, 1.31990977], - ] + xp_assert_close( + as_ndarray( + matrix_chromatic_adaptation_vk20( + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + transform="Von Kries", + coefficients=CONDITIONS_DEGREE_OF_ADAPTATION_VK20[ + "Simple Von Kries" + ], + ) ), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [ + [0.98446157, -0.05474538, 0.06773143], + [-0.00601339, 1.00479590, 0.00121235], + [0.00000000, 0.00000000, 1.31990977], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_matrix_chromatic_adaptation_vk20(self) -> None: + def test_n_dimensional_matrix_chromatic_adaptation_vk20( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.fairchild2020.\ matrix_chromatic_adaptation_vk20` definition n-dimensional arrays support. """ - XYZ_p = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_n = np.array([0.96429568, 1.00000000, 0.82510460]) - M = matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n) - - XYZ_p = np.tile(XYZ_p, (6, 1)) - XYZ_n = np.tile(XYZ_n, (6, 1)) - M = np.reshape(np.tile(M, (6, 1)), (6, 3, 3)) - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n), - M, - decimal=TOLERANCE_ABSOLUTE_TESTS, + XYZ_p = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_n = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + M = as_ndarray(matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n)) + + XYZ_p = xp.tile(xp_as_array(XYZ_p, xp=xp), (6, 1)) + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + M = xp_reshape(xp.tile(xp_as_array(M, xp=xp), (6, 1)), (6, 3, 3), xp=xp) + xp_assert_close( + as_ndarray(matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n)), + as_ndarray(M), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_p = np.reshape(XYZ_p, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - M = np.reshape(M, (2, 3, 3, 3)) - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n), - M, - decimal=TOLERANCE_ABSOLUTE_TESTS, + XYZ_p = xp_reshape(xp_as_array(XYZ_p, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + M = xp_reshape(xp_as_array(M, xp=xp), (2, 3, 3, 3), xp=xp) + xp_assert_close( + as_ndarray(matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n)), + as_ndarray(M), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_matrix_chromatic_adaptation_vk20(self) -> None: + def test_domain_range_scale_matrix_chromatic_adaptation_vk20( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.fairchild2020.\ matrix_chromatic_adaptation_vk20` definition domain and range scale support. """ - XYZ_p = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_n = np.array([0.96429568, 1.00000000, 0.82510460]) - XYZ_r = np.array([0.97941176, 1.00000000, 1.73235294]) - M = matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n) + XYZ_p = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_n = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + XYZ_r = xp_as_array([0.97941176, 1.00000000, 1.73235294], xp=xp) + M = as_ndarray(matrix_chromatic_adaptation_vk20(XYZ_p, XYZ_n)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_almost_equal( - matrix_chromatic_adaptation_vk20( - XYZ_p * factor, XYZ_n * factor, XYZ_r * factor + xp_assert_close( + as_ndarray( + matrix_chromatic_adaptation_vk20( + XYZ_p * factor, XYZ_n * factor, XYZ_r * factor + ) ), M, - decimal=TOLERANCE_ABSOLUTE_TESTS, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -184,137 +203,153 @@ def test_nan_matrix_chromatic_adaptation_vk20(self) -> None: matrix_chromatic_adaptation_vk20(cases, cases) -class TestChromaticAdaptationVonKries(unittest.TestCase): +class TestChromaticAdaptationVonKries: """ Define :func:`colour.adaptation.fairchild2020.chromatic_adaptation_vK20` definition unit tests methods. """ - def test_chromatic_adaptation_vK20(self) -> None: + def test_chromatic_adaptation_vK20(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.fairchild2020.chromatic_adaptation_vK20` definition. """ - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + ) ), - np.array([0.21468842, 0.12456164, 0.04662558]), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [0.21468842, 0.12456164, 0.04662558], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - np.array([0.14222010, 0.23042768, 0.10495772]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([1.09846607, 1.00000000, 0.35582280]), + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp), + ) ), - np.array([0.12834138, 0.23276404, 0.13688781]), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [0.12834138, 0.23276404, 0.13688781], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - np.array([0.07818780, 0.06157201, 0.28099326]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.99144661, 1.00000000, 0.67315942]), + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp), + ) ), - np.array([0.07908008, 0.06167829, 0.28354175]), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [0.07908008, 0.06167829, 0.28354175], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - transform="XYZ Scaling", + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + transform="XYZ Scaling", + ) ), - np.array([0.21318495, 0.12197225, 0.04681536]), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [0.21318495, 0.12197225, 0.04681536], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - transform="Bradford", + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + transform="Bradford", + ) ), - np.array([0.21538376, 0.12508852, 0.04664559]), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [0.21538376, 0.12508852, 0.04664559], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - transform="Von Kries", - coefficients=CONDITIONS_DEGREE_OF_ADAPTATION_VK20["Simple Von Kries"], + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), + transform="Von Kries", + coefficients=CONDITIONS_DEGREE_OF_ADAPTATION_VK20[ + "Simple Von Kries" + ], + ) ), - np.array([0.20013269, 0.12137749, 0.06780313]), - decimal=TOLERANCE_ABSOLUTE_TESTS, + [0.20013269, 0.12137749, 0.06780313], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_vK20(self) -> None: + def test_n_dimensional_chromatic_adaptation_vK20(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.fairchild2020.chromatic_adaptation_vK20` definition n-dimensional arrays support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_p = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_n = np.array([0.96429568, 1.00000000, 0.82510460]) - XYZ_a = chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n) - - XYZ = np.tile(XYZ, (6, 1)) - XYZ_p = np.tile(XYZ_p, (6, 1)) - XYZ_n = np.tile(XYZ_n, (6, 1)) - XYZ_a = np.tile(XYZ_a, (6, 1)) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n), - XYZ_a, - decimal=TOLERANCE_ABSOLUTE_TESTS, + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_p = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_n = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + XYZ_a = as_ndarray(chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n)) + + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + XYZ_p = xp.tile(xp_as_array(XYZ_p, xp=xp), (6, 1)) + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + XYZ_a = xp.tile(xp_as_array(XYZ_a, xp=xp), (6, 1)) + xp_assert_close( + as_ndarray(chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n)), + as_ndarray(XYZ_a), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_p = np.reshape(XYZ_p, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - XYZ_a = np.reshape(XYZ_a, (2, 3, 3)) - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n), - XYZ_a, - decimal=TOLERANCE_ABSOLUTE_TESTS, + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_p = xp_reshape(xp_as_array(XYZ_p, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + XYZ_a = xp_reshape(xp_as_array(XYZ_a, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + as_ndarray(chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n)), + as_ndarray(XYZ_a), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_vK20(self) -> None: + def test_domain_range_scale_chromatic_adaptation_vK20(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.fairchild2020.chromatic_adaptation_vK20` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_p = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_n = np.array([0.96429568, 1.00000000, 0.82510460]) - XYZ_r = np.array([0.97941176, 1.00000000, 1.73235294]) - XYZ_a = chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_p = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_n = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + XYZ_r = xp_as_array([0.97941176, 1.00000000, 1.73235294], xp=xp) + XYZ_a = as_ndarray(chromatic_adaptation_vK20(XYZ, XYZ_p, XYZ_n)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_almost_equal( - chromatic_adaptation_vK20( - XYZ * factor, - XYZ_p * factor, - XYZ_n * factor, - XYZ_r * factor, + xp_assert_close( + as_ndarray( + chromatic_adaptation_vK20( + XYZ * factor, + XYZ_p * factor, + XYZ_n * factor, + XYZ_r * factor, + ) ), XYZ_a * factor, - decimal=TOLERANCE_ABSOLUTE_TESTS, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors diff --git a/colour/adaptation/tests/test_li2025.py b/colour/adaptation/tests/test_li2025.py index 140acaead2..3bfacbb32b 100644 --- a/colour/adaptation/tests/test_li2025.py +++ b/colour/adaptation/tests/test_li2025.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.adaptation import chromatic_adaptation_Li2025 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,121 +40,127 @@ class TestChromaticAdaptationLi2025: definition unit tests methods. """ - def test_chromatic_adaptation_Li2025(self) -> None: + def test_chromatic_adaptation_Li2025(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.li2025.chromatic_adaptation_Li2025` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Li2025( - XYZ_s=np.array([48.900, 43.620, 6.250]), - XYZ_ws=np.array([109.850, 100, 35.585]), - XYZ_wd=np.array([95.047, 100, 108.883]), + XYZ_s=xp_as_array([48.900, 43.620, 6.250], xp=xp), + XYZ_ws=xp_as_array([109.850, 100, 35.585], xp=xp), + XYZ_wd=xp_as_array([95.047, 100, 108.883], xp=xp), L_A=318.31, F_surround=1.0, ), - np.array([40.00725815, 43.70148954, 21.32902932]), + [40.00725815, 43.70148954, 21.32902932], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Li2025( - XYZ_s=np.array([52.034, 58.824, 23.703]), - XYZ_ws=np.array([92.288, 100, 38.775]), - XYZ_wd=np.array([105.432, 100, 137.392]), + XYZ_s=xp_as_array([52.034, 58.824, 23.703], xp=xp), + XYZ_ws=xp_as_array([92.288, 100, 38.775], xp=xp), + XYZ_wd=xp_as_array([105.432, 100, 137.392], xp=xp), L_A=318.31, F_surround=1.0, ), - np.array([59.99869086, 58.81067197, 83.41018242]), + [59.99869086, 58.81067197, 83.41018242], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Li2025( - XYZ_s=np.array([48.900, 43.620, 6.250]), - XYZ_ws=np.array([109.850, 100, 35.585]), - XYZ_wd=np.array([95.047, 100, 108.883]), + XYZ_s=xp_as_array([48.900, 43.620, 6.250], xp=xp), + XYZ_ws=xp_as_array([109.850, 100, 35.585], xp=xp), + XYZ_wd=xp_as_array([95.047, 100, 108.883], xp=xp), L_A=20.0, F_surround=1.0, ), - np.array([41.22388901, 43.69034082, 19.26604215]), + [41.22388901, 43.69034082, 19.26604215], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Li2025( - XYZ_s=np.array([48.900, 43.620, 6.250]), - XYZ_ws=np.array([109.850, 100, 35.585]), - XYZ_wd=np.array([95.047, 100, 108.883]), + XYZ_s=xp_as_array([48.900, 43.620, 6.250], xp=xp), + XYZ_ws=xp_as_array([109.850, 100, 35.585], xp=xp), + XYZ_wd=xp_as_array([95.047, 100, 108.883], xp=xp), L_A=318.31, F_surround=1.0, discount_illuminant=True, ), - np.array([39.95779686, 43.70194278, 21.41289865]), + [39.95779686, 43.70194278, 21.41289865], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_Li2025(self) -> None: + def test_n_dimensional_chromatic_adaptation_Li2025(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.li2025.chromatic_adaptation_Li2025` definition n-dimensional arrays support. """ - XYZ_s = np.array([48.900, 43.620, 6.250]) - XYZ_ws = np.array([109.850, 100, 35.585]) - XYZ_wd = np.array([95.047, 100, 108.883]) + XYZ_s = xp_as_array([48.900, 43.620, 6.250], xp=xp) + XYZ_ws = xp_as_array([109.850, 100, 35.585], xp=xp) + XYZ_wd = xp_as_array([95.047, 100, 108.883], xp=xp) L_A = 318.31 F_surround = 1.0 - XYZ_d = chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround) + XYZ_d = as_ndarray( + chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround) + ) - XYZ_s = np.tile(XYZ_s, (6, 1)) - XYZ_d = np.tile(XYZ_d, (6, 1)) - np.testing.assert_allclose( + XYZ_s = xp.tile(xp_as_array(XYZ_s, xp=xp), (6, 1)) + XYZ_d = xp.tile(xp_as_array(XYZ_d, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround), XYZ_d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_ws = np.tile(XYZ_ws, (6, 1)) - XYZ_wd = np.tile(XYZ_wd, (6, 1)) - L_A = np.tile(L_A, 6) - F_surround = np.tile(F_surround, 6) - np.testing.assert_allclose( + XYZ_ws = xp.tile(xp_as_array(XYZ_ws, xp=xp), (6, 1)) + XYZ_wd = xp.tile(xp_as_array(XYZ_wd, xp=xp), (6, 1)) + L_A = xp.tile(xp_as_array(L_A, xp=xp), (6,)) + F_surround = xp.tile(xp_as_array(F_surround, xp=xp), (6,)) + xp_assert_close( chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround), XYZ_d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_s = np.reshape(XYZ_s, (2, 3, 3)) - XYZ_ws = np.reshape(XYZ_ws, (2, 3, 3)) - XYZ_wd = np.reshape(XYZ_wd, (2, 3, 3)) - L_A = np.reshape(L_A, (2, 3)) - F_surround = np.reshape(F_surround, (2, 3)) - XYZ_d = np.reshape(XYZ_d, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_s = xp_reshape(xp_as_array(XYZ_s, xp=xp), (2, 3, 3), xp=xp) + XYZ_ws = xp_reshape(xp_as_array(XYZ_ws, xp=xp), (2, 3, 3), xp=xp) + XYZ_wd = xp_reshape(xp_as_array(XYZ_wd, xp=xp), (2, 3, 3), xp=xp) + L_A = xp_reshape(xp_as_array(L_A, xp=xp), (2, 3), xp=xp) + F_surround = xp_reshape(xp_as_array(F_surround, xp=xp), (2, 3), xp=xp) + XYZ_d = xp_reshape(xp_as_array(XYZ_d, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround), XYZ_d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_Li2025(self) -> None: + def test_domain_range_scale_chromatic_adaptation_Li2025( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.li2025.chromatic_adaptation_Li2025` definition domain and range scale support. """ - XYZ_s = np.array([48.900, 43.620, 6.250]) - XYZ_ws = np.array([109.850, 100, 35.585]) - XYZ_wd = np.array([95.047, 100, 108.883]) + XYZ_s = xp_as_array([48.900, 43.620, 6.250], xp=xp) + XYZ_ws = xp_as_array([109.850, 100, 35.585], xp=xp) + XYZ_wd = xp_as_array([95.047, 100, 108.883], xp=xp) L_A = 318.31 F_surround = 1.0 - XYZ_d = chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround) + XYZ_d = as_ndarray( + chromatic_adaptation_Li2025(XYZ_s, XYZ_ws, XYZ_wd, L_A, F_surround) + ) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Li2025( XYZ_s * factor, XYZ_ws * factor, diff --git a/colour/adaptation/tests/test_vonkries.py b/colour/adaptation/tests/test_vonkries.py index d856fca39d..32ae2f0c4f 100644 --- a/colour/adaptation/tests/test_vonkries.py +++ b/colour/adaptation/tests/test_vonkries.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -11,7 +16,14 @@ matrix_chromatic_adaptation_VonKries, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -32,147 +44,143 @@ class TestMatrixChromaticAdaptationVonKries: matrix_chromatic_adaptation_VonKries` definition unit tests methods. """ - def test_matrix_chromatic_adaptation_VonKries(self) -> None: + def test_matrix_chromatic_adaptation_VonKries(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.vonkries.\ matrix_chromatic_adaptation_VonKries` definition. """ - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), - ), - np.array( - [ - [1.04257389, 0.03089108, -0.05281257], - [0.02219345, 1.00185663, -0.02107375], - [-0.00116488, -0.00342053, 0.76178907], - ] + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), ), + [ + [1.04257389, 0.03089108, -0.05281257], + [0.02219345, 1.00185663, -0.02107375], + [-0.00116488, -0.00342053, 0.76178907], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([1.09846607, 1.00000000, 0.35582280]), - ), - np.array( - [ - [1.17159793, 0.16088780, -0.16158366], - [0.11462057, 0.96182051, -0.06497572], - [-0.00413024, -0.00912739, 0.33871096], - ] + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp), ), + [ + [1.17159793, 0.16088780, -0.16158366], + [0.11462057, 0.96182051, -0.06497572], + [-0.00413024, -0.00912739, 0.33871096], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.99144661, 1.00000000, 0.67315942]), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp), ), np.linalg.inv( - matrix_chromatic_adaptation_VonKries( - np.array([0.99144661, 1.00000000, 0.67315942]), - np.array([0.95045593, 1.00000000, 1.08905775]), + as_ndarray( + matrix_chromatic_adaptation_VonKries( + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + ) ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), transform="XYZ Scaling", ), - np.array( - [ - [1.01456117, 0.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.75763163], - ] - ), + [ + [1.01456117, 0.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.75763163], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), transform="Bradford", ), - np.array( - [ - [1.04792979, 0.02294687, -0.05019227], - [0.02962781, 0.99043443, -0.01707380], - [-0.00924304, 0.01505519, 0.75187428], - ] - ), + [ + [1.04792979, 0.02294687, -0.05019227], + [0.02962781, 0.99043443, -0.01707380], + [-0.00924304, 0.01505519, 0.75187428], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), transform="Von Kries", ), - np.array( - [ - [1.01611856, 0.05535971, -0.05219186], - [0.00608087, 0.99555604, -0.00122642], - [0.00000000, 0.00000000, 0.75763163], - ] - ), + [ + [1.01611856, 0.05535971, -0.05219186], + [0.00608087, 0.99555604, -0.00122642], + [0.00000000, 0.00000000, 0.75763163], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_matrix_chromatic_adaptation_VonKries(self) -> None: + def test_n_dimensional_matrix_chromatic_adaptation_VonKries( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.vonkries.\ matrix_chromatic_adaptation_VonKries` definition n-dimensional arrays support. """ - XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) - M = matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr) + XYZ_w = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_wr = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + M = as_ndarray(matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr)) - XYZ_w = np.tile(XYZ_w, (6, 1)) - XYZ_wr = np.tile(XYZ_wr, (6, 1)) - M = np.reshape(np.tile(M, (6, 1)), (6, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + XYZ_wr = xp.tile(xp_as_array(XYZ_wr, xp=xp), (6, 1)) + M = xp_reshape(xp.tile(xp_as_array(M, xp=xp), (6, 1)), (6, 3, 3), xp=xp) + xp_assert_close( matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr), M, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ_wr = np.reshape(XYZ_wr, (2, 3, 3)) - M = np.reshape(M, (2, 3, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ_wr = xp_reshape(xp_as_array(XYZ_wr, xp=xp), (2, 3, 3), xp=xp) + M = xp_reshape(xp_as_array(M, xp=xp), (2, 3, 3, 3), xp=xp) + xp_assert_close( matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr), M, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_matrix_chromatic_adaptation_VonKries(self) -> None: + def test_domain_range_scale_matrix_chromatic_adaptation_VonKries( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.vonkries.\ matrix_chromatic_adaptation_VonKries` definition domain and range scale support. """ - XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) - M = matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr) + XYZ_w = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_wr = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + M = as_ndarray(matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( matrix_chromatic_adaptation_VonKries( XYZ_w * factor, XYZ_wr * factor ), @@ -198,121 +206,123 @@ class TestChromaticAdaptationVonKries: definition unit tests methods. """ - def test_chromatic_adaptation_VonKries(self) -> None: + def test_chromatic_adaptation_VonKries(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.vonkries.chromatic_adaptation_VonKries` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), ), - np.array([0.21638819, 0.12570000, 0.03847494]), + [0.21638819, 0.12570000, 0.03847494], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( - np.array([0.14222010, 0.23042768, 0.10495772]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([1.09846607, 1.00000000, 0.35582280]), + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([1.09846607, 1.00000000, 0.35582280], xp=xp), ), - np.array([0.18673833, 0.23111171, 0.03285972]), + [0.18673833, 0.23111171, 0.03285972], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( - np.array([0.07818780, 0.06157201, 0.28099326]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.99144661, 1.00000000, 0.67315942]), + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.99144661, 1.00000000, 0.67315942], xp=xp), ), - np.array([0.06385467, 0.05509729, 0.17506386]), + [0.06385467, 0.05509729, 0.17506386], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), transform="XYZ Scaling", ), - np.array([0.20954755, 0.12197225, 0.03891917]), + [0.20954755, 0.12197225, 0.03891917], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), transform="Bradford", ), - np.array([0.21666003, 0.12604777, 0.03855068]), + [0.21666003, 0.12604777, 0.03855068], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.95045593, 1.00000000, 1.08905775]), - np.array([0.96429568, 1.00000000, 0.82510460]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp), + xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp), transform="Von Kries", ), - np.array([0.21394049, 0.12262315, 0.03891917]), + [0.21394049, 0.12262315, 0.03891917], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_VonKries(self) -> None: + def test_n_dimensional_chromatic_adaptation_VonKries(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.vonkries.chromatic_adaptation_VonKries` definition n-dimensional arrays support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) - XYZ_a = chromatic_adaptation_VonKries(XYZ, XYZ_w, XYZ_wr) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_w = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_wr = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + XYZ_a = as_ndarray(chromatic_adaptation_VonKries(XYZ, XYZ_w, XYZ_wr)) - XYZ = np.tile(XYZ, (6, 1)) - XYZ_w = np.tile(XYZ_w, (6, 1)) - XYZ_wr = np.tile(XYZ_wr, (6, 1)) - XYZ_a = np.tile(XYZ_a, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + XYZ_wr = xp.tile(xp_as_array(XYZ_wr, xp=xp), (6, 1)) + XYZ_a = xp.tile(xp_as_array(XYZ_a, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_VonKries(XYZ, XYZ_w, XYZ_wr), XYZ_a, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ_wr = np.reshape(XYZ_wr, (2, 3, 3)) - XYZ_a = np.reshape(XYZ_a, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ_wr = xp_reshape(xp_as_array(XYZ_wr, xp=xp), (2, 3, 3), xp=xp) + XYZ_a = xp_reshape(xp_as_array(XYZ_a, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_VonKries(XYZ, XYZ_w, XYZ_wr), XYZ_a, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_VonKries(self) -> None: + def test_domain_range_scale_chromatic_adaptation_VonKries( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.vonkries.chromatic_adaptation_VonKries` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) - XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) - XYZ_a = chromatic_adaptation_VonKries(XYZ, XYZ_w, XYZ_wr) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_w = xp_as_array([0.95045593, 1.00000000, 1.08905775], xp=xp) + XYZ_wr = xp_as_array([0.96429568, 1.00000000, 0.82510460], xp=xp) + XYZ_a = as_ndarray(chromatic_adaptation_VonKries(XYZ, XYZ_w, XYZ_wr)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_VonKries( XYZ * factor, XYZ_w * factor, XYZ_wr * factor ), diff --git a/colour/adaptation/tests/test_zhai2018.py b/colour/adaptation/tests/test_zhai2018.py index 6bbc87fbba..6a16923774 100644 --- a/colour/adaptation/tests/test_zhai2018.py +++ b/colour/adaptation/tests/test_zhai2018.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.adaptation import chromatic_adaptation_Zhai2018 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,134 +40,138 @@ class TestChromaticAdaptationZhai2018: definition unit tests methods. """ - def test_chromatic_adaptation_Zhai2018(self) -> None: + def test_chromatic_adaptation_Zhai2018(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.zhai2018.chromatic_adaptation_Zhai2018` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Zhai2018( - XYZ_b=np.array([48.900, 43.620, 6.250]), - XYZ_wb=np.array([109.850, 100, 35.585]), - XYZ_wd=np.array([95.047, 100, 108.883]), + XYZ_b=xp_as_array([48.900, 43.620, 6.250], xp=xp), + XYZ_wb=xp_as_array([109.850, 100, 35.585], xp=xp), + XYZ_wd=xp_as_array([95.047, 100, 108.883], xp=xp), D_b=0.9407, D_d=0.9800, - XYZ_wo=np.array([100, 100, 100]), + XYZ_wo=xp_as_array([100, 100, 100], xp=xp), ), - np.array([39.18561644, 42.15461798, 19.23672036]), + [39.18561644, 42.15461798, 19.23672036], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Zhai2018( - XYZ_b=np.array([48.900, 43.620, 6.250]), - XYZ_wb=np.array([109.850, 100, 35.585]), - XYZ_wd=np.array([95.047, 100, 108.883]), + XYZ_b=xp_as_array([48.900, 43.620, 6.250], xp=xp), + XYZ_wb=xp_as_array([109.850, 100, 35.585], xp=xp), + XYZ_wd=xp_as_array([95.047, 100, 108.883], xp=xp), D_b=0.9407, D_d=0.9800, - XYZ_wo=np.array([100, 100, 100]), + XYZ_wo=xp_as_array([100, 100, 100], xp=xp), transform="CAT16", ), - np.array([40.37398343, 43.69426311, 20.51733764]), + [40.37398343, 43.69426311, 20.51733764], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Zhai2018( - XYZ_b=np.array([52.034, 58.824, 23.703]), - XYZ_wb=np.array([92.288, 100, 38.775]), - XYZ_wd=np.array([105.432, 100, 137.392]), + XYZ_b=xp_as_array([52.034, 58.824, 23.703], xp=xp), + XYZ_wb=xp_as_array([92.288, 100, 38.775], xp=xp), + XYZ_wd=xp_as_array([105.432, 100, 137.392], xp=xp), D_b=0.6709, D_d=0.5331, - XYZ_wo=np.array([97.079, 100, 141.798]), + XYZ_wo=xp_as_array([97.079, 100, 141.798], xp=xp), ), - np.array([57.03242915, 58.93434364, 64.76261333]), + [57.03242915, 58.93434364, 64.76261333], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Zhai2018( - XYZ_b=np.array([52.034, 58.824, 23.703]), - XYZ_wb=np.array([92.288, 100, 38.775]), - XYZ_wd=np.array([105.432, 100, 137.392]), + XYZ_b=xp_as_array([52.034, 58.824, 23.703], xp=xp), + XYZ_wb=xp_as_array([92.288, 100, 38.775], xp=xp), + XYZ_wd=xp_as_array([105.432, 100, 137.392], xp=xp), D_b=0.6709, D_d=0.5331, - XYZ_wo=np.array([97.079, 100, 141.798]), + XYZ_wo=xp_as_array([97.079, 100, 141.798], xp=xp), transform="CAT16", ), - np.array([56.77130011, 58.81317888, 64.66922808]), + [56.77130011, 58.81317888, 64.66922808], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Zhai2018( - XYZ_b=np.array([48.900, 43.620, 6.250]), - XYZ_wb=np.array([109.850, 100, 35.585]), - XYZ_wd=np.array([95.047, 100, 108.883]), + XYZ_b=xp_as_array([48.900, 43.620, 6.250], xp=xp), + XYZ_wb=xp_as_array([109.850, 100, 35.585], xp=xp), + XYZ_wd=xp_as_array([95.047, 100, 108.883], xp=xp), ), - np.array([38.72444735, 42.09232891, 20.05297620]), + [38.72444735, 42.09232891, 20.05297620], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_chromatic_adaptation_Zhai2018(self) -> None: + def test_n_dimensional_chromatic_adaptation_Zhai2018(self, xp: ModuleType) -> None: """ Test :func:`colour.adaptation.zhai2018.chromatic_adaptation_Zhai2018` definition n-dimensional arrays support. """ - XYZ_b = np.array([48.900, 43.620, 6.250]) - XYZ_wb = np.array([109.850, 100, 35.585]) - XYZ_wd = np.array([95.047, 100, 108.883]) + XYZ_b = xp_as_array([48.900, 43.620, 6.250], xp=xp) + XYZ_wb = xp_as_array([109.850, 100, 35.585], xp=xp) + XYZ_wd = xp_as_array([95.047, 100, 108.883], xp=xp) D_b = 0.9407 D_d = 0.9800 - XYZ_d = chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd, D_b, D_d) + XYZ_d = as_ndarray( + chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd, D_b, D_d) + ) - XYZ_b = np.tile(XYZ_b, (6, 1)) - XYZ_d = np.tile(XYZ_d, (6, 1)) - np.testing.assert_allclose( + XYZ_b = xp.tile(xp_as_array(XYZ_b, xp=xp), (6, 1)) + XYZ_d = xp.tile(xp_as_array(XYZ_d, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd, D_b, D_d), XYZ_d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_wb = np.tile(XYZ_wb, (6, 1)) - XYZ_wd = np.tile(XYZ_wd, (6, 1)) - D_b = np.tile(D_b, (6, 1)) - D_d = np.tile(D_d, (6, 1)) - np.testing.assert_allclose( + XYZ_wb = xp.tile(xp_as_array(XYZ_wb, xp=xp), (6, 1)) + XYZ_wd = xp.tile(xp_as_array(XYZ_wd, xp=xp), (6, 1)) + D_b = xp.tile(xp_as_array(D_b, xp=xp), (6, 1)) + D_d = xp.tile(xp_as_array(D_d, xp=xp), (6, 1)) + xp_assert_close( chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd, D_b, D_d), XYZ_d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_b = np.reshape(XYZ_b, (2, 3, 3)) - XYZ_wb = np.reshape(XYZ_wb, (2, 3, 3)) - XYZ_wd = np.reshape(XYZ_wd, (2, 3, 3)) - D_b = np.reshape(D_b, (2, 3, 1)) - D_d = np.reshape(D_d, (2, 3, 1)) - XYZ_d = np.reshape(XYZ_d, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_b = xp_reshape(xp_as_array(XYZ_b, xp=xp), (2, 3, 3), xp=xp) + XYZ_wb = xp_reshape(xp_as_array(XYZ_wb, xp=xp), (2, 3, 3), xp=xp) + XYZ_wd = xp_reshape(xp_as_array(XYZ_wd, xp=xp), (2, 3, 3), xp=xp) + D_b = xp_reshape(xp_as_array(D_b, xp=xp), (2, 3, 1), xp=xp) + D_d = xp_reshape(xp_as_array(D_d, xp=xp), (2, 3, 1), xp=xp) + XYZ_d = xp_reshape(xp_as_array(XYZ_d, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd, D_b, D_d), XYZ_d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_chromatic_adaptation_Zhai2018(self) -> None: + def test_domain_range_scale_chromatic_adaptation_Zhai2018( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.adaptation.zhai2018.chromatic_adaptation_Zhai2018` definition domain and range scale support. """ - XYZ_b = np.array([48.900, 43.620, 6.250]) - XYZ_wb = np.array([109.850, 100, 35.585]) - XYZ_wd = np.array([95.047, 100, 108.883]) - XYZ_d = chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd) + XYZ_b = xp_as_array([48.900, 43.620, 6.250], xp=xp) + XYZ_wb = xp_as_array([109.850, 100, 35.585], xp=xp) + XYZ_wd = xp_as_array([95.047, 100, 108.883], xp=xp) + XYZ_d = as_ndarray(chromatic_adaptation_Zhai2018(XYZ_b, XYZ_wb, XYZ_wd)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( chromatic_adaptation_Zhai2018( XYZ_b * factor, XYZ_wb * factor, XYZ_wd * factor ), diff --git a/colour/adaptation/vonkries.py b/colour/adaptation/vonkries.py index 174715b325..17fa79408c 100644 --- a/colour/adaptation/vonkries.py +++ b/colour/adaptation/vonkries.py @@ -19,8 +19,6 @@ import typing -import numpy as np - from colour.adaptation import CHROMATIC_ADAPTATION_TRANSFORMS from colour.algebra import sdiv, sdiv_mode, vecmul @@ -34,11 +32,13 @@ Range1, ) from colour.utilities import ( + array_namespace, as_float_array, from_range_1, row_as_diagonal, to_domain_1, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -95,6 +95,7 @@ def matrix_chromatic_adaptation_VonKries( Examples -------- + >>> import numpy as np >>> XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) >>> XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) >>> matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr) @@ -118,13 +119,15 @@ def matrix_chromatic_adaptation_VonKries( XYZ_w = as_float_array(XYZ_w) XYZ_wr = as_float_array(XYZ_wr) + xp = array_namespace(XYZ_w, XYZ_wr) + transform = validate_method( transform, tuple(CHROMATIC_ADAPTATION_TRANSFORMS), '"{0}" chromatic adaptation transform is invalid, it must be one of {1}!', ) - M = CHROMATIC_ADAPTATION_TRANSFORMS[transform] + M = xp_as_float_array(CHROMATIC_ADAPTATION_TRANSFORMS[transform], xp=xp, like=XYZ_w) RGB_w = vecmul(M, XYZ_w) RGB_wr = vecmul(M, XYZ_wr) @@ -134,9 +137,9 @@ def matrix_chromatic_adaptation_VonKries( D = row_as_diagonal(D) - M_CAT = np.matmul(np.linalg.inv(M), D) + M_CAT = xp.matmul(xp.linalg.inv(M), D) - return np.matmul(M_CAT, M) + return xp.matmul(M_CAT, M) def chromatic_adaptation_VonKries( @@ -192,6 +195,7 @@ def chromatic_adaptation_VonKries( Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_w = np.array([0.95045593, 1.00000000, 1.08905775]) >>> XYZ_wr = np.array([0.96429568, 1.00000000, 0.82510460]) diff --git a/colour/adaptation/zhai2018.py b/colour/adaptation/zhai2018.py index 88546827dd..b1044d9295 100644 --- a/colour/adaptation/zhai2018.py +++ b/colour/adaptation/zhai2018.py @@ -18,8 +18,6 @@ import typing -import numpy as np - from colour.adaptation import CHROMATIC_ADAPTATION_TRANSFORMS from colour.algebra import vecmul @@ -32,12 +30,13 @@ Range100, ) from colour.utilities import ( - as_float_array, + array_namespace, from_range_100, get_domain_range_scale, optional, to_domain_100, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -134,6 +133,7 @@ def chromatic_adaptation_Zhai2018( Examples -------- + >>> import numpy as np >>> XYZ_b = np.array([48.900, 43.620, 6.250]) >>> XYZ_wb = np.array([109.850, 100, 35.585]) >>> XYZ_wd = np.array([95.047, 100, 108.883]) @@ -157,20 +157,26 @@ def chromatic_adaptation_Zhai2018( XYZ_wo = to_domain_100( optional( XYZ_wo, - np.array([1, 1, 1]) + [1, 1, 1] if get_domain_range_scale() == "reference" - else np.array([0.01, 0.01, 0.01]), + else [0.01, 0.01, 0.01], ) ) - D_b = as_float_array(D_b) - D_d = as_float_array(D_d) + + xp = array_namespace(XYZ_b, XYZ_wb, XYZ_wd, XYZ_wo, D_b, D_d) + + XYZ_wb = xp_as_float_array(XYZ_wb, xp=xp, like=XYZ_b) + XYZ_wd = xp_as_float_array(XYZ_wd, xp=xp, like=XYZ_b) + XYZ_wo = xp_as_float_array(XYZ_wo, xp=xp, like=XYZ_b) + D_b = xp_as_float_array(D_b, xp=xp, like=XYZ_b) + D_d = xp_as_float_array(D_d, xp=xp, like=XYZ_b) Y_wb = XYZ_wb[..., 1][..., None] Y_wd = XYZ_wd[..., 1][..., None] Y_wo = XYZ_wo[..., 1][..., None] transform = validate_method(transform, ("CAT02", "CAT16")) - M = CHROMATIC_ADAPTATION_TRANSFORMS[transform] + M = xp_as_float_array(CHROMATIC_ADAPTATION_TRANSFORMS[transform], xp=xp, like=XYZ_b) RGB_b = vecmul(M, XYZ_b) RGB_wb = vecmul(M, XYZ_wb) @@ -184,6 +190,6 @@ def chromatic_adaptation_Zhai2018( RGB_d = D_RGB * RGB_b - XYZ_d = vecmul(np.linalg.inv(M), RGB_d) + XYZ_d = vecmul(xp.linalg.inv(M), RGB_d) return from_range_100(XYZ_d) diff --git a/colour/algebra/__init__.py b/colour/algebra/__init__.py index 230b5bcfb4..98e808c744 100644 --- a/colour/algebra/__init__.py +++ b/colour/algebra/__init__.py @@ -24,7 +24,7 @@ sdiv, sdiv_mode, set_sdiv_mode, - set_spow_enable, + set_spow_enabled, smooth, smoothstep_function, spow, @@ -78,7 +78,7 @@ "sdiv", "sdiv_mode", "set_sdiv_mode", - "set_spow_enable", + "set_spow_enabled", "smooth", "smoothstep_function", "spow", diff --git a/colour/algebra/common.py b/colour/algebra/common.py index 52b9f35697..e2ca97683a 100644 --- a/colour/algebra/common.py +++ b/colour/algebra/common.py @@ -11,6 +11,7 @@ from __future__ import annotations +import contextvars import functools import typing @@ -22,6 +23,7 @@ ArrayLike, Callable, DTypeFloat, + Literal, NDArray, NDArrayFloat, Self, @@ -29,14 +31,19 @@ ) from colour.constants import EPSILON -from colour.hints import Literal, cast from colour.utilities import ( + array_namespace, as_float, as_float_array, + is_numpy_namespace, optional, runtime_warning, tsplit, validate_method, + xp_as_float_array, + xp_eigh, + xp_matrix_transpose, + xp_nan_to_num, ) __author__ = "Colour Developers" @@ -52,7 +59,7 @@ "sdiv_mode", "sdiv", "is_spow_enabled", - "set_spow_enable", + "set_spow_enabled", "spow_enable", "spow", "normalise_vector", @@ -69,35 +76,45 @@ "eigen_decomposition", ] -_SDIV_MODE: Literal[ - "Numpy", - "Ignore", - "Warning", - "Raise", - "Ignore Zero Conversion", - "Warning Zero Conversion", - "Ignore Limit Conversion", - "Warning Limit Conversion", - "Replace With Epsilon", - "Warning Replace With Epsilon", -] = "Ignore Zero Conversion" +_SDIV_MODE: contextvars.ContextVar[ + Literal[ + "Numpy", + "Ignore", + "Warning", + "Raise", + "Ignore Zero Conversion", + "Warning Zero Conversion", + "Ignore Limit Conversion", + "Warning Limit Conversion", + "Replace With Epsilon", + "Warning Replace With Epsilon", + ] + | str +] = contextvars.ContextVar("_SDIV_MODE", default="Ignore Zero Conversion") """ -Global variable storing the current *Colour* safe division function mode. +:class:`contextvars.ContextVar` storing the current *Colour* safe division +function mode. The :class:`contextvars.ContextVar` keeps nested +:class:`sdiv_mode` contexts independent across concurrent threads and async +tasks. Read it via :func:`get_sdiv_mode` and toggle it via +:func:`set_sdiv_mode` or :class:`sdiv_mode`. """ -def get_sdiv_mode() -> Literal[ - "Numpy", - "Ignore", - "Warning", - "Raise", - "Ignore Zero Conversion", - "Warning Zero Conversion", - "Ignore Limit Conversion", - "Warning Limit Conversion", - "Replace With Epsilon", - "Warning Replace With Epsilon", -]: +def get_sdiv_mode() -> ( + Literal[ + "Numpy", + "Ignore", + "Warning", + "Raise", + "Ignore Zero Conversion", + "Warning Zero Conversion", + "Ignore Limit Conversion", + "Warning Limit Conversion", + "Replace With Epsilon", + "Warning Replace With Epsilon", + ] + | str +): """ Return the current *Colour* safe division mode. @@ -118,7 +135,7 @@ def get_sdiv_mode() -> Literal[ 'ignore zero conversion' """ - return _SDIV_MODE + return _SDIV_MODE.get() def set_sdiv_mode( @@ -157,13 +174,7 @@ def set_sdiv_mode( raise """ - global _SDIV_MODE # noqa: PLW0603 - - _SDIV_MODE = cast( - "Literal['Numpy', 'Ignore', 'Warning', 'Raise', " # pyright: ignore - "'Ignore Zero Conversion', 'Warning Zero Conversion', " - "'Ignore Limit Conversion', 'Warning Limit Conversion', " - "'Replace With Epsilon', 'Warning Replace With Epsilon']", + _SDIV_MODE.set( validate_method( mode, ( @@ -215,36 +226,56 @@ def __init__( "Replace With Epsilon", "Warning Replace With Epsilon", ] + | str | None ) = None, ) -> None: self._mode = optional(mode, get_sdiv_mode()) - self._previous_mode = get_sdiv_mode() + # Token stack: nested or recursive ``__enter__`` / ``__exit__`` + # pairs against the same instance (e.g. via the decorator form on + # a recursive function) push and pop independent reset tokens. + self._tokens: list[contextvars.Token] = [] def __enter__(self) -> Self: """ - Set the *Colour* safe/symmetrical power function state to the - specified value upon entering the context manager. + Set the *Colour* safe division function mode upon entering the + context manager. """ - set_sdiv_mode(self._mode) + self._tokens.append( + _SDIV_MODE.set( + validate_method( + self._mode, + ( + "Numpy", + "Ignore", + "Warning", + "Raise", + "Ignore Zero Conversion", + "Warning Zero Conversion", + "Ignore Limit Conversion", + "Warning Limit Conversion", + "Replace With Epsilon", + "Warning Replace With Epsilon", + ), + ) + ) + ) return self def __exit__(self, *args: Any) -> None: """ - Restore the *Colour* safe / symmetrical power function enabled state - upon exiting the context manager. + Restore the previous *Colour* safe division function mode upon + exiting the context manager. """ - set_sdiv_mode(self._previous_mode) + _SDIV_MODE.reset(self._tokens.pop()) def __call__(self, function: Callable) -> Callable: """ - Call the wrapped definition. - - The decorator applies the specified spectral power distribution - state to the wrapped function during its execution. + Decorate and call the specified function with safe division mode + control. """ @functools.wraps(function) @@ -334,11 +365,13 @@ def sdiv(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: array([0.00000000e+00, 1.00000000e+00, ...]) """ - a = as_float_array(a) - b = as_float_array(b) + xp = array_namespace(a, b) + + a = xp_as_float_array(a, xp=xp, like=b) + b = xp_as_float_array(b, xp=xp, like=a) mode = validate_method( - _SDIV_MODE, + _SDIV_MODE.get(), ( "Numpy", "Ignore", @@ -356,42 +389,71 @@ def sdiv(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: if mode == "numpy": c = a / b elif mode == "ignore": - with np.errstate(divide="ignore", invalid="ignore"): + if is_numpy_namespace(xp): + with np.errstate(divide="ignore", invalid="ignore"): + c = a / b + else: c = a / b elif mode == "warning": - with np.errstate(divide="warn", invalid="warn"): + if is_numpy_namespace(xp): + with np.errstate(divide="warn", invalid="warn"): + c = a / b + else: c = a / b elif mode == "raise": - with np.errstate(divide="raise", invalid="raise"): + if is_numpy_namespace(xp): + with np.errstate(divide="raise", invalid="raise"): + c = a / b + else: c = a / b elif mode == "ignore zero conversion": - with np.errstate(divide="ignore", invalid="ignore"): - c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0) + if is_numpy_namespace(xp): + with np.errstate(divide="ignore", invalid="ignore"): + c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0) + else: + d = a / b + c = xp.where(xp.isnan(d) | xp.isinf(d), 0.0, d) elif mode == "warning zero conversion": - with np.errstate(divide="warn", invalid="warn"): - c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0) + if is_numpy_namespace(xp): + with np.errstate(divide="warn", invalid="warn"): + c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0) + else: + d = a / b + c = xp.where(xp.isnan(d) | xp.isinf(d), 0.0, d) elif mode == "ignore limit conversion": - with np.errstate(divide="ignore", invalid="ignore"): - c = np.nan_to_num(a / b) + if is_numpy_namespace(xp): + with np.errstate(divide="ignore", invalid="ignore"): + c = np.nan_to_num(a / b) + else: + c = xp_nan_to_num(a / b, xp=xp) elif mode == "warning limit conversion": - with np.errstate(divide="warn", invalid="warn"): - c = np.nan_to_num(a / b) + if is_numpy_namespace(xp): + with np.errstate(divide="warn", invalid="warn"): + c = np.nan_to_num(a / b) + else: + c = xp_nan_to_num(a / b, xp=xp) elif mode == "replace with epsilon": - b = np.where(b == 0, EPSILON, b) - c = a / b + b = xp.where(b == 0, EPSILON, b) + c = a / b # pyright: ignore elif mode == "warning replace with epsilon": - if np.any(b == 0): + if xp.any(b == 0): runtime_warning("Zero(s) detected in denominator, replacing with EPSILON.") - b = np.where(b == 0, EPSILON, b) - c = a / b + b = xp.where(b == 0, EPSILON, b) + c = a / b # pyright: ignore - return c + return c # pyright: ignore -_SPOW_ENABLED: bool = True +_SPOW_ENABLED: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_SPOW_ENABLED", default=True +) """ -Global variable storing the current *Colour* safe / symmetrical power function -enabled state. +:class:`contextvars.ContextVar` storing the current *Colour* safe / +symmetrical power function enabled state. The +:class:`contextvars.ContextVar` keeps nested :class:`spow_enable` contexts +independent across concurrent threads and async tasks. Read it via +:func:`is_spow_enabled` and toggle it via :func:`set_spow_enabled` or +:class:`spow_enable`. """ @@ -414,10 +476,10 @@ def is_spow_enabled() -> bool: True """ - return _SPOW_ENABLED + return _SPOW_ENABLED.get() -def set_spow_enable(enable: bool) -> None: +def set_spow_enabled(enable: bool) -> None: """ Set the *Colour* safe/symmetrical power function enabled state. @@ -430,15 +492,13 @@ def set_spow_enable(enable: bool) -> None: -------- >>> with spow_enable(is_spow_enabled()): ... print(is_spow_enabled()) - ... set_spow_enable(False) + ... set_spow_enabled(False) ... print(is_spow_enabled()) True False """ - global _SPOW_ENABLED # noqa: PLW0603 - - _SPOW_ENABLED = enable + _SPOW_ENABLED.set(enable) class spow_enable: @@ -461,7 +521,10 @@ class spow_enable: def __init__(self, enable: bool) -> None: self._enable = enable - self._previous_state = is_spow_enabled() + # Token stack: nested or recursive ``__enter__`` / ``__exit__`` + # pairs against the same instance (e.g. via the decorator form on + # a recursive function) push and pop independent reset tokens. + self._tokens: list[contextvars.Token[bool]] = [] def __enter__(self) -> Self: """ @@ -469,17 +532,17 @@ def __enter__(self) -> Self: upon entering the context manager. """ - set_spow_enable(self._enable) + self._tokens.append(_SPOW_ENABLED.set(self._enable)) return self def __exit__(self, *args: Any) -> None: """ - Set the *Colour* safe / symmetrical power function enabled state - upon exiting the context manager. + Restore the previous *Colour* safe / symmetrical power function + enabled state upon exiting the context manager. """ - set_spow_enable(self._previous_state) + _SPOW_ENABLED.reset(self._tokens.pop()) def __call__(self, function: Callable) -> Callable: """Call the wrapped definition.""" @@ -507,7 +570,7 @@ def spow(a: ArrayLike, p: ArrayLike) -> DTypeFloat | NDArrayFloat: This definition avoids NaN generation when array :math:`a` is negative and power :math:`p` is fractional. This behaviour can be enabled or - disabled with the :func:`colour.algebra.set_spow_enable` definition or + disabled with the :func:`colour.algebra.set_spow_enabled` definition or with the :func:`spow_enable` context manager. Parameters @@ -532,15 +595,22 @@ def spow(a: ArrayLike, p: ArrayLike) -> DTypeFloat | NDArrayFloat: np.float64(0.0) """ - if not _SPOW_ENABLED: - return np.power(a, p) + xp = array_namespace(a, p) - a = as_float_array(a) - p = as_float_array(p) + a = xp_as_float_array(a, xp=xp, like=p) + p = xp_as_float_array(p, xp=xp, like=a) + + if not _SPOW_ENABLED.get(): + return a**p - a_p = np.sign(a) * np.abs(a) ** p + # ``a == 0`` is replaced by ``1.0`` in the base before exponentiation + # to keep the unconditional ``base ** p`` clean of ``NaN`` / ``inf`` + # warnings on every backend; the ``xp.sign(a)`` factor zeroes the + # zero-base contribution back out. + base = xp.where(a == 0, 1.0, xp.abs(a)) + a_p = xp.sign(a) * base**p - return as_float(0 if a_p.ndim == 0 and np.isnan(a_p) else a_p) + return as_float(a_p) def normalise_vector(a: ArrayLike) -> NDArrayFloat: @@ -569,8 +639,10 @@ def normalise_vector(a: ArrayLike) -> NDArrayFloat: a = as_float_array(a) + xp = array_namespace(a) + with sdiv_mode(): - return sdiv(a, np.linalg.norm(a)) + return sdiv(a, xp.linalg.vector_norm(a)) def normalise_maximum( @@ -608,12 +680,14 @@ def normalise_maximum( a = as_float_array(a) - maximum = np.max(a, axis=axis) + xp = array_namespace(a) + + maximum = xp.max(a, axis=axis) with sdiv_mode(): a = a * sdiv(1, maximum[..., None]) * factor - return np.clip(a, 0, factor) if clip else a + return xp.clip(a, 0, factor) if clip else a def vecmul(m: ArrayLike, v: ArrayLike) -> NDArrayFloat: @@ -639,6 +713,15 @@ def vecmul(m: ArrayLike, v: ArrayLike) -> NDArrayFloat: :class:`numpy.ndarray` Multiplied vector array :math:`v`. + Notes + ----- + - Both operands are converted to the active array namespace at + :attr:`colour.constants.DTYPE_FLOAT_DEFAULT` and placed on the + vector :math:`v` device before multiplication; the result is a + fully-converted backend array. Callers therefore pass operands + raw, including module-level *NumPy* matrix constants, and neither + pre-convert :math:`m` nor cast the product back. + Examples -------- >>> m = np.array( @@ -660,7 +743,12 @@ def vecmul(m: ArrayLike, v: ArrayLike) -> NDArrayFloat: [0.1954094..., 0.0620396..., 0.0527952...]]) """ - return np.matmul(as_float_array(m), as_float_array(v)[..., None]).squeeze(-1) + xp = array_namespace(m, v) + + m = xp_as_float_array(m, xp=xp, like=v) + v = xp_as_float_array(v, xp=xp, like=m) + + return xp.squeeze(xp.matmul(m, v[..., None]), axis=-1) def euclidean_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: @@ -692,7 +780,12 @@ def euclidean_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: np.float64(451.7133019...) """ - return as_float(np.linalg.norm(as_float_array(a) - as_float_array(b), axis=-1)) + xp = array_namespace(a, b) + + a = xp_as_float_array(a, xp=xp, like=b) + b = xp_as_float_array(b, xp=xp, like=a) + + return as_float(xp.linalg.vector_norm(a - b, axis=-1)) def manhattan_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: @@ -724,7 +817,12 @@ def manhattan_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: np.float64(604.9396351...) """ - return as_float(np.sum(np.abs(as_float_array(a) - as_float_array(b)), axis=-1)) + a = as_float_array(a) + b = as_float_array(b) + + xp = array_namespace(a, b) + + return as_float(xp.sum(xp.abs(a - b), axis=-1)) def linear_conversion( @@ -799,12 +897,15 @@ def linstep_function( """ x = as_float_array(x) - a = as_float_array(a) - b = as_float_array(b) + + xp = array_namespace(x, a, b) + + a = xp_as_float_array(a, xp=xp, like=x) + b = xp_as_float_array(b, xp=xp, like=x) y = (1.0 - x) * a + x * b - return np.clip(y, a, b) if clip else y + return xp.clip(y, a, b) if clip else y lerp = linstep_function @@ -850,10 +951,13 @@ def smoothstep_function( """ x = as_float_array(x) - a = as_float_array(a) - b = as_float_array(b) - i = np.clip((x - a) / (b - a), 0, 1) if clip else x + xp = array_namespace(x, a, b) + + a = xp_as_float_array(a, xp=xp, like=x) + b = xp_as_float_array(b, xp=xp, like=x) + + i = xp.clip((x - a) / (b - a), 0, 1) if clip else x return (i**2) * (3.0 - 2.0 * i) @@ -887,7 +991,14 @@ def is_identity(a: ArrayLike) -> bool: False """ - return np.array_equal(np.identity(len(np.diag(a))), a) + a = as_float_array(a) + + xp = array_namespace(a) + + if a.ndim != 2 or a.shape[0] != a.shape[1]: + return False + + return bool(xp.all(a == xp_as_float_array(np.eye(a.shape[0]), xp=xp, like=a))) def eigen_decomposition( @@ -958,17 +1069,19 @@ def eigen_decomposition( A = as_float_array(a) + xp = array_namespace(A) + if covariance_matrix: - A = np.dot(np.transpose(A), A) + A = xp.matmul(xp_matrix_transpose(A, xp=xp), A) - w, v = np.linalg.eigh(A) + w, v = xp_eigh(A, xp=xp) if eigen_w_v_count is not None: w = w[-eigen_w_v_count:] v = v[..., -eigen_w_v_count:] if descending_order: - w = np.flipud(w) - v = np.fliplr(v) + w = xp.flip(w, axis=0) + v = xp.flip(v, axis=1) return w, v diff --git a/colour/algebra/coordinates/tests/test_transformations.py b/colour/algebra/coordinates/tests/test_transformations.py index e5b752aa6f..e9f520995f 100644 --- a/colour/algebra/coordinates/tests/test_transformations.py +++ b/colour/algebra/coordinates/tests/test_transformations.py @@ -5,6 +5,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -18,7 +23,13 @@ spherical_to_cartesian, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -43,50 +54,46 @@ class TestCartesianToSpherical: cartesian_to_spherical` definition unit tests methods. """ - def test_cartesian_to_spherical(self) -> None: + def test_cartesian_to_spherical(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cartesian_to_spherical` definition. """ - np.testing.assert_allclose( - cartesian_to_spherical(np.array([3, 1, 6])), - np.array([6.78232998, 0.48504979, 0.32175055]), + xp_assert_close( + cartesian_to_spherical(xp_as_array([3, 1, 6], xp=xp)), + [6.78232998, 0.48504979, 0.32175055], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cartesian_to_spherical(np.array([-1, 9, 16])), - np.array([18.38477631, 0.51501513, 1.68145355]), + xp_assert_close( + cartesian_to_spherical(xp_as_array([-1, 9, 16], xp=xp)), + [18.38477631, 0.51501513, 1.68145355], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cartesian_to_spherical(np.array([6.3434, -0.9345, 18.5675])), - np.array([19.64342307, 0.33250603, -0.14626640]), + xp_assert_close( + cartesian_to_spherical(xp_as_array([6.3434, -0.9345, 18.5675], xp=xp)), + [19.64342307, 0.33250603, -0.14626640], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_cartesian_to_spherical(self) -> None: + def test_n_dimensional_cartesian_to_spherical(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cartesian_to_spherical` definition n-dimensional arrays support. """ - a_i = np.array([3, 1, 6]) - a_o = cartesian_to_spherical(a_i) + a_i = xp_as_array([3, 1, 6], xp=xp) + a_o = as_ndarray(cartesian_to_spherical(a_i)) - a_i = np.tile(a_i, (6, 1)) - a_o = np.tile(a_o, (6, 1)) - np.testing.assert_allclose( - cartesian_to_spherical(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp.tile(xp_as_array(a_i, xp=xp), (6, 1)) + a_o = xp.tile(xp_as_array(a_o, xp=xp), (6, 1)) + xp_assert_close(cartesian_to_spherical(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) - a_i = np.reshape(a_i, (2, 3, 3)) - a_o = np.reshape(a_o, (2, 3, 3)) - np.testing.assert_allclose( - cartesian_to_spherical(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp_reshape(xp_as_array(a_i, xp=xp), (2, 3, 3), xp=xp) + a_o = xp_reshape(xp_as_array(a_o, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(cartesian_to_spherical(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_cartesian_to_spherical(self) -> None: @@ -106,50 +113,52 @@ class TestSphericalToCartesian: spherical_to_cartesian` definition unit tests methods. """ - def test_spherical_to_cartesian(self) -> None: + def test_spherical_to_cartesian(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ spherical_to_cartesian` definition. """ - np.testing.assert_allclose( - spherical_to_cartesian(np.array([6.78232998, 0.48504979, 0.32175055])), - np.array([3.00000000, 0.99999999, 6.00000000]), + xp_assert_close( + spherical_to_cartesian( + xp_as_array([6.78232998, 0.48504979, 0.32175055], xp=xp) + ), + [3.00000000, 0.99999999, 6.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - spherical_to_cartesian(np.array([18.38477631, 0.51501513, 1.68145355])), - np.array([-1.00000003, 9.00000007, 15.99999996]), + xp_assert_close( + spherical_to_cartesian( + xp_as_array([18.38477631, 0.51501513, 1.68145355], xp=xp) + ), + [-1.00000003, 9.00000007, 15.99999996], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - spherical_to_cartesian(np.array([19.64342307, 0.33250603, -0.14626640])), - np.array([6.34339996, -0.93449999, 18.56750001]), + xp_assert_close( + spherical_to_cartesian( + xp_as_array([19.64342307, 0.33250603, -0.14626640], xp=xp) + ), + [6.34339996, -0.93449999, 18.56750001], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_spherical_to_cartesian(self) -> None: + def test_n_dimensional_spherical_to_cartesian(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ spherical_to_cartesian` definition n-dimensional arrays support. """ - a_i = np.array([6.78232998, 0.48504979, 0.32175055]) - a_o = spherical_to_cartesian(a_i) + a_i = xp_as_array([6.78232998, 0.48504979, 0.32175055], xp=xp) + a_o = as_ndarray(spherical_to_cartesian(a_i)) - a_i = np.tile(a_i, (6, 1)) - a_o = np.tile(a_o, (6, 1)) - np.testing.assert_allclose( - spherical_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp.tile(xp_as_array(a_i, xp=xp), (6, 1)) + a_o = xp.tile(xp_as_array(a_o, xp=xp), (6, 1)) + xp_assert_close(spherical_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) - a_i = np.reshape(a_i, (2, 3, 3)) - a_o = np.reshape(a_o, (2, 3, 3)) - np.testing.assert_allclose( - spherical_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp_reshape(xp_as_array(a_i, xp=xp), (2, 3, 3), xp=xp) + a_o = xp_reshape(xp_as_array(a_o, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(spherical_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_spherical_to_cartesian(self) -> None: @@ -169,50 +178,46 @@ class TestCartesianToPolar: cartesian_to_polar` definition unit tests methods. """ - def test_cartesian_to_polar(self) -> None: + def test_cartesian_to_polar(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cartesian_to_polar` definition. """ - np.testing.assert_allclose( - cartesian_to_polar(np.array([3, 1])), - np.array([3.16227766, 0.32175055]), + xp_assert_close( + cartesian_to_polar(xp_as_array([3, 1], xp=xp)), + [3.16227766, 0.32175055], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cartesian_to_polar(np.array([-1, 9])), - np.array([9.05538514, 1.68145355]), + xp_assert_close( + cartesian_to_polar(xp_as_array([-1, 9], xp=xp)), + [9.05538514, 1.68145355], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cartesian_to_polar(np.array([6.3434, -0.9345])), - np.array([6.41186508, -0.14626640]), + xp_assert_close( + cartesian_to_polar(xp_as_array([6.3434, -0.9345], xp=xp)), + [6.41186508, -0.14626640], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_cartesian_to_polar(self) -> None: + def test_n_dimensional_cartesian_to_polar(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cartesian_to_polar` definition n-dimensional arrays support. """ - a_i = np.array([3, 1]) - a_o = cartesian_to_polar(a_i) + a_i = xp_as_array([3, 1], xp=xp) + a_o = as_ndarray(cartesian_to_polar(a_i)) - a_i = np.tile(a_i, (6, 1)) - a_o = np.tile(a_o, (6, 1)) - np.testing.assert_allclose( - cartesian_to_polar(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp.tile(xp_as_array(a_i, xp=xp), (6, 1)) + a_o = xp.tile(xp_as_array(a_o, xp=xp), (6, 1)) + xp_assert_close(cartesian_to_polar(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) - a_i = np.reshape(a_i, (2, 3, 2)) - a_o = np.reshape(a_o, (2, 3, 2)) - np.testing.assert_allclose( - cartesian_to_polar(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp_reshape(xp_as_array(a_i, xp=xp), (2, 3, 2), xp=xp) + a_o = xp_reshape(xp_as_array(a_o, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(cartesian_to_polar(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_cartesian_to_polar(self) -> None: @@ -232,50 +237,46 @@ class TestPolarToCartesian: polar_to_cartesian` definition unit tests methods. """ - def test_polar_to_cartesian(self) -> None: + def test_polar_to_cartesian(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ polar_to_cartesian` definition. """ - np.testing.assert_allclose( - polar_to_cartesian(np.array([0.32175055, 1.08574654])), - np.array([0.15001697, 0.28463718]), + xp_assert_close( + polar_to_cartesian(xp_as_array([0.32175055, 1.08574654], xp=xp)), + [0.15001697, 0.28463718], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - polar_to_cartesian(np.array([1.68145355, 1.05578119])), - np.array([0.82819662, 1.46334425]), + xp_assert_close( + polar_to_cartesian(xp_as_array([1.68145355, 1.05578119], xp=xp)), + [0.82819662, 1.46334425], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - polar_to_cartesian(np.array([-0.14626640, 1.23829030])), - np.array([-0.04774323, -0.13825500]), + xp_assert_close( + polar_to_cartesian(xp_as_array([-0.14626640, 1.23829030], xp=xp)), + [-0.04774323, -0.13825500], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_polar_to_cartesian(self) -> None: + def test_n_dimensional_polar_to_cartesian(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ polar_to_cartesian` definition n-dimensional arrays support. """ - a_i = np.array([3.16227766, 0.32175055]) - a_o = polar_to_cartesian(a_i) + a_i = xp_as_array([3.16227766, 0.32175055], xp=xp) + a_o = as_ndarray(polar_to_cartesian(a_i)) - a_i = np.tile(a_i, (6, 1)) - a_o = np.tile(a_o, (6, 1)) - np.testing.assert_allclose( - polar_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp.tile(xp_as_array(a_i, xp=xp), (6, 1)) + a_o = xp.tile(xp_as_array(a_o, xp=xp), (6, 1)) + xp_assert_close(polar_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) - a_i = np.reshape(a_i, (2, 3, 2)) - a_o = np.reshape(a_o, (2, 3, 2)) - np.testing.assert_allclose( - polar_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_i = xp_reshape(xp_as_array(a_i, xp=xp), (2, 3, 2), xp=xp) + a_o = xp_reshape(xp_as_array(a_o, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(polar_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_polar_to_cartesian(self) -> None: @@ -295,49 +296,53 @@ class TestCartesianToCylindrical: cartesian_to_cylindrical` definition unit tests methods. """ - def test_cartesian_to_cylindrical(self) -> None: + def test_cartesian_to_cylindrical(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cartesian_to_cylindrical` definition. """ - np.testing.assert_allclose( - cartesian_to_cylindrical(np.array([3, 1, 6])), - np.array([3.16227766, 0.32175055, 6.00000000]), + xp_assert_close( + cartesian_to_cylindrical(xp_as_array([3, 1, 6], xp=xp)), + [3.16227766, 0.32175055, 6.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cartesian_to_cylindrical(np.array([-1, 9, 16])), - np.array([9.05538514, 1.68145355, 16.00000000]), + xp_assert_close( + cartesian_to_cylindrical(xp_as_array([-1, 9, 16], xp=xp)), + [9.05538514, 1.68145355, 16.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cartesian_to_cylindrical(np.array([6.3434, -0.9345, 18.5675])), - np.array([6.41186508, -0.14626640, 18.56750000]), + xp_assert_close( + cartesian_to_cylindrical(xp_as_array([6.3434, -0.9345, 18.5675], xp=xp)), + [6.41186508, -0.14626640, 18.56750000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_cartesian_to_cylindrical(self) -> None: + def test_n_dimensional_cartesian_to_cylindrical(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cartesian_to_cylindrical` definition n-dimensional arrays support. """ - a_i = np.array([3, 1, 6]) - a_o = cartesian_to_cylindrical(a_i) + a_i = xp_as_array([3, 1, 6], xp=xp) + a_o = as_ndarray(cartesian_to_cylindrical(a_i)) - a_i = np.tile(a_i, (6, 1)) - a_o = np.tile(a_o, (6, 1)) - np.testing.assert_allclose( - cartesian_to_cylindrical(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS + a_i = xp.tile(xp_as_array(a_i, xp=xp), (6, 1)) + a_o = xp.tile(xp_as_array(a_o, xp=xp), (6, 1)) + xp_assert_close( + cartesian_to_cylindrical(a_i), + a_o, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a_i = np.reshape(a_i, (2, 3, 3)) - a_o = np.reshape(a_o, (2, 3, 3)) - np.testing.assert_allclose( - cartesian_to_cylindrical(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS + a_i = xp_reshape(xp_as_array(a_i, xp=xp), (2, 3, 3), xp=xp) + a_o = xp_reshape(xp_as_array(a_o, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + cartesian_to_cylindrical(a_i), + a_o, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -358,49 +363,59 @@ class TestCylindricalToCartesian: cylindrical_to_cartesian` definition unit tests methods. """ - def test_cylindrical_to_cartesian(self) -> None: + def test_cylindrical_to_cartesian(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cylindrical_to_cartesian` definition. """ - np.testing.assert_allclose( - cylindrical_to_cartesian(np.array([0.32175055, 1.08574654, 6.78232998])), - np.array([0.15001697, 0.28463718, 6.78232998]), + xp_assert_close( + cylindrical_to_cartesian( + xp_as_array([0.32175055, 1.08574654, 6.78232998], xp=xp) + ), + [0.15001697, 0.28463718, 6.78232998], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cylindrical_to_cartesian(np.array([1.68145355, 1.05578119, 18.38477631])), - np.array([0.82819662, 1.46334425, 18.38477631]), + xp_assert_close( + cylindrical_to_cartesian( + xp_as_array([1.68145355, 1.05578119, 18.38477631], xp=xp) + ), + [0.82819662, 1.46334425, 18.38477631], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cylindrical_to_cartesian(np.array([-0.14626640, 1.23829030, 19.64342307])), - np.array([-0.04774323, -0.13825500, 19.64342307]), + xp_assert_close( + cylindrical_to_cartesian( + xp_as_array([-0.14626640, 1.23829030, 19.64342307], xp=xp) + ), + [-0.04774323, -0.13825500, 19.64342307], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_cylindrical_to_cartesian(self) -> None: + def test_n_dimensional_cylindrical_to_cartesian(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.coordinates.transformations.\ cylindrical_to_cartesian` definition n-dimensional arrays support. """ - a_i = np.array([3.16227766, 0.32175055, 6.00000000]) - a_o = cylindrical_to_cartesian(a_i) + a_i = xp_as_array([3.16227766, 0.32175055, 6.00000000], xp=xp) + a_o = as_ndarray(cylindrical_to_cartesian(a_i)) - a_i = np.tile(a_i, (6, 1)) - a_o = np.tile(a_o, (6, 1)) - np.testing.assert_allclose( - cylindrical_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS + a_i = xp.tile(xp_as_array(a_i, xp=xp), (6, 1)) + a_o = xp.tile(xp_as_array(a_o, xp=xp), (6, 1)) + xp_assert_close( + cylindrical_to_cartesian(a_i), + a_o, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a_i = np.reshape(a_i, (2, 3, 3)) - a_o = np.reshape(a_o, (2, 3, 3)) - np.testing.assert_allclose( - cylindrical_to_cartesian(a_i), a_o, atol=TOLERANCE_ABSOLUTE_TESTS + a_i = xp_reshape(xp_as_array(a_i, xp=xp), (2, 3, 3), xp=xp) + a_o = xp_reshape(xp_as_array(a_o, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + cylindrical_to_cartesian(a_i), + a_o, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors diff --git a/colour/algebra/coordinates/transformations.py b/colour/algebra/coordinates/transformations.py index 590fb3e3f9..ada5f97ae5 100644 --- a/colour/algebra/coordinates/transformations.py +++ b/colour/algebra/coordinates/transformations.py @@ -38,14 +38,12 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import as_float_array, tsplit, tstack +from colour.utilities import array_namespace, as_float_array, tsplit, tstack __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -89,17 +87,21 @@ def cartesian_to_spherical(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([3, 1, 6]) >>> cartesian_to_spherical(a) # doctest: +ELLIPSIS array([6.7823299..., 0.4850497..., 0.3217505...]) """ - x, y, z = tsplit(a) + a = as_float_array(a) + + xp = array_namespace(a) - rho = np.linalg.norm(a, axis=-1) + x, y, z = tsplit(a) + rho = xp.linalg.vector_norm(a, axis=-1) with sdiv_mode(): - theta = np.arccos(sdiv(z, rho)) - phi = np.arctan2(y, x) + theta = xp.acos(sdiv(z, rho)) + phi = xp.atan2(y, x) return tstack([rho, theta, phi]) @@ -129,16 +131,21 @@ def spherical_to_cartesian(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([6.78232998, 0.48504979, 0.32175055]) >>> spherical_to_cartesian(a) # doctest: +ELLIPSIS array([3.0000000..., 0.9999999..., 5.9999999...]) """ + a = as_float_array(a) + + xp = array_namespace(a) + rho, theta, phi = tsplit(a) - x = rho * np.sin(theta) * np.cos(phi) - y = rho * np.sin(theta) * np.sin(phi) - z = rho * np.cos(theta) + x = rho * xp.sin(theta) * xp.cos(phi) + y = rho * xp.sin(theta) * xp.sin(phi) + z = rho * xp.cos(theta) return tstack([x, y, z]) @@ -167,15 +174,20 @@ def cartesian_to_polar(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([3, 1]) >>> cartesian_to_polar(a) # doctest: +ELLIPSIS array([3.1622776..., 0.3217505...]) """ + a = as_float_array(a) + + xp = array_namespace(a) + x, y = tsplit(a) - rho = np.hypot(x, y) - phi = np.arctan2(y, x) + rho = xp.hypot(x, y) + phi = xp.atan2(y, x) return tstack([rho, phi]) @@ -204,15 +216,20 @@ def polar_to_cartesian(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([3.16227766, 0.32175055]) >>> polar_to_cartesian(a) # doctest: +ELLIPSIS array([3. , 0.9999999...]) """ + a = as_float_array(a) + + xp = array_namespace(a) + rho, phi = tsplit(a) - x = rho * np.cos(phi) - y = rho * np.sin(phi) + x = rho * xp.cos(phi) + y = rho * xp.sin(phi) return tstack([x, y]) @@ -241,6 +258,7 @@ def cartesian_to_cylindrical(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([3, 1, 6]) >>> cartesian_to_cylindrical(a) # doctest: +ELLIPSIS array([3.1622776..., 0.3217505..., 6. ]) @@ -278,6 +296,7 @@ def cylindrical_to_cartesian(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([3.16227766, 0.32175055, 6.00000000]) >>> cylindrical_to_cartesian(a) # doctest: +ELLIPSIS array([3. , 0.9999999..., 6. ]) diff --git a/colour/algebra/extrapolation.py b/colour/algebra/extrapolation.py index 48e751e91b..42b6247ae0 100644 --- a/colour/algebra/extrapolation.py +++ b/colour/algebra/extrapolation.py @@ -25,7 +25,7 @@ import numpy as np from colour.algebra import NullInterpolator, sdiv, sdiv_mode -from colour.constants import DTYPE_FLOAT_DEFAULT +from colour.constants import DTYPE_FLOAT_DEFAULT, DTYPE_INT_DEFAULT if typing.TYPE_CHECKING: from colour.hints import ( @@ -40,12 +40,17 @@ ) from colour.utilities import ( + array_namespace, as_float, as_float_array, attest, is_numeric, optional, validate_method, + xp_as_array, + xp_astype, + xp_atleast_1d, + xp_reshape, ) __author__ = "Colour Developers" @@ -353,47 +358,95 @@ def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: xi = self._interpolator.x yi = self._interpolator.y + xp = array_namespace(x, xi, yi) + + # Source the device from whichever of ``x`` / ``xi`` / ``yi`` is + # already on the target backend so the *NumPy*-backed members are + # promoted onto the live device rather than the backend default + # (which on *Torch-MPS* is *CPU* unless explicitly switched). + # *NumPy* 2.0 added a string ``device = "cpu"`` attribute to its + # arrays, so ``device is not None`` alone would falsely match + # *NumPy* inputs; the backend *Torch* / *JAX* device objects + # expose ``.type``, so use that as the discriminator. + device_source = next( + (a for a in (x, xi, yi) if hasattr(getattr(a, "device", None), "type")), + None, + ) + + x = xp_as_array(x, xp=xp, like=device_source) + xi = xp_as_array(xi, xp=xp, like=device_source) + yi = xp_as_array(yi, xp=xp, like=device_source) + + # Promote rank-1 ``yi`` to rank-2 internally so the boundary and + # scatter logic operates on a single code path; the trailing axis + # is squeezed back on return when the input was rank-1. + input_rank = yi.ndim + if input_rank == 1: + yi = yi[..., None] + below = x < xi[0] above = x > xi[-1] - in_range = np.logical_and(x >= xi[0], x <= xi[-1]) + in_range = xp.logical_and(x >= xi[0], x <= xi[-1]) - y = np.zeros_like(x) + # ``y`` of shape ``x.shape + (yi.shape[1],)``; ``zeros_like`` on a + # broadcast intermediate inherits ``x``'s device, which matters on + # backends like Torch-MPS where ``zeros(shape)`` defaults to CPU. + y = xp.zeros_like(x[..., None] + yi[0]) + below_b = below[..., None] + above_b = above[..., None] + x_offset_low = (x - xi[0])[..., None] + x_offset_high = (x - xi[-1])[..., None] if self._method == "linear": with sdiv_mode(): - y = np.where( - below, - yi[0] + (x - xi[0]) * sdiv(yi[1] - yi[0], xi[1] - xi[0]), + y = xp.where( + below_b, + yi[0] + x_offset_low * sdiv(yi[1] - yi[0], xi[1] - xi[0]), y, ) - y = np.where( - above, - yi[-1] + (x - xi[-1]) * sdiv(yi[-1] - yi[-2], xi[-1] - xi[-2]), + y = xp.where( + above_b, + yi[-1] + x_offset_high * sdiv(yi[-1] - yi[-2], xi[-1] - xi[-2]), y, ) elif self._method == "constant": - y = np.where(below, yi[0], y) - y = np.where(above, yi[-1], y) + y = xp.where(below_b, yi[0], y) + y = xp.where(above_b, yi[-1], y) if self._left is not None: - y = np.where(below, self._left, y) + y = xp.where(below_b, self._left, y) if self._right is not None: - y = np.where(above, self._right, y) + y = xp.where(above_b, self._right, y) - if np.any(in_range): - # Flatten for multi-dimensional array support - shape = x.shape - x_ravel = np.ravel(x) - in_range_ravel = np.ravel(in_range) - y_ravel = np.ravel(y) + if xp.any(in_range): + # Flatten the query axes; ``y`` keeps its trailing signal axis + # so the scatter preserves it. + x_ravel = xp_reshape(x, (-1,), xp=xp) + in_range_ravel = xp_reshape(in_range, (-1,), xp=xp) + y_ravel = xp_reshape(y, (-1, yi.shape[1]), xp=xp) - interpolated_values = np.atleast_1d( - self._interpolator(x_ravel[in_range_ravel]) + interpolated_values = xp_atleast_1d( + self._interpolator(x_ravel[in_range_ravel]), xp=xp ) - # Scatter interpolated values back to full array positions - dense_idx = np.cumsum(in_range_ravel.astype(np.int64)) - 1 - safe_idx = np.clip(dense_idx, 0, len(interpolated_values) - 1) - y_ravel = np.where(in_range_ravel, interpolated_values[safe_idx], y_ravel) - y = np.reshape(y_ravel, shape) + # The underlying interpolator's ``y`` may be rank-1 (matching + # ``input_rank``); promote its output here so the scatter is + # uniform with the rank-2 ``y_ravel``. + if interpolated_values.ndim == 1: + interpolated_values = interpolated_values[..., None] + + dense_idx = ( + xp.cumulative_sum(xp_astype(in_range_ravel, DTYPE_INT_DEFAULT, xp=xp)) + - 1 + ) + safe_idx = xp.clip(dense_idx, 0, interpolated_values.shape[0] - 1) + y_ravel = xp.where( + in_range_ravel[..., None], + interpolated_values[safe_idx], + y_ravel, + ) + y = xp_reshape(y_ravel, (*x.shape, yi.shape[1]), xp=xp) + + if input_rank == 1: + y = y[..., 0] return y diff --git a/colour/algebra/interpolation.py b/colour/algebra/interpolation.py index b33b186752..b5c5fc3eee 100644 --- a/colour/algebra/interpolation.py +++ b/colour/algebra/interpolation.py @@ -112,18 +112,32 @@ from colour.hints import NDArrayFloat, NDArrayReal, cast from colour.utilities import ( CanonicalMapping, + array_namespace, as_array, as_float, as_float_array, as_float_scalar, as_int_array, + as_ndarray, attest, closest_indexes, + first_item, interval, is_numeric, optional, runtime_warning, validate_method, + xp_as_float_array, + xp_as_int_array, + xp_astype, + xp_atleast_1d, + xp_interp, + xp_isclose, + xp_pad, + xp_reshape, + xp_select, + xp_sinc, + xp_squeeze, ) __author__ = "Colour Developers" @@ -133,6 +147,7 @@ __email__ = "colour-developers@colour-science.org" __status__ = "Production" + __all__ = [ "kernel_nearest_neighbour", "kernel_linear", @@ -184,7 +199,11 @@ def kernel_nearest_neighbour(x: ArrayLike) -> NDArrayFloat: array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0]) """ - return np.where(np.abs(x) < 0.5, 1, 0) + x = as_float_array(x) + + xp = array_namespace(x) + + return xp.where(xp.abs(x) < 0.5, 1, 0) def kernel_linear(x: ArrayLike) -> NDArrayFloat: @@ -217,7 +236,11 @@ def kernel_linear(x: ArrayLike) -> NDArrayFloat: 0.4444444..., 0.3333333..., 0.2222222..., 0.1111111..., 0. ]) """ - return np.where(np.abs(x) < 1, 1 - np.abs(x), 0) + x = as_float_array(x) + + xp = array_namespace(x) + + return xp.where(xp.abs(x) < 1, 1 - xp.abs(x), 0) def kernel_sinc(x: ArrayLike, a: float = 3) -> NDArrayFloat: @@ -259,9 +282,11 @@ def kernel_sinc(x: ArrayLike, a: float = 3) -> NDArrayFloat: x = as_float_array(x) + xp = array_namespace(x) + attest(bool(a >= 1), '"a" must be equal or superior to 1!') - return np.where(np.abs(x) < a, np.sinc(x), 0) + return xp.where(xp.abs(x) < a, xp_sinc(x, xp=xp), 0) def kernel_lanczos(x: ArrayLike, a: float = 3) -> NDArrayFloat: @@ -300,9 +325,11 @@ def kernel_lanczos(x: ArrayLike, a: float = 3) -> NDArrayFloat: x = as_float_array(x) + xp = array_namespace(x) + attest(bool(a >= 1), '"a" must be equal or superior to 1!') - return np.where(np.abs(x) < a, np.sinc(x) * np.sinc(x / a), 0) + return xp.where(xp.abs(x) < a, xp_sinc(x, xp=xp) * xp_sinc(x / a, xp=xp), 0) def kernel_cardinal_spline( @@ -345,8 +372,10 @@ def kernel_cardinal_spline( x = as_float_array(x) - x_abs = np.abs(x) - y = np.where( + xp = array_namespace(x) + + x_abs = xp.abs(x) + y = xp.where( x_abs < 1, (-6 * a - 9 * b + 12) * x_abs**3 + (6 * a + 12 * b - 18) * x_abs**2 - 2 * b + 6, (-6 * a - b) * x_abs**3 @@ -355,7 +384,7 @@ def kernel_cardinal_spline( + 24 * a + 8 * b, ) - y = np.where(x_abs >= 2, 0, y) + y = xp.where(x_abs >= 2, 0, y) return 1 / 6 * y @@ -455,11 +484,11 @@ def __init__( ) -> None: dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - self._x_p: NDArrayFloat = np.array([]) - self._y_p: NDArrayFloat = np.array([]) + self._x_p: NDArrayFloat = np.asarray([]) + self._y_p: NDArrayFloat = np.asarray([]) - self._x: NDArrayFloat = np.array([]) - self._y: NDArrayFloat = np.array([]) + self._x: NDArrayFloat = np.asarray([]) + self._y: NDArrayFloat = np.asarray([]) self._window: float = 3 self._padding_kwargs: dict = { "pad_width": (window, window), @@ -500,7 +529,9 @@ def x(self) -> NDArrayFloat: def x(self, value: ArrayLike) -> None: """Setter for the **self.x** property.""" - value = np.atleast_1d(value).astype(self._dtype) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( value.ndim == 1, @@ -517,14 +548,15 @@ def x(self, value: ArrayLike) -> None: self._x = as_array(value, self._dtype) - self._x_p = np.pad( + self._x_p = xp_pad( self._x, as_int_array([self._window, self._window]), "linear_ramp", end_values=( - np.min(self._x) - self._window * value_interval[0], - np.max(self._x) + self._window * value_interval[0], + float(xp.min(self._x) - self._window * value_interval[0]), + float(xp.max(self._x) + self._window * value_interval[0]), ), + xp=xp, ) @property @@ -550,17 +582,25 @@ def y(self) -> NDArrayFloat: def y(self, value: ArrayLike) -> None: """Setter for the **self.y** property.""" - value = np.atleast_1d(value).astype(self._dtype) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( - value.ndim == 1, - '"y" dependent variable must have exactly one dimension!', + value.ndim in (1, 2), + '"y" dependent variable must have one or two dimensions!', ) self._y = as_array(value, self._dtype) if self._window is not None: - self._y_p = np.pad(self._y, **self._padding_kwargs) + pad_kwargs = dict(self._padding_kwargs) + if self._y.ndim == 2: + # Pad only along the leading axis for rank-2 ``y``; the + # trailing axis is preserved. + pad_width = pad_kwargs["pad_width"] + pad_kwargs["pad_width"] = (pad_width, (0, 0)) + self._y_p = xp_pad(self._y, xp=xp, **pad_kwargs) @property def window(self) -> float: @@ -731,6 +771,10 @@ def __call__(self, x: ArrayLike) -> NDArrayFloat: xi = self._evaluate(x) + # Collapse the ``xp_atleast_1d`` leading axis for 0-d ``x``. + if x.ndim == 0: + xi = first_item(xi) + return as_float(xi) def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: @@ -748,25 +792,37 @@ def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: Interpolated values at the specified point. """ + xp = array_namespace(x, self._x, self._y) + + # Promoted to at least 1-D so the gather pipeline broadcasts uniformly. + x = xp_atleast_1d(xp_as_float_array(x, xp=xp, like=self._x), xp=xp) + self._validate_dimensions() self._validate_interpolation_range(x) - x_interval = interval(self._x)[0] - x_f = np.floor(x / x_interval) - - windows = x_f[..., None] + np.arange(-self._window + 1, self._window + 1) - clip_l = min(self._x_p) / x_interval - clip_h = max(self._x_p) / x_interval - windows = np.clip(windows, clip_l, clip_h) - clip_l - windows = as_int_array(np.around(windows)) + x_interval = float(interval(self._x)[0]) + x_f = xp.floor(x / x_interval) - return np.sum( - self._y_p[windows] - * self._kernel( - x[..., None] / x_interval - windows - min(self._x_p) / x_interval, - **self._kernel_kwargs, - ), - axis=-1, + windows = x_f[..., None] + xp_as_float_array( + np.arange(-self._window + 1, self._window + 1), xp=xp, like=self._x + ) + clip_l = float(min(self._x_p)) / x_interval + clip_h = float(max(self._x_p)) / x_interval + windows = xp.clip(windows, clip_l, clip_h) - clip_l + windows = as_int_array(xp.round(windows)) + + weights = self._kernel( + x[..., None] / x_interval - windows - float(min(self._x_p)) / x_interval, + **self._kernel_kwargs, + ) + if self._y.ndim == 2: + # Append a trailing axis to the kernel weights so the product + # with ``_y_p[windows]`` of shape ``(N_query, 2 * window, N_*)`` + # broadcasts over ``y``'s trailing axis. + weights = weights[..., None] + return xp.sum( + xp_as_float_array(self._y_p, xp=xp, like=self._x)[windows] * weights, + axis=1, ) def _validate_dimensions(self) -> None: @@ -806,15 +862,17 @@ def _validate_interpolation_range(self, x: NDArrayFloat) -> None: If the point is outside the valid interpolation range. """ + xp = array_namespace(x) + below_interpolation_range = x < self._x[0] above_interpolation_range = x > self._x[-1] - if below_interpolation_range.any(): + if bool(xp.any(below_interpolation_range)): error = f'"{x}" is below interpolation range.' raise ValueError(error) - if above_interpolation_range.any(): + if bool(xp.any(above_interpolation_range)): error = f'"{x}" is above interpolation range.' raise ValueError(error) @@ -951,7 +1009,9 @@ def x(self) -> NDArrayFloat: def x(self, value: ArrayLike) -> None: """Setter for the **self.x** property.""" - value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( value.ndim == 1, @@ -989,11 +1049,13 @@ def y(self) -> NDArrayFloat: def y(self, value: ArrayLike) -> None: """Setter for the **self.y** property.""" - value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( - value.ndim == 1, - '"y" dependent variable must have exactly one dimension!', + value.ndim in (1, 2), + '"y" dependent variable must have one or two dimensions!', ) self._y = value @@ -1002,7 +1064,6 @@ def __call__(self, x: ArrayLike) -> NDArrayFloat: """ Evaluate the interpolating polynomial at specified point(s). - Parameters ---------- x @@ -1035,10 +1096,40 @@ def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: Interpolated points values. """ + xp = array_namespace(x, self._x, self._y) + + x = xp_as_float_array(x, xp=xp, like=self._x) + self._validate_dimensions() self._validate_interpolation_range(x) - return np.interp(x, self._x, self._y) + if self._y.ndim == 1: + return xp_interp( + x, + xp_as_float_array(self._x, xp=xp, like=self._x), + self._y, + xp=xp, + ) + + # Manual linear interpolation for rank-2 ``y``: bracket each query + # point with ``searchsorted``, compute the normalised interpolation + # parameter ``t``, and lerp the bracketing ``y`` rows. Indexing is + # done CPU-side via numpy so backends without integer-tensor + # advanced indexing support (e.g. MPS) work correctly. + self_x = xp_as_float_array(self._x, xp=xp, like=self._x) + i_np = np.clip(as_ndarray(xp.searchsorted(self_x, x) - 1), 0, len(self._x) - 2) + self_x_np = as_ndarray(self_x) + self_y_np = as_ndarray(xp_as_float_array(self._y, xp=xp)) + + x_low = xp_as_float_array(self_x_np[i_np], xp=xp, like=self._x) + x_high = xp_as_float_array(self_x_np[i_np + 1], xp=xp, like=self._x) + y_low = xp_as_float_array(self_y_np[i_np], xp=xp, like=self._x) + y_high = xp_as_float_array(self_y_np[i_np + 1], xp=xp, like=self._x) + + with sdiv_mode(): + t = sdiv(x - x_low, x_high - x_low) + + return y_low + (y_high - y_low) * t[..., None] def _validate_dimensions(self) -> None: """Validate that the variables dimensions are the same.""" @@ -1054,15 +1145,17 @@ def _validate_dimensions(self) -> None: def _validate_interpolation_range(self, x: NDArrayFloat) -> None: """Validate specified point to be in interpolation range.""" + xp = array_namespace(x) + below_interpolation_range = x < self._x[0] above_interpolation_range = x > self._x[-1] - if below_interpolation_range.any(): + if bool(xp.any(below_interpolation_range)): error = f'"{x}" is below interpolation range.' raise ValueError(error) - if above_interpolation_range.any(): + if bool(xp.any(above_interpolation_range)): error = f'"{x}" is above interpolation range.' raise ValueError(error) @@ -1155,8 +1248,8 @@ def __init__( ) -> None: dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - self._xp: NDArrayFloat = np.array([]) - self._yp: NDArrayFloat = np.array([]) + self._x_p: NDArrayFloat = np.array([]) + self._y_p: NDArrayFloat = np.array([]) self._x: NDArrayFloat = np.array([]) self._y: NDArrayFloat = np.array([]) @@ -1194,7 +1287,9 @@ def x(self) -> NDArrayFloat: def x(self, value: ArrayLike) -> None: """Setter for the **self.x** property.""" - value = as_array(np.atleast_1d(value), self._dtype) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( value.ndim == 1, @@ -1210,7 +1305,7 @@ def x(self, value: ArrayLike) -> None: xp3 = self._x[-1] + value_interval xp4 = self._x[-1] + value_interval * 2 - self._xp = np.concatenate( + self._x_p = xp.concat( [ as_array([xp1, xp2], self._dtype), value, @@ -1248,11 +1343,13 @@ def y(self) -> NDArrayFloat: def y(self, value: ArrayLike) -> None: """Setter for the **self.y** property.""" - value = as_array(np.atleast_1d(value), self._dtype) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( - value.ndim == 1, - '"y" dependent variable must have exactly one dimension!', + value.ndim in (1, 2), + '"y" dependent variable must have one or two dimensions!', ) attest( @@ -1262,22 +1359,22 @@ def y(self, value: ArrayLike) -> None: self._y = value - yp1, yp2, yp3, yp4 = ( - np.sum( - self.SPRAGUE_C_COEFFICIENTS - * np.asarray((value[0:6], value[0:6], value[-6:], value[-6:])), + # ``SPRAGUE_C_COEFFICIENTS`` is ``(4, 6)``; for rank-2 ``y`` the + # coefficient tensor expands to ``(4, 6, 1)`` so the product with + # the stacked boundary windows broadcasts over ``y``'s trailing + # axis. The sum collapses the window axis. + C = xp_as_float_array(self.SPRAGUE_C_COEFFICIENTS, xp=xp, like=value) + if value.ndim == 2: + C = C[..., None] + yp = ( + xp.sum( + C * xp.stack([value[0:6], value[0:6], value[-6:], value[-6:]]), axis=1, ) / 209 ) - self._yp = np.concatenate( - [ - as_array([yp1, yp2], self._dtype), - value, - as_array([yp3, yp4], self._dtype), - ] - ) + self._y_p = xp.concat([yp[:2], value, yp[2:]], axis=0) def __call__(self, x: ArrayLike) -> NDArrayFloat: """ @@ -1298,6 +1395,10 @@ def __call__(self, x: ArrayLike) -> NDArrayFloat: xi = self._evaluate(x) + # Collapse the ``xp_atleast_1d`` leading axis for 0-d ``x``. + if x.ndim == 0: + xi = first_item(xi) + return as_float(xi) def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: @@ -1315,36 +1416,66 @@ def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: Interpolated point values. """ + xp = array_namespace(x, self._x, self._y) + + # Promoted to at least 1-D so the stack / matmul pipeline broadcasts uniformly. + x = xp_atleast_1d(xp_as_float_array(x, xp=xp, like=self._x), xp=xp) + self._validate_dimensions() self._validate_interpolation_range(x) - i = np.searchsorted(self._xp, x) - 1 + i = xp.searchsorted(xp_as_float_array(self._x_p, xp=xp, like=self._x), x) - 1 + # Convert index to numpy for CPU-side gather so that backends without + # integer-tensor advanced indexing support (e.g. MPS) work correctly. + i_np = as_ndarray(i) + # ``t`` is the normalised interpolation parameter, the local + # fractional distance ``(x - x_i) / (x_{i+1} - x_i)`` within the + # bracketing interval. with sdiv_mode(): - X = sdiv(x - self._xp[i], self._xp[i + 1] - self._xp[i]) - - r = self._yp + _x_p_np = as_ndarray(xp_as_float_array(self._x_p, xp=xp)) + t = sdiv( + x - xp_as_float_array(_x_p_np[i_np], xp=xp, like=self._x), + xp_as_float_array( + _x_p_np[i_np + 1] - _x_p_np[i_np], xp=xp, like=self._x + ), + ) - r_s = np.asarray((r[i - 2], r[i - 1], r[i], r[i + 1], r[i + 2], r[i + 3])) - w_s = np.asarray( + r_np = as_ndarray(xp_as_float_array(self._y_p, xp=xp)) + r_s = xp.stack( + [ + xp_as_float_array(r_np[i_np + k], xp=xp, like=self._x) + for k in (-2, -1, 0, 1, 2, 3) + ] + ) + w_s = xp_as_float_array( ( (2, -16, 0, 16, -2, 0), (-1, 16, -30, 16, -1, 0), (-9, 39, -70, 66, -33, 7), (13, -64, 126, -124, 61, -12), (-5, 25, -50, 50, -25, 5), - ) + ), + xp=xp, + like=self._x, + ) + # Flatten the trailing axes of ``r_s`` so a single ``matmul`` over + # the window axis handles rank-1 and rank-2 ``y`` uniformly. + r_s_shape = r_s.shape + r_s_flat = xp_reshape(r_s, (r_s_shape[0], -1), xp=xp) + a = xp_reshape( + xp.matmul(w_s, r_s_flat) / 24, (w_s.shape[0], *r_s_shape[1:]), xp=xp ) - a = np.dot(w_s, r_s) / 24 - - # Fancy vector code here... use underlying numpy structures to accelerate - # parts of the linear algebra. - - y = r[i] + (a.reshape(5, -1) * X ** np.arange(1, 6).reshape(-1, 1)).sum(axis=0) - - if y.size == 1: - return y[0] - return y + powers = xp_as_float_array([[1], [2], [3], [4], [5]], xp=xp, like=self._x) + # Polynomial basis ``t**k`` for k in 1..5; a trailing axis is + # appended for rank-2 ``y`` so it broadcasts over ``y``'s + # trailing axis. + basis = t**powers + if self._y.ndim == 2: + basis = basis[..., None] + return xp_as_float_array(r_np[i_np], xp=xp, like=self._x) + (a * basis).sum( + axis=0 + ) def _validate_dimensions(self) -> None: """Validate that the variables dimensions are the same.""" @@ -1360,15 +1491,17 @@ def _validate_dimensions(self) -> None: def _validate_interpolation_range(self, x: NDArrayFloat) -> None: """Validate specified point to be in interpolation range.""" + xp = array_namespace(x) + below_interpolation_range = x < self._x[0] above_interpolation_range = x > self._x[-1] - if below_interpolation_range.any(): + if bool(xp.any(below_interpolation_range)): error = f'"{x}" is below interpolation range.' raise ValueError(error) - if above_interpolation_range.any(): + if bool(xp.any(above_interpolation_range)): error = f'"{x}" is above interpolation range.' raise ValueError(error) @@ -1393,9 +1526,17 @@ class CubicSplineInterpolator(scipy.interpolate.interp1d): - This class is a wrapper around *scipy.interpolate.interp1d* class. """ - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__(self, x: ArrayLike, y: ArrayLike, *args: Any, **kwargs: Any) -> None: kwargs["kind"] = "cubic" - super().__init__(*args, **kwargs) + # Interpolate along the leading axis so rank-2 ``y`` evaluates + # batched along its trailing axis; rank-1 ``y`` is unaffected. + kwargs.setdefault("axis", 0) + super().__init__(as_ndarray(x), as_ndarray(y), *args, **kwargs) + + def __call__(self, x: Any) -> Any: + """Evaluate, converting non-numpy arrays to numpy for scipy.""" + + return super().__call__(as_ndarray(x)) class PchipInterpolator(scipy.interpolate.PchipInterpolator): @@ -1424,9 +1565,11 @@ class PchipInterpolator(scipy.interpolate.PchipInterpolator): """ def __init__(self, x: ArrayLike, y: ArrayLike, *args: Any, **kwargs: Any) -> None: - super().__init__(as_float_array(x), as_float_array(y), *args, **kwargs) + x = as_ndarray(x) + y = as_ndarray(y) + super().__init__(x, y, *args, **kwargs) - self._y: NDArrayFloat = as_float_array(y) + self._y: NDArrayFloat = y @property def y(self) -> NDArrayFloat: @@ -1515,7 +1658,7 @@ def __init__( y: ArrayLike, absolute_tolerance: float = TOLERANCE_ABSOLUTE_DEFAULT, relative_tolerance: float = TOLERANCE_RELATIVE_DEFAULT, - default: float = np.nan, + default: float = float("nan"), dtype: Type[DTypeReal] | None = None, *args: Any, # noqa: ARG002 **kwargs: Any, # noqa: ARG002 @@ -1526,7 +1669,7 @@ def __init__( self._y: NDArrayFloat = np.array([]) self._absolute_tolerance: float = TOLERANCE_ABSOLUTE_DEFAULT self._relative_tolerance: float = TOLERANCE_RELATIVE_DEFAULT - self._default: float = np.nan + self._default: float = float("nan") self._dtype: Type[DTypeReal] = dtype self.x = x @@ -1564,7 +1707,9 @@ def x(self) -> NDArrayFloat: def x(self, value: ArrayLike) -> None: """Setter for the **self.x** property.""" - value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( value.ndim == 1, @@ -1602,11 +1747,13 @@ def y(self) -> NDArrayFloat: def y(self, value: ArrayLike) -> None: """Setter for the **self.y** property.""" - value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) + xp = array_namespace(value) + + value = xp_astype(xp_atleast_1d(value, xp=xp), self._dtype, xp=xp) attest( - value.ndim == 1, - '"y" dependent variable must have exactly one dimension!', + value.ndim in (1, 2), + '"y" dependent variable must have one or two dimensions!', ) self._y = value @@ -1717,7 +1864,6 @@ def __call__(self, x: ArrayLike) -> NDArrayFloat: """ Evaluate the interpolator at specified point(s). - Parameters ---------- x @@ -1733,6 +1879,11 @@ def __call__(self, x: ArrayLike) -> NDArrayFloat: xi = self._evaluate(x) + # Collapse the synthetic leading axis for 0-d ``x``; guarded against + # the rank-1 ``y`` path that already returns a 0-d scalar. + if x.ndim == 0 and xi.ndim > 0: + xi = first_item(xi) + return as_float(xi) def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: @@ -1750,20 +1901,31 @@ def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: Interpolated points values. """ + xp = array_namespace(x, self._x, self._y) + + x = xp_as_float_array(x, xp=xp, like=self._x) + self._validate_dimensions() self._validate_interpolation_range(x) - indexes = closest_indexes(self._x, x) - values = self._y[indexes] - close = np.isclose( - self._x[indexes], + sx = xp_as_float_array(self._x, xp=xp, like=self._x) + sy = xp_as_float_array(self._y, xp=xp, like=self._x) + indexes = closest_indexes(sx, x) + # ``close`` is always ``(N_query,)``; a trailing axis is appended + # for rank-2 ``y`` so it broadcasts over ``y``'s trailing axis. + values = sy[indexes] + close = xp_isclose( + sx[indexes], x, - rtol=self._absolute_tolerance, - atol=self._relative_tolerance, + rtol=self._relative_tolerance, + atol=self._absolute_tolerance, + xp=xp, ) - values = np.where(~close, self._default, values) + if self._y.ndim == 2: + close = close[..., None] + values = xp.where(~close, self._default, values) - return np.squeeze(values) + return xp_squeeze(values, xp=xp) if self._y.ndim == 1 else values def _validate_dimensions(self) -> None: """Validate that the variables dimensions are the same.""" @@ -1779,15 +1941,17 @@ def _validate_dimensions(self) -> None: def _validate_interpolation_range(self, x: NDArrayFloat) -> None: """Validate specified point to be in interpolation range.""" + xp = array_namespace(x) + below_interpolation_range = x < self._x[0] above_interpolation_range = x > self._x[-1] - if below_interpolation_range.any(): + if bool(xp.any(below_interpolation_range)): error = f'"{x}" is below interpolation range.' raise ValueError(error) - if above_interpolation_range.any(): + if bool(xp.any(above_interpolation_range)): error = f'"{x}" is above interpolation range.' raise ValueError(error) @@ -1827,7 +1991,7 @@ def lagrange_coefficients(r: float, n: int = 4) -> NDArrayFloat: basis = [(r - r_i[i]) / (r_i[j] - r_i[i]) for i in range(len(r_i)) if i != j] L_n.append(reduce(lambda x, y: x * y, basis)) - return np.array(L_n) + return np.asarray(L_n) def table_interpolation_trilinear(V_xyz: ArrayLike, table: ArrayLike) -> NDArrayFloat: @@ -1880,17 +2044,21 @@ def table_interpolation_trilinear(V_xyz: ArrayLike, table: ArrayLike) -> NDArray """ V_xyz = cast("NDArrayFloat", V_xyz) + + xp = array_namespace(V_xyz) + original_shape = V_xyz.shape - V_xyz = cast("NDArrayFloat", np.clip(V_xyz, 0, 1).reshape(-1, 3)) + + V_xyz = cast("NDArrayFloat", xp_reshape(xp.clip(V_xyz, 0, 1), (-1, 3), xp=xp)) # Index computation table = cast("NDArrayFloat", table) - i_m = np.array(table.shape[:-1]) - 1 + i_m = xp_as_int_array(table.shape[:-1], xp=xp) - 1 V_xyz_s = V_xyz * i_m - i_f = V_xyz_s.astype(DTYPE_INT_DEFAULT) - i_f = np.clip(i_f, 0, i_m) - i_c = np.minimum(i_f + 1, i_m) + i_f = xp_astype(V_xyz_s, DTYPE_INT_DEFAULT, xp=xp) + i_f = xp.clip(i_f, 0, i_m) + i_c = xp.minimum(i_f + 1, i_m) # Relative coordinates (fractional part) frac = V_xyz_s - i_f @@ -1925,7 +2093,7 @@ def table_interpolation_trilinear(V_xyz: ArrayLike, table: ArrayLike) -> NDArray + v111 * (dx * dy * dz) ) - return result.reshape(original_shape) + return xp_reshape(result, original_shape, xp=xp) def table_interpolation_tetrahedral(V_xyz: ArrayLike, table: ArrayLike) -> NDArrayFloat: @@ -1978,17 +2146,21 @@ def table_interpolation_tetrahedral(V_xyz: ArrayLike, table: ArrayLike) -> NDArr """ V_xyz = cast("NDArrayFloat", V_xyz) + + xp = array_namespace(V_xyz) + original_shape = V_xyz.shape - V_xyz = cast("NDArrayFloat", np.clip(V_xyz, 0, 1).reshape(-1, 3)) + + V_xyz = cast("NDArrayFloat", xp_reshape(xp.clip(V_xyz, 0, 1), (-1, 3), xp=xp)) # Index computation table = cast("NDArrayFloat", table) - i_m = np.array(table.shape[:-1]) - 1 + i_m = xp_as_int_array(table.shape[:-1], xp=xp) - 1 V_xyz_s = V_xyz * i_m - i_f = V_xyz_s.astype(DTYPE_INT_DEFAULT) - i_f = np.clip(i_f, 0, i_m) - i_c = np.minimum(i_f + 1, i_m) + i_f = xp_astype(V_xyz_s, DTYPE_INT_DEFAULT, xp=xp) + i_f = xp.clip(i_f, 0, i_m) + i_c = xp.minimum(i_f + 1, i_m) # Relative coordinates r = V_xyz_s - i_f @@ -2009,19 +2181,19 @@ def table_interpolation_tetrahedral(V_xyz: ArrayLike, table: ArrayLike) -> NDArr V111 = table[cx, cy, cz] # Expand dimensions for broadcasting - x = x[:, np.newaxis] - y = y[:, np.newaxis] - z = z[:, np.newaxis] + x = x[:, None] + y = y[:, None] + z = z[:, None] # Tetrahedral interpolation - select tetrahedron based on position - xyz_o = np.select( + xyz_o = xp_select( [ - np.logical_and(x > y, y > z), - np.logical_and(x > z, z >= y), - np.logical_and(z >= x, x > y), - np.logical_and(y >= x, x > z), - np.logical_and(y >= z, z >= x), - np.logical_and(z > y, y >= x), + xp.logical_and(x > y, y > z), + xp.logical_and(x > z, z >= y), + xp.logical_and(z >= x, x > y), + xp.logical_and(y >= x, x > z), + xp.logical_and(y >= z, z >= x), + xp.logical_and(z > y, y >= x), ], [ (1 - x) * V000 + (x - y) * V100 + (y - z) * V110 + z * V111, @@ -2031,9 +2203,10 @@ def table_interpolation_tetrahedral(V_xyz: ArrayLike, table: ArrayLike) -> NDArr (1 - y) * V000 + (y - z) * V010 + (z - x) * V011 + x * V111, (1 - z) * V000 + (z - y) * V001 + (y - x) * V011 + x * V111, ], + xp=xp, ) - return xyz_o.reshape(original_shape) + return xp_reshape(xyz_o, original_shape, xp=xp) TABLE_INTERPOLATION_METHODS = CanonicalMapping( @@ -2164,28 +2337,36 @@ def linear_interpolation_index_and_factor( (array([0, 0, 2, 3]...), array([0. , 0.5, 0.5, 0. ])) """ - value = as_float_array(value) - break_points = as_float_array(break_points) + xp = array_namespace(value, break_points) - clamped = np.clip(value, break_points[0], break_points[-1]) + value = xp_as_float_array(value, xp=xp, like=break_points) + break_points = xp_as_float_array(break_points, xp=xp, like=value) + + clamped = xp.clip(value, break_points[0], break_points[-1]) # Upper bound search starting from break_points[1]. next_idx = ( - np.searchsorted(break_points[1:], clamped.ravel(), side="right").reshape( - clamped.shape + xp_reshape( + xp.searchsorted( + break_points[1:], + xp_reshape(clamped, (-1,), xp=xp), + side="right", + ), + clamped.shape, + xp=xp, ) + 1 ) at_end = next_idx >= len(break_points) - index = np.where(at_end, len(break_points) - 1, next_idx - 1) + index = xp.where(at_end, len(break_points) - 1, next_idx - 1) - safe_next = np.minimum(next_idx, len(break_points) - 1) + safe_next = xp.clip(next_idx, max=len(break_points) - 1) denominator = break_points[safe_next] - break_points[index] - factor = np.where( + factor = xp.where( at_end | (denominator == 0), 0.0, - (clamped - break_points[index]) / np.where(denominator == 0, 1.0, denominator), + (clamped - break_points[index]) / xp.where(denominator == 0, 1.0, denominator), ) - return index.astype(np.intp), factor + return xp_astype(index, DTYPE_INT_DEFAULT, xp=xp), factor diff --git a/colour/algebra/regression.py b/colour/algebra/regression.py index 856111c11d..62cd437c05 100644 --- a/colour/algebra/regression.py +++ b/colour/algebra/regression.py @@ -19,11 +19,16 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat +from colour.utilities import ( + array_namespace, + as_float_array, + xp_atleast_2d, + xp_matrix_transpose, +) + __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" @@ -60,6 +65,7 @@ def least_square_mapping_MoorePenrose(y: ArrayLike, x: ArrayLike) -> NDArrayFloa Examples -------- + >>> import numpy as np >>> prng = np.random.RandomState(2) >>> y = prng.random_sample((24, 3)) >>> x = y + (prng.random_sample((24, 3)) - 0.5) * 0.5 @@ -69,7 +75,15 @@ def least_square_mapping_MoorePenrose(y: ArrayLike, x: ArrayLike) -> NDArrayFloa [ 0.0572550..., -0.2052633..., 1.1015194...]]) """ - y = np.atleast_2d(y) - x = np.atleast_2d(x) + y = as_float_array(y) + x = as_float_array(x) + + xp = array_namespace(y, x) + + y = xp_atleast_2d(y, xp=xp) + x = xp_atleast_2d(x, xp=xp) - return np.dot(np.transpose(x), np.linalg.pinv(np.transpose(y))) + return xp.matmul( + xp_matrix_transpose(x, xp=xp), + xp.linalg.pinv(xp_matrix_transpose(y, xp=xp)), + ) diff --git a/colour/algebra/tests/test_common.py b/colour/algebra/tests/test_common.py index 574ebed2fd..422086cc4c 100644 --- a/colour/algebra/tests/test_common.py +++ b/colour/algebra/tests/test_common.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -21,14 +26,26 @@ sdiv, sdiv_mode, set_sdiv_mode, - set_spow_enable, + set_spow_enabled, smoothstep_function, spow, spow_enable, vecmul, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ColourRuntimeWarning, ignore_numpy_errors +from colour.utilities import ( + ColourRuntimeWarning, + array_api_enable, + as_ndarray, + ignore_numpy_errors, + is_numpy_namespace, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_create_diagonal, + xp_linspace, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -166,49 +183,64 @@ class TestSdiv: tests methods. """ - def test_sdiv(self) -> None: + @pytest.mark.mps_xfail("MPS float32 overflow") + def test_sdiv(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.sdiv` definition.""" - a = np.array([0, 1, 2]) - b = np.array([2, 1, 0]) + a = xp_as_array([0, 1, 2], xp=xp) + b = xp_as_array([2, 1, 0], xp=xp) - with sdiv_mode("Numpy"): - pytest.warns(RuntimeWarning, sdiv, a, b) + if is_numpy_namespace(xp): + with sdiv_mode("Numpy"), pytest.warns(RuntimeWarning): + sdiv(a, b) with sdiv_mode("Ignore"): - np.testing.assert_equal(sdiv(a, b), np.array([0, 1, np.inf])) + xp_assert_equal(as_ndarray(sdiv(a, b)), [0, 1, np.inf]) - with sdiv_mode("Warning"): - pytest.warns(RuntimeWarning, sdiv, a, b) - np.testing.assert_equal(sdiv(a, b), np.array([0, 1, np.inf])) + if is_numpy_namespace(xp): + with sdiv_mode("Warning"): + with pytest.warns(RuntimeWarning): + sdiv(a, b) + xp_assert_equal(as_ndarray(sdiv(a, b)), [0, 1, np.inf]) - with sdiv_mode("Raise"): - pytest.raises(FloatingPointError, sdiv, a, b) + if is_numpy_namespace(xp): + with sdiv_mode("Raise"), pytest.raises(FloatingPointError): + sdiv(a, b) with sdiv_mode("Ignore Zero Conversion"): - np.testing.assert_equal(sdiv(a, b), np.array([0, 1, 0])) + xp_assert_equal(as_ndarray(sdiv(a, b)), [0, 1, 0]) - with sdiv_mode("Warning Zero Conversion"): - pytest.warns(RuntimeWarning, sdiv, a, b) - np.testing.assert_equal(sdiv(a, b), np.array([0, 1, 0])) + if is_numpy_namespace(xp): + with sdiv_mode("Warning Zero Conversion"): + with pytest.warns(RuntimeWarning): + sdiv(a, b) + xp_assert_equal(as_ndarray(sdiv(a, b)), [0, 1, 0]) with sdiv_mode("Ignore Limit Conversion"): - np.testing.assert_equal(sdiv(a, b), np.nan_to_num(np.array([0, 1, np.inf]))) + xp_assert_equal(as_ndarray(sdiv(a, b)), np.nan_to_num([0, 1, np.inf])) - with sdiv_mode("Warning Limit Conversion"): - pytest.warns(RuntimeWarning, sdiv, a, b) - np.testing.assert_equal(sdiv(a, b), np.nan_to_num(np.array([0, 1, np.inf]))) + if is_numpy_namespace(xp): + with sdiv_mode("Warning Limit Conversion"): + with pytest.warns(RuntimeWarning): + sdiv(a, b) + xp_assert_equal(as_ndarray(sdiv(a, b)), np.nan_to_num([0, 1, np.inf])) with sdiv_mode("Replace With Epsilon"): - np.testing.assert_allclose( - sdiv(a, b), np.array([0, 1, 2 / np.finfo(np.double).eps]) + xp_assert_close( + sdiv(a, b), + xp_as_array([0, 1, float(2 / np.finfo(np.double).eps)], xp=xp), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - with sdiv_mode("Warning Replace With Epsilon"): - pytest.warns(ColourRuntimeWarning, sdiv, a, b) - np.testing.assert_allclose( - sdiv(a, b), np.array([0, 1, 2 / np.finfo(np.double).eps]) - ) + if is_numpy_namespace(xp): + with sdiv_mode("Warning Replace With Epsilon"): + with pytest.warns(ColourRuntimeWarning): + sdiv(a, b) + xp_assert_close( + sdiv(a, b), + xp_as_array([0, 1, float(2 / np.finfo(np.double).eps)], xp=xp), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) class TestIsSpowEnabled: @@ -229,19 +261,19 @@ def test_is_spow_enabled(self) -> None: class TestSetSpowEnabled: """ - Define :func:`colour.algebra.common.set_spow_enable` definition unit + Define :func:`colour.algebra.common.set_spow_enabled` definition unit tests methods. """ - def test_set_spow_enable(self) -> None: - """Test :func:`colour.algebra.common.set_spow_enable` definition.""" + def test_set_spow_enabled(self) -> None: + """Test :func:`colour.algebra.common.set_spow_enabled` definition.""" with spow_enable(is_spow_enabled()): - set_spow_enable(True) + set_spow_enabled(True) assert is_spow_enabled() with spow_enable(is_spow_enabled()): - set_spow_enable(False) + set_spow_enabled(False) assert not is_spow_enabled() @@ -283,26 +315,26 @@ class TestSpow: tests methods. """ - def test_spow(self) -> None: + def test_spow(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.spow` definition.""" assert spow(2, 2) == 4.0 assert spow(-2, 2) == -4.0 - np.testing.assert_allclose( - spow([2, -2, -2, 0], [2, 2, 0.15, 0]), - np.array([4.00000000, -4.00000000, -1.10956947, 0.00000000]), + xp_assert_close( + spow( + xp_as_array([2, -2, -2, 0], xp=xp), xp_as_array([2, 2, 0.15, 0], xp=xp) + ), + [4.00000000, -4.00000000, -1.10956947, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) with spow_enable(True): - np.testing.assert_allclose( - spow(-2, 0.15), -1.10956947, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(spow(-2, 0.15), -1.10956947, atol=TOLERANCE_ABSOLUTE_TESTS) with spow_enable(False): - np.testing.assert_equal(spow(-2, 0.15), np.nan) + xp_assert_equal(spow(-2, 0.15), np.nan) class TestNormaliseVector: @@ -311,24 +343,24 @@ class TestNormaliseVector: tests methods. """ - def test_normalise_vector(self) -> None: + def test_normalise_vector(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.normalise_vector` definition.""" - np.testing.assert_allclose( - normalise_vector(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.84197033, 0.49722560, 0.20941026]), + xp_assert_close( + normalise_vector(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.84197033, 0.49722560, 0.20941026], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - normalise_vector(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.48971705, 0.79344877, 0.36140872]), + xp_assert_close( + normalise_vector(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.48971705, 0.79344877, 0.36140872], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - normalise_vector(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.26229003, 0.20655044, 0.94262445]), + xp_assert_close( + normalise_vector(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.26229003, 0.20655044, 0.94262445], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -339,75 +371,75 @@ class TestNormaliseMaximum: tests methods. """ - def test_normalise_maximum(self) -> None: + def test_normalise_maximum(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.normalise_maximum` definition.""" - np.testing.assert_allclose( - normalise_maximum(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([1.00000000, 0.59055003, 0.24871454]), + xp_assert_close( + normalise_maximum(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [1.00000000, 0.59055003, 0.24871454], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_maximum( - np.array( + xp_as_array( [ [0.20654008, 0.12197225, 0.05136952], [0.14222010, 0.23042768, 0.10495772], [0.07818780, 0.06157201, 0.28099326], - ] + ], + xp=xp, ) ), - np.array( - [ - [0.73503571, 0.43407536, 0.18281406], - [0.50613349, 0.82004700, 0.37352398], - [0.27825507, 0.21912273, 1.00000000], - ] - ), + [ + [0.73503571, 0.43407536, 0.18281406], + [0.50613349, 0.82004700, 0.37352398], + [0.27825507, 0.21912273, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_maximum( - np.array( + xp_as_array( [ [0.20654008, 0.12197225, 0.05136952], [0.14222010, 0.23042768, 0.10495772], [0.07818780, 0.06157201, 0.28099326], - ] + ], + xp=xp, ), axis=-1, ), - np.array( - [ - [1.00000000, 0.59055003, 0.24871454], - [0.61720059, 1.00000000, 0.45549094], - [0.27825507, 0.21912273, 1.00000000], - ] - ), + [ + [1.00000000, 0.59055003, 0.24871454], + [0.61720059, 1.00000000, 0.45549094], + [0.27825507, 0.21912273, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_maximum( - np.array([0.20654008, 0.12197225, 0.05136952]), factor=10 + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), factor=10 ), - np.array([10.00000000, 5.90550028, 2.48714535]), + [10.00000000, 5.90550028, 2.48714535], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - normalise_maximum(np.array([-0.11518475, -0.10080000, 0.05089373])), - np.array([0.00000000, 0.00000000, 1.00000000]), + xp_assert_close( + normalise_maximum( + xp_as_array([-0.11518475, -0.10080000, 0.05089373], xp=xp) + ), + [0.00000000, 0.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_maximum( - np.array([-0.20654008, -0.12197225, 0.05136952]), clip=False + xp_as_array([-0.20654008, -0.12197225, 0.05136952], xp=xp), clip=False ), - np.array([-4.02067374, -2.37440899, 1.00000000]), + [-4.02067374, -2.37440899, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -418,7 +450,7 @@ class TestVectorDot: methods. """ - def test_vecmul(self) -> None: + def test_vecmul(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.vecmul` definition.""" m = np.array( @@ -428,14 +460,27 @@ def test_vecmul(self) -> None: [0.0030, 0.0136, 0.9834], ] ) - m = np.reshape(np.tile(m, (6, 1)), (6, 3, 3)) + m = xp_reshape(xp.tile(xp_as_array(m, xp=xp), (6, 1)), (6, 3, 3), xp=xp) v = np.array([0.20654008, 0.12197225, 0.05136952]) - v = np.tile(v, (6, 1)) + v = xp.tile(xp_as_array(v, xp=xp), (6, 1)) - np.testing.assert_allclose( + xp_assert_close( vecmul(m, v), - np.array( + [ + [0.19540944, 0.06203965, 0.05279523], + [0.19540944, 0.06203965, 0.05279523], + [0.19540944, 0.06203965, 0.05279523], + [0.19540944, 0.06203965, 0.05279523], + [0.19540944, 0.06203965, 0.05279523], + [0.19540944, 0.06203965, 0.05279523], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + with array_api_enable(True): + xp_assert_close( + vecmul(m, v), [ [0.19540944, 0.06203965, 0.05279523], [0.19540944, 0.06203965, 0.05279523], @@ -443,10 +488,9 @@ def test_vecmul(self) -> None: [0.19540944, 0.06203965, 0.05279523], [0.19540944, 0.06203965, 0.05279523], [0.19540944, 0.06203965, 0.05279523], - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) + ], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) class TestEuclideanDistance: @@ -455,58 +499,62 @@ class TestEuclideanDistance: tests methods. """ - def test_euclidean_distance(self) -> None: + def test_euclidean_distance(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.euclidean_distance` definition.""" - np.testing.assert_allclose( + xp_assert_close( euclidean_distance( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), ), 451.71330197, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( euclidean_distance( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), ), 52.64986116, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( euclidean_distance( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 8.32281957, -73.58297716]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 8.32281957, -73.58297716], xp=xp), ), 346.06489172, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_euclidean_distance(self) -> None: + def test_n_dimensional_euclidean_distance(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.common.euclidean_distance` definition n-dimensional arrays support. """ - a = np.array([100.00000000, 21.57210357, 272.22819350]) - b = np.array([100.00000000, 426.67945353, 72.39590835]) - distance = euclidean_distance(a, b) + a = xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp) + b = xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp) + distance = as_ndarray(euclidean_distance(a, b)) - a = np.tile(a, (6, 1)) - b = np.tile(b, (6, 1)) - distance = np.tile(distance, 6) - np.testing.assert_allclose( - euclidean_distance(a, b), distance, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp.tile(xp_as_array(a, xp=xp), (6, 1)) + b = xp.tile(xp_as_array(b, xp=xp), (6, 1)) + distance = xp.tile(xp_as_array(distance, xp=xp), (6,)) + xp_assert_close( + euclidean_distance(a, b), + distance, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 3)) - b = np.reshape(b, (2, 3, 3)) - distance = np.reshape(distance, (2, 3)) - np.testing.assert_allclose( - euclidean_distance(a, b), distance, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 3), xp=xp) + b = xp_reshape(xp_as_array(b, xp=xp), (2, 3, 3), xp=xp) + distance = xp_reshape(xp_as_array(distance, xp=xp), (2, 3), xp=xp) + xp_assert_close( + euclidean_distance(a, b), + distance, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -527,58 +575,62 @@ class TestManhattanDistance: tests methods. """ - def test_manhattan_distance(self) -> None: + def test_manhattan_distance(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.manhattan_distance` definition.""" - np.testing.assert_allclose( + xp_assert_close( manhattan_distance( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), ), 604.93963510999993, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( manhattan_distance( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), ), 56.705054670000052, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( manhattan_distance( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 8.32281957, -73.58297716]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 8.32281957, -73.58297716], xp=xp), ), 359.06045465999995, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_manhattan_distance(self) -> None: + def test_n_dimensional_manhattan_distance(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.common.manhattan_distance` definition n-dimensional arrays support. """ - a = np.array([100.00000000, 21.57210357, 272.22819350]) - b = np.array([100.00000000, 426.67945353, 72.39590835]) - distance = manhattan_distance(a, b) + a = xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp) + b = xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp) + distance = as_ndarray(manhattan_distance(a, b)) - a = np.tile(a, (6, 1)) - b = np.tile(b, (6, 1)) - distance = np.tile(distance, 6) - np.testing.assert_allclose( - manhattan_distance(a, b), distance, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp.tile(xp_as_array(a, xp=xp), (6, 1)) + b = xp.tile(xp_as_array(b, xp=xp), (6, 1)) + distance = xp.tile(xp_as_array(distance, xp=xp), (6,)) + xp_assert_close( + manhattan_distance(a, b), + distance, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 3)) - b = np.reshape(b, (2, 3, 3)) - distance = np.reshape(distance, (2, 3)) - np.testing.assert_allclose( - manhattan_distance(a, b), distance, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 3), xp=xp) + b = xp_reshape(xp_as_array(b, xp=xp), (2, 3, 3), xp=xp) + distance = xp_reshape(xp_as_array(distance, xp=xp), (2, 3), xp=xp) + xp_assert_close( + manhattan_distance(a, b), + distance, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -599,27 +651,27 @@ class TestLinearConversion: tests methods. """ - def test_linear_conversion(self) -> None: + def test_linear_conversion(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.linear_conversion` definition.""" - np.testing.assert_allclose( + xp_assert_close( linear_conversion( - np.linspace(0, 1, 10), np.array([0, 1]), np.array([1, np.pi]) - ), - np.array( - [ - 1.00000000, - 1.23795474, - 1.47590948, - 1.71386422, - 1.95181896, - 2.18977370, - 2.42772844, - 2.66568318, - 2.90363791, - 3.14159265, - ] + xp_linspace(0, 1, num=10, xp=xp), # pyright: ignore + xp_as_array([0, 1], xp=xp), + xp_as_array([1, np.pi], xp=xp), ), + [ + 1.00000000, + 1.23795474, + 1.47590948, + 1.71386422, + 1.95181896, + 2.18977370, + 2.42772844, + 2.66568318, + 2.90363791, + 3.14159265, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -630,53 +682,49 @@ class TestLinstepFunction: tests methods. """ - def test_linstep_function(self) -> None: + def test_linstep_function(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.linstep_function` definition.""" - np.testing.assert_allclose( + xp_assert_close( linstep_function( - np.linspace(0, 1, 10), - np.linspace(0, 1, 10), - np.linspace(0, 2, 10), - ), - np.array( - [ - 0.00000000, - 0.12345679, - 0.27160494, - 0.44444444, - 0.64197531, - 0.86419753, - 1.11111111, - 1.38271605, - 1.67901235, - 2.00000000, - ] + xp_linspace(0, 1, num=10, xp=xp), # pyright: ignore + xp_linspace(0, 1, num=10, xp=xp), # pyright: ignore + xp_linspace(0, 2, num=10, xp=xp), # pyright: ignore ), + [ + 0.00000000, + 0.12345679, + 0.27160494, + 0.44444444, + 0.64197531, + 0.86419753, + 1.11111111, + 1.38271605, + 1.67901235, + 2.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( linstep_function( - np.linspace(0, 2, 10), - np.linspace(0.25, 0.5, 10), - np.linspace(0.5, 0.75, 10), + xp_linspace(0, 2, num=10, xp=xp), # pyright: ignore + xp_linspace(0.25, 0.5, num=10, xp=xp), # pyright: ignore + xp_linspace(0.5, 0.75, num=10, xp=xp), # pyright: ignore clip=True, ), - np.array( - [ - 0.25000000, - 0.33333333, - 0.41666667, - 0.50000000, - 0.58333333, - 0.63888889, - 0.66666667, - 0.69444444, - 0.72222222, - 0.75000000, - ] - ), + [ + 0.25000000, + 0.33333333, + 0.41666667, + 0.50000000, + 0.58333333, + 0.63888889, + 0.66666667, + 0.69444444, + 0.72222222, + 0.75000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -687,22 +735,22 @@ class TestSmoothstepFunction: tests methods. """ - def test_smoothstep_function(self) -> None: + def test_smoothstep_function(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.smoothstep_function` definition.""" assert smoothstep_function(0.5) == 0.5 assert smoothstep_function(0.25) == 0.15625 assert smoothstep_function(0.75) == 0.84375 - x = np.linspace(-2, 2, 5) - np.testing.assert_allclose( - smoothstep_function(x), - np.array([28.00000, 5.00000, 0.00000, 1.00000, -4.00000]), + x = xp_linspace(-2, 2, num=5, xp=xp) + xp_assert_close( + smoothstep_function(x), # pyright: ignore + [28.00000, 5.00000, 0.00000, 1.00000, -4.00000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - smoothstep_function(x, -2, 2, clip=True), - np.array([0.00000, 0.15625, 0.50000, 0.84375, 1.00000]), + xp_assert_close( + smoothstep_function(x, -2, 2, clip=True), # pyright: ignore + [0.00000, 0.15625, 0.50000, 0.84375, 1.00000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -713,18 +761,22 @@ class TestIsIdentity: methods. """ - def test_is_identity(self) -> None: + def test_is_identity(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.is_identity` definition.""" - assert is_identity(np.reshape(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]), (3, 3))) + assert is_identity( + xp_reshape(xp_as_array([1, 0, 0, 0, 1, 0, 0, 0, 1], xp=xp), (3, 3), xp=xp) + ) assert not is_identity( - np.reshape(np.array([1, 2, 0, 0, 1, 0, 0, 0, 1]), (3, 3)) + xp_reshape(xp_as_array([1, 2, 0, 0, 1, 0, 0, 0, 1], xp=xp), (3, 3), xp=xp) ) - assert is_identity(np.reshape(np.array([1, 0, 0, 1]), (2, 2))) + assert is_identity(xp_reshape(xp_as_array([1, 0, 0, 1], xp=xp), (2, 2), xp=xp)) - assert not is_identity(np.reshape(np.array([1, 2, 0, 1]), (2, 2))) + assert not is_identity( + xp_reshape(xp_as_array([1, 2, 0, 1], xp=xp), (2, 2), xp=xp) + ) class TestEigenDecomposition: @@ -733,35 +785,39 @@ class TestEigenDecomposition: tests methods. """ - def test_is_identity(self) -> None: + def test_is_identity(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.common.eigen_decomposition` definition.""" - a = np.diag([1, 2, 3]) + a = xp_create_diagonal(xp_as_array([1, 2, 3], xp=xp), xp=xp) w, v = eigen_decomposition(a) - np.testing.assert_equal(w, np.array([3.0, 2.0, 1.0])) - np.testing.assert_equal( - v, np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]) + xp_assert_equal(as_ndarray(w), [3.0, 2.0, 1.0]) + xp_assert_equal( + as_ndarray(v), + [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], ) w, v = eigen_decomposition(a, 1) - np.testing.assert_equal(w, np.array([3.0])) - np.testing.assert_equal(v, np.array([[0.0], [0.0], [1.0]])) + xp_assert_equal(as_ndarray(w), [3.0]) + xp_assert_equal(as_ndarray(v), [[0.0], [0.0], [1.0]]) w, v = eigen_decomposition(a, descending_order=False) - np.testing.assert_equal(w, np.array([1.0, 2.0, 3.0])) - np.testing.assert_equal( - v, np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) + xp_assert_equal(as_ndarray(w), [1.0, 2.0, 3.0]) + xp_assert_equal( + as_ndarray(v), + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], ) w, v = eigen_decomposition(a, covariance_matrix=True) - np.testing.assert_equal(w, np.array([9.0, 4.0, 1.0])) - np.testing.assert_equal( - v, np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]) + xp_assert_equal(as_ndarray(w), [9.0, 4.0, 1.0]) + xp_assert_equal( + as_ndarray(v), + [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], ) w, v = eigen_decomposition(a, descending_order=False, covariance_matrix=True) - np.testing.assert_equal(w, np.array([1.0, 4.0, 9.0])) - np.testing.assert_equal( - v, np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) + xp_assert_equal(as_ndarray(w), [1.0, 4.0, 9.0]) + xp_assert_equal( + as_ndarray(v), + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], ) diff --git a/colour/algebra/tests/test_extrapolation.py b/colour/algebra/tests/test_extrapolation.py index c258493cc1..6ebfc443f8 100644 --- a/colour/algebra/tests/test_extrapolation.py +++ b/colour/algebra/tests/test_extrapolation.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -13,7 +18,14 @@ PchipInterpolator, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_assert_equal, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -101,7 +113,7 @@ def test_right(self) -> None: ) assert extrapolator.right == 0 - def test__call__(self) -> None: + def test__call__(self, xp: ModuleType) -> None: """ Test :meth:`colour.algebra.extrapolation.Extrapolator.__call__` method. @@ -113,51 +125,93 @@ def test__call__(self) -> None: extrapolator = Extrapolator( LinearInterpolator(np.array([5, 6, 7]), np.array([5, 6, 7])) ) - np.testing.assert_array_equal(extrapolator((4, 8)), (4, 8)) - assert extrapolator(4) == 4 + xp_assert_equal(extrapolator(xp_as_array([4, 8], xp=xp)), (4, 8)) + xp_assert_equal(extrapolator(xp_as_array([4], xp=xp)), [4]) extrapolator = Extrapolator( LinearInterpolator(np.array([3, 4, 5]), np.array([1, 2, 3])), method="Constant", ) - np.testing.assert_array_equal(extrapolator((0.1, 0.2, 8, 9)), (1, 1, 3, 3)) - assert extrapolator(0.1) == 1.0 + xp_assert_equal( + extrapolator(xp_as_array([0.1, 0.2, 8, 9], xp=xp)), + (1, 1, 3, 3), + ) + xp_assert_equal(extrapolator(xp_as_array([0.1], xp=xp)), [1.0]) extrapolator = Extrapolator( LinearInterpolator(np.array([3, 4, 5]), np.array([1, 2, 3])), method="Constant", left=0, ) - np.testing.assert_array_equal(extrapolator((0.1, 0.2, 8, 9)), (0, 0, 3, 3)) - assert extrapolator(0.1) == 0 + xp_assert_equal( + extrapolator(xp_as_array([0.1, 0.2, 8, 9], xp=xp)), + (0, 0, 3, 3), + ) + xp_assert_equal(extrapolator(xp_as_array([0.1], xp=xp)), [0]) extrapolator = Extrapolator( LinearInterpolator(np.array([3, 4, 5]), np.array([1, 2, 3])), method="Constant", right=0, ) - np.testing.assert_array_equal(extrapolator((0.1, 0.2, 8, 9)), (1, 1, 0, 0)) - assert extrapolator(9) == 0 + xp_assert_equal( + extrapolator(xp_as_array([0.1, 0.2, 8, 9], xp=xp)), + (1, 1, 0, 0), + ) + xp_assert_equal(extrapolator(xp_as_array([9], xp=xp)), [0]) extrapolator = Extrapolator( CubicSplineInterpolator(np.array([3, 4, 5, 6]), np.array([1, 2, 3, 4])) ) - np.testing.assert_allclose( - extrapolator((0.1, 0.2, 8.0, 9.0)), + xp_assert_close( + extrapolator(xp_as_array([0.1, 0.2, 8.0, 9.0], xp=xp)), (-1.9, -1.8, 6.0, 7.0), atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert extrapolator(9) == 7 + xp_assert_close( + extrapolator(xp_as_array([9], xp=xp)), + [7], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) extrapolator = Extrapolator( PchipInterpolator(np.array([3, 4, 5]), np.array([1, 2, 3])) ) - np.testing.assert_allclose( - extrapolator((0.1, 0.2, 8.0, 9.0)), + xp_assert_close( + extrapolator(xp_as_array([0.1, 0.2, 8.0, 9.0], xp=xp)), (-1.9, -1.8, 6.0, 7.0), atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert extrapolator(9) == 7.0 + xp_assert_close( + extrapolator(xp_as_array([9], xp=xp)), + [7.0], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + x = np.array([3, 4, 5]) + y = np.array([1, 2, 3]) + x_e = xp_as_array([0.1, 0.2, 4.5, 8.0, 9.0], xp=xp) + reference = as_ndarray(Extrapolator(LinearInterpolator(x, y))(x_e)) + xp_assert_close( + as_ndarray(Extrapolator(LinearInterpolator(x, np.transpose([y, y])))(x_e)), + np.transpose([reference, reference]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + reference = as_ndarray( + Extrapolator(LinearInterpolator(x, y), method="Constant", left=0)(x_e) + ) + xp_assert_close( + as_ndarray( + Extrapolator( + LinearInterpolator(x, np.transpose([y, y])), + method="Constant", + left=0, + )(x_e) + ), + np.transpose([reference, reference]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) @ignore_numpy_errors def test_nan__call__(self) -> None: diff --git a/colour/algebra/tests/test_interpolation.py b/colour/algebra/tests/test_interpolation.py index f0b4e50e11..1c10a490a0 100644 --- a/colour/algebra/tests/test_interpolation.py +++ b/colour/algebra/tests/test_interpolation.py @@ -10,6 +10,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import os from itertools import product @@ -39,7 +44,15 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.hints import NDArrayFloat, cast from colour.io import LUT3D, read_LUT -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_linspace, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -506,43 +519,41 @@ class TestKernelNearestNeighbour: definition unit tests methods. """ - def test_kernel_nearest(self) -> None: + def test_kernel_nearest(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.interpolation.kernel_nearest_neighbour` definition. """ - np.testing.assert_allclose( - kernel_nearest_neighbour(np.linspace(-5, 5, 25)), - np.array( - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ] - ), + xp_assert_close( + kernel_nearest_neighbour(xp_linspace(-5, 5, num=25, xp=xp)), # pyright: ignore + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -553,40 +564,38 @@ class TestKernelLinear: unit tests methods. """ - def test_kernel_linear(self) -> None: + def test_kernel_linear(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.interpolation.kernel_linear` definition.""" - np.testing.assert_allclose( - kernel_linear(np.linspace(-5, 5, 25)), - np.array( - [ - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.16666667, - 0.58333333, - 1.00000000, - 0.58333333, - 0.16666667, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - ] - ), + xp_assert_close( + kernel_linear(xp_linspace(-5, 5, num=25, xp=xp)), # pyright: ignore + [ + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.16666667, + 0.58333333, + 1.00000000, + 0.58333333, + 0.16666667, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -597,74 +606,70 @@ class TestKernelSinc: unit tests methods. """ - def test_kernel_sinc(self) -> None: + def test_kernel_sinc(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.interpolation.kernel_sinc` definition.""" - np.testing.assert_allclose( - kernel_sinc(np.linspace(-5, 5, 25)), - np.array( - [ - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.02824617, - 0.12732395, - 0.03954464, - -0.16539867, - -0.18006326, - 0.19098593, - 0.73791298, - 1.00000000, - 0.73791298, - 0.19098593, - -0.18006326, - -0.16539867, - 0.03954464, - 0.12732395, - 0.02824617, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - ] - ), + xp_assert_close( + kernel_sinc(xp_linspace(-5, 5, num=25, xp=xp)), # pyright: ignore + [ + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.02824617, + 0.12732395, + 0.03954464, + -0.16539867, + -0.18006326, + 0.19098593, + 0.73791298, + 1.00000000, + 0.73791298, + 0.19098593, + -0.18006326, + -0.16539867, + 0.03954464, + 0.12732395, + 0.02824617, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - kernel_sinc(np.linspace(-5, 5, 25), 1), - np.array( - [ - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.19098593, - 0.73791298, - 1.00000000, - 0.73791298, - 0.19098593, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - ] - ), + xp_assert_close( + kernel_sinc(xp_linspace(-5, 5, num=25, xp=xp), 1), # pyright: ignore + [ + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.19098593, + 0.73791298, + 1.00000000, + 0.73791298, + 0.19098593, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -675,74 +680,70 @@ class TestKernelLanczos: unit tests methods. """ - def test_kernel_lanczos(self) -> None: + def test_kernel_lanczos(self, xp: ModuleType) -> None: """Test :func:`colour.algebra.interpolation.kernel_lanczos` definition.""" - np.testing.assert_allclose( - kernel_lanczos(np.linspace(-5, 5, 25)), - np.array( - [ - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 8.06009483e-04, - 2.43170841e-02, - 1.48478897e-02, - -9.33267411e-02, - -1.32871018e-01, - 1.67651704e-01, - 7.14720157e-01, - 1.00000000e00, - 7.14720157e-01, - 1.67651704e-01, - -1.32871018e-01, - -9.33267411e-02, - 1.48478897e-02, - 2.43170841e-02, - 8.06009483e-04, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - ] - ), + xp_assert_close( + kernel_lanczos(xp_linspace(-5, 5, num=25, xp=xp)), # pyright: ignore + [ + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 8.06009483e-04, + 2.43170841e-02, + 1.48478897e-02, + -9.33267411e-02, + -1.32871018e-01, + 1.67651704e-01, + 7.14720157e-01, + 1.00000000e00, + 7.14720157e-01, + 1.67651704e-01, + -1.32871018e-01, + -9.33267411e-02, + 1.48478897e-02, + 2.43170841e-02, + 8.06009483e-04, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - kernel_lanczos(np.linspace(-5, 5, 25), 1), - np.array( - [ - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.03647563, - 0.54451556, - 1.00000000, - 0.54451556, - 0.03647563, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - ] - ), + xp_assert_close( + kernel_lanczos(xp_linspace(-5, 5, num=25, xp=xp), 1), # pyright: ignore + [ + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.03647563, + 0.54451556, + 1.00000000, + 0.54451556, + 0.03647563, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -753,77 +754,73 @@ class TestKernelCardinalSpline: definition unit tests methods. """ - def test_kernel_cardinal_spline(self) -> None: + def test_kernel_cardinal_spline(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.interpolation.kernel_cardinal_spline` definition. """ - np.testing.assert_allclose( - kernel_cardinal_spline(np.linspace(-5, 5, 25)), - np.array( - [ - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - -0.03703704, - -0.0703125, - 0.13194444, - 0.67447917, - 1.00000000, - 0.67447917, - 0.13194444, - -0.0703125, - -0.03703704, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - ] - ), + xp_assert_close( + kernel_cardinal_spline(xp_linspace(-5, 5, num=25, xp=xp)), # pyright: ignore + [ + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + -0.03703704, + -0.0703125, + 0.13194444, + 0.67447917, + 1.00000000, + 0.67447917, + 0.13194444, + -0.0703125, + -0.03703704, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - kernel_cardinal_spline(np.linspace(-5, 5, 25), 0, 1), - np.array( - [ - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00617284, - 0.0703125, - 0.26157407, - 0.52922454, - 0.66666667, - 0.52922454, - 0.26157407, - 0.0703125, - 0.00617284, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - 0.00000000, - ] - ), + xp_assert_close( + kernel_cardinal_spline(xp_linspace(-5, 5, num=25, xp=xp), 0, 1), # pyright: ignore + [ + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00617284, + 0.0703125, + 0.26157407, + 0.52922454, + 0.66666667, + 0.52922454, + 0.26157407, + 0.0703125, + 0.00617284, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + 0.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -869,7 +866,7 @@ def test_x(self) -> None: x = y = np.linspace(0, 1, 10) kernel_interpolator = KernelInterpolator(x, y) - np.testing.assert_equal(kernel_interpolator.x, x) + xp_assert_equal(kernel_interpolator.x, x) def test_y(self) -> None: """ @@ -883,7 +880,7 @@ def test_y(self) -> None: x = y = np.linspace(0, 1, 10) kernel_interpolator = KernelInterpolator(x, y) - np.testing.assert_equal(kernel_interpolator.y, y) + xp_assert_equal(kernel_interpolator.y, y) def test_window(self) -> None: """ @@ -937,14 +934,10 @@ def test_raise_exception___init__(self) -> None: method raised exception. """ - pytest.raises( - ValueError, - KernelInterpolator, - np.linspace(0, 1, 10), - np.linspace(0, 1, 15), - ) + with pytest.raises(ValueError): + KernelInterpolator(np.linspace(0, 1, 10), np.linspace(0, 1, 15)) - def test__call__(self) -> None: + def test__call__(self, xp: ModuleType) -> None: """ Test :meth:`colour.algebra.interpolation.KernelInterpolator.__call__` method. @@ -958,179 +951,169 @@ def test__call__(self) -> None: x_i = np.linspace(11, 25, 25) kernel_interpolator = KernelInterpolator(x, y) - np.testing.assert_allclose( - kernel_interpolator(x_i), - np.array( - [ - 4.43848790, - 4.26286480, - 3.64640076, - 2.77982023, - 2.13474499, - 2.08206794, - 2.50585862, - 3.24992692, - 3.84593162, - 4.06289704, - 3.80825633, - 3.21068994, - 2.65177161, - 2.32137382, - 2.45995375, - 2.88799997, - 3.43843598, - 3.79504892, - 3.79937086, - 3.47673343, - 2.99303182, - 2.59305006, - 2.47805594, - 2.82957843, - 3.14159265, - ] - ), + xp_assert_close( + kernel_interpolator(xp_as_array(x_i, xp=xp)), + [ + 4.43848790, + 4.26286480, + 3.64640076, + 2.77982023, + 2.13474499, + 2.08206794, + 2.50585862, + 3.24992692, + 3.84593162, + 4.06289704, + 3.80825633, + 3.21068994, + 2.65177161, + 2.32137382, + 2.45995375, + 2.88799997, + 3.43843598, + 3.79504892, + 3.79937086, + 3.47673343, + 2.99303182, + 2.59305006, + 2.47805594, + 2.82957843, + 3.14159265, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) kernel_interpolator = KernelInterpolator(x, y, kernel=kernel_sinc) - np.testing.assert_allclose( - kernel_interpolator(x_i), - np.array( - [ - 4.43848790, - 4.47570010, - 3.84353906, - 3.05959493, - 2.53514958, - 2.19916874, - 2.93225625, - 3.32187855, - 4.09458791, - 4.23088094, - 3.92591447, - 3.53263071, - 2.65177161, - 2.73541557, - 2.65740315, - 3.17077616, - 3.69624479, - 3.87159620, - 4.06433758, - 3.56283868, - 3.28312289, - 2.79652091, - 2.62481419, - 3.22117115, - 3.14159265, - ] - ), + xp_assert_close( + kernel_interpolator(xp_as_array(x_i, xp=xp)), + [ + 4.43848790, + 4.47570010, + 3.84353906, + 3.05959493, + 2.53514958, + 2.19916874, + 2.93225625, + 3.32187855, + 4.09458791, + 4.23088094, + 3.92591447, + 3.53263071, + 2.65177161, + 2.73541557, + 2.65740315, + 3.17077616, + 3.69624479, + 3.87159620, + 4.06433758, + 3.56283868, + 3.28312289, + 2.79652091, + 2.62481419, + 3.22117115, + 3.14159265, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) kernel_interpolator = KernelInterpolator(x, y, window=1) - np.testing.assert_allclose( - kernel_interpolator(x_i), - np.array( - [ - 4.43848790, - 4.96712277, - 4.09584229, - 3.23991575, - 2.80418924, - 2.28470276, - 3.20024753, - 3.41120944, - 4.46416970, - 4.57878168, - 4.15371498, - 3.92841633, - 2.65177161, - 3.02110187, - 2.79812654, - 3.44218674, - 4.00032377, - 4.01356870, - 4.47633386, - 3.70912627, - 3.58365067, - 3.14325415, - 2.88247572, - 3.37531662, - 3.14159265, - ] - ), + xp_assert_close( + kernel_interpolator(xp_as_array(x_i, xp=xp)), + [ + 4.43848790, + 4.96712277, + 4.09584229, + 3.23991575, + 2.80418924, + 2.28470276, + 3.20024753, + 3.41120944, + 4.46416970, + 4.57878168, + 4.15371498, + 3.92841633, + 2.65177161, + 3.02110187, + 2.79812654, + 3.44218674, + 4.00032377, + 4.01356870, + 4.47633386, + 3.70912627, + 3.58365067, + 3.14325415, + 2.88247572, + 3.37531662, + 3.14159265, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) kernel_interpolator = KernelInterpolator(x, y, window=1, kernel_kwargs={"a": 1}) - np.testing.assert_allclose( - kernel_interpolator(x_i), - np.array( - [ - 4.43848790, - 3.34379320, - 3.62463711, - 2.34585418, - 2.04767083, - 2.09444849, - 2.13349835, - 3.10304927, - 3.29553153, - 3.59884738, - 3.48484031, - 2.72974983, - 2.65177161, - 2.03850468, - 2.29470194, - 2.76179863, - 2.80189050, - 3.75979450, - 2.98422257, - 3.48444099, - 2.49208997, - 2.46516442, - 2.42336082, - 2.25975903, - 3.14159265, - ] - ), + xp_assert_close( + kernel_interpolator(xp_as_array(x_i, xp=xp)), + [ + 4.43848790, + 3.34379320, + 3.62463711, + 2.34585418, + 2.04767083, + 2.09444849, + 2.13349835, + 3.10304927, + 3.29553153, + 3.59884738, + 3.48484031, + 2.72974983, + 2.65177161, + 2.03850468, + 2.29470194, + 2.76179863, + 2.80189050, + 3.75979450, + 2.98422257, + 3.48444099, + 2.49208997, + 2.46516442, + 2.42336082, + 2.25975903, + 3.14159265, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) kernel_interpolator = KernelInterpolator( x, y, padding_kwargs={"pad_width": (3, 3), "mode": "mean"} ) - np.testing.assert_allclose( - kernel_interpolator(x_i), - np.array( - [ - 4.4384879, - 4.35723245, - 3.62918155, - 2.77471295, - 2.13474499, - 2.08206794, - 2.50585862, - 3.24992692, - 3.84593162, - 4.06289704, - 3.80825633, - 3.21068994, - 2.65177161, - 2.32137382, - 2.45995375, - 2.88799997, - 3.43843598, - 3.79504892, - 3.79937086, - 3.47673343, - 2.99303182, - 2.59771985, - 2.49380017, - 2.76339043, - 3.14159265, - ] - ), + xp_assert_close( + kernel_interpolator(xp_as_array(x_i, xp=xp)), + [ + 4.4384879, + 4.35723245, + 3.62918155, + 2.77471295, + 2.13474499, + 2.08206794, + 2.50585862, + 3.24992692, + 3.84593162, + 4.06289704, + 3.80825633, + 3.21068994, + 2.65177161, + 2.32137382, + 2.45995375, + 2.88799997, + 3.43843598, + 3.79504892, + 3.79937086, + 3.47673343, + 2.99303182, + 2.59771985, + 2.49380017, + 2.76339043, + 3.14159265, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1140,15 +1123,24 @@ def test__call__(self) -> None: y = np.sin(x_1 / len(x_1) * np.pi * 6) / (x_1 / len(x_1)) x_i = np.linspace(1, 9, 25) - np.testing.assert_allclose( - KernelInterpolator(x_1, y)(x_i), - KernelInterpolator(x_2, y)(x_i * 10), + xp_assert_close( + KernelInterpolator(x_1, y)(xp_as_array(x_i, xp=xp)), + as_ndarray(KernelInterpolator(x_2, y)(xp_as_array(x_i * 10, xp=xp))), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + xp_assert_close( + KernelInterpolator(x_1, y)(xp_as_array(x_i, xp=xp)), + as_ndarray(KernelInterpolator(x_3, y)(xp_as_array(x_i / 10, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - KernelInterpolator(x_1, y)(x_i), - KernelInterpolator(x_3, y)(x_i / 10), + reference = as_ndarray(KernelInterpolator(x_1, y)(xp_as_array(x_i, xp=xp))) + kernel_interpolator = KernelInterpolator(x_1, y) + kernel_interpolator.y = np.transpose([y, y]) + xp_assert_close( + as_ndarray(kernel_interpolator(xp_as_array(x_i, xp=xp))), + np.transpose([reference, reference]), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1161,9 +1153,11 @@ def test_raise_exception___call__(self) -> None: x = y = np.linspace(0, 1, 10) kernel_interpolator = KernelInterpolator(x, y) - pytest.raises(ValueError, kernel_interpolator, -1) + with pytest.raises(ValueError): + kernel_interpolator(-1) - pytest.raises(ValueError, kernel_interpolator, 11) + with pytest.raises(ValueError): + kernel_interpolator(11) @ignore_numpy_errors def test_nan__call__(self) -> None: @@ -1249,9 +1243,10 @@ def test_raise_exception___init__(self) -> None: """ x, y = np.linspace(0, 1, 10), np.linspace(0, 1, 15) - pytest.raises(ValueError, LinearInterpolator, x, y) + with pytest.raises(ValueError): + LinearInterpolator(x, y) - def test__call__(self) -> None: + def test__call__(self, xp: ModuleType) -> None: """ Test :meth:`colour.algebra.interpolation.LinearInterpolator.__call__` method. @@ -1267,20 +1262,31 @@ def test__call__(self) -> None: for i, value in enumerate( np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval) ): - np.testing.assert_allclose( + xp_assert_close( DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES[i], - linear_interpolator(value), + as_ndarray(linear_interpolator(xp_as_array([value], xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - linear_interpolator( - np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval) - ), + x_e = np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval) + xp_assert_close( + linear_interpolator(xp_as_array(x_e, xp=xp)), DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES, atol=TOLERANCE_ABSOLUTE_TESTS, ) + linear_interpolator.y = np.transpose([DATA_POINTS_A, DATA_POINTS_A]) + xp_assert_close( + as_ndarray(linear_interpolator(xp_as_array(x_e, xp=xp))), + np.transpose( + [ + DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES, + DATA_POINTS_A_LINEAR_INTERPOLATED_10_SAMPLES, + ] + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + def test_raise_exception___call__(self) -> None: """ Test :meth:`colour.algebra.interpolation.LinearInterpolator.__call__` @@ -1290,9 +1296,11 @@ def test_raise_exception___call__(self) -> None: x = y = np.linspace(0, 1, 10) linear_interpolator = LinearInterpolator(x, y) - pytest.raises(ValueError, linear_interpolator, -1) + with pytest.raises(ValueError): + linear_interpolator(-1) - pytest.raises(ValueError, linear_interpolator, 11) + with pytest.raises(ValueError): + linear_interpolator(11) @ignore_numpy_errors def test_nan__call__(self) -> None: @@ -1343,9 +1351,10 @@ def test_raise_exception___init__(self) -> None: """ x, y = np.linspace(0, 1, 10), np.linspace(0, 1, 15) - pytest.raises(ValueError, SpragueInterpolator, x, y) + with pytest.raises(ValueError): + SpragueInterpolator(x, y) - def test__call__(self) -> None: + def test__call__(self, xp: ModuleType) -> None: """ Test :meth:`colour.algebra.interpolation.SpragueInterpolator.__call__` method. @@ -1361,20 +1370,31 @@ def test__call__(self) -> None: for i, value in enumerate( np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval) ): - np.testing.assert_allclose( + xp_assert_close( DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES[i], - sprague_interpolator(value), + as_ndarray(sprague_interpolator(xp_as_array([value], xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sprague_interpolator( - np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval) - ), + x_e = np.arange(0, len(DATA_POINTS_A) - 1 + interval, interval) + xp_assert_close( + sprague_interpolator(xp_as_array(x_e, xp=xp)), DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES, atol=TOLERANCE_ABSOLUTE_TESTS, ) + sprague_interpolator.y = np.transpose([DATA_POINTS_A, DATA_POINTS_A]) + xp_assert_close( + as_ndarray(sprague_interpolator(xp_as_array(x_e, xp=xp))), + np.transpose( + [ + DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES, + DATA_POINTS_A_SPRAGUE_INTERPOLATED_10_SAMPLES, + ] + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + def test_raise_exception___call__(self) -> None: """ Test :meth:`colour.algebra.interpolation.SpragueInterpolator.__call__` @@ -1384,9 +1404,11 @@ def test_raise_exception___call__(self) -> None: x = y = np.linspace(0, 1, 10) sprague_interpolator = SpragueInterpolator(x, y) - pytest.raises(ValueError, sprague_interpolator, -1) + with pytest.raises(ValueError): + sprague_interpolator(-1) - pytest.raises(ValueError, sprague_interpolator, 11) + with pytest.raises(ValueError): + sprague_interpolator(11) @ignore_numpy_errors def test_nan__call__(self) -> None: @@ -1411,7 +1433,7 @@ class TestCubicSplineInterpolator: unit tests methods. """ - def test__call__(self) -> None: + def test__call__(self, xp: ModuleType) -> None: """ Test :meth:`colour.algebra.interpolation.CubicSplineInterpolator.\ __call__` method. @@ -1425,14 +1447,27 @@ def test__call__(self) -> None: if not is_scipy_installed(): # pragma: no cover return - np.testing.assert_allclose( - CubicSplineInterpolator( - np.linspace(0, 1, len(DATA_POINTS_A)), DATA_POINTS_A - )(np.linspace(0, 1, len(DATA_POINTS_A) * 2)), + x = np.linspace(0, 1, len(DATA_POINTS_A)) + x_e = xp_linspace(0, 1, num=len(DATA_POINTS_A) * 2, xp=xp) + xp_assert_close( + CubicSplineInterpolator(x, DATA_POINTS_A)(x_e), DATA_POINTS_A_CUBIC_SPLINE_INTERPOLATED_X2_SAMPLES, atol=TOLERANCE_ABSOLUTE_TESTS, ) + xp_assert_close( + CubicSplineInterpolator(x, np.transpose([DATA_POINTS_A, DATA_POINTS_A]))( + x_e + ), + np.transpose( + [ + DATA_POINTS_A_CUBIC_SPLINE_INTERPOLATED_X2_SAMPLES, + DATA_POINTS_A_CUBIC_SPLINE_INTERPOLATED_X2_SAMPLES, + ] + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + class TestPchipInterpolator: """ @@ -1467,12 +1502,21 @@ def test_y(self) -> None: if not is_scipy_installed(): # pragma: no cover return - interpolator = PchipInterpolator(np.linspace(0, 1, 10), np.linspace(0, 1, 10)) - - interpolator.y = np.linspace(0, 1, 10) + x = np.linspace(0, 1, 10) + y = np.linspace(0, 1, 10) + interpolator = PchipInterpolator(x, y) + interpolator.y = y assert interpolator(np.array(5)) == 5 + x_e = np.linspace(0, 1, 19) + reference = PchipInterpolator(x, y)(x_e) + xp_assert_close( + PchipInterpolator(x, np.transpose([y, y]))(x_e), + np.transpose([reference, reference]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + class TestNullInterpolator: """ @@ -1508,7 +1552,7 @@ def test_x(self) -> None: x = y = np.linspace(0, 1, 10) null_interpolator = NullInterpolator(x, y) - np.testing.assert_equal(null_interpolator.x, x) + xp_assert_equal(null_interpolator.x, x) def test_y(self) -> None: """ @@ -1522,7 +1566,7 @@ def test_y(self) -> None: x = y = np.linspace(0, 1, 10) null_interpolator = NullInterpolator(x, y) - np.testing.assert_equal(null_interpolator.y, y) + xp_assert_equal(null_interpolator.y, y) def test_absolute_tolerance(self) -> None: """ @@ -1533,7 +1577,7 @@ def test_absolute_tolerance(self) -> None: x = y = np.linspace(0, 1, 10) null_interpolator = NullInterpolator(x, y, absolute_tolerance=0.1) - np.testing.assert_equal(null_interpolator.absolute_tolerance, 0.1) + xp_assert_equal(null_interpolator.absolute_tolerance, 0.1) def test_relative_tolerance(self) -> None: """ @@ -1544,7 +1588,7 @@ def test_relative_tolerance(self) -> None: x = y = np.linspace(0, 1, 10) null_interpolator = NullInterpolator(x, y, relative_tolerance=0.1) - np.testing.assert_equal(null_interpolator.relative_tolerance, 0.1) + xp_assert_equal(null_interpolator.relative_tolerance, 0.1) def test_default(self) -> None: """ @@ -1555,7 +1599,7 @@ def test_default(self) -> None: x = y = np.linspace(0, 1, 10) null_interpolator = NullInterpolator(x, y, default=0) - np.testing.assert_equal(null_interpolator.default, 0) + xp_assert_equal(null_interpolator.default, 0) def test_raise_exception___init__(self) -> None: """ @@ -1564,9 +1608,10 @@ def test_raise_exception___init__(self) -> None: """ x, y = np.linspace(0, 1, 10), np.linspace(0, 1, 15) - pytest.raises(ValueError, NullInterpolator, x, y) + with pytest.raises(ValueError): + NullInterpolator(x, y) - def test__call__(self) -> None: + def test__call__(self, xp: ModuleType) -> None: """ Test :meth:`colour.algebra.interpolation.NullInterpolator.__call__` method. @@ -1577,16 +1622,26 @@ def test__call__(self) -> None: x = np.arange(len(DATA_POINTS_A)) null_interpolator = NullInterpolator(x, DATA_POINTS_A) - np.testing.assert_allclose( - null_interpolator(np.array([0.75, 2.0, 3.0, 4.75])), - np.array([np.nan, 12.46, 9.51, np.nan]), + xp_assert_close( + null_interpolator(xp_as_array([0.75, 2.0, 3.0, 4.75], xp=xp)), + [np.nan, 12.46, 9.51, np.nan], atol=TOLERANCE_ABSOLUTE_TESTS, ) null_interpolator = NullInterpolator(x, DATA_POINTS_A, 0.25, 0.25) - np.testing.assert_allclose( - null_interpolator(np.array([0.75, 2.0, 3.0, 4.75])), - np.array([12.32, 12.46, 9.51, 4.33]), + x_e = xp_as_array([0.75, 2.0, 3.0, 4.75], xp=xp) + xp_assert_close( + null_interpolator(x_e), + [12.32, 12.46, 9.51, 4.33], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + null_interpolator = NullInterpolator( + x, np.transpose([DATA_POINTS_A, DATA_POINTS_A]), 0.25, 0.25 + ) + xp_assert_close( + null_interpolator(x_e), + np.transpose([[12.32, 12.46, 9.51, 4.33], [12.32, 12.46, 9.51, 4.33]]), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1599,9 +1654,11 @@ def test_raise_exception___call__(self) -> None: x = y = np.linspace(0, 1, 10) null_interpolator = NullInterpolator(x, y) - pytest.raises(ValueError, null_interpolator, -1) + with pytest.raises(ValueError): + null_interpolator(-1) - pytest.raises(ValueError, null_interpolator, 11) + with pytest.raises(ValueError): + null_interpolator(11) @ignore_numpy_errors def test_nan__call__(self) -> None: @@ -1642,14 +1699,10 @@ def test_lagrange_coefficients(self) -> None: """ lc = [lagrange_coefficients(i, 3) for i in np.linspace(0.05, 0.95, 19)] - np.testing.assert_allclose( - lc, LAGRANGE_COEFFICIENTS_A, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(lc, LAGRANGE_COEFFICIENTS_A, atol=TOLERANCE_ABSOLUTE_TESTS) lc = [lagrange_coefficients(i, 4) for i in np.linspace(1.05, 1.95, 19)] - np.testing.assert_allclose( - lc, LAGRANGE_COEFFICIENTS_B, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(lc, LAGRANGE_COEFFICIENTS_B, atol=TOLERANCE_ABSOLUTE_TESTS) class TestTableInterpolationTrilinear: @@ -1658,7 +1711,7 @@ class TestTableInterpolationTrilinear: table_interpolation_trilinear` definition unit tests methods. """ - def test_interpolation_trilinear(self) -> None: + def test_interpolation_trilinear(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.interpolation.\ table_interpolation_trilinear` definition. @@ -1666,30 +1719,28 @@ def test_interpolation_trilinear(self) -> None: prng = np.random.RandomState(4) - V_xyz = random_triplet_generator(16, random_state=prng) - - np.testing.assert_allclose( - table_interpolation_trilinear(V_xyz, LUT_TABLE), - np.array( - [ - [1.07937594, -0.02773926, 0.55498254], - [0.53983424, 0.37099516, 0.13994561], - [1.13449122, -0.00305380, 0.13792909], - [0.73411897, 1.00141020, 0.59348239], - [0.74066176, 0.44679540, 0.55030394], - [0.20634750, 0.84797880, 0.55905579], - [0.92348649, 0.73112515, 0.42362820], - [0.03639248, 0.70357649, 0.52375041], - [0.29215488, 0.19697840, 0.44603879], - [0.47793470, 0.08696360, 0.70288463], - [0.88883354, 0.68680856, 0.87404642], - [0.21430977, 0.16796653, 0.19634247], - [0.82118989, 0.69239283, 0.39932389], - [1.06679072, 0.37974319, 0.49759377], - [0.17856230, 0.44755467, 0.62045271], - [0.59220355, 0.93136492, 0.30063692], - ] - ), + V_xyz = xp_as_array(random_triplet_generator(16, random_state=prng), xp=xp) + + xp_assert_close( + table_interpolation_trilinear(V_xyz, xp_as_array(LUT_TABLE, xp=xp)), + [ + [1.07937594, -0.02773926, 0.55498254], + [0.53983424, 0.37099516, 0.13994561], + [1.13449122, -0.00305380, 0.13792909], + [0.73411897, 1.00141020, 0.59348239], + [0.74066176, 0.44679540, 0.55030394], + [0.20634750, 0.84797880, 0.55905579], + [0.92348649, 0.73112515, 0.42362820], + [0.03639248, 0.70357649, 0.52375041], + [0.29215488, 0.19697840, 0.44603879], + [0.47793470, 0.08696360, 0.70288463], + [0.88883354, 0.68680856, 0.87404642], + [0.21430977, 0.16796653, 0.19634247], + [0.82118989, 0.69239283, 0.39932389], + [1.06679072, 0.37974319, 0.49759377], + [0.17856230, 0.44755467, 0.62045271], + [0.59220355, 0.93136492, 0.30063692], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1700,7 +1751,7 @@ class TestTableInterpolationTetrahedral: table_interpolation_tetrahedral` definition unit tests methods. """ - def test_interpolation_tetrahedral(self) -> None: + def test_interpolation_tetrahedral(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.interpolation.\ table_interpolation_tetrahedral` definition. @@ -1708,30 +1759,28 @@ def test_interpolation_tetrahedral(self) -> None: prng = np.random.RandomState(4) - V_xyz = random_triplet_generator(16, random_state=prng) - - np.testing.assert_allclose( - table_interpolation_tetrahedral(V_xyz, LUT_TABLE), - np.array( - [ - [1.08039215, -0.02840092, 0.55855303], - [0.52208945, 0.35297753, 0.13599555], - [1.14373467, -0.00422138, 0.13413290], - [0.71384967, 0.98420883, 0.57982724], - [0.76771576, 0.46280975, 0.55106736], - [0.20861663, 0.85077712, 0.57102264], - [0.90398698, 0.72351675, 0.41151955], - [0.03749453, 0.70226823, 0.52614254], - [0.29626758, 0.21645072, 0.47615873], - [0.46729624, 0.07494851, 0.68892548], - [0.85907681, 0.67744258, 0.84410486], - [0.24335535, 0.20896545, 0.21996717], - [0.79244027, 0.66930773, 0.39213595], - [1.08383608, 0.37985897, 0.49011919], - [0.14683649, 0.43624903, 0.58706947], - [0.61272658, 0.92799297, 0.29650424], - ] - ), + V_xyz = xp_as_array(random_triplet_generator(16, random_state=prng), xp=xp) + + xp_assert_close( + table_interpolation_tetrahedral(V_xyz, xp_as_array(LUT_TABLE, xp=xp)), + [ + [1.08039215, -0.02840092, 0.55855303], + [0.52208945, 0.35297753, 0.13599555], + [1.14373467, -0.00422138, 0.13413290], + [0.71384967, 0.98420883, 0.57982724], + [0.76771576, 0.46280975, 0.55106736], + [0.20861663, 0.85077712, 0.57102264], + [0.90398698, 0.72351675, 0.41151955], + [0.03749453, 0.70226823, 0.52614254], + [0.29626758, 0.21645072, 0.47615873], + [0.46729624, 0.07494851, 0.68892548], + [0.85907681, 0.67744258, 0.84410486], + [0.24335535, 0.20896545, 0.21996717], + [0.79244027, 0.66930773, 0.39213595], + [1.08383608, 0.37985897, 0.49011919], + [0.14683649, 0.43624903, 0.58706947], + [0.61272658, 0.92799297, 0.29650424], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1742,26 +1791,27 @@ class TestTableInterpolation: wrapper definition unit tests methods. """ - def test_table_interpolation(self) -> None: + def test_table_interpolation(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.interpolation.table_interpolation` wrapper definition. """ prng = np.random.RandomState(4) - V_xyz = prng.random_sample((10, 3)) + V_xyz = xp_as_array(prng.random_sample((10, 3)), xp=xp) + LUT = xp_as_array(LUT_TABLE, xp=xp) # Test with "Trilinear" method - np.testing.assert_allclose( - table_interpolation(V_xyz, LUT_TABLE, method="Trilinear"), - table_interpolation_trilinear(V_xyz, LUT_TABLE), + xp_assert_close( + table_interpolation(V_xyz, LUT, method="Trilinear"), + as_ndarray(table_interpolation_trilinear(V_xyz, LUT)), atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test with "Tetrahedral" method - np.testing.assert_allclose( - table_interpolation(V_xyz, LUT_TABLE, method="Tetrahedral"), - table_interpolation_tetrahedral(V_xyz, LUT_TABLE), + xp_assert_close( + table_interpolation(V_xyz, LUT, method="Tetrahedral"), + as_ndarray(table_interpolation_tetrahedral(V_xyz, LUT)), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1772,59 +1822,73 @@ class TestLinearInterpolationIndexAndFactor: linear_interpolation_index_and_factor` definition unit tests methods. """ - def test_linear_interpolation_index_and_factor(self) -> None: + def test_linear_interpolation_index_and_factor(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.interpolation.\ linear_interpolation_index_and_factor` definition. """ - break_points = np.array([0.0, 1.0, 2.0, 3.0]) + break_points = xp_as_array([0.0, 1.0, 2.0, 3.0], xp=xp) # Exact match at start. - index, factor = linear_interpolation_index_and_factor(0.0, break_points) - assert index == 0 - np.testing.assert_allclose(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + index, factor = linear_interpolation_index_and_factor( + xp_as_array([0.0], xp=xp), break_points + ) + xp_assert_equal(index, [0]) + xp_assert_close(factor, [0.0], atol=TOLERANCE_ABSOLUTE_TESTS) # Exact match at end (index = last, factor = 0). - index, factor = linear_interpolation_index_and_factor(3.0, break_points) - assert index == 3 - np.testing.assert_allclose(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + index, factor = linear_interpolation_index_and_factor( + xp_as_array([3.0], xp=xp), break_points + ) + xp_assert_equal(index, [3]) + xp_assert_close(factor, [0.0], atol=TOLERANCE_ABSOLUTE_TESTS) # Midpoint. - index, factor = linear_interpolation_index_and_factor(1.5, break_points) - assert index == 1 - np.testing.assert_allclose(factor, 0.5, atol=TOLERANCE_ABSOLUTE_TESTS) + index, factor = linear_interpolation_index_and_factor( + xp_as_array([1.5], xp=xp), break_points + ) + xp_assert_equal(index, [1]) + xp_assert_close(factor, [0.5], atol=TOLERANCE_ABSOLUTE_TESTS) # Clamped below. - index, factor = linear_interpolation_index_and_factor(-1.0, break_points) - assert index == 0 - np.testing.assert_allclose(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + index, factor = linear_interpolation_index_and_factor( + xp_as_array([-1.0], xp=xp), break_points + ) + xp_assert_equal(index, [0]) + xp_assert_close(factor, [0.0], atol=TOLERANCE_ABSOLUTE_TESTS) # Clamped above (same as end: index = last, factor = 0). - index, factor = linear_interpolation_index_and_factor(5.0, break_points) - assert index == 3 - np.testing.assert_allclose(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + index, factor = linear_interpolation_index_and_factor( + xp_as_array([5.0], xp=xp), break_points + ) + xp_assert_equal(index, [3]) + xp_assert_close(factor, [0.0], atol=TOLERANCE_ABSOLUTE_TESTS) # Degenerate (identical break points). - index, factor = linear_interpolation_index_and_factor(1.0, np.array([1.0, 1.0])) - np.testing.assert_allclose(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + index, factor = linear_interpolation_index_and_factor( + xp_as_array([1.0], xp=xp), xp_as_array([1.0, 1.0], xp=xp) + ) + xp_assert_close(factor, [0.0], atol=TOLERANCE_ABSOLUTE_TESTS) def test_linear_interpolation_index_and_factor_n_dimensional( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.algebra.interpolation.\ linear_interpolation_index_and_factor` definition n-dimensional support. """ - break_points = np.array([0.0, 1.0, 2.0, 3.0]) - values = np.array([0.0, 0.5, 1.5, 2.5, 3.0]) + break_points = xp_as_array([0.0, 1.0, 2.0, 3.0], xp=xp) + values = xp_as_array([0.0, 0.5, 1.5, 2.5, 3.0], xp=xp) index, factor = linear_interpolation_index_and_factor(values, break_points) - assert index.shape == (5,) - assert factor.shape == (5,) + assert as_ndarray(index).shape == (5,) + assert as_ndarray(factor).shape == (5,) - np.testing.assert_array_equal(index, [0, 0, 1, 2, 3]) - np.testing.assert_allclose( - factor, [0.0, 0.5, 0.5, 0.5, 0.0], atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_equal(index, [0, 0, 1, 2, 3]) + xp_assert_close( + factor, + [0.0, 0.5, 0.5, 0.5, 0.0], + atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/algebra/tests/test_prng.py b/colour/algebra/tests/test_prng.py index 103c44f1c7..87a84822d8 100644 --- a/colour/algebra/tests/test_prng.py +++ b/colour/algebra/tests/test_prng.py @@ -18,6 +18,7 @@ from colour.algebra import random_triplet_generator from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import xp_assert_close if typing.TYPE_CHECKING: from colour.hints import NDArrayFloat @@ -70,7 +71,7 @@ def test_random_triplet_generator(self) -> None: """ prng = np.random.RandomState(4) - np.testing.assert_allclose( + xp_assert_close( RANDOM_TRIPLETS, random_triplet_generator(10, random_state=prng), atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/algebra/tests/test_regression.py b/colour/algebra/tests/test_regression.py index e9732aa032..30d60f0643 100644 --- a/colour/algebra/tests/test_regression.py +++ b/colour/algebra/tests/test_regression.py @@ -2,10 +2,16 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.algebra import least_square_mapping_MoorePenrose from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -25,63 +31,39 @@ class TestLeastSquareMappingMoorePenrose: least_square_mapping_MoorePenrose` definition unit tests methods. """ - def test_least_square_mapping_MoorePenrose(self) -> None: + def test_least_square_mapping_MoorePenrose(self, xp: ModuleType) -> None: """ Test :func:`colour.algebra.regression.\ least_square_mapping_MoorePenrose` definition. """ prng = np.random.RandomState(2) - y = prng.random_sample((24, 3)) - x = y + (prng.random_sample((24, 3)) - 0.5) * 0.5 + y_np = prng.random_sample((24, 3)) + x_np = y_np + (prng.random_sample((24, 3)) - 0.5) * 0.5 - np.testing.assert_allclose( - least_square_mapping_MoorePenrose(y, x), - np.array( - [ - [1.05263767, 0.13780789, -0.22763399], - [0.07395843, 1.02939945, -0.10601150], - [0.05725508, -0.20526336, 1.10151945], - ] + xp_assert_close( + least_square_mapping_MoorePenrose( + xp_as_array(y_np, xp=xp), xp_as_array(x_np, xp=xp) ), + [ + [1.05263767, 0.13780789, -0.22763399], + [0.07395843, 1.02939945, -0.10601150], + [0.05725508, -0.20526336, 1.10151945], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - y = prng.random_sample((4, 3, 2)) - x = y + (prng.random_sample((4, 3, 2)) - 0.5) * 0.5 - np.testing.assert_allclose( - least_square_mapping_MoorePenrose(y, x), - np.array( - [ - [ - [ - [1.05968114, -0.0896093, -0.02923021], - [3.77254737, 0.06682885, -2.78161763], - ], - [ - [-0.77388532, 1.78761209, -0.44050114], - [-4.1282882, 0.55185528, 5.049136], - ], - [ - [0.36246422, -0.56421525, 1.4208154], - [2.07589501, 0.40261387, -1.47059455], - ], - ], - [ - [ - [0.237067, 0.4794514, 0.04004058], - [0.67778963, 0.15901967, 0.23854131], - ], - [ - [-0.4225357, 0.99316309, -0.14598921], - [-3.46789045, 1.09102153, 3.31051434], - ], - [ - [-0.91661817, 1.49060435, -0.45074387], - [-4.18896905, 0.25487186, 4.75951391], - ], - ], - ] + y_np = prng.random_sample((4, 3, 2)) + x_np = y_np + (prng.random_sample((4, 3, 2)) - 0.5) * 0.5 + xp_assert_close( + least_square_mapping_MoorePenrose( + xp_as_array(y_np, xp=xp), xp_as_array(x_np, xp=xp) ), + [ + [[1.07636527, -0.256201], [0.06625818, 0.80475283]], + [[0.51513719, 0.52756206], [1.87771063, 0.13030182]], + [[1.16325211, -0.29657976], [0.25479095, 0.92809262]], + [[1.37286297, -0.49899538], [0.10981647, 0.68105929]], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/appearance/atd95.py b/colour/appearance/atd95.py index ff84b602b5..056764aec7 100644 --- a/colour/appearance/atd95.py +++ b/colour/appearance/atd95.py @@ -26,20 +26,23 @@ from __future__ import annotations +import typing from dataclasses import dataclass, field -import numpy as np +from colour.algebra import sdiv, sdiv_mode, spow, vecmul + +if typing.TYPE_CHECKING: + from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat -from colour.algebra import spow, vecmul -from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat # noqa: TC001 from colour.utilities import ( MixinDataclassArithmetic, + array_namespace, as_float, - as_float_array, from_range_degrees, to_domain_100, tsplit, tstack, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -53,10 +56,6 @@ "CAM_ReferenceSpecification_ATD95", "CAM_Specification_ATD95", "XYZ_to_ATD95", - "luminance_to_retinal_illuminance", - "XYZ_to_LMS_ATD95", - "opponent_colour_dimensions", - "final_response", ] @@ -123,7 +122,12 @@ class CAM_Specification_ATD95(MixinDataclassArithmetic): Parameters ---------- h - *Hue* angle :math:`H` in degrees. + *Hue* :math:`H = T_2 / D_2` per *Guth (1995)* Equation 14.24; + the raw opponent-channel ratio, not a hue angle in degrees. A + proper hue angle can be obtained from + :func:`numpy.arctan2`\\ ``(T_2, D_2)`` per *Fairchild (2013)* + p.243, which notes the raw ratio is equivocal (equal for + complementary hues, infinite or undefined in some cases). C Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses the terms saturation and chroma interchangeably. However, :math:`C` @@ -225,6 +229,7 @@ def XYZ_to_ATD95( Examples -------- + >>> import numpy as np >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_0 = np.array([95.05, 100.00, 108.88]) >>> Y_0 = 318.31 @@ -240,180 +245,67 @@ def XYZ_to_ATD95( XYZ = to_domain_100(XYZ) XYZ_0 = to_domain_100(XYZ_0) - Y_0 = as_float_array(Y_0) - k_1 = as_float_array(k_1) - k_2 = as_float_array(k_2) - sigma = as_float_array(sigma) - - XYZ = luminance_to_retinal_illuminance(XYZ, Y_0) - XYZ_0 = luminance_to_retinal_illuminance(XYZ_0, Y_0) - - # Computing adaptation model. - LMS = XYZ_to_LMS_ATD95(XYZ) - XYZ_a = k_1[..., None] * XYZ + k_2[..., None] * XYZ_0 - LMS_a = XYZ_to_LMS_ATD95(XYZ_a) - - LMS_g = LMS * (sigma[..., None] / (sigma[..., None] + LMS_a)) - - # Computing opponent colour dimensions. - A_1, T_1, D_1, A_2, T_2, D_2 = tsplit(opponent_colour_dimensions(LMS_g)) - - # Computing the correlate of *brightness* :math:`Br`. - Br = spow(A_1**2 + T_1**2 + D_1**2, 0.5) - - # Computing the correlate of *saturation* :math:`C`. - C = spow(T_2**2 + D_2**2, 0.5) / A_2 - - # Computing the *hue* :math:`H`. Note that the reference does not take the - # modulus of the :math:`H`, thus :math:`H` can exceed 360 degrees. - H = T_2 / D_2 - - return CAM_Specification_ATD95( - h=as_float(from_range_degrees(H)), - C=C, - Q=Br, - A_1=A_1, - T_1=T_1, - D_1=D_1, - A_2=A_2, - T_2=T_2, - D_2=D_2, - ) - - -def luminance_to_retinal_illuminance(XYZ: ArrayLike, Y_c: ArrayLike) -> NDArrayFloat: - """ - Convert luminance in :math:`cd/m^2` to retinal illuminance in trolands. - - This function converts photometric luminance values to retinal illuminance - by applying a power transformation that accounts for pupil area effects - under the specified adapting field luminance conditions. - - Parameters - ---------- - XYZ - *CIE XYZ* tristimulus values in photometric units. - Y_c - Absolute adapting field luminance in :math:`cd/m^2`. - - Returns - ------- - :class:`numpy.ndarray` - Retinal illuminance values in trolands corresponding to the - tristimulus values. - - Examples - -------- - >>> XYZ = np.array([19.01, 20.00, 21.78]) - >>> Y_0 = 318.31 - >>> luminance_to_retinal_illuminance(XYZ, Y_0) # doctest: +ELLIPSIS - array([479.4445924..., 499.3174313..., 534.5631673...]) - """ - - XYZ = as_float_array(XYZ) - Y_c = as_float_array(Y_c) - - return 18 * spow(Y_c[..., None] * XYZ / 100, 0.8) - - -def XYZ_to_LMS_ATD95(XYZ: ArrayLike) -> NDArrayFloat: - """ - Convert *CIE XYZ* tristimulus values to *LMS* cone responses using the - *ATD95* colour appearance model. - - Parameters - ---------- - XYZ - *CIE XYZ* tristimulus values. - - Returns - ------- - :class:`numpy.ndarray` - *LMS* cone responses. - - Examples - -------- - >>> XYZ = np.array([19.01, 20.00, 21.78]) - >>> XYZ_to_LMS_ATD95(XYZ) # doctest: +ELLIPSIS - array([6.2283272..., 7.4780666..., 3.8859772...]) - """ - - LMS = vecmul( - [ - [0.2435, 0.8524, -0.0516], - [-0.3954, 1.1642, 0.0837], - [0.0000, 0.0400, 0.6225], - ], - XYZ, - ) - LMS = LMS * np.array([0.66, 1.0, 0.43]) - LMS_p = spow(LMS, 0.7) + xp = array_namespace(XYZ, XYZ_0, Y_0, k_1, k_2, sigma) - return LMS_p + np.array([0.024, 0.036, 0.31]) + Y_0 = xp_as_float_array(Y_0, xp=xp, like=XYZ) + k_1 = xp_as_float_array(k_1, xp=xp, like=XYZ) + k_2 = xp_as_float_array(k_2, xp=xp, like=XYZ) + sigma = xp_as_float_array(sigma, xp=xp, like=XYZ) + # Converting luminance in :math:`cd/m^2` to retinal illuminance in trolands + # for the stimulus and the reference white. + XYZ = 18 * spow(Y_0[..., None] * XYZ / 100, 0.8) + XYZ_0 = 18 * spow(Y_0[..., None] * XYZ_0 / 100, 0.8) -def opponent_colour_dimensions(LMS_g: ArrayLike) -> NDArrayFloat: - """ - Compute opponent colour dimensions from the specified post-adaptation cone - signals. - - Parameters - ---------- - LMS_g - Post-adaptation cone signals. - - Returns - ------- - :class:`numpy.ndarray` - Opponent colour dimensions. + # Computing the adaptation stimulus :math:`XYZ_a` then deriving the + # post-adaptation cone signals via the *ATD95* :math:`XYZ \\rightarrow LMS` + # transform applied to both the stimulus and the adaptation stimulus. + XYZ_a = k_1[..., None] * XYZ + k_2[..., None] * XYZ_0 + LMS_scales = xp_as_float_array([0.66, 1.0, 0.43], xp=xp, like=XYZ) + LMS_offsets = xp_as_float_array([0.024, 0.036, 0.31], xp=xp, like=XYZ) + LMS_matrix = [ + [0.2435, 0.8524, -0.0516], + [-0.3954, 1.1642, 0.0837], + [0.0000, 0.0400, 0.6225], + ] + LMS = spow(vecmul(LMS_matrix, XYZ) * LMS_scales, 0.7) + LMS_offsets + LMS_a = spow(vecmul(LMS_matrix, XYZ_a) * LMS_scales, 0.7) + LMS_offsets - Examples - -------- - >>> LMS_g = np.array([6.95457922, 7.08945043, 6.44069316]) - >>> opponent_colour_dimensions(LMS_g) # doctest: +ELLIPSIS - array([0.1787931..., 0.0286942..., 0.0107584..., 0.0192182..., ...]) - """ + LMS_g = LMS * (sigma[..., None] / (sigma[..., None] + LMS_a)) + # Computing opponent colour dimensions: 6 linear combinations of the + # post-adaptation cone signals, each passed through the saturating final + # response :math:`v / (200 + |v|)`. L_g, M_g, S_g = tsplit(LMS_g) - A_1i = 3.57 * L_g + 2.64 * M_g T_1i = 7.18 * L_g - 6.21 * M_g D_1i = -0.7 * L_g + 0.085 * M_g + S_g A_2i = 0.09 * A_1i T_2i = 0.43 * T_1i + 0.76 * D_1i D_2i = D_1i + stage = tstack([A_1i, T_1i, D_1i, A_2i, T_2i, D_2i]) + stage_final = stage / (200 + xp.abs(stage)) + A_1, T_1, D_1, A_2, T_2, D_2 = tsplit(stage_final) - A_1 = final_response(A_1i) - T_1 = final_response(T_1i) - D_1 = final_response(D_1i) - A_2 = final_response(A_2i) - T_2 = final_response(T_2i) - D_2 = final_response(D_2i) - - return tstack([A_1, T_1, D_1, A_2, T_2, D_2]) - - -def final_response(value: ArrayLike) -> NDArrayFloat: - """ - Compute the final response of the specified opponent colour dimension. - - Parameters - ---------- - value - Opponent colour dimension. - - Returns - ------- - :class:`numpy.ndarray` - Final response of the opponent colour dimension. - - Examples - -------- - >>> final_response(43.54399695501678) # doctest: +ELLIPSIS - np.float64(0.1787931...) - """ + # Computing the correlate of *brightness* :math:`Br`. + Br = spow(A_1**2 + T_1**2 + D_1**2, 0.5) - value = as_float_array(value) + # Computing the correlate of *saturation* :math:`C` and the *hue* + # :math:`H`. Note that the reference does not take the modulus of the + # :math:`H`, thus :math:`H` can exceed 360 degrees. + with sdiv_mode(): + C = sdiv(spow(T_2**2 + D_2**2, 0.5), A_2) + H = sdiv(T_2, D_2) - return as_float(value / (200 + np.abs(value))) + return CAM_Specification_ATD95( + h=as_float(from_range_degrees(H)), + C=as_float(C), + Q=as_float(Br), + A_1=as_float(A_1), + T_1=as_float(T_1), + D_1=as_float(D_1), + A_2=as_float(A_2), + T_2=as_float(T_2), + D_2=as_float(D_2), + ) diff --git a/colour/appearance/cam16.py b/colour/appearance/cam16.py index 65893194b4..d5960e19df 100644 --- a/colour/appearance/cam16.py +++ b/colour/appearance/cam16.py @@ -26,30 +26,13 @@ import numpy as np from colour.adaptation import CAT_CAT16 -from colour.algebra import spow, vecmul +from colour.algebra import sdiv, sdiv_mode, spow, vecmul from colour.appearance.ciecam02 import ( VIEWING_CONDITIONS_CIECAM02, InductionFactors_CIECAM02, - P, - achromatic_response_forward, - achromatic_response_inverse, - brightness_correlate, - chroma_correlate, - colourfulness_correlate, - degree_of_adaptation, - eccentricity_factor, - hue_angle, hue_quadrature, - lightness_correlate, - matrix_post_adaptation_non_linear_response_compression, - opponent_colour_dimensions_forward, - opponent_colour_dimensions_inverse, - post_adaptation_non_linear_response_compression_forward, - post_adaptation_non_linear_response_compression_inverse, - saturation_correlate, - temporary_magnitude_quantity_inverse, - viewing_conditions_dependent_parameters, ) +from colour.constants import EPSILON from colour.hints import ( # noqa: TC001 Annotated, ArrayLike, @@ -61,8 +44,8 @@ CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, - as_float_array, from_range_100, from_range_degrees, has_only_nan, @@ -70,6 +53,10 @@ to_domain_100, to_domain_degrees, tsplit, + tstack, + xp_as_float_array, + xp_degrees, + xp_radians, ) __author__ = "Colour Developers" @@ -186,7 +173,7 @@ def XYZ_to_CAM16( InductionFactors_CIECAM02 | InductionFactors_CAM16 ) = VIEWING_CONDITIONS_CAM16["Average"], discount_illuminant: bool = False, - compute_H: bool = True, + compute_H: bool = False, ) -> Annotated[CAM_Specification_CAM16, (100, 100, 360, 100, 100, 100, 400)]: """ Compute the *CAM16* colour appearance model correlates from the specified @@ -214,8 +201,10 @@ def XYZ_to_CAM16( discount_illuminant Truth value indicating if the illuminant should be discounted. compute_H - Whether to compute *Hue* :math:`h` quadrature :math:`H`. - :math:`H` is rarely used, and expensive to compute. + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -261,7 +250,10 @@ def XYZ_to_CAM16( >>> L_A = 318.31 >>> Y_b = 20.0 >>> surround = VIEWING_CONDITIONS_CAM16["Average"] - >>> XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS + >>> XYZ_to_CAM16( + ... XYZ, XYZ_w, L_A, Y_b, surround, + ... compute_H=True, + ... ) # doctest: +ELLIPSIS CAM_Specification_CAM16(J=np.float64(41.7312079...), \ C=np.float64(0.1033557...), h=np.float64(217.0679597...), \ s=np.float64(2.3450150...), Q=np.float64(195.3717089...), \ @@ -270,79 +262,111 @@ def XYZ_to_CAM16( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) - _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) - - # Step 0 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. - RGB_w = vecmul(MATRIX_16, XYZ_w) - - # Computing degree of adaptation :math:`D`. - D = ( - np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) - if not discount_illuminant - else ones(L_A.shape) - ) - n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + xp = array_namespace(XYZ, XYZ_w, L_A, Y_b) - D_RGB = D[..., None] * Y_w[..., None] / RGB_w + 1 - D[..., None] - RGB_wc = D_RGB * RGB_w - - # Applying forward post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) + XYZ = xp_as_float_array(XYZ, xp=xp) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=XYZ) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) - # Computing achromatic responses for the whitepoint. - A_w = achromatic_response_forward(RGB_aw, N_bb) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - # Step 1 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L`, + # chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`, + # base exponential non-linearity :math:`z`. Same formulation as + # in *CIECAM02*. + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + with sdiv_mode(): + N_bb = 0.725 * spow(sdiv(1, n), 0.2) + N_cb = N_bb + z = 1.48 + xp.sqrt(n) + + # Converting *CIE XYZ* tristimulus values to *CAT16* sharpened *RGB* + # values for the stimulus and reference white. *CAM16* uses the + # *CAT16* matrix directly rather than *CIECAM02*'s *CAT02* + # sharpening followed by *Hunt-Pointer-Estevez* transform. RGB = vecmul(MATRIX_16, XYZ) + RGB_w = vecmul(MATRIX_16, XYZ_w) - # Step 2 + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*, clipped to :math:`[0, 1]` and bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ) + else: + F = xp_as_float_array(surround.F, xp=xp, like=XYZ) + D = xp.clip(F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)), 0, 1) + + # Computing full chromatic adaptation, applied to the stimulus and + # the reference white via a shared adaptation factor. + D_RGB = D[..., None] * Y_w[..., None] / RGB_w + 1 - D[..., None] RGB_c = D_RGB * RGB + RGB_wc = D_RGB * RGB_w - # Step 3 - # Applying forward post-adaptation non-linear response compression. - RGB_a = post_adaptation_non_linear_response_compression_forward(RGB_c, F_L) - - # Step 4 - # Converting to preliminary cartesian coordinates. - a, b = tsplit(opponent_colour_dimensions_forward(RGB_a)) - - # Computing the *hue* angle :math:`h`. - h = hue_angle(a, b) + # Applying forward post-adaptation non-linear response compression, + # same sign-preserving form as in *CIECAM02* per *Luo (2013)*. In + # *CAM16* the compression is applied directly to the chromatically + # adapted *RGB* (no intermediate *HPE* transform). + F_L_RGB_c = spow(F_L[..., None] * xp.abs(RGB_c) / 100, 0.42) + RGB_a = (400 * xp.sign(RGB_c) * F_L_RGB_c) / (27.13 + F_L_RGB_c) + 0.1 + F_L_RGB_wc = spow(F_L[..., None] * xp.abs(RGB_wc) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_wc) * F_L_RGB_wc) / (27.13 + F_L_RGB_wc) + 0.1 + + # Computing the opponent colour dimensions :math:`a` and :math:`b`, + # same formulation as in *CIECAM02*. + Ra, Ga, Ba = tsplit(RGB_a) + a = Ra - 12 * Ga / 11 + Ba / 11 + b = (Ra + Ga - 2 * Ba) / 9 + + # Computing the *hue* angle :math:`h` in degrees in + # :math:`[0, 360)`, same as in *CIECAM02*. + h = xp_degrees(xp.atan2(b, a)) % 360 + + # Computing eccentricity factor :math:`e_t`, same as in *CIECAM02*. + e_t = 1 / 4 * (xp.cos(2 + xp_radians(h)) + 3.8) + + # Computing achromatic responses :math:`A` for the stimulus and + # :math:`A_w` for the whitepoint, same as in *CIECAM02*. + A = (2 * Ra + Ga + (1 / 20) * Ba - 0.305) * N_bb + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = (2 * Raw + Gaw + (1 / 20) * Baw - 0.305) * N_bb + + # Computing the correlate of *Lightness* :math:`J`, same form as + # in *CIECAM02*. + c = surround.c + with sdiv_mode(): + J = 100 * spow(sdiv(A, A_w), c * z) + + # Computing the correlate of *brightness* :math:`Q`, same form as + # in *CIECAM02*. + Q = (4 / c) * xp.sqrt(J / 100) * (A_w + 4) * spow(F_L, 0.25) + + # Computing the temporary magnitude quantity :math:`t` and the + # correlate of *chroma* :math:`C`, same forms as in *CIECAM02*. + N_c = surround.N_c + with sdiv_mode(): + t = ((50000 / 13) * N_c * N_cb) * sdiv( + e_t * spow(a**2 + b**2, 0.5), Ra + Ga + 21 * Ba / 20 + ) + C = spow(t, 0.9) * spow(J / 100, 0.5) * spow(1.64 - 0.29**n, 0.73) - # Step 5 - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) + # Computing the correlate of *colourfulness* :math:`M` and the + # correlate of *saturation* :math:`s`, same forms as in *CIECAM02*. + M = C * spow(F_L, 0.25) + with sdiv_mode(): + s = 100 * spow(sdiv(M, Q), 0.5) - # Computing hue :math:`h` quadrature :math:`H`. - H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan) + # Computing hue :math:`h` quadrature :math:`H` only when requested + # via ``compute_H``; the bin search is shared with *CIECAM02* and + # delegates to :func:`hue_quadrature`. # TODO: Compute hue composition. - - # Step 6 - # Computing achromatic responses for the stimulus. - A = achromatic_response_forward(RGB_a, N_bb) - - # Step 7 - # Computing the correlate of *Lightness* :math:`J`. - J = lightness_correlate(A, A_w, surround.c, z) - - # Step 8 - # Computing the correlate of *brightness* :math:`Q`. - Q = brightness_correlate(surround.c, J, A_w, F_L) - - # Step 9 - # Computing the correlate of *chroma* :math:`C`. - C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a) - - # Computing the correlate of *colourfulness* :math:`M`. - M = colourfulness_correlate(C, F_L) - - # Computing the correlate of *saturation* :math:`s`. - s = saturation_correlate(M, Q) + H = hue_quadrature(h) if compute_H else xp.full_like(h, float("nan")) return CAM_Specification_CAM16( J=as_float(from_range_100(J)), @@ -458,33 +482,64 @@ def CAM16_to_XYZ( C = to_domain_100(C) h = to_domain_degrees(h) M = to_domain_100(M) - L_A = as_float_array(L_A) XYZ_w = to_domain_100(XYZ_w) + + xp = array_namespace(J, C, h, M, XYZ_w, L_A) + + J = xp_as_float_array(J, xp=xp) + C = xp_as_float_array(C, xp=xp, like=J) + h = xp_as_float_array(h, xp=xp, like=J) + M = xp_as_float_array(M, xp=xp, like=J) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=J) + L_A = xp_as_float_array(L_A, xp=xp, like=J) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - # Step 0 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L`, + # chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`, + # base exponential non-linearity :math:`z`. Same formulation as + # in *CIECAM02*. + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + with sdiv_mode(): + N_bb = 0.725 * spow(sdiv(1, n), 0.2) + N_cb = N_bb + z = 1.48 + xp.sqrt(n) + + # Converting *CIE XYZ* tristimulus values to *CAT16* sharpened *RGB* + # values for the reference white. RGB_w = vecmul(MATRIX_16, XYZ_w) - # Computing degree of adaptation :math:`D`. - D = ( - np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) - if not discount_illuminant - else ones(L_A.shape) - ) - - n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*, clipped to :math:`[0, 1]` and bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=J) + else: + F = xp_as_float_array(surround.F, xp=xp, like=J) + D = xp.clip(F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)), 0, 1) + # Computing full chromatic adaptation for the reference white. D_RGB = D[..., None] * Y_w[..., None] / RGB_w + 1 - D[..., None] RGB_wc = D_RGB * RGB_w - # Applying forward post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) + # Applying forward post-adaptation non-linear response compression + # to the whitepoint, same form as in *CIECAM02*. + F_L_RGB_wc = spow(F_L[..., None] * xp.abs(RGB_wc) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_wc) * F_L_RGB_wc) / (27.13 + F_L_RGB_wc) + 0.1 - # Computing achromatic responses for the whitepoint. - A_w = achromatic_response_forward(RGB_aw, N_bb) + # Computing achromatic response :math:`A_w` for the whitepoint, + # same as in *CIECAM02*. + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = (2 * Raw + Gaw + (1 / 20) * Baw - 0.305) * N_bb - # Step 1 + # Recovering the correlate of *chroma* :math:`C` from the correlate + # of *colourfulness* :math:`M` when only :math:`M` has been + # provided. if has_only_nan(C) and not has_only_nan(M): C = M / spow(F_L, 0.25) elif has_only_nan(C): @@ -495,37 +550,104 @@ def CAM16_to_XYZ( raise ValueError(error) - # Step 2 - # Computing temporary magnitude quantity :math:`t`. - t = temporary_magnitude_quantity_inverse(C, J, n) - - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) - - # Computing achromatic response :math:`A` for the stimulus. - A = achromatic_response_inverse(A_w, J, surround.c, z) - - # Computing *P_1* to *P_3*. - P_n = P(surround.N_c, N_cb, e_t, t, A, N_bb) - _P_1, P_2, _P_3 = tsplit(P_n) - - # Step 3 - # Computing opponent colour dimensions :math:`a` and :math:`b`. - ab = opponent_colour_dimensions_inverse(P_n, h) - a, b = tsplit(ab) * np.where(t == 0, 0, 1) - - # Step 4 - # Applying post-adaptation non-linear response compression matrix. - RGB_a = matrix_post_adaptation_non_linear_response_compression(P_2, a, b) + # Computing temporary magnitude quantity :math:`t`, same form as + # in *CIECAM02*. + J_prime = xp.clip(J, min=EPSILON) + t = spow(C / (xp.sqrt(J_prime / 100) * spow(1.64 - 0.29**n, 0.73)), 1 / 0.9) + + # Computing eccentricity factor :math:`e_t`, same as in *CIECAM02*. + e_t = 1 / 4 * (xp.cos(2 + xp_radians(h)) + 3.8) + + # Computing achromatic response :math:`A` for the stimulus, same + # inverse form as in *CIECAM02*. + c = surround.c + A = A_w * spow(J / 100, 1 / (c * z)) + + # Computing points :math:`P_1`, :math:`P_2`, :math:`P_3`, same + # form as in *CIECAM02*. + N_c = surround.N_c + with sdiv_mode(): + P_1 = sdiv((50000 / 13) * N_c * N_cb * e_t, t) + P_2 = A / N_bb + 0.305 + P_3 = xp.full_like(P_1, 21 / 20) + + # Computing opponent colour dimensions :math:`a` and :math:`b` + # via the sin / cos branching protecting against the numerical + # singularity near the hue axis. Same as in *CIECAM02*. + hr = xp_radians(h) + sin_hr = xp.sin(hr) + cos_hr = xp.cos(hr) + with sdiv_mode(): + cos_hr_sin_hr = sdiv(cos_hr, sin_hr) + sin_hr_cos_hr = sdiv(sin_hr, cos_hr) + P_4 = sdiv(P_1, sin_hr) + P_5 = sdiv(P_1, cos_hr) + n_ab = P_2 * (2 + P_3) * (460 / 1403) + + abs_sin_ge_cos = xp.abs(sin_hr) >= xp.abs(cos_hr) + abs_sin_lt_cos = xp.abs(sin_hr) < xp.abs(cos_hr) + + a = xp.zeros_like(hr) + b = xp.zeros_like(hr) + b = xp.where( + abs_sin_ge_cos, + n_ab + / ( + P_4 + + (2 + P_3) * (220 / 1403) * cos_hr_sin_hr + - (27 / 1403) + + P_3 * (6300 / 1403) + ), + b, + ) + a = xp.where(abs_sin_ge_cos, b * cos_hr_sin_hr, a) + a = xp.where( + abs_sin_lt_cos, + n_ab + / ( + P_5 + + (2 + P_3) * (220 / 1403) + - ((27 / 1403) - P_3 * (6300 / 1403)) * sin_hr_cos_hr + ), + a, + ) + b = xp.where(abs_sin_lt_cos, a * sin_hr_cos_hr, b) + t_mask = xp.where(t == 0, 0, 1) + a = a * t_mask + b = b * t_mask + + # Applying post-adaptation non-linear response compression matrix + # to recover the compressed *RGB* array. Same as in *CIECAM02*. + RGB_a = ( + vecmul( + [ + [460, 451, 288], + [460, -891, -261], + [460, -220, -6300], + ], + tstack([P_2, a, b]), + ) + / 1403 + ) - # Step 5 - # Applying inverse post-adaptation non-linear response compression. - RGB_c = post_adaptation_non_linear_response_compression_inverse(RGB_a, F_L) + # Applying inverse post-adaptation non-linear response compression, + # same form as in *CIECAM02*. + RGB_c = ( + xp.sign(RGB_a - 0.1) + * 100 + / F_L[..., None] + * spow( + (27.13 * xp.abs(RGB_a - 0.1)) / (400 - xp.abs(RGB_a - 0.1)), + 1 / 0.42, + ) + ) - # Step 6 + # Applying inverse full chromatic adaptation, using the precomputed + # ``D_RGB`` adaptation factor. RGB = RGB_c / D_RGB - # Step 7 + # Converting *CAT16* sharpened *RGB* values back to *CIE XYZ* + # tristimulus values. XYZ = vecmul(MATRIX_INVERSE_16, RGB) return from_range_100(XYZ) diff --git a/colour/appearance/ciecam02.py b/colour/appearance/ciecam02.py index b1195c6c7b..343e76b475 100644 --- a/colour/appearance/ciecam02.py +++ b/colour/appearance/ciecam02.py @@ -43,23 +43,27 @@ from colour.appearance.hunt import ( MATRIX_HPE_TO_XYZ, MATRIX_XYZ_TO_HPE, - luminance_level_adaptation_factor, ) from colour.colorimetry import CCS_ILLUMINANTS from colour.constants import EPSILON if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, Domain100, Range100, Tuple + from colour.hints import ( + Annotated, + ArrayLike, + Domain100, + NDArrayFloat, + Range100, + ) -from colour.hints import Annotated, NDArrayFloat, cast from colour.models import xy_to_XYZ from colour.utilities import ( CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, - as_int_array, from_range_100, from_range_degrees, has_only_nan, @@ -68,7 +72,10 @@ to_domain_degrees, tsplit, tstack, - zeros, + xp_as_float_array, + xp_degrees, + xp_radians, + xp_select, ) from colour.utilities.documentation import DocstringDict, is_documentation_building @@ -88,32 +95,8 @@ "CAM_Specification_CIECAM02", "XYZ_to_CIECAM02", "CIECAM02_to_XYZ", - "chromatic_induction_factors", "base_exponential_non_linearity", - "viewing_conditions_dependent_parameters", - "degree_of_adaptation", - "full_chromatic_adaptation_forward", - "full_chromatic_adaptation_inverse", - "RGB_to_rgb", - "rgb_to_RGB", - "post_adaptation_non_linear_response_compression_forward", - "post_adaptation_non_linear_response_compression_inverse", - "opponent_colour_dimensions_forward", - "opponent_colour_dimensions_inverse", - "hue_angle", "hue_quadrature", - "eccentricity_factor", - "achromatic_response_forward", - "achromatic_response_inverse", - "lightness_correlate", - "brightness_correlate", - "temporary_magnitude_quantity_forward", - "temporary_magnitude_quantity_inverse", - "chroma_correlate", - "colourfulness_correlate", - "saturation_correlate", - "P", - "matrix_post_adaptation_non_linear_response_compression", ] CAT_INVERSE_CAT02: NDArrayFloat = np.linalg.inv(CAT_CAT02) @@ -234,7 +217,7 @@ def XYZ_to_CIECAM02( Y_b: ArrayLike, surround: InductionFactors_CIECAM02 = VIEWING_CONDITIONS_CIECAM02["Average"], discount_illuminant: bool = False, - compute_H: bool = True, + compute_H: bool = False, ) -> Annotated[CAM_Specification_CIECAM02, (100, 100, 360, 100, 100, 100, 400)]: """ Compute the *CIECAM02* colour appearance model correlates from the @@ -261,8 +244,10 @@ def XYZ_to_CIECAM02( discount_illuminant Truth value indicating if the illuminant should be discounted. compute_H - Whether to compute *Hue* :math:`h` quadrature :math:`H`. :math:`H` - is rarely used, and expensive to compute. + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -309,7 +294,10 @@ def XYZ_to_CIECAM02( >>> L_A = 318.31 >>> Y_b = 20.0 >>> surround = VIEWING_CONDITIONS_CIECAM02["Average"] - >>> XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS + >>> XYZ_to_CIECAM02( + ... XYZ, XYZ_w, L_A, Y_b, surround, + ... compute_H=True, + ... ) # doctest: +ELLIPSIS CAM_Specification_CIECAM02(J=np.float64(41.7310911...), \ C=np.float64(0.1047077...), h=np.float64(219.0484326...), \ s=np.float64(2.3603053...), Q=np.float64(195.3713259...), \ @@ -318,67 +306,119 @@ def XYZ_to_CIECAM02( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) + + xp = array_namespace(XYZ, XYZ_w, L_A, Y_b) + + XYZ = xp_as_float_array(XYZ, xp=xp) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=XYZ) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) - n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L`, + # chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`, + # base exponential non-linearity :math:`z`. + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + with sdiv_mode(): + N_bb = 0.725 * spow(sdiv(1, n), 0.2) + N_cb = N_bb + z = 1.48 + xp.sqrt(n) # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform # sharpened *RGB* values. RGB = vecmul(CAT_CAT02, XYZ) RGB_w = vecmul(CAT_CAT02, XYZ_w) - # Computing degree of adaptation :math:`D`. - D = ( - degree_of_adaptation(surround.F, L_A) - if not discount_illuminant - else ones(L_A.shape) - ) - - # Computing full chromatic adaptation. - RGB_c = full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D) - RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D) - - # Converting to *Hunt-Pointer-Estevez* colourspace. - RGB_p = RGB_to_rgb(RGB_c) - RGB_pw = RGB_to_rgb(RGB_wc) - - # Applying forward post-adaptation non-linear response compression. - RGB_a = post_adaptation_non_linear_response_compression_forward(RGB_p, F_L) - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_pw, F_L) - - # Converting to preliminary cartesian coordinates. - a, b = tsplit(opponent_colour_dimensions_forward(RGB_a)) + # Computing degree of adaptation :math:`D`, bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ) + else: + F = xp_as_float_array(surround.F, xp=xp, like=XYZ) + D = F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) + + # Computing full chromatic adaptation, applied to the stimulus and + # to the reference white, following *CIE (2004)* Equations 16.4a-16.6a + # (the technical-report variant retaining the :math:`Y_W` factor). + # *Fairchild (2013)* p.269 recommends the simpler Equations 16.4-16.6 + # without :math:`Y_W`; the two are equivalent when :math:`Y_W = 100` and + # the project default scaling normalises to that. + with sdiv_mode(): + RGB_c = (Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None]) * RGB + RGB_wc = (Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None]) * RGB_w - # Computing the *hue* angle :math:`h`. - h = hue_angle(a, b) + # Converting to *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` + # colourspace, applied to both stimulus and white. + MATRIX_XYZ_HPE_x_CAT_INVERSE = xp.matmul( + xp_as_float_array(MATRIX_XYZ_TO_HPE, xp=xp, like=XYZ), + xp_as_float_array(CAT_INVERSE_CAT02, xp=xp, like=XYZ), + ) + RGB_p = vecmul(MATRIX_XYZ_HPE_x_CAT_INVERSE, RGB_c) + RGB_pw = vecmul(MATRIX_XYZ_HPE_x_CAT_INVERSE, RGB_wc) + + # Applying forward post-adaptation non-linear response compression, + # sign-preserving for negative values per *Luo (2013)*. + F_L_RGB_p = spow(F_L[..., None] * xp.abs(RGB_p) / 100, 0.42) + RGB_a = (400 * xp.sign(RGB_p) * F_L_RGB_p) / (27.13 + F_L_RGB_p) + 0.1 + F_L_RGB_pw = spow(F_L[..., None] * xp.abs(RGB_pw) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_pw) * F_L_RGB_pw) / (27.13 + F_L_RGB_pw) + 0.1 + + # Converting to preliminary cartesian coordinates :math:`a`, + # :math:`b`. + Ra, Ga, Ba = tsplit(RGB_a) + a = Ra - 12 * Ga / 11 + Ba / 11 + b = (Ra + Ga - 2 * Ba) / 9 - # Computing hue :math:`h` quadrature :math:`H`. - H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan) - # TODO: Compute hue composition. + # Computing the *hue* angle :math:`h` in degrees in + # :math:`[0, 360)`. + h = xp_degrees(xp.atan2(b, a)) % 360 - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) + # Computing eccentricity factor :math:`e_t`. + e_t = 1 / 4 * (xp.cos(2 + xp_radians(h)) + 3.8) - # Computing achromatic responses for the stimulus and the whitepoint. - A = achromatic_response_forward(RGB_a, N_bb) - A_w = achromatic_response_forward(RGB_aw, N_bb) + # Computing achromatic responses :math:`A` for the stimulus and + # :math:`A_w` for the whitepoint. + A = (2 * Ra + Ga + (1 / 20) * Ba - 0.305) * N_bb + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = (2 * Raw + Gaw + (1 / 20) * Baw - 0.305) * N_bb # Computing the correlate of *Lightness* :math:`J`. - J = lightness_correlate(A, A_w, surround.c, z) + c = surround.c + with sdiv_mode(): + J = 100 * spow(sdiv(A, A_w), c * z) # Computing the correlate of *brightness* :math:`Q`. - Q = brightness_correlate(surround.c, J, A_w, F_L) + Q = (4 / c) * xp.sqrt(J / 100) * (A_w + 4) * spow(F_L, 0.25) - # Computing the correlate of *chroma* :math:`C`. - C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a) + # Computing the temporary magnitude quantity :math:`t` and the + # correlate of *chroma* :math:`C`. + N_c = surround.N_c + with sdiv_mode(): + t = ((50000 / 13) * N_c * N_cb) * sdiv( + e_t * spow(a**2 + b**2, 0.5), Ra + Ga + 21 * Ba / 20 + ) + C = spow(t, 0.9) * spow(J / 100, 0.5) * spow(1.64 - 0.29**n, 0.73) # Computing the correlate of *colourfulness* :math:`M`. - M = colourfulness_correlate(C, F_L) + M = C * spow(F_L, 0.25) # Computing the correlate of *saturation* :math:`s`. - s = saturation_correlate(M, Q) + with sdiv_mode(): + s = 100 * spow(sdiv(M, Q), 0.5) + + # Computing hue :math:`h` quadrature :math:`H` only when requested + # via ``compute_H``; the :math:`H` quadrature is rarely consumed + # and the bin-search delegates to :func:`hue_quadrature` which is + # kept as a public reference shared with the *ZCAM* and *sCAM* + # paths. + # TODO: Compute hue composition. + H = hue_quadrature(h) if compute_H else xp.full_like(h, float("nan")) return CAM_Specification_CIECAM02( J=as_float(from_range_100(J)), @@ -492,11 +532,32 @@ def CIECAM02_to_XYZ( C = to_domain_100(C) h = to_domain_degrees(h) M = to_domain_100(M) - L_A = as_float_array(L_A) XYZ_w = to_domain_100(XYZ_w) + + xp = array_namespace(J, C, h, M, XYZ_w, L_A) + + J = xp_as_float_array(J, xp=xp) + C = xp_as_float_array(C, xp=xp, like=J) + h = xp_as_float_array(h, xp=xp, like=J) + M = xp_as_float_array(M, xp=xp, like=J) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=J) + L_A = xp_as_float_array(L_A, xp=xp, like=J) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L`, + # chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`, + # base exponential non-linearity :math:`z`. + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + with sdiv_mode(): + N_bb = 0.725 * spow(sdiv(1, n), 0.2) + N_cb = N_bb + z = 1.48 + xp.sqrt(n) if has_only_nan(C) and not has_only_nan(M): C = M / spow(F_L, 0.25) @@ -509,93 +570,146 @@ def CIECAM02_to_XYZ( raise ValueError(error) # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform - # sharpened *RGB* values. + # sharpened *RGB* values for the reference white. RGB_w = vecmul(CAT_CAT02, XYZ_w) - # Computing degree of adaptation :math:`D`. - D = ( - degree_of_adaptation(surround.F, L_A) - if not discount_illuminant - else ones(L_A.shape) - ) + # Computing degree of adaptation :math:`D`, bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=J) + else: + F = xp_as_float_array(surround.F, xp=xp, like=J) + D = F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) - # Computing full chromatic adaptation. - RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D) + # Computing full chromatic adaptation for the reference white. + with sdiv_mode(): + RGB_wc = (Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None]) * RGB_w - # Converting to *Hunt-Pointer-Estevez* colourspace. - RGB_pw = RGB_to_rgb(RGB_wc) + # Converting to *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` + # colourspace. + MATRIX_XYZ_HPE_x_CAT_INVERSE = xp.matmul( + xp_as_float_array(MATRIX_XYZ_TO_HPE, xp=xp, like=J), + xp_as_float_array(CAT_INVERSE_CAT02, xp=xp, like=J), + ) + RGB_pw = vecmul(MATRIX_XYZ_HPE_x_CAT_INVERSE, RGB_wc) - # Applying post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_pw, F_L) + # Applying forward post-adaptation non-linear response compression + # to the whitepoint. + F_L_RGB_pw = spow(F_L[..., None] * xp.abs(RGB_pw) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_pw) * F_L_RGB_pw) / (27.13 + F_L_RGB_pw) + 0.1 - # Computing achromatic response for the whitepoint. - A_w = achromatic_response_forward(RGB_aw, N_bb) + # Computing achromatic response :math:`A_w` for the whitepoint. + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = (2 * Raw + Gaw + (1 / 20) * Baw - 0.305) * N_bb - # Computing temporary magnitude quantity :math:`t`. - t = temporary_magnitude_quantity_inverse(C, J, n) + # Computing the temporary magnitude quantity :math:`t`. + J_prime = xp.clip(J, min=EPSILON) + t = spow(C / (xp.sqrt(J_prime / 100) * spow(1.64 - 0.29**n, 0.73)), 1 / 0.9) - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) + # Computing eccentricity factor :math:`e_t`. + e_t = 1 / 4 * (xp.cos(2 + xp_radians(h)) + 3.8) # Computing achromatic response :math:`A` for the stimulus. - A = achromatic_response_inverse(A_w, J, surround.c, z) + c = surround.c + A = A_w * spow(J / 100, 1 / (c * z)) - # Computing *P_1* to *P_3*. - P_n = P(surround.N_c, N_cb, e_t, t, A, N_bb) - _P_1, P_2, _P_3 = tsplit(P_n) + # Computing points :math:`P_1`, :math:`P_2`, :math:`P_3`. + N_c = surround.N_c + with sdiv_mode(): + P_1 = sdiv((50000 / 13) * N_c * N_cb * e_t, t) + P_2 = A / N_bb + 0.305 + P_3 = xp.full_like(P_1, 21 / 20) + + # Computing opponent colour dimensions :math:`a` and :math:`b` + # from the points :math:`P_n` and hue :math:`h` via the sin / cos + # branching that protects against the numerical singularity near + # the hue axis. + hr = xp_radians(h) + sin_hr = xp.sin(hr) + cos_hr = xp.cos(hr) + with sdiv_mode(): + cos_hr_sin_hr = sdiv(cos_hr, sin_hr) + sin_hr_cos_hr = sdiv(sin_hr, cos_hr) + P_4 = sdiv(P_1, sin_hr) + P_5 = sdiv(P_1, cos_hr) + n_ab = P_2 * (2 + P_3) * (460 / 1403) - # Computing opponent colour dimensions :math:`a` and :math:`b`. - ab = opponent_colour_dimensions_inverse(P_n, h) - a, b = tsplit(ab) * np.where(t == 0, 0, 1) + abs_sin_ge_cos = xp.abs(sin_hr) >= xp.abs(cos_hr) + abs_sin_lt_cos = xp.abs(sin_hr) < xp.abs(cos_hr) - # Applying post-adaptation non-linear response compression matrix. - RGB_a = matrix_post_adaptation_non_linear_response_compression(P_2, a, b) + a = xp.zeros_like(hr) + b = xp.zeros_like(hr) + b = xp.where( + abs_sin_ge_cos, + n_ab + / ( + P_4 + + (2 + P_3) * (220 / 1403) * cos_hr_sin_hr + - (27 / 1403) + + P_3 * (6300 / 1403) + ), + b, + ) + a = xp.where(abs_sin_ge_cos, b * cos_hr_sin_hr, a) + a = xp.where( + abs_sin_lt_cos, + n_ab + / ( + P_5 + + (2 + P_3) * (220 / 1403) + - ((27 / 1403) - P_3 * (6300 / 1403)) * sin_hr_cos_hr + ), + a, + ) + b = xp.where(abs_sin_lt_cos, a * sin_hr_cos_hr, b) + t_mask = xp.where(t == 0, 0, 1) + a = a * t_mask + b = b * t_mask + + # Applying post-adaptation non-linear response compression matrix + # to recover the compressed *RGB* array. + RGB_a = ( + vecmul( + [ + [460, 451, 288], + [460, -891, -261], + [460, -220, -6300], + ], + tstack([P_2, a, b]), + ) + / 1403 + ) # Applying inverse post-adaptation non-linear response compression. - RGB_p = post_adaptation_non_linear_response_compression_inverse(RGB_a, F_L) + RGB_p = ( + xp.sign(RGB_a - 0.1) + * 100 + / F_L[..., None] + * spow( + (27.13 * xp.abs(RGB_a - 0.1)) / (400 - xp.abs(RGB_a - 0.1)), + 1 / 0.42, + ) + ) - # Converting to *Hunt-Pointer-Estevez* colourspace. - RGB_c = rgb_to_RGB(RGB_p) + # Converting from *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` + # colourspace back to adapted *RGB*. + CAT_x_MATRIX_HPE = xp.matmul( + xp_as_float_array(CAT_CAT02, xp=xp, like=J), + xp_as_float_array(MATRIX_HPE_TO_XYZ, xp=xp, like=J), + ) + RGB_c = vecmul(CAT_x_MATRIX_HPE, RGB_p) # Applying inverse full chromatic adaptation. - RGB = full_chromatic_adaptation_inverse(RGB_c, RGB_w, Y_w, D) + with sdiv_mode(): + RGB = RGB_c / (Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None]) - # Converting *CMCCAT2000* transform sharpened *RGB* values to *CIE XYZ* - # tristimulus values. + # Converting *CMCCAT2000* transform sharpened *RGB* values to + # *CIE XYZ* tristimulus values. XYZ = vecmul(CAT_INVERSE_CAT02, RGB) return from_range_100(XYZ) -def chromatic_induction_factors(n: ArrayLike) -> NDArrayFloat: - """ - Compute the chromatic induction factors :math:`N_{bb}` and - :math:`N_{cb}`. - - Parameters - ---------- - n - Function of the luminance factor of the background :math:`n`. - - Returns - ------- - :class:`numpy.ndarray` - Chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`. - - Examples - -------- - >>> chromatic_induction_factors(0.2) # doctest: +ELLIPSIS - array([1.000304, 1.000304]) - """ - - n = as_float_array(n) - - with sdiv_mode(): - N_bb = N_cb = 0.725 * spow(sdiv(1, n), 0.2) - - return tstack([N_bb, N_cb]) - - def base_exponential_non_linearity( n: ArrayLike, ) -> NDArrayFloat: @@ -620,1033 +734,73 @@ def base_exponential_non_linearity( n = as_float_array(n) - return 1.48 + np.sqrt(n) - - -def viewing_conditions_dependent_parameters( - Y_b: ArrayLike, - Y_w: ArrayLike, - L_A: ArrayLike, -) -> Tuple[ - NDArrayFloat, - NDArrayFloat, - NDArrayFloat, - NDArrayFloat, - NDArrayFloat, -]: - """ - Compute the viewing condition dependent parameters. - - Parameters - ---------- - Y_b - Adapting field *Y* tristimulus value :math:`Y_b`. - Y_w - Whitepoint *Y* tristimulus value :math:`Y_w`. - L_A - Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - - Returns - ------- - :class:`tuple` - Viewing condition dependent parameters :math:`(n, F_L, F_{Lb}, - F_{Lw}, z)` where :math:`n` is the background induction factor, - :math:`F_L` is the luminance adaptation factor, :math:`F_{Lb}` and - :math:`F_{Lw}` are the background and whitepoint luminance - adaptation factors respectively, and :math:`z` is the base linear - exponent for the nonlinear response compression. - - Examples - -------- - >>> viewing_conditions_dependent_parameters(20.0, 100.0, 318.31) - ... # doctest: +ELLIPSIS - (np.float64(0.2...), np.float64(1.1675444...), np.float64(1.0003040...), \ -np.float64(1.0003040...), np.float64(1.9272135...)) - """ - - Y_b = as_float_array(Y_b) - Y_w = as_float_array(Y_w) - - with sdiv_mode(): - n = sdiv(Y_b, Y_w) - - F_L = luminance_level_adaptation_factor(L_A) - N_bb, N_cb = tsplit(chromatic_induction_factors(n)) - z = base_exponential_non_linearity(n) - - return n, F_L, N_bb, N_cb, z - - -def degree_of_adaptation(F: ArrayLike, L_A: ArrayLike) -> NDArrayFloat: - """ - Compute the degree of adaptation :math:`D` from the specified surround - maximum degree of adaptation :math:`F` and adapting field *luminance* - :math:`L_A` in :math:`cd/m^2`. - - Parameters - ---------- - F - Surround maximum degree of adaptation :math:`F`. - L_A - Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - - Returns - ------- - :class:`numpy.ndarray` - Degree of adaptation :math:`D`. - - Examples - -------- - >>> degree_of_adaptation(1.0, 318.31) # doctest: +ELLIPSIS - np.float64(0.9944687...) - """ - - F = as_float_array(F) - L_A = as_float_array(L_A) - - return F * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92)) - - -def full_chromatic_adaptation_forward( - RGB: ArrayLike, - RGB_w: ArrayLike, - Y_w: ArrayLike, - D: ArrayLike, -) -> NDArrayFloat: - """ - Apply full chromatic adaptation to the specified *CMCCAT2000* transform - sharpened *RGB* array using the specified *CMCCAT2000* transform sharpened - whitepoint *RGB_w* array. - - Parameters - ---------- - RGB - *CMCCAT2000* transform sharpened *RGB* array. - RGB_w - *CMCCAT2000* transform sharpened whitepoint *RGB_w* array. - Y_w - Whitepoint *Y* tristimulus value :math:`Y_w`. - D - Degree of adaptation :math:`D`. - - Returns - ------- - :class:`numpy.ndarray` - Adapted *RGB* array. - - Examples - -------- - >>> RGB = np.array([18.985456, 20.707422, 21.747482]) - >>> RGB_w = np.array([94.930528, 103.536988, 108.717742]) - >>> Y_w = 100.0 - >>> D = 0.994468780088 - >>> full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D) - ... # doctest: +ELLIPSIS - array([19.9937078..., 20.0039363..., 20.0132638...]) - """ - - RGB = as_float_array(RGB) - RGB_w = as_float_array(RGB_w) - Y_w = as_float_array(Y_w) - D = as_float_array(D) - - with sdiv_mode(): - RGB_c = (Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None]) * RGB - - return cast("NDArrayFloat", RGB_c) - - -def full_chromatic_adaptation_inverse( - RGB: ArrayLike, - RGB_w: ArrayLike, - Y_w: ArrayLike, - D: ArrayLike, -) -> NDArrayFloat: - """ - Revert full chromatic adaptation of the specified *CMCCAT2000* transform - sharpened *RGB* array using the specified *CMCCAT2000* transform sharpened - whitepoint :math:`RGB_w` array. - - Parameters - ---------- - RGB - *CMCCAT2000* transform sharpened *RGB* array. - RGB_w - *CMCCAT2000* transform sharpened whitepoint :math:`RGB_w` array. - Y_w - Whitepoint *Y* tristimulus value :math:`Y_w`. - D - Degree of adaptation :math:`D`. - - Returns - ------- - :class:`numpy.ndarray` - Adapted *RGB* array. - - Examples - -------- - >>> RGB = np.array([19.99370783, 20.00393634, 20.01326387]) - >>> RGB_w = np.array([94.930528, 103.536988, 108.717742]) - >>> Y_w = 100.0 - >>> D = 0.994468780088 - >>> full_chromatic_adaptation_inverse(RGB, RGB_w, Y_w, D) - array([18.985456, 20.707422, 21.747482]) - """ - - RGB = as_float_array(RGB) - RGB_w = as_float_array(RGB_w) - Y_w = as_float_array(Y_w) - D = as_float_array(D) - - with sdiv_mode(): - RGB_c = RGB / (Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None]) - - return cast("NDArrayFloat", RGB_c) - - -def RGB_to_rgb(RGB: ArrayLike) -> NDArrayFloat: - """ - Convert the specified *RGB* array to *Hunt-Pointer-Estevez* - :math:`\\rho\\gamma\\beta` colourspace. - - Parameters - ---------- - RGB - *RGB* array. - - Returns - ------- - :class:`numpy.ndarray` - *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array. - - Examples - -------- - >>> RGB = np.array([19.99370783, 20.00393634, 20.01326387]) - >>> RGB_to_rgb(RGB) # doctest: +ELLIPSIS - array([19.9969397..., 20.0018612..., 20.0135053...]) - """ - - return vecmul(np.matmul(MATRIX_XYZ_TO_HPE, CAT_INVERSE_CAT02), RGB) - + xp = array_namespace(n) -def rgb_to_RGB(rgb: ArrayLike) -> NDArrayFloat: - """ - Convert from *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` - colourspace array to adapted *RGB* array. - - Parameters - ---------- - rgb - *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array. - - Returns - ------- - :class:`numpy.ndarray` - Adapted *RGB* array. + return 1.48 + xp.sqrt(n) - Examples - -------- - >>> rgb = np.array([19.99693975, 20.00186123, 20.01350530]) - >>> rgb_to_RGB(rgb) # doctest: +ELLIPSIS - array([19.9937078..., 20.0039363..., 20.0132638...]) - """ - - return vecmul(np.matmul(CAT_CAT02, MATRIX_HPE_TO_XYZ), rgb) - -def post_adaptation_non_linear_response_compression_forward( - RGB: ArrayLike, F_L: ArrayLike -) -> NDArrayFloat: +def hue_quadrature(h: ArrayLike) -> NDArrayFloat: """ - Apply post-adaptation non-linear response compression to the specified - *CMCCAT2000* transform sharpened *RGB* array. + Compute hue quadrature from the specified hue :math:`h` angle in degrees. Parameters ---------- - RGB - *CMCCAT2000* transform sharpened *RGB* array. - F_L - *Luminance* level adaptation factor :math:`F_L`. + h + Hue :math:`h` angle in degrees. Returns ------- :class:`numpy.ndarray` - Compressed *CMCCAT2000* transform sharpened *RGB* array. - - Notes - ----- - - This definition implements negative values handling as per - :cite:`Luo2013`. + Hue quadrature. Examples -------- - >>> RGB = np.array([19.99693975, 20.00186123, 20.01350530]) - >>> F_L = 1.16754446415 - >>> post_adaptation_non_linear_response_compression_forward(RGB, F_L) - ... # doctest: +ELLIPSIS - array([7.9463202..., 7.9471152..., 7.9489959...]) - """ - - RGB = as_float_array(RGB) - F_L = as_float_array(F_L) - - F_L_RGB = spow(F_L[..., None] * np.absolute(RGB) / 100, 0.42) - - return (400 * np.sign(RGB) * F_L_RGB) / (27.13 + F_L_RGB) + 0.1 - - -def post_adaptation_non_linear_response_compression_inverse( - RGB: ArrayLike, F_L: ArrayLike -) -> NDArrayFloat: + >>> hue_quadrature(219.0484326582719) # doctest: +ELLIPSIS + np.float64(278.0607358...) """ - Remove post-adaptation non-linear response compression from the specified - *CMCCAT2000* transform sharpened *RGB* array. - Parameters - ---------- - RGB - *CMCCAT2000* transform sharpened *RGB* array. - F_L - *Luminance* level adaptation factor :math:`F_L`. + h = as_float_array(h) - Returns - ------- - :class:`numpy.ndarray` - Uncompressed *CMCCAT2000* transform sharpened *RGB* array. + xp = array_namespace(h) - Examples - -------- - >>> RGB = np.array([7.94632020, 7.94711528, 7.94899595]) - >>> F_L = 1.16754446415 - >>> post_adaptation_non_linear_response_compression_inverse(RGB, F_L) - ... # doctest: +ELLIPSIS - array([19.9969397..., 20.0018612..., 20.0135052...]) - """ + h = as_float_array(xp.where(xp.isnan(h), 0, h)) - RGB = as_float_array(RGB) - F_L = as_float_array(F_L) + # Hue quadrature bin boundaries from the *CIE 159:2004* table; the + # intervals are unrolled (rather than gathered via ``searchsorted``) + # so the computation stays portable across array backends. + h_i = HUE_DATA_FOR_HUE_QUADRATURE["h_i"] + e_i = HUE_DATA_FOR_HUE_QUADRATURE["e_i"] + H_i = HUE_DATA_FOR_HUE_QUADRATURE["H_i"] - return ( - np.sign(RGB - 0.1) - * 100 - / F_L[..., None] - * spow( - (27.13 * np.absolute(RGB - 0.1)) / (400 - np.absolute(RGB - 0.1)), - 1 / 0.42, - ) - ) - - -def opponent_colour_dimensions_forward(RGB: ArrayLike) -> NDArrayFloat: - """ - Compute opponent colour dimensions from compressed *CMCCAT2000* transform - sharpened *RGB* array for forward *CIECAM02* implementation. - - Parameters - ---------- - RGB - Compressed *CMCCAT2000* transform sharpened *RGB* array. - - Returns - ------- - :class:`numpy.ndarray` - Opponent colour dimensions. - - Examples - -------- - >>> RGB = np.array([7.94632020, 7.94711528, 7.94899595]) - >>> opponent_colour_dimensions_forward(RGB) # doctest: +ELLIPSIS - array([-0.0006241..., -0.0005062...]) - """ - - R, G, B = tsplit(RGB) - - a = R - 12 * G / 11 + B / 11 - b = (R + G - 2 * B) / 9 - - return tstack([a, b]) - - -def opponent_colour_dimensions_inverse(P_n: ArrayLike, h: ArrayLike) -> NDArrayFloat: - """ - Compute opponent colour dimensions from the specified points :math:`P_n` - and hue :math:`h` in degrees for the inverse *CIECAM02* implementation. - - Parameters - ---------- - P_n - Points :math:`P_n`. - h - Hue :math:`h` in degrees. - - Returns - ------- - :class:`numpy.ndarray` - Opponent colour dimensions. - - Examples - -------- - >>> P_n = np.array([30162.89081534, 24.23720547, 1.05000000]) - >>> h = -140.95156734 - >>> opponent_colour_dimensions_inverse(P_n, h) # doctest: +ELLIPSIS - array([-0.0006241..., -0.0005062...]) - """ - - P_1, P_2, P_3 = tsplit(P_n) - hr = np.radians(h) - - sin_hr = np.sin(hr) - cos_hr = np.cos(hr) - - with sdiv_mode(): - cos_hr_sin_hr = sdiv(cos_hr, sin_hr) - sin_hr_cos_hr = sdiv(sin_hr, cos_hr) - - P_4 = sdiv(P_1, sin_hr) - P_5 = sdiv(P_1, cos_hr) - - n = P_2 * (2 + P_3) * (460 / 1403) - - a = zeros(hr.shape) - b = zeros(hr.shape) - - abs_sin_hr_gt_cos_hr = np.abs(sin_hr) >= np.abs(cos_hr) - abs_sin_hr_lt_cos_hr = np.abs(sin_hr) < np.abs(cos_hr) - - b = np.where( - abs_sin_hr_gt_cos_hr, - n - / ( - P_4 - + (2 + P_3) * (220 / 1403) * cos_hr_sin_hr - - (27 / 1403) - + P_3 * (6300 / 1403) - ), - b, - ) - - a = np.where( - abs_sin_hr_gt_cos_hr, - b * cos_hr_sin_hr, - a, - ) - - a = np.where( - abs_sin_hr_lt_cos_hr, - n - / ( - P_5 - + (2 + P_3) * (220 / 1403) - - ((27 / 1403) - P_3 * (6300 / 1403)) * sin_hr_cos_hr - ), - a, - ) - - b = np.where( - abs_sin_hr_lt_cos_hr, - a * sin_hr_cos_hr, - b, - ) - - return tstack([a, b]) - - -def hue_angle(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: - """ - Compute the *hue* angle :math:`h` in degrees from the specified opponent - colour dimensions. - - Parameters - ---------- - a - Opponent colour dimension :math:`a`. - b - Opponent colour dimension :math:`b`. - - Returns - ------- - :class:`numpy.ndarray` - *Hue* angle :math:`h` in degrees. - - Examples - -------- - >>> a = -0.000624112068243 - >>> b = -0.000506270106773 - >>> hue_angle(a, b) # doctest: +ELLIPSIS - np.float64(219.0484326...) - """ - - a = as_float_array(a) - b = as_float_array(b) - - h = np.degrees(np.arctan2(b, a)) % 360 - - return as_float(h) - - -def hue_quadrature(h: ArrayLike) -> NDArrayFloat: - """ - Compute hue quadrature from the specified hue :math:`h` angle in degrees. - - Parameters - ---------- - h - Hue :math:`h` angle in degrees. - - Returns - ------- - :class:`numpy.ndarray` - Hue quadrature. - - Examples - -------- - >>> hue_quadrature(219.0484326582719) # doctest: +ELLIPSIS - np.float64(278.0607358...) - """ - - h = as_float_array(h) - - h_i = HUE_DATA_FOR_HUE_QUADRATURE["h_i"] - e_i = HUE_DATA_FOR_HUE_QUADRATURE["e_i"] - H_i = HUE_DATA_FOR_HUE_QUADRATURE["H_i"] - - # *np.searchsorted* returns an erroneous index if a *nan* is used as input. - h = np.where(np.isnan(h), 0, h) - i = as_int_array(np.searchsorted(h_i, h, side="left") - 1) - - h_ii = h_i[i] - e_ii = e_i[i] - H_ii = H_i[i] - h_ii1 = h_i[i + 1] - e_ii1 = e_i[i + 1] - - H = H_ii + ((100 * (h - h_ii) / e_ii) / ((h - h_ii) / e_ii + (h_ii1 - h) / e_ii1)) - - H = np.where( - h < 20.14, - 385.9 + (14.1 * h / 0.856) / (h / 0.856 + (20.14 - h) / 0.8), - H, - ) - H = np.where( - h >= 237.53, - H_ii + ((85.9 * (h - h_ii) / e_ii) / ((h - h_ii) / e_ii + (360 - h) / 0.856)), - H, + def _H( + h_k: float, e_k: float, H_k: float, h_k1: float, e_k1: float + ) -> NDArrayFloat: + """Compute hue quadrature for a single bin.""" + + t1 = (h - h_k) / e_k + t2 = (h_k1 - h) / e_k1 + return H_k + 100 * t1 / (t1 + t2) + + H_0 = _H(h_i[0], e_i[0], H_i[0], h_i[1], e_i[1]) + H_1 = _H(h_i[1], e_i[1], H_i[1], h_i[2], e_i[2]) + H_2 = _H(h_i[2], e_i[2], H_i[2], h_i[3], e_i[3]) + + # Last interval and wrap-around use special formulas that account for + # the circular hue boundary at 360 degrees. + t1_3 = (h - h_i[3]) / e_i[3] + H_3 = H_i[3] + (85.9 * t1_3) / (t1_3 + (360 - h) / 0.856) + H_wrap = 385.9 + (14.1 * h / 0.856) / (h / 0.856 + (h_i[0] - h) / e_i[0]) + + H = xp_select( + [ + (h >= h_i[0]) & (h < h_i[1]), + (h >= h_i[1]) & (h < h_i[2]), + (h >= h_i[2]) & (h < h_i[3]), + (h >= h_i[3]), + ], + [H_0, H_1, H_2, H_3], + default=H_wrap, + xp=xp, ) return as_float(H) - - -def eccentricity_factor(h: ArrayLike) -> NDArrayFloat: - """ - Compute the eccentricity factor :math:`e_t` from the specified hue - :math:`h` angle in degrees for forward *CIECAM02* implementation. - - Parameters - ---------- - h - Hue :math:`h` angle in degrees. - - Returns - ------- - :class:`numpy.ndarray` - Eccentricity factor :math:`e_t`. - - Examples - -------- - >>> eccentricity_factor(-140.951567342) # doctest: +ELLIPSIS - np.float64(1.1740054...) - """ - - h = as_float_array(h) - - return 1 / 4 * (np.cos(2 + h * np.pi / 180) + 3.8) - - -def achromatic_response_forward(RGB: ArrayLike, N_bb: ArrayLike) -> NDArrayFloat: - """ - Compute the achromatic response :math:`A` from the specified compressed - *CMCCAT2000* transform sharpened *RGB* array and :math:`N_{bb}` chromatic - induction factor for forward *CIECAM02* implementation. - - Parameters - ---------- - RGB - Compressed *CMCCAT2000* transform sharpened *RGB* array. - N_bb - Chromatic induction factor :math:`N_{bb}`. - - Returns - ------- - :class:`numpy.ndarray` - Achromatic response :math:`A`. - - Examples - -------- - >>> RGB = np.array([7.94632020, 7.94711528, 7.94899595]) - >>> N_bb = 1.000304004559381 - >>> achromatic_response_forward(RGB, N_bb) # doctest: +ELLIPSIS - np.float64(23.9394809...) - """ - - R, G, B = tsplit(RGB) - - return (2 * R + G + (1 / 20) * B - 0.305) * N_bb - - -def achromatic_response_inverse( - A_w: ArrayLike, - J: ArrayLike, - c: ArrayLike, - z: ArrayLike, -) -> NDArrayFloat: - """ - Compute the achromatic response :math:`A` from the specified achromatic - response :math:`A_w` for the whitepoint, *Lightness* correlate - :math:`J`, surround exponential non-linearity :math:`c` and base - exponential non-linearity :math:`z` for inverse *CIECAM02* - implementation. - - Parameters - ---------- - A_w - Achromatic response :math:`A_w` for the whitepoint. - J - *Lightness* correlate :math:`J`. - c - Surround exponential non-linearity :math:`c`. - z - Base exponential non-linearity :math:`z`. - - Returns - ------- - :class:`numpy.ndarray` - Achromatic response :math:`A`. - - Examples - -------- - >>> A_w = 46.1882087914 - >>> J = 41.73109113251392 - >>> c = 0.69 - >>> z = 1.927213595499958 - >>> achromatic_response_inverse(A_w, J, c, z) # doctest: +ELLIPSIS - np.float64(23.9394809...) - """ - - A_w = as_float_array(A_w) - J = as_float_array(J) - c = as_float_array(c) - z = as_float_array(z) - - return A_w * spow(J / 100, 1 / (c * z)) - - -def lightness_correlate( - A: ArrayLike, - A_w: ArrayLike, - c: ArrayLike, - z: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *Lightness* correlate :math:`J`. - - Parameters - ---------- - A - Achromatic response :math:`A` for the stimulus. - A_w - Achromatic response :math:`A_w` for the whitepoint. - c - Surround exponential non-linearity :math:`c`. - z - Base exponential non-linearity :math:`z`. - - Returns - ------- - :class:`numpy.ndarray` - *Lightness* correlate :math:`J`. - - Examples - -------- - >>> A = 23.9394809667 - >>> A_w = 46.1882087914 - >>> c = 0.69 - >>> z = 1.9272135955 - >>> lightness_correlate(A, A_w, c, z) # doctest: +ELLIPSIS - np.float64(41.7310911...) - """ - - A = as_float_array(A) - A_w = as_float_array(A_w) - c = as_float_array(c) - z = as_float_array(z) - - with sdiv_mode(): - return 100 * spow(sdiv(A, A_w), c * z) - - -def brightness_correlate( - c: ArrayLike, - J: ArrayLike, - A_w: ArrayLike, - F_L: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *brightness* correlate :math:`Q`. - - Parameters - ---------- - c - Surround exponential non-linearity :math:`c`. - J - *Lightness* correlate :math:`J`. - A_w - Achromatic response :math:`A_w` for the whitepoint. - F_L - *Luminance* level adaptation factor :math:`F_L`. - - Returns - ------- - :class:`numpy.ndarray` - *Brightness* correlate :math:`Q`. - - Examples - -------- - >>> c = 0.69 - >>> J = 41.7310911325 - >>> A_w = 46.1882087914 - >>> F_L = 1.16754446415 - >>> brightness_correlate(c, J, A_w, F_L) # doctest: +ELLIPSIS - np.float64(195.3713259...) - """ - - c = as_float_array(c) - J = as_float_array(J) - A_w = as_float_array(A_w) - F_L = as_float_array(F_L) - - return (4 / c) * np.sqrt(J / 100) * (A_w + 4) * spow(F_L, 0.25) - - -def temporary_magnitude_quantity_forward( - N_c: ArrayLike, - N_cb: ArrayLike, - e_t: ArrayLike, - a: ArrayLike, - b: ArrayLike, - RGB_a: ArrayLike, -) -> NDArrayFloat: - """ - Compute the temporary magnitude quantity :math:`t` for forward - *CIECAM02* implementation. - - Parameters - ---------- - N_c - Surround chromatic induction factor :math:`N_{c}`. - N_cb - Chromatic induction factor :math:`N_{cb}`. - e_t - Eccentricity factor :math:`e_t`. - a - Opponent colour dimension :math:`a`. - b - Opponent colour dimension :math:`b`. - RGB_a - Compressed stimulus *CMCCAT2000* transform sharpened *RGB* array. - - Returns - ------- - :class:`numpy.ndarray` - Temporary magnitude quantity :math:`t`. - - Examples - -------- - >>> N_c = 1.0 - >>> N_cb = 1.00030400456 - >>> e_t = 1.174005472851914 - >>> a = -0.000624112068243 - >>> b = -0.000506270106773 - >>> RGB_a = np.array([7.94632020, 7.94711528, 7.94899595]) - >>> temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a) - ... # doctest: +ELLIPSIS - np.float64(0.1497462...) - """ - - N_c = as_float_array(N_c) - N_cb = as_float_array(N_cb) - e_t = as_float_array(e_t) - a = as_float_array(a) - b = as_float_array(b) - Ra, Ga, Ba = tsplit(RGB_a) - - with sdiv_mode(): - return ((50000 / 13) * N_c * N_cb) * sdiv( - e_t * spow(a**2 + b**2, 0.5), Ra + Ga + 21 * Ba / 20 - ) - - -def temporary_magnitude_quantity_inverse( - C: ArrayLike, J: ArrayLike, n: ArrayLike -) -> NDArrayFloat: - """ - Compute the temporary magnitude quantity :math:`t` for inverse - *CIECAM02* implementation. - - Parameters - ---------- - C - *Chroma* correlate :math:`C`. - J - *Lightness* correlate :math:`J`. - n - Function of the luminance factor of the background :math:`n`. - - Returns - ------- - :class:`numpy.ndarray` - Temporary magnitude quantity :math:`t`. - - Examples - -------- - >>> C = 68.8364136888275 - >>> J = 41.749268505999 - >>> n = 0.2 - >>> temporary_magnitude_quantity_inverse(C, J, n) # doctest: +ELLIPSIS - np.float64(202.3873619...) - """ - - C = as_float_array(C) - J_prime = np.maximum(J, EPSILON) - n = as_float_array(n) - - return spow(C / (np.sqrt(J_prime / 100) * spow(1.64 - 0.29**n, 0.73)), 1 / 0.9) - - -def chroma_correlate( - J: ArrayLike, - n: ArrayLike, - N_c: ArrayLike, - N_cb: ArrayLike, - e_t: ArrayLike, - a: ArrayLike, - b: ArrayLike, - RGB_a: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *chroma* correlate :math:`C`. - - Parameters - ---------- - J - *Lightness* correlate :math:`J`. - n - Function of the luminance factor of the background :math:`n`. - N_c - Surround chromatic induction factor :math:`N_{c}`. - N_cb - Chromatic induction factor :math:`N_{cb}`. - e_t - Eccentricity factor :math:`e_t`. - a - Opponent colour dimension :math:`a`. - b - Opponent colour dimension :math:`b`. - RGB_a - Compressed stimulus *CMCCAT2000* transform sharpened *RGB* - array. - - Returns - ------- - :class:`numpy.ndarray` - *Chroma* correlate :math:`C`. - - Examples - -------- - >>> J = 41.7310911325 - >>> n = 0.2 - >>> N_c = 1.0 - >>> N_cb = 1.00030400456 - >>> e_t = 1.17400547285 - >>> a = -0.000624112068243 - >>> b = -0.000506270106773 - >>> RGB_a = np.array([7.94632020, 7.94711528, 7.94899595]) - >>> chroma_correlate(J, n, N_c, N_cb, e_t, a, b, RGB_a) - ... # doctest: +ELLIPSIS - np.float64(0.1047077...) - """ - - J = as_float_array(J) - n = as_float_array(n) - - t = temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a) - - return spow(t, 0.9) * spow(J / 100, 0.5) * spow(1.64 - 0.29**n, 0.73) - - -def colourfulness_correlate(C: ArrayLike, F_L: ArrayLike) -> NDArrayFloat: - """ - Compute the *colourfulness* correlate :math:`M`. - - Parameters - ---------- - C - *Chroma* correlate :math:`C`. - F_L - *Luminance* level adaptation factor :math:`F_L`. - - Returns - ------- - :class:`numpy.ndarray` - *Colourfulness* correlate :math:`M`. - - Examples - -------- - >>> C = 0.104707757171 - >>> F_L = 1.16754446415 - >>> colourfulness_correlate(C, F_L) # doctest: +ELLIPSIS - np.float64(0.1088421...) - """ - - C = as_float_array(C) - F_L = as_float_array(F_L) - - return C * spow(F_L, 0.25) - - -def saturation_correlate(M: ArrayLike, Q: ArrayLike) -> NDArrayFloat: - """ - Compute the *saturation* correlate :math:`s`. - - Parameters - ---------- - M - *Colourfulness* correlate :math:`M`. - Q - *Brightness* correlate :math:`Q`. - - Returns - ------- - :class:`numpy.ndarray` - *Saturation* correlate :math:`s`. - - Examples - -------- - >>> M = 0.108842175669 - >>> Q = 195.371325966 - >>> saturation_correlate(M, Q) # doctest: +ELLIPSIS - np.float64(2.3603053...) - """ - - M = as_float_array(M) - Q = as_float_array(Q) - - with sdiv_mode(): - return 100 * spow(sdiv(M, Q), 0.5) - - -def P( - N_c: ArrayLike, - N_cb: ArrayLike, - e_t: ArrayLike, - t: ArrayLike, - A: ArrayLike, - N_bb: ArrayLike, -) -> NDArrayFloat: - """ - Compute the points :math:`P_1`, :math:`P_2` and :math:`P_3`. - - Parameters - ---------- - N_c - Surround chromatic induction factor :math:`N_{c}`. - N_cb - Chromatic induction factor :math:`N_{cb}`. - e_t - Eccentricity factor :math:`e_t`. - t - Temporary magnitude quantity :math:`t`. - A - Achromatic response :math:`A` for the stimulus. - N_bb - Chromatic induction factor :math:`N_{bb}`. - - Returns - ------- - :class:`numpy.ndarray` - Points :math:`P`. - - Examples - -------- - >>> N_c = 1.0 - >>> N_cb = 1.00030400456 - >>> e_t = 1.174005472851914 - >>> t = 0.149746202921 - >>> A = 23.9394809667 - >>> N_bb = 1.00030400456 - >>> P(N_c, N_cb, e_t, t, A, N_bb) # doctest: +ELLIPSIS - array([3.0162890...e+04, 2.4237205...e+01, 1.0500000...e+00]) - """ - - N_c = as_float_array(N_c) - N_cb = as_float_array(N_cb) - e_t = as_float_array(e_t) - t = as_float_array(t) - A = as_float_array(A) - N_bb = as_float_array(N_bb) - - with sdiv_mode(): - P_1 = sdiv((50000 / 13) * N_c * N_cb * e_t, t) - - P_2 = A / N_bb + 0.305 - P_3 = ones(P_1.shape) * (21 / 20) - - return tstack([P_1, P_2, P_3]) - - -def matrix_post_adaptation_non_linear_response_compression( - P_2: ArrayLike, a: ArrayLike, b: ArrayLike -) -> NDArrayFloat: - """ - Apply post-adaptation non-linear response compression matrix to - specified opponent colour components. - - Parameters - ---------- - P_2 - Point :math:`P_2` representing the post-adaptation response value. - a - Opponent colour dimension :math:`a` component. - b - Opponent colour dimension :math:`b` component. - - Returns - ------- - :class:`numpy.ndarray` - Array of compressed points :math:`P` containing three values - after non-linear response compression. - - Examples - -------- - >>> P_2 = 24.2372054671 - >>> a = -0.000624112068243 - >>> b = -0.000506270106773 - >>> matrix_post_adaptation_non_linear_response_compression(P_2, a, b) - ... # doctest: +ELLIPSIS - array([7.9463202..., 7.9471152..., 7.9489959...]) - """ - - P_2 = as_float_array(P_2) - a = as_float_array(a) - b = as_float_array(b) - - return ( - vecmul( - [ - [460, 451, 288], - [460, -891, -261], - [460, -220, -6300], - ], - tstack([P_2, a, b]), - ) - / 1403 - ) diff --git a/colour/appearance/ciecam16.py b/colour/appearance/ciecam16.py index d961d8c609..45396912f8 100644 --- a/colour/appearance/ciecam16.py +++ b/colour/appearance/ciecam16.py @@ -21,47 +21,33 @@ from __future__ import annotations +import typing from dataclasses import astuple, dataclass, field -import numpy as np - -from colour.algebra import spow, vecmul +from colour.algebra import sdiv, sdiv_mode, spow, vecmul from colour.appearance.cam16 import MATRIX_16, MATRIX_INVERSE_16 from colour.appearance.ciecam02 import ( VIEWING_CONDITIONS_CIECAM02, InductionFactors_CIECAM02, - P, - achromatic_response_forward, - achromatic_response_inverse, - brightness_correlate, - chroma_correlate, - colourfulness_correlate, - degree_of_adaptation, - eccentricity_factor, - hue_angle, hue_quadrature, - lightness_correlate, - matrix_post_adaptation_non_linear_response_compression, - opponent_colour_dimensions_forward, - opponent_colour_dimensions_inverse, - post_adaptation_non_linear_response_compression_forward, - saturation_correlate, - temporary_magnitude_quantity_inverse, - viewing_conditions_dependent_parameters, -) -from colour.hints import ( # noqa: TC001 - Annotated, - ArrayLike, - Domain100, - NDArrayFloat, - Range100, ) +from colour.constants import EPSILON + +if typing.TYPE_CHECKING: + from colour.hints import ( + Annotated, + ArrayLike, + Domain100, + NDArrayFloat, + Range100, + ) + from colour.utilities import ( CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, - as_float_array, from_range_100, from_range_degrees, has_only_nan, @@ -69,6 +55,11 @@ to_domain_100, to_domain_degrees, tsplit, + tstack, + xp_as_float_array, + xp_degrees, + xp_radians, + xp_select, ) __author__ = "Colour Developers" @@ -84,10 +75,6 @@ "CAM_Specification_CIECAM16", "XYZ_to_CIECAM16", "CIECAM16_to_XYZ", - "f_e_forward", - "f_e_inverse", - "f_q", - "d_f_q", ] @@ -180,7 +167,7 @@ def XYZ_to_CIECAM16( InductionFactors_CIECAM02 | InductionFactors_CIECAM16 ) = VIEWING_CONDITIONS_CIECAM16["Average"], discount_illuminant: bool = False, - compute_H: bool = True, + compute_H: bool = False, ) -> Annotated[CAM_Specification_CIECAM16, (100, 100, 360, 100, 100, 100, 400)]: """ Compute the *CIECAM16* colour appearance model correlates from the @@ -207,8 +194,10 @@ def XYZ_to_CIECAM16( discount_illuminant Truth value indicating if the illuminant should be discounted. compute_H - Whether to compute *Hue* :math:`h` quadrature :math:`H`. - :math:`H` is rarely used, and expensive to compute. + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -249,12 +238,16 @@ def XYZ_to_CIECAM16( Examples -------- + >>> import numpy as np >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_w = np.array([95.05, 100.00, 108.88]) >>> L_A = 318.31 >>> Y_b = 20.0 >>> surround = VIEWING_CONDITIONS_CIECAM16["Average"] - >>> XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS + >>> XYZ_to_CIECAM16( + ... XYZ, XYZ_w, L_A, Y_b, surround, + ... compute_H=True, + ... ) # doctest: +ELLIPSIS CAM_Specification_CIECAM16(J=np.float64(41.7312079...), \ C=np.float64(0.1033557...), h=np.float64(217.0679597...), \ s=np.float64(2.3450150...), Q=np.float64(195.3717089...), \ @@ -263,79 +256,141 @@ def XYZ_to_CIECAM16( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) - _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) - - # Step 0 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. - RGB_w = vecmul(MATRIX_16, XYZ_w) - - # Computing degree of adaptation :math:`D`. - D = ( - np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) - if not discount_illuminant - else ones(L_A.shape) - ) - - n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) - D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None] - RGB_wc = D_RGB * RGB_w + xp = array_namespace(XYZ, XYZ_w, L_A, Y_b) - # Applying forward post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) + XYZ = xp_as_float_array(XYZ, xp=xp) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=XYZ) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) - # Computing achromatic responses for the whitepoint. - A_w = achromatic_response_forward(RGB_aw, N_bb) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - # Step 1 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L`, + # chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`, + # base exponential non-linearity :math:`z`. Same formulation as + # in *CIECAM02*. + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + with sdiv_mode(): + N_bb = 0.725 * spow(sdiv(1, n), 0.2) + N_cb = N_bb + z = 1.48 + xp.sqrt(n) + + # Converting *CIE XYZ* tristimulus values to *CAT16* sharpened *RGB* + # values for the stimulus and the reference white, same matrix as + # *CAM16*. RGB = vecmul(MATRIX_16, XYZ) + RGB_w = vecmul(MATRIX_16, XYZ_w) - # Step 2 + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*, clipped to :math:`[0, 1]` and bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ) + else: + F = xp_as_float_array(surround.F, xp=xp, like=XYZ) + D = xp.clip(F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)), 0, 1) + + # Computing full chromatic adaptation. *CIECAM16* uses :math:`100` + # in place of :math:`Y_w` in the adaptation factor, unlike + # *CIECAM02* / *CAM16*. + D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None] RGB_c = D_RGB * RGB + RGB_wc = D_RGB * RGB_w - # Step 3 - # Applying forward post-adaptation non-linear response compression. - RGB_a = f_e_forward(RGB_c, F_L) + 0.1 + # Applying forward post-adaptation non-linear response compression + # via the *CIECAM16* 3-branch piecewise function with linear + # extensions outside the :math:`[0.26, 150]` range. The :math:`+0.1` + # offset is added back per the model definition. The whitepoint goes + # through *CIECAM02*'s original Michaelis-Menten compression + # :math:`(400 \\cdot |x|^{0.42}) / (27.13 + |x|^{0.42}) + 0.1` per + # the *CIECAM16* spec. + F_L_RGB_wc = spow(F_L[..., None] * xp.abs(RGB_wc) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_wc) * F_L_RGB_wc) / (27.13 + F_L_RGB_wc) + 0.1 - # Step 4 - # Converting to preliminary cartesian coordinates. - a, b = tsplit(opponent_colour_dimensions_forward(RGB_a)) + q_L, q_U = 0.26, 150 + F_L_q_L_p = spow((F_L[..., None] * q_L) / 100, 0.42) + f_q_F_L_q_L = (400 * F_L_q_L_p) / (27.13 + F_L_q_L_p) + F_L_q_U_p = spow((F_L[..., None] * q_U) / 100, 0.42) + f_q_F_L_q_U = (400 * F_L_q_U_p) / (27.13 + F_L_q_U_p) + F_L_q_U_lin = (F_L[..., None] * q_U) / 100 + d_f_q_F_L_q_U = (1.68 * 27.13 * F_L[..., None] * spow(F_L_q_U_lin, -0.58)) / ( + 27.13 + spow(F_L_q_U_lin, 0.42) + ) ** 2 + F_L_RGB_c_lin = (F_L[..., None] * RGB_c) / 100 + F_L_RGB_c_p = spow(F_L_RGB_c_lin, 0.42) + f_q_F_L_RGB_c = (400 * F_L_RGB_c_p) / (27.13 + F_L_RGB_c_p) + RGB_a = ( + xp_select( + [ + RGB_c > q_U, + xp.logical_and(q_L <= RGB_c, RGB_c <= q_U), + RGB_c < q_L, + ], + [ + f_q_F_L_q_U + d_f_q_F_L_q_U * (RGB_c - q_U), + f_q_F_L_RGB_c, + f_q_F_L_q_L * RGB_c / q_L, + ], + xp=xp, + ) + + 0.1 + ) - # Computing the *hue* angle :math:`h`. - h = hue_angle(a, b) + # Computing the opponent colour dimensions :math:`a` and :math:`b`, + # same as in *CIECAM02*. + Ra, Ga, Ba = tsplit(RGB_a) + a = Ra - 12 * Ga / 11 + Ba / 11 + b = (Ra + Ga - 2 * Ba) / 9 + + # Computing the *hue* angle :math:`h` in degrees in + # :math:`[0, 360)`, same as in *CIECAM02*. + h = xp_degrees(xp.atan2(b, a)) % 360 + + # Computing eccentricity factor :math:`e_t`, same as in *CIECAM02*. + e_t = 1 / 4 * (xp.cos(2 + xp_radians(h)) + 3.8) + + # Computing achromatic responses :math:`A` for the stimulus and + # :math:`A_w` for the whitepoint, same as in *CIECAM02*. + A = (2 * Ra + Ga + (1 / 20) * Ba - 0.305) * N_bb + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = (2 * Raw + Gaw + (1 / 20) * Baw - 0.305) * N_bb + + # Computing the correlate of *Lightness* :math:`J`, same form as + # in *CIECAM02*. + c = surround.c + with sdiv_mode(): + J = 100 * spow(sdiv(A, A_w), c * z) + + # Computing the correlate of *brightness* :math:`Q`, same form as + # in *CIECAM02*. + Q = (4 / c) * xp.sqrt(J / 100) * (A_w + 4) * spow(F_L, 0.25) + + # Computing the temporary magnitude quantity :math:`t` and the + # correlate of *chroma* :math:`C`, same forms as in *CIECAM02*. + N_c = surround.N_c + with sdiv_mode(): + t = ((50000 / 13) * N_c * N_cb) * sdiv( + e_t * spow(a**2 + b**2, 0.5), Ra + Ga + 21 * Ba / 20 + ) + C = spow(t, 0.9) * spow(J / 100, 0.5) * spow(1.64 - 0.29**n, 0.73) - # Step 5 - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) + # Computing the correlate of *colourfulness* :math:`M` and the + # correlate of *saturation* :math:`s`, same forms as in *CIECAM02*. + M = C * spow(F_L, 0.25) + with sdiv_mode(): + s = 100 * spow(sdiv(M, Q), 0.5) - # Computing hue :math:`h` quadrature :math:`H`. - H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan) + # Computing hue :math:`h` quadrature :math:`H` only when requested + # via ``compute_H``; the bin search is shared with *CIECAM02* and + # delegates to :func:`hue_quadrature`. # TODO: Compute hue composition. - - # Step 6 - # Computing achromatic responses for the stimulus. - A = achromatic_response_forward(RGB_a, N_bb) - - # Step 7 - # Computing the correlate of *Lightness* :math:`J`. - J = lightness_correlate(A, A_w, surround.c, z) - - # Step 8 - # Computing the correlate of *brightness* :math:`Q`. - Q = brightness_correlate(surround.c, J, A_w, F_L) - - # Step 9 - # Computing the correlate of *chroma* :math:`C`. - C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a) - - # Computing the correlate of *colourfulness* :math:`M`. - M = colourfulness_correlate(C, F_L) - - # Computing the correlate of *saturation* :math:`s`. - s = saturation_correlate(M, Q) + H = hue_quadrature(h) if compute_H else xp.full_like(h, float("nan")) return CAM_Specification_CIECAM16( J=as_float(from_range_100(J)), @@ -434,6 +489,7 @@ def CIECAM16_to_XYZ( Examples -------- + >>> import numpy as np >>> specification = CAM_Specification_CIECAM16( ... J=41.731207905126638, C=0.103355738709070, h=217.067959767393010 ... ) @@ -450,33 +506,67 @@ def CIECAM16_to_XYZ( C = to_domain_100(C) h = to_domain_degrees(h) M = to_domain_100(M) - L_A = as_float_array(L_A) XYZ_w = to_domain_100(XYZ_w) - _X_w, Y_w, _Z_w = tsplit(XYZ_w) - # Step 0 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. - RGB_w = vecmul(MATRIX_16, XYZ_w) + xp = array_namespace(J, C, h, M, XYZ_w, L_A) - # Computing degree of adaptation :math:`D`. - D = ( - np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) - if not discount_illuminant - else ones(L_A.shape) - ) + J = xp_as_float_array(J, xp=xp) + C = xp_as_float_array(C, xp=xp, like=J) + h = xp_as_float_array(h, xp=xp, like=J) + M = xp_as_float_array(M, xp=xp, like=J) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=J) + L_A = xp_as_float_array(L_A, xp=xp, like=J) + + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L`, + # chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`, + # base exponential non-linearity :math:`z`. Same formulation as + # in *CIECAM02*. + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + with sdiv_mode(): + N_bb = 0.725 * spow(sdiv(1, n), 0.2) + N_cb = N_bb + z = 1.48 + xp.sqrt(n) + + # Converting *CIE XYZ* tristimulus values to *CAT16* sharpened *RGB* + # values for the reference white. + RGB_w = vecmul(MATRIX_16, XYZ_w) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*, clipped to :math:`[0, 1]` and bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=J) + else: + F = xp_as_float_array(surround.F, xp=xp, like=J) + D = xp.clip(F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)), 0, 1) + + # Computing full chromatic adaptation for the reference white. + # *CIECAM16* uses :math:`100` in place of :math:`Y_w` in the + # adaptation factor, unlike *CIECAM02* / *CAM16*. D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None] RGB_wc = D_RGB * RGB_w - # Applying forward post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) + # Applying forward post-adaptation non-linear response compression + # to the whitepoint, same form as in *CIECAM02* per the *CIECAM16* + # spec. + F_L_RGB_wc = spow(F_L[..., None] * xp.abs(RGB_wc) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_wc) * F_L_RGB_wc) / (27.13 + F_L_RGB_wc) + 0.1 - # Computing achromatic responses for the whitepoint. - A_w = achromatic_response_forward(RGB_aw, N_bb) + # Computing achromatic response :math:`A_w` for the whitepoint, + # same as in *CIECAM02*. + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = (2 * Raw + Gaw + (1 / 20) * Baw - 0.305) * N_bb - # Step 1 + # Recovering the correlate of *chroma* :math:`C` from the correlate + # of *colourfulness* :math:`M` when only :math:`M` has been + # provided. if has_only_nan(C) and not has_only_nan(M): C = M / spow(F_L, 0.25) elif has_only_nan(C): @@ -487,207 +577,120 @@ def CIECAM16_to_XYZ( raise ValueError(error) - # Step 2 - # Computing temporary magnitude quantity :math:`t`. - t = temporary_magnitude_quantity_inverse(C, J, n) - - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) - - # Computing achromatic response :math:`A` for the stimulus. - A = achromatic_response_inverse(A_w, J, surround.c, z) - - # Computing *P_1* to *P_3*. - P_n = P(surround.N_c, N_cb, e_t, t, A, N_bb) - _P_1, P_2, _P_3 = tsplit(P_n) - - # Step 3 - # Computing opponent colour dimensions :math:`a` and :math:`b`. - ab = opponent_colour_dimensions_inverse(P_n, h) - a, b = tsplit(ab) * np.where(t == 0, 0, 1) - - # Step 4 - # Applying post-adaptation non-linear response compression matrix. - RGB_a = matrix_post_adaptation_non_linear_response_compression(P_2, a, b) - - # Step 5 - # Applying inverse post-adaptation non-linear response compression. - RGB_c = f_e_inverse(RGB_a - 0.1, F_L) - - # Step 6 - RGB = RGB_c / D_RGB - - # Step 7 - XYZ = vecmul(MATRIX_INVERSE_16, RGB) - - return from_range_100(XYZ) - - -def f_e_forward(RGB_c: ArrayLike, F_L: ArrayLike) -> NDArrayFloat: - """ - Compute the post-adaptation cone responses. - - Parameters - ---------- - RGB_c - *CMCCAT2000* transform sharpened :math:`RGB_c` array. - F_L - *Luminance* level adaptation factor :math:`F_L`. - - Returns - ------- - :class:`numpy.ndarray` - Compressed *CMCCAT2000* transform sharpened :math:`RGB_a` array. - - Notes - ----- - - This definition is different from :cite:`Li2017` and provides linear - extensions under 0.26 and above 150. It also omits the 0.1 offset - that is now part of the general model. - - Examples - -------- - >>> RGB_c = np.array([19.99693975, 20.00186123, 20.01350530]) - >>> F_L = 1.16754446415 - >>> f_e_forward(RGB_c, F_L) - ... # doctest: +ELLIPSIS - array([7.8463202..., 7.8471152..., 7.8489959...]) - """ - - RGB_c = as_float_array(RGB_c) - F_L = as_float_array(F_L) - q_L, q_U = 0.26, 150 - - f_q_F_L_q_U = f_q(F_L, q_U)[..., None] - f_q_F_L_q_L = f_q(F_L, q_L)[..., None] - f_q_F_L_RGB_c = f_q(F_L[..., None], RGB_c) - d_f_q_F_L_q_U = d_f_q(F_L, q_U)[..., None] - - return np.select( - [ - RGB_c > q_U, - np.logical_and(q_L <= RGB_c, RGB_c <= q_U), - RGB_c < q_L, - ], - [ - f_q_F_L_q_U + d_f_q_F_L_q_U * (RGB_c - q_U), - f_q_F_L_RGB_c, - f_q_F_L_q_L * RGB_c / q_L, - ], + # Computing temporary magnitude quantity :math:`t`, same form as + # in *CIECAM02*. + J_prime = xp.clip(J, min=EPSILON) + t = spow(C / (xp.sqrt(J_prime / 100) * spow(1.64 - 0.29**n, 0.73)), 1 / 0.9) + + # Computing eccentricity factor :math:`e_t`, same as in *CIECAM02*. + e_t = 1 / 4 * (xp.cos(2 + xp_radians(h)) + 3.8) + + # Computing achromatic response :math:`A` for the stimulus, same + # inverse form as in *CIECAM02*. + c = surround.c + A = A_w * spow(J / 100, 1 / (c * z)) + + # Computing points :math:`P_1`, :math:`P_2`, :math:`P_3`, same + # form as in *CIECAM02*. + N_c = surround.N_c + with sdiv_mode(): + P_1 = sdiv((50000 / 13) * N_c * N_cb * e_t, t) + P_2 = A / N_bb + 0.305 + P_3 = xp.full_like(P_1, 21 / 20) + + # Computing opponent colour dimensions :math:`a` and :math:`b` + # via the sin / cos branching protecting against the numerical + # singularity near the hue axis. Same as in *CIECAM02*. + hr = xp_radians(h) + sin_hr = xp.sin(hr) + cos_hr = xp.cos(hr) + with sdiv_mode(): + cos_hr_sin_hr = sdiv(cos_hr, sin_hr) + sin_hr_cos_hr = sdiv(sin_hr, cos_hr) + P_4 = sdiv(P_1, sin_hr) + P_5 = sdiv(P_1, cos_hr) + n_ab = P_2 * (2 + P_3) * (460 / 1403) + + abs_sin_ge_cos = xp.abs(sin_hr) >= xp.abs(cos_hr) + abs_sin_lt_cos = xp.abs(sin_hr) < xp.abs(cos_hr) + + a = xp.zeros_like(hr) + b = xp.zeros_like(hr) + b = xp.where( + abs_sin_ge_cos, + n_ab + / ( + P_4 + + (2 + P_3) * (220 / 1403) * cos_hr_sin_hr + - (27 / 1403) + + P_3 * (6300 / 1403) + ), + b, + ) + a = xp.where(abs_sin_ge_cos, b * cos_hr_sin_hr, a) + a = xp.where( + abs_sin_lt_cos, + n_ab + / ( + P_5 + + (2 + P_3) * (220 / 1403) + - ((27 / 1403) - P_3 * (6300 / 1403)) * sin_hr_cos_hr + ), + a, + ) + b = xp.where(abs_sin_lt_cos, a * sin_hr_cos_hr, b) + t_mask = xp.where(t == 0, 0, 1) + a = a * t_mask + b = b * t_mask + + # Applying post-adaptation non-linear response compression matrix + # to recover the compressed *RGB* array. Same as in *CIECAM02*. + RGB_a = ( + vecmul( + [ + [460, 451, 288], + [460, -891, -261], + [460, -220, -6300], + ], + tstack([P_2, a, b]), + ) + / 1403 ) - -def f_e_inverse(RGB_a: ArrayLike, F_L: ArrayLike) -> NDArrayFloat: - """ - Compute the inverse of the forward eccentricity factor modified cone-like - responses. - - Parameters - ---------- - RGB_a - *CMCCAT2000* transform sharpened :math:`RGB_a` array. - F_L - *Luminance* level adaptation factor :math:`F_L`. - - Returns - ------- - :class:`numpy.ndarray` - Compressed *CMCCAT2000* transform sharpened :math:`RGB_c` array. - - Notes - ----- - - This definition is different from :cite:`Li2017` and provides linear - extensions under 0.26 and above 150. It also omits the 0.1 offset - that is now part of the general model. - - Examples - -------- - >>> RGB_a = np.array([7.8463202, 7.84711528, 7.84899595]) - >>> F_L = 1.16754446415 - >>> f_e_inverse(RGB_a, F_L) - ... # doctest: +ELLIPSIS - array([19.9969397..., 20.0018612..., 20.0135052...]) - """ - - RGB_a = as_float_array(RGB_a) - F_L = as_float_array(F_L) + # Applying inverse post-adaptation non-linear response compression + # via the *CIECAM16* 3-branch piecewise function with linear + # extensions outside the :math:`[0.26, 150]` range. The :math:`-0.1` + # offset removes the *CIECAM16* general model offset before the + # inversion. + RGB_a_p = RGB_a - 0.1 q_L, q_U = 0.26, 150 - - f_q_F_L_q_U = f_q(F_L, q_U)[..., None] - f_q_F_L_q_L = f_q(F_L, q_L)[..., None] - d_f_q_F_L_q_U = d_f_q(F_L, q_U)[..., None] - - return np.select( + F_L_q_L_p = spow((F_L[..., None] * q_L) / 100, 0.42) + f_q_F_L_q_L = (400 * F_L_q_L_p) / (27.13 + F_L_q_L_p) + F_L_q_U_p = spow((F_L[..., None] * q_U) / 100, 0.42) + f_q_F_L_q_U = (400 * F_L_q_U_p) / (27.13 + F_L_q_U_p) + F_L_q_U_lin = (F_L[..., None] * q_U) / 100 + d_f_q_F_L_q_U = (1.68 * 27.13 * F_L[..., None] * spow(F_L_q_U_lin, -0.58)) / ( + 27.13 + spow(F_L_q_U_lin, 0.42) + ) ** 2 + RGB_c = xp_select( [ - RGB_a > f_q_F_L_q_U, - np.logical_and(f_q_F_L_q_L <= RGB_a, RGB_a <= f_q_F_L_q_U), - RGB_a < f_q_F_L_q_L, + RGB_a_p > f_q_F_L_q_U, + xp.logical_and(f_q_F_L_q_L <= RGB_a_p, RGB_a_p <= f_q_F_L_q_U), + RGB_a_p < f_q_F_L_q_L, ], [ - q_U + (RGB_a - f_q_F_L_q_U) / d_f_q_F_L_q_U, - 100 / F_L[..., None] * spow((27.13 * RGB_a) / (400 - RGB_a), 1 / 0.42), - q_L * RGB_a / f_q_F_L_q_L, + q_U + (RGB_a_p - f_q_F_L_q_U) / d_f_q_F_L_q_U, + 100 / F_L[..., None] * spow((27.13 * RGB_a_p) / (400 - RGB_a_p), 1 / 0.42), + q_L * RGB_a_p / f_q_F_L_q_L, ], + xp=xp, ) + # Applying inverse full chromatic adaptation. + RGB = RGB_c / D_RGB -def f_q(F_L: ArrayLike, q: ArrayLike) -> NDArrayFloat: - """ - Evaluate the :math:`f(q)` function for chromatic adaptation. - - Parameters - ---------- - F_L - *Luminance* level adaptation factor :math:`F_L`. - q - :math:`q` parameter. - - Returns - ------- - :class:`numpy.ndarray` - Evaluated :math:`f(q)` function result. - - Examples - -------- - >>> f_q(1.17, 0.26) # doctest: +ELLIPSIS - np.float64(1.2886520...) - """ - - F_L = as_float_array(F_L) - q = as_float_array(q) - - F_L_q_100 = spow((F_L * q) / 100, 0.42) - - return (400 * F_L_q_100) / (27.13 + F_L_q_100) - - -def d_f_q(F_L: ArrayLike, q: ArrayLike) -> NDArrayFloat: - """ - Compute the :math:`f'(q)` function derivative. - - Parameters - ---------- - F_L - *Luminance* level adaptation factor :math:`F_L`. - q - :math:`q` parameter. - - Returns - ------- - :class:`numpy.ndarray` - Evaluated :math:`f'(q)` function derivative. - - Examples - -------- - >>> d_f_q(1.17, 0.26) # doctest: +ELLIPSIS - np.float64(2.0749623...) - """ - - F_L = as_float_array(F_L) - q = as_float_array(q) - - F_L_q_100 = (F_L * q) / 100 + # Converting *CAT16* sharpened *RGB* values back to *CIE XYZ* + # tristimulus values. + XYZ = vecmul(MATRIX_INVERSE_16, RGB) - return (1.68 * 27.13 * F_L * spow(F_L_q_100, -0.58)) / ( - 27.13 + spow(F_L_q_100, 0.42) - ) ** 2 + return from_range_100(XYZ) diff --git a/colour/appearance/hellwig2022.py b/colour/appearance/hellwig2022.py index 54a039eab1..6ac9c7c7d8 100644 --- a/colour/appearance/hellwig2022.py +++ b/colour/appearance/hellwig2022.py @@ -28,40 +28,28 @@ import typing from dataclasses import astuple, dataclass, field -import numpy as np - from colour.algebra import sdiv, sdiv_mode, spow, vecmul from colour.appearance.cam16 import MATRIX_16, MATRIX_INVERSE_16 from colour.appearance.ciecam02 import ( VIEWING_CONDITIONS_CIECAM02, InductionFactors_CIECAM02, - achromatic_response_inverse, - base_exponential_non_linearity, - degree_of_adaptation, - hue_angle, hue_quadrature, - lightness_correlate, - matrix_post_adaptation_non_linear_response_compression, - opponent_colour_dimensions_forward, - post_adaptation_non_linear_response_compression_forward, - post_adaptation_non_linear_response_compression_inverse, ) -from colour.appearance.hunt import luminance_level_adaptation_factor if typing.TYPE_CHECKING: - from colour.hints import Tuple - -from colour.hints import ( # noqa: TC001 - Annotated, - ArrayLike, - Domain100, - NDArrayFloat, - Range100, -) + from colour.hints import ( + Annotated, + ArrayLike, + Domain100, + NDArrayFloat, + Range100, + ) + from colour.utilities import ( CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, from_range_100, @@ -72,6 +60,9 @@ to_domain_degrees, tsplit, tstack, + xp_as_float_array, + xp_degrees, + xp_radians, ) __author__ = "Colour Developers" @@ -87,15 +78,7 @@ "CAM_Specification_Hellwig2022", "XYZ_to_Hellwig2022", "Hellwig2022_to_XYZ", - "viewing_conditions_dependent_parameters", - "achromatic_response_forward", - "opponent_colour_dimensions_inverse", - "eccentricity_factor", - "brightness_correlate", - "colourfulness_correlate", - "chroma_correlate", - "saturation_correlate", - "P_p", + "eccentricity_factor_Hellwig2022", "hue_angle_dependency_Hellwig2022", ] @@ -209,7 +192,7 @@ def XYZ_to_Hellwig2022( InductionFactors_CIECAM02 | InductionFactors_Hellwig2022 ) = VIEWING_CONDITIONS_HELLWIG2022["Average"], discount_illuminant: bool = False, - compute_H: bool = True, + compute_H: bool = False, ) -> Annotated[ CAM_Specification_Hellwig2022, (100, 100, 360, 100, 100, 100, 400, 100, 100) ]: @@ -241,8 +224,10 @@ def XYZ_to_Hellwig2022( discount_illuminant Truth value indicating if the illuminant should be discounted. compute_H - Whether to compute *Hue* :math:`h` quadrature :math:`H`. :math:`H` is - rarely used, and expensive to compute. + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -287,12 +272,13 @@ def XYZ_to_Hellwig2022( Examples -------- + >>> import numpy as np >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_w = np.array([95.05, 100.00, 108.88]) >>> L_A = 318.31 >>> Y_b = 20.0 >>> surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - >>> XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround) + >>> XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) ... # doctest: +ELLIPSIS CAM_Specification_Hellwig2022(J=np.float64(41.7312079...), \ C=np.float64(0.0257636...), h=np.float64(217.0679597...), \ @@ -303,83 +289,107 @@ def XYZ_to_Hellwig2022( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) - _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) - - # Step 0 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. - RGB_w = vecmul(MATRIX_16, XYZ_w) - # Computing degree of adaptation :math:`D`. - D = ( - np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) - if not discount_illuminant - else ones(L_A.shape) - ) + xp = array_namespace(XYZ, XYZ_w, L_A, Y_b) - F_L, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + XYZ = xp_as_float_array(XYZ, xp=xp) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=XYZ) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) - D_RGB = D[..., None] * Y_w[..., None] / RGB_w + 1 - D[..., None] - RGB_wc = D_RGB * RGB_w - - # Applying forward post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - # Computing achromatic responses for the whitepoint. - A_w = achromatic_response_forward(RGB_aw) + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L` + # (same as *Hunt*) and base exponential non-linearity :math:`z` + # (same as *CIECAM02*). + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + z = 1.48 + xp.sqrt(n) - # Step 1 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. + # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values + # using the *CAM16* matrix, for the stimulus and the reference white. RGB = vecmul(MATRIX_16, XYZ) + RGB_w = vecmul(MATRIX_16, XYZ_w) - # Step 2 - RGB_c = D_RGB * RGB - - # Step 3 - # Applying forward post-adaptation non-linear response compression. - RGB_a = post_adaptation_non_linear_response_compression_forward(RGB_c, F_L) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*, clipped to :math:`[0, 1]` and bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ) + else: + F = xp_as_float_array(surround.F, xp=xp, like=XYZ) + D = xp.clip(F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)), 0, 1) - # Step 4 - # Converting to preliminary cartesian coordinates. - a, b = tsplit(opponent_colour_dimensions_forward(RGB_a)) + # Computing full chromatic adaptation, applied to the stimulus and + # the reference white via a shared adaptation factor. + D_RGB = D[..., None] * Y_w[..., None] / RGB_w + 1 - D[..., None] + RGB_c = D_RGB * RGB + RGB_wc = D_RGB * RGB_w - # Computing the *hue* angle :math:`h`. - h = hue_angle(a, b) + # Applying forward post-adaptation non-linear response compression, + # same sign-preserving form as in *CIECAM02* per *Luo (2013)*. + F_L_RGB_c = spow(F_L[..., None] * xp.abs(RGB_c) / 100, 0.42) + RGB_a = (400 * xp.sign(RGB_c) * F_L_RGB_c) / (27.13 + F_L_RGB_c) + 0.1 + F_L_RGB_wc = spow(F_L[..., None] * xp.abs(RGB_wc) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_wc) * F_L_RGB_wc) / (27.13 + F_L_RGB_wc) + 0.1 + + # Computing the opponent colour dimensions :math:`a` and :math:`b`, + # same formulation as in *CIECAM02*. + Ra, Ga, Ba = tsplit(RGB_a) + a = Ra - 12 * Ga / 11 + Ba / 11 + b = (Ra + Ga - 2 * Ba) / 9 + + # Computing the *hue* angle :math:`h` in degrees in + # :math:`[0, 360)`, same as in *CIECAM02*. + h = xp_degrees(xp.atan2(b, a)) % 360 + + e_t = eccentricity_factor_Hellwig2022(h) + + # Computing achromatic responses :math:`A` for the stimulus and + # :math:`A_w` for the whitepoint, using the *Hellwig 2022* weights + # which simplify the *CIECAM02* form to :math:`2R + G + 0.05 B - 0.305`. + A = 2 * Ra + Ga + 0.05 * Ba - 0.305 + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = 2 * Raw + Gaw + 0.05 * Baw - 0.305 + + # Computing the correlate of *Lightness* :math:`J`, same form as + # in *CIECAM02*. + c = surround.c + with sdiv_mode(): + J = 100 * spow(sdiv(A, A_w), c * z) + + # Computing the correlate of *brightness* :math:`Q`. *Hellwig 2022* + # drops the :math:`F_L^{0.25}` term that appears in *CIECAM02*'s + # formulation. + Q = (2 / c) * (J / 100) * A_w + + # Computing the correlate of *colourfulness* :math:`M`, *Hellwig + # 2022* form built directly from the opponent dimensions rather + # than the *CIECAM02* temporary magnitude quantity :math:`t`. + N_c = surround.N_c + M = 43.0 * N_c * e_t * xp.hypot(a, b) + + # Computing the correlate of *chroma* :math:`C` and the correlate + # of *saturation* :math:`s`, *Hellwig 2022* simplifications of the + # *CIECAM02* expressions. + with sdiv_mode(): + C = 35 * sdiv(M, A_w) + s = 100 * sdiv(M, Q) - # Step 5 - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) + # *Helmholtz-Kohlrausch* effect extension: hue angle dependency + # specific to *Hellwig 2022*. + J_HK = J + hue_angle_dependency_Hellwig2022(h) * spow(C, 0.587) + Q_HK = (2 / c) * (J_HK / 100) * A_w - # Computing hue :math:`h` quadrature :math:`H`. - H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan) + # Computing hue :math:`h` quadrature :math:`H` only when requested + # via ``compute_H``; the bin search is shared with *CIECAM02* and + # delegates to :func:`hue_quadrature`. # TODO: Compute hue composition. - - # Step 6 - # Computing achromatic responses for the stimulus. - A = achromatic_response_forward(RGB_a) - - # Step 7 - # Computing the correlate of *Lightness* :math:`J`. - J = lightness_correlate(A, A_w, surround.c, z) - - # Step 8 - # Computing the correlate of *brightness* :math:`Q`. - Q = brightness_correlate(surround.c, J, A_w) - - # Step 9 - # Computing the correlate of *colourfulness* :math:`M`. - M = colourfulness_correlate(surround.N_c, e_t, a, b) - - # Computing the correlate of *chroma* :math:`C`. - C = chroma_correlate(M, A_w) - - # Computing the correlate of *saturation* :math:`s`. - s = saturation_correlate(M, Q) - - # *Helmholtz-Kohlrausch* Effect Extension. - J_HK = J + hue_angle_dependency_Hellwig2022(h) * spow(C, 0.587) - Q_HK = (2 / surround.c) * (J_HK / 100) * A_w + H = hue_quadrature(h) if compute_H else xp.full_like(h, float("nan")) return CAM_Specification_Hellwig2022( J=as_float(from_range_100(J)), @@ -487,6 +497,7 @@ def Hellwig2022_to_XYZ( Examples -------- + >>> import numpy as np >>> specification = CAM_Specification_Hellwig2022( ... J=41.731207905126638, C=0.025763615829912909, h=217.06795976739301 ... ) @@ -512,6 +523,10 @@ def Hellwig2022_to_XYZ( h = to_domain_degrees(h) M = to_domain_100(M) + # *Helmholtz-Kohlrausch* effect extension, inverted: recover the + # plain *Lightness* :math:`J` from :math:`J_{HK}` when only the + # latter has been provided, using the *Hellwig 2022*-specific + # 2-harmonic Fourier hue angle dependency. if has_only_nan(J) and not has_only_nan(J_HK): J_HK = to_domain_100(J_HK) @@ -528,31 +543,60 @@ def Hellwig2022_to_XYZ( L_A = as_float_array(L_A) XYZ_w = to_domain_100(XYZ_w) + + xp = array_namespace(J, C, h, M, XYZ_w, L_A) + + J = xp_as_float_array(J, xp=xp) + C = xp_as_float_array(C, xp=xp, like=J) + h = xp_as_float_array(h, xp=xp, like=J) + M = xp_as_float_array(M, xp=xp, like=J) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=J) + L_A = xp_as_float_array(L_A, xp=xp, like=J) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - # Step 0 - # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values. - RGB_w = vecmul(MATRIX_16, XYZ_w) + # Viewing condition dependent parameters: background induction + # factor :math:`n`, luminance level adaptation factor :math:`F_L` + # (same as *Hunt*) and base exponential non-linearity :math:`z` + # (same as *CIECAM02*). + with sdiv_mode(): + n = sdiv(Y_b, Y_w) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) + z = 1.48 + xp.sqrt(n) - # Computing degree of adaptation :math:`D`. - D = ( - np.clip(degree_of_adaptation(surround.F, L_A), 0, 1) - if not discount_illuminant - else ones(L_A.shape) - ) + # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values + # using the *CAM16* matrix for the reference white. + RGB_w = vecmul(MATRIX_16, XYZ_w) - F_L, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*, clipped to :math:`[0, 1]` and bypassed entirely when + # ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=J) + else: + F = xp_as_float_array(surround.F, xp=xp, like=J) + D = xp.clip(F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)), 0, 1) + # Computing full chromatic adaptation for the reference white. D_RGB = D[..., None] * Y_w[..., None] / RGB_w + 1 - D[..., None] RGB_wc = D_RGB * RGB_w - # Applying forward post-adaptation non-linear response compression. - RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L) + # Applying forward post-adaptation non-linear response compression + # to the whitepoint, same sign-preserving form as in *CIECAM02* + # per *Luo (2013)*. + F_L_RGB_wc = spow(F_L[..., None] * xp.abs(RGB_wc) / 100, 0.42) + RGB_aw = (400 * xp.sign(RGB_wc) * F_L_RGB_wc) / (27.13 + F_L_RGB_wc) + 0.1 - # Computing achromatic responses for the whitepoint. - A_w = achromatic_response_forward(RGB_aw) + # Computing achromatic response :math:`A_w` for the whitepoint, + # *Hellwig 2022* weights. + Raw, Gaw, Baw = tsplit(RGB_aw) + A_w = 2 * Raw + Gaw + 0.05 * Baw - 0.305 - # Step 1 + # Recovering the correlate of *colourfulness* :math:`M` from the + # correlate of *chroma* :math:`C` via the *Hellwig 2022* inverse + # relation, when only :math:`C` has been provided. if has_only_nan(M) and not has_only_nan(C): M = (C * A_w) / 35 elif has_only_nan(M): @@ -563,157 +607,71 @@ def Hellwig2022_to_XYZ( raise ValueError(error) - # Step 2 - # Computing eccentricity factor *e_t*. - e_t = eccentricity_factor(h) - - # Computing achromatic response :math:`A` for the stimulus. - A = achromatic_response_inverse(A_w, J, surround.c, z) + e_t = eccentricity_factor_Hellwig2022(h) - # Computing *P_p_1* to *P_p_2*. - P_p_n = P_p(surround.N_c, e_t, A) - P_p_1, P_p_2 = tsplit(P_p_n) + # Computing achromatic response :math:`A` for the stimulus, + # same inverse form as in *CIECAM02*. + c = surround.c + A = A_w * spow(J / 100, 1 / (c * z)) - # Step 3 - # Computing opponent colour dimensions :math:`a` and :math:`b`. - ab = opponent_colour_dimensions_inverse(P_p_1, h, M) - a, b = tsplit(ab) + # Computing points :math:`P'_1` and :math:`P'_2`, the *Hellwig + # 2022* simplification of *CIECAM02*'s :math:`P_n` triple. + N_c = surround.N_c + P_p_1 = 43 * N_c * e_t + P_p_2 = A - # Step 4 - # Applying post-adaptation non-linear response compression matrix. - RGB_a = matrix_post_adaptation_non_linear_response_compression(P_p_2, a, b) + # Computing opponent colour dimensions :math:`a` and :math:`b` + # from :math:`P'_1`, :math:`h` and :math:`M` via the *Hellwig 2022* + # closed-form rather than *CIECAM02*'s sin/cos-branched inverse. + hr = xp_radians(h) + with sdiv_mode(): + gamma = sdiv(M, P_p_1) + a = gamma * xp.cos(hr) + b = gamma * xp.sin(hr) + + # Applying post-adaptation non-linear response compression matrix, + # same form as in *CIECAM02*. + RGB_a = ( + vecmul( + [ + [460, 451, 288], + [460, -891, -261], + [460, -220, -6300], + ], + tstack([P_p_2, a, b]), + ) + / 1403 + ) - # Step 5 - # Applying inverse post-adaptation non-linear response compression. - RGB_c = post_adaptation_non_linear_response_compression_inverse(RGB_a + 0.1, F_L) + # Applying inverse post-adaptation non-linear response compression, + # same form as in *CIECAM02*. The :math:`+0.1` offset compensates + # for the *Hellwig 2022* formulation of the matrix step. + RGB_a_p = RGB_a + 0.1 + RGB_c = ( + xp.sign(RGB_a_p - 0.1) + * 100 + / F_L[..., None] + * spow( + (27.13 * xp.abs(RGB_a_p - 0.1)) / (400 - xp.abs(RGB_a_p - 0.1)), + 1 / 0.42, + ) + ) - # Step 6 + # Applying inverse full chromatic adaptation. RGB = RGB_c / D_RGB - # Step 7 + # Converting sharpened *RGB* values back to *CIE XYZ* tristimulus + # values using the inverse *CAM16* matrix. XYZ = vecmul(MATRIX_INVERSE_16, RGB) return from_range_100(XYZ) -def viewing_conditions_dependent_parameters( - Y_b: ArrayLike, - Y_w: ArrayLike, - L_A: ArrayLike, -) -> Tuple[NDArrayFloat, NDArrayFloat]: - """ - Compute the viewing condition dependent parameters. - - Parameters - ---------- - Y_b - Adapting field *Y* tristimulus value :math:`Y_b`. - Y_w - Whitepoint *Y* tristimulus value :math:`Y_w`. - L_A - Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - - Returns - ------- - :class:`tuple` - Viewing condition dependent parameters. - - Examples - -------- - >>> viewing_conditions_dependent_parameters(20.0, 100.0, 318.31) - ... # doctest: +ELLIPSIS - (np.float64(1.1675444...), np.float64(1.9272135...)) - """ - - Y_b = as_float_array(Y_b) - Y_w = as_float_array(Y_w) - - with sdiv_mode(): - n = sdiv(Y_b, Y_w) - - F_L = luminance_level_adaptation_factor(L_A) - z = base_exponential_non_linearity(n) - - return F_L, z - - -def achromatic_response_forward(RGB: ArrayLike) -> NDArrayFloat: - """ - Compute the achromatic response :math:`A` from the specified compressed - *CAM16* transform sharpened *RGB* array for forward *Hellwig and Fairchild - (2022)* implementation. - - Parameters - ---------- - RGB - Compressed *CAM16* transform sharpened *RGB* array. - - Returns - ------- - :class:`numpy.ndarray` - Achromatic response :math:`A`. - - Examples - -------- - >>> RGB = np.array([7.94634384, 7.94713791, 7.9488967]) - >>> achromatic_response_forward(RGB) # doctest: +ELLIPSIS - np.float64(23.9322704...) - """ - - R, G, B = tsplit(RGB) - - return 2 * R + G + 0.05 * B - 0.305 - - -def opponent_colour_dimensions_inverse( - P_p_1: ArrayLike, h: ArrayLike, M: ArrayLike -) -> NDArrayFloat: - """ - Compute opponent colour dimensions from the specified point :math:`P'_1`, - hue :math:`h` in degrees and correlate of *colourfulness* :math:`M` for - inverse *Hellwig and Fairchild (2022)* implementation. - - Parameters - ---------- - P_p_1 - Point :math:`P'_1`. - h - Hue :math:`h` in degrees. - M - Correlate of *colourfulness* :math:`M`. - - Returns - ------- - :class:`numpy.ndarray` - Opponent colour dimensions. - - Examples - -------- - >>> P_p_1 = 48.7719436928 - >>> h = 217.067959767393 - >>> M = 0.0387637282462 - >>> opponent_colour_dimensions_inverse(P_p_1, h, M) # doctest: +ELLIPSIS - array([-0.0006341..., -0.0004790...]) - """ - - P_p_1 = as_float_array(P_p_1) - M = as_float_array(M) - - hr = np.radians(h) - - with sdiv_mode(): - gamma = M / P_p_1 - - a = gamma * np.cos(hr) - b = gamma * np.sin(hr) - - return tstack([a, b]) - - -def eccentricity_factor(h: ArrayLike) -> NDArrayFloat: +def eccentricity_factor_Hellwig2022(h: ArrayLike) -> NDArrayFloat: """ Compute the eccentricity factor :math:`e_t` from the specified hue - :math:`h` angle in degrees for forward *CIECAM02* implementation. + :math:`h` angle in degrees for the *Hellwig and Fairchild (2022)* colour + appearance model. Parameters ---------- @@ -725,228 +683,39 @@ def eccentricity_factor(h: ArrayLike) -> NDArrayFloat: :class:`numpy.ndarray` Eccentricity factor :math:`e_t`. - Examples - -------- - >>> eccentricity_factor(217.067959767393) # doctest: +ELLIPSIS - np.float64(0.9945215...) - """ - - h = as_float_array(h) - - hr = np.radians(h) - - _h = hr - _2_h = 2 * hr - _3_h = 3 * hr - _4_h = 4 * hr - - return ( - -0.0582 * np.cos(_h) - - 0.0258 * np.cos(_2_h) - - 0.1347 * np.cos(_3_h) - + 0.0289 * np.cos(_4_h) - - 0.1475 * np.sin(_h) - - 0.0308 * np.sin(_2_h) - + 0.0385 * np.sin(_3_h) - + 0.0096 * np.sin(_4_h) - + 1 - ) - - -def brightness_correlate( - c: ArrayLike, - J: ArrayLike, - A_w: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *brightness* correlate :math:`Q`. - - Parameters - ---------- - c - Surround exponential non-linearity :math:`c`. - J - *Lightness* correlate :math:`J`. - A_w - Achromatic response :math:`A_w` for the whitepoint. - - Returns - ------- - :class:`numpy.ndarray` - *Brightness* correlate :math:`Q`. - - Examples - -------- - >>> c = 0.69 - >>> J = 41.7310911325 - >>> A_w = 46.1741997997 - >>> brightness_correlate(c, J, A_w) # doctest: +ELLIPSIS - np.float64(55.8521663...) - """ - - c = as_float_array(c) - J = as_float_array(J) - A_w = as_float_array(A_w) - - with sdiv_mode(): - return (2 / c) * (J / 100) * A_w - - -def colourfulness_correlate( - N_c: ArrayLike, - e_t: ArrayLike, - a: ArrayLike, - b: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *colourfulness* correlate :math:`M`. - - Parameters - ---------- - N_c - Surround chromatic induction factor :math:`N_c`. - e_t - Eccentricity factor :math:`e_t`. - a - Opponent colour dimension :math:`a`. - b - Opponent colour dimension :math:`b`. - - Returns - ------- - :class:`numpy.ndarray` - *Colourfulness* correlate :math:`M`. - - Examples - -------- - >>> N_c = 1 - >>> e_t = 1.13423124867 - >>> a = -0.00063418423001 - >>> b = -0.000479072513542 - >>> colourfulness_correlate(N_c, e_t, a, b) # doctest: +ELLIPSIS - np.float64(0.0387637...) - """ - - N_c = as_float_array(N_c) - e_t = as_float_array(e_t) - a = as_float_array(a) - b = as_float_array(b) - - return 43.0 * N_c * e_t * np.hypot(a, b) - - -def chroma_correlate( - M: ArrayLike, - A_w: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *chroma* correlate :math:`C`. - - Parameters - ---------- - M - *Colourfulness* correlate :math:`M`. - A_w - Achromatic response :math:`A_w` for the whitepoint. - - Returns - ------- - :class:`numpy.ndarray` - *Chroma* correlate :math:`C`. - - Examples - -------- - >>> M = 0.0387637282462 - >>> A_w = 46.1741997997 - >>> chroma_correlate(M, A_w) # doctest: +ELLIPSIS - np.float64(0.0293828...) - """ - - M = as_float_array(M) - A_w = as_float_array(A_w) - - with sdiv_mode(): - return 35 * sdiv(M, A_w) - - -def saturation_correlate(M: ArrayLike, Q: ArrayLike) -> NDArrayFloat: - """ - Compute the *saturation* correlate :math:`s`. - - Parameters + References ---------- - M - *Colourfulness* correlate :math:`M`. - Q - *Brightness* correlate :math:`Q`. - - Returns - ------- - :class:`numpy.ndarray` - *Saturation* correlate :math:`s`. + :cite:`Hellwig2022` Examples -------- - >>> M = 0.0387637282462 - >>> Q = 55.8523226578 - >>> saturation_correlate(M, Q) # doctest: +ELLIPSIS - np.float64(0.0694039...) - """ - - M = as_float_array(M) - Q = as_float_array(Q) - - with sdiv_mode(): - return 100 * sdiv(M, Q) - - -def P_p( - N_c: ArrayLike, - e_t: ArrayLike, - A: ArrayLike, -) -> NDArrayFloat: + >>> eccentricity_factor_Hellwig2022(217.067959767393) # doctest: +ELLIPSIS + np.float64(0.9945215...) """ - Compute the points :math:`P'_1` and :math:`P'_2`. - Parameters - ---------- - N_c - Surround chromatic induction factor :math:`N_{c}`. - e_t - Eccentricity factor :math:`e_t`. - A - Achromatic response :math:`A` for the stimulus. - - Returns - ------- - :class:`numpy.ndarray` - Points :math:`P'` as an array containing :math:`P'_1` and - :math:`P'_2`. + h = as_float_array(h) - Examples - -------- - >>> N_c = 1 - >>> e_t = 1.13423124867 - >>> A = 23.9322704261 - >>> P_p(N_c, e_t, A) # doctest: +ELLIPSIS - array([48.7719436..., 23.9322704...]) - """ + xp = array_namespace(h) - N_c = as_float_array(N_c) - e_t = as_float_array(e_t) - A = as_float_array(A) + hr = xp_radians(h) - P_p_1 = 43 * N_c * e_t - P_p_2 = A - - return tstack([P_p_1, P_p_2]) + return as_float( + -0.0582 * xp.cos(hr) + - 0.0258 * xp.cos(2 * hr) + - 0.1347 * xp.cos(3 * hr) + + 0.0289 * xp.cos(4 * hr) + - 0.1475 * xp.sin(hr) + - 0.0308 * xp.sin(2 * hr) + + 0.0385 * xp.sin(3 * hr) + + 0.0096 * xp.sin(4 * hr) + + 1 + ) -def hue_angle_dependency_Hellwig2022( - h: ArrayLike, -) -> NDArrayFloat: +def hue_angle_dependency_Hellwig2022(h: ArrayLike) -> NDArrayFloat: """ - Compute the hue angle dependency of the *Helmholtz-Kohlrausch* effect. + Compute the hue angle dependency of the *Helmholtz-Kohlrausch* effect for + the *Hellwig and Fairchild (2022)* colour appearance model. Parameters ---------- @@ -964,19 +733,20 @@ def hue_angle_dependency_Hellwig2022( Examples -------- - >>> hue_angle_dependency_Hellwig2022(217.06795976739301) - ... # doctest: +ELLIPSIS + >>> hue_angle_dependency_Hellwig2022(217.06795976739301) # doctest: +ELLIPSIS np.float64(1.2768219...) """ h = as_float_array(h) - h_r = np.radians(h) + xp = array_namespace(h) + + hr = xp_radians(h) return as_float( - -0.160 * np.cos(h_r) - + 0.132 * np.cos(2 * h_r) - - 0.405 * np.sin(h_r) - + 0.080 * np.sin(2 * h_r) + -0.160 * xp.cos(hr) + + 0.132 * xp.cos(2 * hr) + - 0.405 * xp.sin(hr) + + 0.080 * xp.sin(2 * hr) + 0.792 ) diff --git a/colour/appearance/hke.py b/colour/appearance/hke.py index 2cec2f37a1..2c9f8705ce 100644 --- a/colour/appearance/hke.py +++ b/colour/appearance/hke.py @@ -28,14 +28,20 @@ import typing -import numpy as np - from colour.algebra import spow if typing.TYPE_CHECKING: from colour.hints import ArrayLike, DTypeFloat, Literal, NDArray, NDArrayFloat -from colour.utilities import CanonicalMapping, as_float_array, tsplit, validate_method +from colour.utilities import ( + CanonicalMapping, + array_namespace, + as_float, + as_float_array, + tsplit, + validate_method, + xp_as_float_array, +) __author__ = "Ilia Sibiryakov" __copyright__ = "Copyright 2013 Colour Developers" @@ -117,14 +123,17 @@ def HelmholtzKohlrausch_effect_object_Nayatani1997( array([2.2468383..., 1.4619799..., 1.1801658..., 0.9031318..., 1.7999376...]) """ + xp = array_namespace(uv, uv_c, L_a) + u, v = tsplit(uv) u_c, v_c = tsplit(uv_c) method = validate_method(method, tuple(HKE_NAYATANI1997_METHODS)) + L_a = xp_as_float_array(L_a, xp=xp, like=u) K_Br = coefficient_K_Br_Nayatani1997(L_a) - q = coefficient_q_Nayatani1997(np.arctan2(v - v_c, u - u_c)) - S_uv = 13 * np.sqrt((u - u_c) ** 2 + (v - v_c) ** 2) + q = coefficient_q_Nayatani1997(xp.atan2(v - v_c, u - u_c)) + S_uv = 13 * xp.sqrt((u - u_c) ** 2 + (v - v_c) ** 2) return 1 + (HKE_NAYATANI1997_METHODS[method] * q + 0.0872 * K_Br) * S_uv @@ -222,6 +231,7 @@ def coefficient_q_Nayatani1997( -------- This recreates *FIG. A-1*. + >>> import numpy as np >>> import matplotlib.pyplot as plt >>> angles = [(np.pi * 2 / 100 * i) for i in range(100)] >>> q_values = coefficient_q_Nayatani1997(angles) @@ -233,18 +243,20 @@ def coefficient_q_Nayatani1997( theta = as_float_array(theta) + xp = array_namespace(theta) + theta_2, theta_3, theta_4 = 2 * theta, 3 * theta, 4 * theta return ( -0.01585 - - 0.03017 * np.cos(theta) - - 0.04556 * np.cos(theta_2) - - 0.02667 * np.cos(theta_3) - - 0.00295 * np.cos(theta_4) - + 0.14592 * np.sin(theta) - + 0.05084 * np.sin(theta_2) - - 0.01900 * np.sin(theta_3) - - 0.00764 * np.sin(theta_4) + - 0.03017 * xp.cos(theta) + - 0.04556 * xp.cos(theta_2) + - 0.02667 * xp.cos(theta_3) + - 0.00295 * xp.cos(theta_4) + + 0.14592 * xp.sin(theta) + + 0.05084 * xp.sin(theta_2) + - 0.01900 * xp.sin(theta_3) + - 0.00764 * xp.sin(theta_4) ) @@ -287,6 +299,8 @@ def coefficient_K_Br_Nayatani1997(L_a: ArrayLike) -> DTypeFloat | NDArrayFloat: np.float64(1.0001284...) """ + L_a = as_float_array(L_a) + L_a_4495 = spow(L_a, 0.4495) - return (L_a_4495 * 6.362 + 6.469) * 0.2717 / (L_a_4495 + 6.469) + return as_float((L_a_4495 * 6.362 + 6.469) * 0.2717 / (L_a_4495 + 6.469)) diff --git a/colour/appearance/hunt.py b/colour/appearance/hunt.py index c627ae4291..fce94ae56b 100644 --- a/colour/appearance/hunt.py +++ b/colour/appearance/hunt.py @@ -35,15 +35,17 @@ CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, from_range_degrees, - ones, to_domain_100, tsplit, tstack, usage_warning, - zeros, + xp_as_float_array, + xp_degrees, + xp_interp, ) __author__ = "Colour Developers" @@ -63,25 +65,7 @@ "CAM_Specification_Hunt", "XYZ_to_Hunt", "luminance_level_adaptation_factor", - "illuminant_scotopic_luminance", "XYZ_to_rgb", - "f_n", - "chromatic_adaptation", - "adjusted_reference_white_signals", - "achromatic_post_adaptation_signal", - "colour_difference_signals", - "hue_angle", - "eccentricity_factor", - "low_luminance_tritanopia_factor", - "yellowness_blueness_response", - "redness_greenness_response", - "overall_chromatic_response", - "saturation_correlate", - "achromatic_signal", - "brightness_correlate", - "lightness_correlate", - "chroma_correlate", - "colourfulness_correlate", ] @@ -379,6 +363,13 @@ def XYZ_to_Hunt( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) XYZ_b = to_domain_100(XYZ_b) + + xp = array_namespace(XYZ, XYZ_w, XYZ_b, L_A) + + XYZ = xp_as_float_array(XYZ, xp=xp) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=XYZ) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + _X, Y, _Z = tsplit(XYZ) _X_w, Y_w, _Z_w = tsplit(XYZ_w) X_b, Y_b, _Z_b = tsplit(XYZ_b) @@ -398,9 +389,13 @@ def XYZ_to_Hunt( if surround.N_cb is None: N_cb = 0.725 * spow(Y_w / Y_b, 0.2) usage_warning(f'Unspecified "N_cb" argument, using approximation: "{N_cb}"') + else: + N_cb = surround.N_cb if surround.N_bb is None: N_bb = 0.725 * spow(Y_w / Y_b, 0.2) usage_warning(f'Unspecified "N_bb" argument, using approximation: "{N_bb}"') + else: + N_bb = surround.N_bb if L_AS is None and CCT_w is None: error = ( @@ -411,7 +406,10 @@ def XYZ_to_Hunt( raise ValueError(error) if L_AS is None and CCT_w is not None: - L_AS = illuminant_scotopic_luminance(L_A, CCT_w) + # Approximating scotopic luminance :math:`L_{AS}` from the correlated + # colour temperature :math:`T_{cp}` per *Hunt (2004)*, "The + # Reproduction of Colour", 6th ed., section on scotopic responses. + L_AS = 2.26 * L_A * spow((cast("NDArrayFloat", CCT_w) / 4000) - 0.4, 1 / 3) usage_warning( f'Unspecified "L_AS" argument, using approximation from "CCT": "{L_AS}"' ) @@ -432,6 +430,11 @@ def XYZ_to_Hunt( f'scotopic response "S_w" arguments, using approximation: ' f'"{S}", "{S_w}"' ) + else: + # Both ``S`` and ``S_w`` are non-*None* here: the mixed-*None* case + # raises at the ``ValueError`` guard above. + S_p = xp_as_float_array(cast("ArrayLike", S), xp=xp, like=XYZ) + S_w_p = xp_as_float_array(cast("ArrayLike", S_w), xp=xp, like=XYZ) if p is None: usage_warning( @@ -440,106 +443,172 @@ def XYZ_to_Hunt( "contrast!" ) - XYZ_p = tstack([X_p, Y_p, Z_p]) + XYZ_p = xp_as_float_array(tstack([X_p, Y_p, Z_p]), xp=xp, like=XYZ) # Computing luminance level adaptation factor :math:`F_L`. - F_L = luminance_level_adaptation_factor(L_A) - - # Computing test sample chromatic adaptation. - rgb_a = chromatic_adaptation( - XYZ, - XYZ_w, - XYZ_b, - L_A, - F_L, - XYZ_p, - p, - helson_judd_effect, - discount_illuminant, - ) + k = 1 / (5 * L_A + 1) + k4 = k**4 + F_L = 0.2 * k4 * (5 * L_A) + 0.1 * (1 - k4) ** 2 * spow(5 * L_A, 1 / 3) - # Computing reference white chromatic adaptation. - rgb_aw = chromatic_adaptation( - XYZ_w, - XYZ_w, - XYZ_b, - L_A, - F_L, - XYZ_p, - p, - helson_judd_effect, - discount_illuminant, - ) + # Computing chromatic adaptation common to the stimulus and reference + # white. Only the final cone-response step is computed twice; the + # adaptation factors :math:`h_{rgb}`, :math:`F_{rgb}`, :math:`D_{rgb}` + # and :math:`B_{rgb}` depend only on the white and the viewing + # conditions. + rgb = vecmul(MATRIX_XYZ_TO_HPE, XYZ) + rgb_w = vecmul(MATRIX_XYZ_TO_HPE, XYZ_w) + h_rgb = 3 * rgb_w / xp.sum(rgb_w, axis=-1)[..., None] + if not discount_illuminant: + L_A_p = spow(L_A, 1 / 3) + F_rgb = (1 + L_A_p + h_rgb) / (1 + L_A_p + (1 / h_rgb)) + else: + F_rgb = xp.ones_like(h_rgb) - # Computing opponent colour dimensions. - # Computing achromatic post adaptation signals. - A_a = achromatic_post_adaptation_signal(rgb_a) - A_aw = achromatic_post_adaptation_signal(rgb_aw) + def _f_n(x: NDArrayFloat) -> NDArrayFloat: + x_p = spow(x, 0.73) - # Computing colour difference signals. - C = colour_difference_signals(rgb_a) - C_w = colour_difference_signals(rgb_aw) + return 40 * (x_p / (x_p + 2)) - # ------------------------------------------------------------------------- - # Computing the *hue* angle :math:`h_s`. - # ------------------------------------------------------------------------- - h = hue_angle(C) - # hue_w = hue_angle(C_w) - # TODO: Implement hue quadrature & composition computation. + if helson_judd_effect: + Y_b_Y_w = Y_b / Y_w + Y_b_Y_w_F_L = Y_b_Y_w * F_L + D_rgb = _f_n(Y_b_Y_w_F_L * F_rgb[..., 1]) - _f_n(Y_b_Y_w_F_L * F_rgb) + else: + D_rgb = xp.zeros_like(F_rgb) + B_rgb = 10**7 / (10**7 + 5 * L_A[..., None] * (rgb_w / 100)) - # ------------------------------------------------------------------------- - # Computing the correlate of *saturation* :math:`s`. - # ------------------------------------------------------------------------- - # Computing eccentricity factors. - e_s = eccentricity_factor(h) + # Proximal-/background-adjusted reference white ``rgb_w_adapted`` used as + # the adaptation denominator (*Fairchild (2013)* Eq. 12.23-12.28). ``rgb_w`` + # itself stays unadjusted: only the adaptation reference is adjusted, not + # the white stimulus. + if XYZ_p is not None and p is not None: + p = xp_as_float_array(p, xp=xp, like=XYZ) + rgb_p = vecmul(MATRIX_XYZ_TO_HPE, XYZ_p) + rgb_b = vecmul(MATRIX_XYZ_TO_HPE, XYZ_b) + p_rgb = rgb_p / rgb_b + rgb_w_adapted = ( + rgb_w + * (spow((1 - p) * p_rgb + (1 + p) / p_rgb, 0.5)) + / (spow((1 + p) * p_rgb + (1 - p) / p_rgb, 0.5)) + ) + else: + rgb_w_adapted = rgb_w + + # Final cone-response step for the stimulus and the reference white, + # ``rgb_a = 1 + B_rgb * (f_n(F_L * F_rgb * rgb / rgb_w_adapted) + D_rgb)``. + F_L_F_rgb = F_L[..., None] * F_rgb + rgb_n = F_L_F_rgb * rgb / rgb_w_adapted + rgb_a = 1.0 + B_rgb * (_f_n(rgb_n) + D_rgb) + rgb_w_n = F_L_F_rgb * rgb_w / rgb_w_adapted + rgb_aw = 1.0 + B_rgb * (_f_n(rgb_w_n) + D_rgb) + + # Computing the achromatic post-adaptation signals :math:`A_a` and + # :math:`A_{aw}` from the adapted cone responses. + r_a, g_a, b_a = tsplit(rgb_a) + A_a = 2 * r_a + g_a + (1 / 20) * b_a - 3.05 + 1 + r_aw, g_aw, b_aw = tsplit(rgb_aw) + A_aw = 2 * r_aw + g_aw + (1 / 20) * b_aw - 3.05 + 1 + + # Computing the colour difference signals :math:`C_1`, :math:`C_2`, + # :math:`C_3` from the adapted cone responses for the stimulus and + # the reference white. + C_1 = r_a - g_a + C_2 = g_a - b_a + C_3 = b_a - r_a + C_1_w = r_aw - g_aw + C_2_w = g_aw - b_aw + C_3_w = b_aw - r_aw + + # Computing the *hue* angle :math:`h` in degrees in + # :math:`[0, 360)`. + # TODO: Implement hue quadrature & composition computation. + h = xp_degrees(xp.atan2(0.5 * (C_2 - C_3) / 4.5, C_1 - (C_2 / 11))) % 360 + + # Computing the eccentricity factor :math:`e_s` from the hue + # quadrature table with linear extensions outside the + # :math:`[20.14, 237.53]` range. + h_s = xp_as_float_array(HUE_DATA_FOR_HUE_QUADRATURE["h_s"], xp=xp, like=h) + e_s_lut = xp_as_float_array(HUE_DATA_FOR_HUE_QUADRATURE["e_s"], xp=xp, like=h) + e_s = xp_interp(h, h_s, e_s_lut, xp=xp) + e_s = xp.where(h < 20.14, 0.856 - (h / 20.14) * 0.056, e_s) + e_s = xp.where(h > 237.53, 0.856 + 0.344 * (360 - h) / (360 - 237.53), e_s) + + # Computing the low-luminance tritanopia factor :math:`F_t`. + F_t = L_A / (L_A + 0.1) - # Computing low luminance tritanopia factor :math:`F_t`. - F_t = low_luminance_tritanopia_factor(L_A) + # Computing the yellowness-blueness :math:`M_{yb}` and + # redness-greenness :math:`M_{rg}` responses for the stimulus and + # the reference white. + N_c = surround.N_c + yb_factor = e_s * (10 / 13) * N_c * N_cb + M_yb = 100 * (0.5 * (C_2 - C_3) / 4.5) * yb_factor * F_t + M_rg = 100 * (C_1 - (C_2 / 11)) * yb_factor + M_yb_w = 100 * (0.5 * (C_2_w - C_3_w) / 4.5) * yb_factor * F_t + M_rg_w = 100 * (C_1_w - (C_2_w / 11)) * yb_factor - M_yb = yellowness_blueness_response(C, e_s, surround.N_c, N_cb, F_t) - M_rg = redness_greenness_response(C, e_s, surround.N_c, N_cb) - M_yb_w = yellowness_blueness_response(C_w, e_s, surround.N_c, N_cb, F_t) - M_rg_w = redness_greenness_response(C_w, e_s, surround.N_c, N_cb) + # Computing the overall chromatic response :math:`M`. + M = xp.hypot(M_yb, M_rg) + M_w = xp.hypot(M_yb_w, M_rg_w) - # Computing overall chromatic response. - M = overall_chromatic_response(M_yb, M_rg) - M_w = overall_chromatic_response(M_yb_w, M_rg_w) + # Computing the correlate of *saturation* :math:`s`. + s = 50 * M / xp.sum(rgb_a, axis=-1) + + # Computing achromatic signals :math:`A` and :math:`A_w` from the + # achromatic post-adaptation signals and the scotopic response, + # mediated by the scotopic luminance level adaptation factor + # :math:`F_{LS}` and cone bleach factor :math:`B_S`. + L_AS_226 = cast("NDArrayFloat", L_AS) / 2.26 + j_sc = 0.00001 / ((5 * L_AS_226) + 0.00001) + F_LS = 3800 * (j_sc**2) * (5 * L_AS_226) + 0.2 * (spow(1 - (j_sc**2), 0.4)) * ( + spow(5 * L_AS_226, 1 / 6) + ) - s = saturation_correlate(M, rgb_a) + S_S_w = S_p / S_w_p + B_S = 0.5 / (1 + 0.3 * spow((5 * L_AS_226) * S_S_w, 0.3)) + 0.5 / ( + 1 + 5 * (5 * L_AS_226) + ) + A_S = (_f_n(F_LS * S_S_w) * 3.05 * B_S) + 0.3 + A = N_bb * (A_a - 1 + A_S - 0.3 + spow(1 + (0.3**2), 0.5)) - # ------------------------------------------------------------------------- - # Computing the correlate of *brightness* :math:`Q`. - # ------------------------------------------------------------------------- - # Computing achromatic signal :math:`A`. - A = achromatic_signal(cast("NDArrayFloat", L_AS), S_p, S_w_p, N_bb, A_a) - A_w = achromatic_signal(cast("NDArrayFloat", L_AS), S_w_p, S_w_p, N_bb, A_aw) + S_S_w_w = S_w_p / S_w_p + B_S_w = 0.5 / (1 + 0.3 * spow((5 * L_AS_226) * S_S_w_w, 0.3)) + 0.5 / ( + 1 + 5 * (5 * L_AS_226) + ) + A_S_w = (_f_n(F_LS * S_S_w_w) * 3.05 * B_S_w) + 0.3 + A_w = N_bb * (A_aw - 1 + A_S_w - 0.3 + spow(1 + (0.3**2), 0.5)) - Q = brightness_correlate(A, A_w, M, surround.N_b) - brightness_w = brightness_correlate(A_w, A_w, M_w, surround.N_b) + # Computing the correlate of *brightness* :math:`Q` for the + # stimulus and the reference white. # TODO: Implement whiteness-blackness :math:`Q_{wb}` computation. + N_b = surround.N_b + N_1 = spow(7 * A_w, 0.5) / (5.33 * spow(N_b, 0.13)) + N_2 = (7 * A_w * spow(N_b, 0.362)) / 200 + Q = spow(7 * (A + (M / 100)), 0.6) * N_1 - N_2 + brightness_w = spow(7 * (A_w + (M_w / 100)), 0.6) * N_1 - N_2 - # ------------------------------------------------------------------------- # Computing the correlate of *Lightness* :math:`J`. - # ------------------------------------------------------------------------- - J = lightness_correlate(Y_b, Y_w, Q, brightness_w) + Z = 1 + spow(Y_b / Y_w, 0.5) + J = 100 * spow(Q / brightness_w, Z) - # ------------------------------------------------------------------------- # Computing the correlate of *chroma* :math:`C_{94}`. - # ------------------------------------------------------------------------- - C_94 = chroma_correlate(s, Y_b, Y_w, Q, brightness_w) + Y_b_Y_w_ratio = Y_b / Y_w + C_94 = ( + 2.44 + * spow(s, 0.69) + * spow(Q / brightness_w, Y_b_Y_w_ratio) + * (1.64 - spow(0.29, Y_b_Y_w_ratio)) + ) - # ------------------------------------------------------------------------- # Computing the correlate of *colourfulness* :math:`M_{94}`. - # ------------------------------------------------------------------------- - M_94 = colourfulness_correlate(F_L, C_94) + M_94 = spow(F_L, 0.15) * C_94 return CAM_Specification_Hunt( - J=J, - C=C_94, + J=as_float(J), + C=as_float(C_94), h=as_float(from_range_degrees(h)), - s=s, - Q=Q, - M=M_94, + s=as_float(s), + Q=as_float(Q), + M=as_float(M_94), H=None, HC=None, ) @@ -576,37 +645,6 @@ def luminance_level_adaptation_factor( return as_float(F_L) -def illuminant_scotopic_luminance(L_A: ArrayLike, CCT: ArrayLike) -> NDArrayFloat: - """ - Compute the approximate scotopic luminance :math:`L_{AS}` of the - specified illuminant. - - Parameters - ---------- - L_A - Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - CCT - Correlated colour temperature :math:`T_{cp}` of the illuminant. - - Returns - ------- - :class:`numpy.ndarray` - Approximate scotopic luminance :math:`L_{AS}`. - - Examples - -------- - >>> illuminant_scotopic_luminance(318.31, 6504.0) # doctest: +ELLIPSIS - np.float64(769.9376286...) - """ - - L_A = as_float_array(L_A) - CCT = as_float_array(CCT) - - CCT = 2.26 * L_A * spow((CCT / 4000) - 0.4, 1 / 3) - - return as_float(CCT) - - def XYZ_to_rgb(XYZ: ArrayLike) -> NDArrayFloat: """ Convert from *CIE XYZ* tristimulus values to *Hunt-Pointer-Estevez* @@ -630,751 +668,3 @@ def XYZ_to_rgb(XYZ: ArrayLike) -> NDArrayFloat: """ return vecmul(MATRIX_XYZ_TO_HPE, XYZ) - - -def f_n(x: ArrayLike) -> NDArrayFloat: - """ - Define the nonlinear response function of the *Hunt* colour appearance - model used to model the nonlinear behaviour of various visual responses. - - Parameters - ---------- - x - Visual response variable :math:`x`. - - Returns - ------- - :class:`numpy.ndarray` - Modeled visual response variable :math:`x`. - - Examples - -------- - >>> x = np.array([0.23350512, 0.23351103, 0.23355179]) - >>> f_n(x) # doctest: +ELLIPSIS - array([5.8968592..., 5.8969521..., 5.8975927...]) - """ - - x_p = spow(x, 0.73) - x_m = 40 * (x_p / (x_p + 2)) - - return as_float_array(x_m) - - -def chromatic_adaptation( - XYZ: ArrayLike, - XYZ_w: ArrayLike, - XYZ_b: ArrayLike, - L_A: ArrayLike, - F_L: ArrayLike, - XYZ_p: ArrayLike | None = None, - p: ArrayLike | None = None, - helson_judd_effect: bool = False, - discount_illuminant: bool = True, -) -> NDArrayFloat: - """ - Apply chromatic adaptation to the specified *CIE XYZ* tristimulus values. - - Parameters - ---------- - XYZ - *CIE XYZ* tristimulus values of test sample. - XYZ_b - *CIE XYZ* tristimulus values of background. - XYZ_w - *CIE XYZ* tristimulus values of reference white. - L_A - Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - F_L - Luminance adaptation factor :math:`F_L`. - XYZ_p - *CIE XYZ* tristimulus values of proximal field, assumed to be equal - to background if not specified. - p - Simultaneous contrast/assimilation factor :math:`p` with value - normalised to domain [-1, 0] when simultaneous contrast occurs and - normalised to domain [0, 1] when assimilation occurs. - helson_judd_effect - Truth value indicating whether the *Helson-Judd* effect should be - accounted for. - discount_illuminant - Truth value indicating if the illuminant should be discounted. - - Returns - ------- - :class:`numpy.ndarray` - Adapted *CIE XYZ* tristimulus values. - - Examples - -------- - >>> XYZ = np.array([19.01, 20.00, 21.78]) - >>> XYZ_b = np.array([95.05, 100.00, 108.88]) - >>> XYZ_w = np.array([95.05, 100.00, 108.88]) - >>> L_A = 318.31 - >>> F_L = 1.16754446415 - >>> chromatic_adaptation(XYZ, XYZ_w, XYZ_b, L_A, F_L) # doctest: +ELLIPSIS - array([6.8959454..., 6.8959991..., 6.8965708...]) - - # Coverage Doctests - - >>> chromatic_adaptation( - ... XYZ, XYZ_w, XYZ_b, L_A, F_L, discount_illuminant=False - ... ) # doctest: +ELLIPSIS - array([6.8525880..., 6.8874417..., 6.9461478...]) - >>> chromatic_adaptation( - ... XYZ, XYZ_w, XYZ_b, L_A, F_L, helson_judd_effect=True - ... ) # doctest: +ELLIPSIS - array([6.8959454..., 6.8959991..., 6.8965708...]) - >>> chromatic_adaptation( - ... XYZ, XYZ_w, XYZ_b, L_A, F_L, XYZ_p=XYZ_b, p=0.5 - ... ) # doctest: +ELLIPSIS - array([9.2069020..., 9.2070219..., 9.2078373...]) - """ - - XYZ = as_float_array(XYZ) - XYZ_w = as_float_array(XYZ_w) - XYZ_b = as_float_array(XYZ_b) - L_A = as_float_array(L_A) - F_L = as_float_array(F_L) - - rgb = XYZ_to_rgb(XYZ) - rgb_w = XYZ_to_rgb(XYZ_w) - Y_w = XYZ_w[..., 1] - Y_b = XYZ_b[..., 1] - - h_rgb = 3 * rgb_w / np.sum(rgb_w, axis=-1)[..., None] - - # Computing chromatic adaptation factors. - if not discount_illuminant: - L_A_p = spow(L_A, 1 / 3) - F_rgb = cast("NDArrayFloat", (1 + L_A_p + h_rgb) / (1 + L_A_p + (1 / h_rgb))) - else: - F_rgb = ones(cast("NDArrayFloat", h_rgb).shape) - - # Computing Helson-Judd effect parameters. - if helson_judd_effect: - Y_b_Y_w = Y_b / Y_w - D_rgb = f_n(Y_b_Y_w * F_L * F_rgb[..., 1]) - f_n(Y_b_Y_w * F_L * F_rgb) - else: - D_rgb = zeros(F_rgb.shape) - - # Computing cone bleach factors. - B_rgb = 10**7 / (10**7 + 5 * L_A[..., None] * (rgb_w / 100)) - - # Computing adjusted reference white signals. - if XYZ_p is not None and p is not None: - rgb_p = XYZ_to_rgb(XYZ_p) - rgb_w = adjusted_reference_white_signals(rgb_p, B_rgb, rgb_w, p) - - # Computing adapted cone responses. - return 1.0 + B_rgb * (f_n(F_L[..., None] * F_rgb * rgb / rgb_w) + D_rgb) - - -def adjusted_reference_white_signals( - rgb_p: ArrayLike, - rgb_b: ArrayLike, - rgb_w: ArrayLike, - p: ArrayLike, -) -> NDArrayFloat: - """ - Adjust reference white signals for simultaneous chromatic - contrast/assimilation effects. - - Compute adjusted cone signals in the Hunt-Pointer-Estevez - :math:`\\rho\\gamma\\beta` colourspace based on the proximal field, - background, and simultaneous contrast/assimilation factor. - - Parameters - ---------- - rgb_p - Cone signals *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` - colourspace array of the proximal field. - rgb_b - Cone signals *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` - colourspace array of the background. - rgb_w - Cone signals array *Hunt-Pointer-Estevez* - :math:`\\rho\\gamma\\beta` colourspace array of the reference white. - p - Simultaneous contrast / assimilation factor :math:`p` with value - normalised to domain [-1, 0] when simultaneous contrast occurs and - normalised to domain [0, 1] when assimilation occurs. - - Returns - ------- - :class:`numpy.ndarray` - Adjusted cone signals *Hunt-Pointer-Estevez* - :math:`\\rho\\gamma\\beta` colourspace array of the reference white. - - Examples - -------- - >>> rgb_p = np.array([98.07193550, 101.13755950, 100.00000000]) - >>> rgb_b = np.array([0.99984505, 0.99983840, 0.99982674]) - >>> rgb_w = np.array([97.37325710, 101.54968030, 108.88000000]) - >>> p = 0.1 - >>> adjusted_reference_white_signals(rgb_p, rgb_b, rgb_w, p) - ... # doctest: +ELLIPSIS - array([88.0792742..., 91.8569553..., 98.4876543...]) - """ - - rgb_p = as_float_array(rgb_p) - rgb_b = as_float_array(rgb_b) - rgb_w = as_float_array(rgb_w) - p = as_float_array(p) - - p_rgb = rgb_p / rgb_b - return ( - rgb_w - * (spow((1 - p) * p_rgb + (1 + p) / p_rgb, 0.5)) - / (spow((1 + p) * p_rgb + (1 - p) / p_rgb, 0.5)) - ) - - -def achromatic_post_adaptation_signal(rgb: ArrayLike) -> NDArrayFloat: - """ - Compute the achromatic post adaptation signal :math:`A` from the specified - *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array. - - Parameters - ---------- - rgb - *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array. - - Returns - ------- - :class:`numpy.ndarray` - Achromatic post adaptation signal :math:`A`. - - Examples - -------- - >>> rgb = np.array([6.89594549, 6.89599915, 6.89657085]) - >>> achromatic_post_adaptation_signal(rgb) # doctest: +ELLIPSIS - np.float64(18.9827186...) - """ - - r, g, b = tsplit(rgb) - - return 2 * r + g + (1 / 20) * b - 3.05 + 1 - - -def colour_difference_signals(rgb: ArrayLike) -> NDArrayFloat: - """ - Compute the colour difference signals :math:`C_1`, :math:`C_2` and - :math:`C_3` from the specified *Hunt-Pointer-Estevez* - :math:`\\rho\\gamma\\beta` colourspace array. - - Parameters - ---------- - rgb - *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` colourspace array. - - Returns - ------- - :class:`numpy.ndarray` - Colour difference signals :math:`C_1`, :math:`C_2` and :math:`C_3`. - - Examples - -------- - >>> rgb = np.array([6.89594549, 6.89599915, 6.89657085]) - >>> colour_difference_signals(rgb) # doctest: +ELLIPSIS - array([-5.366...e-05, -5.717...e-04, 6.253...e-04]) - """ - - r, g, b = tsplit(rgb) - - C_1 = r - g - C_2 = g - b - C_3 = b - r - - return tstack([C_1, C_2, C_3]) - - -def hue_angle(C: ArrayLike) -> NDArrayFloat: - """ - Compute the *hue* angle :math:`h` in degrees from the specified colour - difference signals :math:`C`. - - Parameters - ---------- - C - Colour difference signals :math:`C`. - - Returns - ------- - :class:`numpy.ndarray` - *Hue* angle :math:`h` in degrees. - - Examples - -------- - >>> C = np.array([-5.365865581996587e-05, -0.000571699383647, 0.000625358039467]) - >>> hue_angle(C) # doctest: +ELLIPSIS - np.float64(269.2737594...) - """ - - C_1, C_2, C_3 = tsplit(C) - - hue = (180 * np.arctan2(0.5 * (C_2 - C_3) / 4.5, C_1 - (C_2 / 11)) / np.pi) % 360 - - return as_float(hue) - - -def eccentricity_factor(hue: ArrayLike) -> NDArrayFloat: - """ - Compute the eccentricity factor :math:`e_s` from the specified hue angle - :math:`h` in degrees. - - Parameters - ---------- - hue - Hue angle :math:`h` in degrees. - - Returns - ------- - :class:`numpy.ndarray` - Eccentricity factor :math:`e_s`. - - Examples - -------- - >>> eccentricity_factor(269.273759) # doctest: +ELLIPSIS - np.float64(1.1108365...) - """ - - hue = as_float_array(hue) - - h_s = HUE_DATA_FOR_HUE_QUADRATURE["h_s"] - e_s = HUE_DATA_FOR_HUE_QUADRATURE["e_s"] - - x = np.interp(hue, h_s, e_s) - x = np.where(hue < 20.14, 0.856 - (hue / 20.14) * 0.056, x) - x = np.where(hue > 237.53, 0.856 + 0.344 * (360 - hue) / (360 - 237.53), x) - - return as_float(x) - - -def low_luminance_tritanopia_factor( - L_A: ArrayLike, -) -> NDArrayFloat: - """ - Compute the low luminance tritanopia factor :math:`F_t` from the specified - adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - - Parameters - ---------- - L_A - Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. - - Returns - ------- - :class:`numpy.ndarray` - Low luminance tritanopia factor :math:`F_t`. - - Examples - -------- - >>> low_luminance_tritanopia_factor(318.31) # doctest: +ELLIPSIS - np.float64(0.9996859...) - """ - - L_A = as_float_array(L_A) - - F_t = L_A / (L_A + 0.1) - - return as_float(F_t) - - -def yellowness_blueness_response( - C: ArrayLike, - e_s: ArrayLike, - N_c: ArrayLike, - N_cb: ArrayLike, - F_t: ArrayLike, -) -> NDArrayFloat: - """ - Compute the yellowness-blueness response :math:`M_{yb}`. - - Parameters - ---------- - C - Colour difference signals :math:`C`. - e_s - Eccentricity factor :math:`e_s`. - N_c - Chromatic surround induction factor :math:`N_c`. - N_cb - Chromatic background induction factor :math:`N_{cb}`. - F_t - Low luminance tritanopia factor :math:`F_t`. - - Returns - ------- - :class:`numpy.ndarray` - Yellowness-blueness response :math:`M_{yb}`. - - Examples - -------- - >>> C = np.array([-5.365865581996587e-05, -0.000571699383647, 0.000625358039467]) - >>> e_s = 1.110836504862630 - >>> N_c = 1.0 - >>> N_cb = 0.725000000000000 - >>> F_t = 0.99968593951195 - >>> yellowness_blueness_response(C, e_s, N_c, N_cb, F_t) - ... # doctest: +ELLIPSIS - np.float64(-0.0082372...) - """ - - _C_1, C_2, C_3 = tsplit(C) - e_s = as_float_array(e_s) - N_c = as_float_array(N_c) - N_cb = as_float_array(N_cb) - F_t = as_float_array(F_t) - - M_yb = 100 * (0.5 * (C_2 - C_3) / 4.5) * (e_s * (10 / 13) * N_c * N_cb * F_t) - - return as_float(M_yb) - - -def redness_greenness_response( - C: ArrayLike, - e_s: ArrayLike, - N_c: ArrayLike, - N_cb: ArrayLike, -) -> NDArrayFloat: - """ - Compute the redness-greenness response :math:`M_{rg}`. - - Parameters - ---------- - C - Colour difference signals :math:`C`. - e_s - Eccentricity factor :math:`e_s`. - N_c - Chromatic surround induction factor :math:`N_c`. - N_cb - Chromatic background induction factor :math:`N_{cb}`. - - Returns - ------- - :class:`numpy.ndarray` - Redness-greenness response :math:`M_{rg}`. - - Examples - -------- - >>> C = np.array([-5.365865581996587e-05, -0.000571699383647, 0.000625358039467]) - >>> e_s = 1.110836504862630 - >>> N_c = 1.0 - >>> N_cb = 0.725000000000000 - >>> redness_greenness_response(C, e_s, N_c, N_cb) # doctest: +ELLIPSIS - np.float64(-0.0001044...) - """ - - C_1, C_2, _C_3 = tsplit(C) - e_s = as_float_array(e_s) - N_c = as_float_array(N_c) - N_cb = as_float_array(N_cb) - - M_rg = 100 * (C_1 - (C_2 / 11)) * (e_s * (10 / 13) * N_c * N_cb) - - return as_float(M_rg) - - -def overall_chromatic_response(M_yb: ArrayLike, M_rg: ArrayLike) -> NDArrayFloat: - """ - Compute the overall chromatic response :math:`M`. - - Parameters - ---------- - M_yb - Yellowness / blueness response :math:`M_{yb}`. - M_rg - Redness / greenness response :math:`M_{rg}`. - - Returns - ------- - :class:`numpy.ndarray` - Overall chromatic response :math:`M`. - - Examples - -------- - >>> M_yb = -0.008237223618825 - >>> M_rg = -0.000104447583276 - >>> overall_chromatic_response(M_yb, M_rg) # doctest: +ELLIPSIS - np.float64(0.0082378...) - """ - - M_yb = as_float_array(M_yb) - M_rg = as_float_array(M_rg) - - return spow((M_yb**2) + (M_rg**2), 0.5) - - -def saturation_correlate(M: ArrayLike, rgb_a: ArrayLike) -> NDArrayFloat: - """ - Compute the *saturation* correlate :math:`s`. - - Parameters - ---------- - M - Overall chromatic response :math:`M`. - rgb_a - Adapted *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` - colourspace array. - - Returns - ------- - :class:`numpy.ndarray` - *Saturation* correlate :math:`s`. - - Examples - -------- - >>> M = 0.008237885787274 - >>> rgb_a = np.array([6.89594549, 6.89599915, 6.89657085]) - >>> saturation_correlate(M, rgb_a) # doctest: +ELLIPSIS - np.float64(0.0199093...) - """ - - M = as_float_array(M) - rgb_a = as_float_array(rgb_a) - - s = 50 * M / np.sum(rgb_a, axis=-1) - - return as_float(s) - - -def achromatic_signal( - L_AS: ArrayLike, - S: ArrayLike, - S_w: ArrayLike, - N_bb: ArrayLike, - A_a: ArrayLike, -) -> NDArrayFloat: - """ - Compute the achromatic signal :math:`A`. - - Parameters - ---------- - L_AS - Scotopic luminance :math:`L_{AS}` of the illuminant. - S - Scotopic response :math:`S` to the stimulus. - S_w - Scotopic response :math:`S_w` for the reference white. - N_bb - Brightness background induction factor :math:`N_{bb}`. - A_a - Achromatic post adaptation signal of the stimulus :math:`A_a`. - - Returns - ------- - :class:`numpy.ndarray` - Achromatic signal :math:`A`. - - Examples - -------- - >>> L_AS = 769.9376286541402 - >>> S = 20.0 - >>> S_w = 100.0 - >>> N_bb = 0.725000000000000 - >>> A_a = 18.982718664838487 - >>> achromatic_signal(L_AS, S, S_w, N_bb, A_a) # doctest: +ELLIPSIS - np.float64(15.5068546...) - """ - - L_AS = as_float_array(L_AS) - S = as_float_array(S) - S_w = as_float_array(S_w) - N_bb = as_float_array(N_bb) - A_a = as_float_array(A_a) - - L_AS_226 = L_AS / 2.26 - - j = 0.00001 / ((5 * L_AS_226) + 0.00001) - - S_S_w = S / S_w - - # Computing scotopic luminance level adaptation factor :math:`F_{LS}`. - F_LS = 3800 * (j**2) * (5 * L_AS_226) - F_LS += 0.2 * (spow(1 - (j**2), 0.4)) * (spow(5 * L_AS_226, 1 / 6)) - - # Computing cone bleach factors :math:`B_S`. - B_S = 0.5 / (1 + 0.3 * spow((5 * L_AS_226) * S_S_w, 0.3)) - B_S += 0.5 / (1 + 5 * (5 * L_AS_226)) - - # Computing adapted scotopic signal :math:`A_S`. - A_S = (f_n(F_LS * S_S_w) * 3.05 * B_S) + 0.3 - - # Computing achromatic signal :math:`A`. - A = N_bb * (A_a - 1 + A_S - 0.3 + np.sqrt(1 + (0.3**2))) - - return as_float(A) - - -def brightness_correlate( - A: ArrayLike, - A_w: ArrayLike, - M: ArrayLike, - N_b: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *brightness* correlate :math:`Q`. - - Parameters - ---------- - A - Achromatic signal :math:`A`. - A_w - Achromatic post adaptation signal of the reference white :math:`A_w`. - M - Overall chromatic response :math:`M`. - N_b - Brightness surround induction factor :math:`N_b`. - - Returns - ------- - :class:`numpy.ndarray` - *Brightness* correlate :math:`Q`. - - Examples - -------- - >>> A = 15.506854623621885 - >>> A_w = 35.718916676317086 - >>> M = 0.008237885787274 - >>> N_b = 75.0 - >>> brightness_correlate(A, A_w, M, N_b) # doctest: +ELLIPSIS - np.float64(22.2097654...) - """ - - A = as_float_array(A) - A_w = as_float_array(A_w) - M = as_float_array(M) - N_b = as_float_array(N_b) - - N_1 = spow(7 * A_w, 0.5) / (5.33 * spow(N_b, 0.13)) - N_2 = (7 * A_w * spow(N_b, 0.362)) / 200 - - return spow(7 * (A + (M / 100)), 0.6) * N_1 - N_2 - - -def lightness_correlate( - Y_b: ArrayLike, - Y_w: ArrayLike, - Q: ArrayLike, - Q_w: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *lightness* correlate :math:`J`. - - Parameters - ---------- - Y_b - Tristimulus value :math:`Y_b` of the background. - Y_w - Tristimulus value :math:`Y_w` of the reference white. - Q - *Brightness* correlate :math:`Q` of the stimulus. - Q_w - *Brightness* correlate :math:`Q_w` of the reference white. - - Returns - ------- - :class:`numpy.ndarray` - *Lightness* correlate :math:`J`. - - Examples - -------- - >>> Y_b = 100.0 - >>> Y_w = 100.0 - >>> Q = 22.209765491265024 - >>> Q_w = 40.518065821226081 - >>> lightness_correlate(Y_b, Y_w, Q, Q_w) # doctest: +ELLIPSIS - np.float64(30.0462678...) - """ - - Y_b = as_float_array(Y_b) - Y_w = as_float_array(Y_w) - Q = as_float_array(Q) - Q_w = as_float_array(Q_w) - - Z = 1 + spow(Y_b / Y_w, 0.5) - - return 100 * spow(Q / Q_w, Z) - - -def chroma_correlate( - s: ArrayLike, - Y_b: ArrayLike, - Y_w: ArrayLike, - Q: ArrayLike, - Q_w: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *chroma* correlate :math:`C_{94}`. - - Parameters - ---------- - s - *Saturation* correlate :math:`s`. - Y_b - Tristimulus value :math:`Y_b` of the background. - Y_w - Tristimulus value :math:`Y_w` of the reference white. - Q - *Brightness* correlate :math:`Q` of the stimulus. - Q_w - *Brightness* correlate :math:`Q_w` of the reference white. - - Returns - ------- - :class:`numpy.ndarray` - *Chroma* correlate :math:`C_{94}`. - - Examples - -------- - >>> s = 0.0199093206929 - >>> Y_b = 100.0 - >>> Y_w = 100.0 - >>> Q = 22.209765491265024 - >>> Q_w = 40.518065821226081 - >>> chroma_correlate(s, Y_b, Y_w, Q, Q_w) # doctest: +ELLIPSIS - np.float64(0.1210508...) - """ - - s = as_float_array(s) - Y_b = as_float_array(Y_b) - Y_w = as_float_array(Y_w) - Q = as_float_array(Q) - Q_w = as_float_array(Q_w) - - Y_b_Y_w = Y_b / Y_w - - return ( - 2.44 * spow(s, 0.69) * (spow(Q / Q_w, Y_b_Y_w)) * (1.64 - spow(0.29, Y_b_Y_w)) - ) - - -def colourfulness_correlate(F_L: ArrayLike, C_94: ArrayLike) -> NDArrayFloat: - """ - Compute the *colourfulness* correlate :math:`M_{94}`. - - Parameters - ---------- - F_L - Luminance adaptation factor :math:`F_L`. - C_94 - *Chroma* correlate :math:`C_{94}`. - - Returns - ------- - :class:`numpy.ndarray` - *Colourfulness* correlate :math:`M_{94}`. - - Examples - -------- - >>> F_L = 1.16754446414718 - >>> C_94 = 0.121050839936176 - >>> colourfulness_correlate(F_L, C_94) # doctest: +ELLIPSIS - np.float64(0.1238964...) - """ - - F_L = as_float_array(F_L) - C_94 = as_float_array(C_94) - - return spow(F_L, 0.15) * C_94 diff --git a/colour/appearance/kim2009.py b/colour/appearance/kim2009.py index e0e49d436c..5be84e8a8d 100644 --- a/colour/appearance/kim2009.py +++ b/colour/appearance/kim2009.py @@ -25,33 +25,33 @@ from __future__ import annotations +import typing from dataclasses import astuple, dataclass, field -import numpy as np - from colour.adaptation import CAT_CAT02 -from colour.algebra import spow, vecmul +from colour.algebra import sdiv, sdiv_mode, spow, vecmul from colour.appearance.ciecam02 import ( CAT_INVERSE_CAT02, + MATRIX_HPE_TO_XYZ, + MATRIX_XYZ_TO_HPE, VIEWING_CONDITIONS_CIECAM02, - RGB_to_rgb, - degree_of_adaptation, - full_chromatic_adaptation_forward, - full_chromatic_adaptation_inverse, hue_quadrature, - rgb_to_RGB, -) -from colour.hints import ( # noqa: TC001 - Annotated, - ArrayLike, - Domain100, - NDArrayFloat, - Range100, ) + +if typing.TYPE_CHECKING: + from colour.hints import ( + Annotated, + ArrayLike, + Domain100, + NDArrayFloat, + Range100, + ) + from colour.utilities import ( CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, from_range_100, @@ -62,6 +62,9 @@ to_domain_degrees, tsplit, tstack, + xp_as_float_array, + xp_degrees, + xp_radians, ) __author__ = "Colour Developers" @@ -230,7 +233,7 @@ def XYZ_to_Kim2009( surround: InductionFactors_Kim2009 = VIEWING_CONDITIONS_KIM2009["Average"], n_c: float = 0.57, discount_illuminant: bool = False, - compute_H: bool = True, + compute_H: bool = False, ) -> Annotated[CAM_Specification_Kim2009, (100, 100, 360, 100, 100, 100, 400)]: """ Compute the *Kim, Weyrich and Kautz (2009)* colour appearance model @@ -249,13 +252,15 @@ def XYZ_to_Kim2009( Media parameters. surround Surround viewing conditions induction factors. + n_c + Cone response sigmoidal curve modulating factor :math:`n_c`. discount_illuminant Truth value indicating if the illuminant should be discounted. compute_H - Whether to compute *Hue* :math:`h` quadrature :math:`H`. - :math:`H` is rarely used, and expensive to compute. - n_c - Cone response sigmoidal curve modulating factor :math:`n_c`. + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -297,12 +302,13 @@ def XYZ_to_Kim2009( Examples -------- + >>> import numpy as np >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_w = np.array([95.05, 100.00, 108.88]) >>> L_A = 318.31 >>> media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] >>> surround = VIEWING_CONDITIONS_KIM2009["Average"] - >>> XYZ_to_Kim2009(XYZ, XYZ_w, L_A, media, surround) + >>> XYZ_to_Kim2009(XYZ, XYZ_w, L_A, media, surround, compute_H=True) ... # doctest: +ELLIPSIS CAM_Specification_Kim2009(J=np.float64(28.8619089...), C=np.float64(0.5592455...), \ h=np.float64(219.0480667...), s=np.float64(9.3837797...), Q=np.float64(52.7138883...), \ @@ -311,28 +317,44 @@ def XYZ_to_Kim2009( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) + + xp = array_namespace(XYZ, XYZ_w, L_A) + + XYZ = xp_as_float_array(XYZ, xp=xp) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=XYZ) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform # sharpened *RGB* values. RGB = vecmul(CAT_CAT02, XYZ) RGB_w = vecmul(CAT_CAT02, XYZ_w) - # Computing degree of adaptation :math:`D`. - D = ( - degree_of_adaptation(surround.F, L_A) - if not discount_illuminant - else ones(L_A.shape) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*; bypassed entirely when ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ) + else: + F = xp_as_float_array(surround.F, xp=xp, like=XYZ) + D = F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) + + # Computing full chromatic adaptation, same formulation as in + # *CIECAM02*, applied to the stimulus and reference white via a + # shared factor. + with sdiv_mode(): + D_factor = Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None] + XYZ_c = D_factor * RGB + XYZ_wc = D_factor * RGB_w + + # Converting to *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` + # colourspace, same transform as in *CIECAM02*. + MATRIX_XYZ_HPE_x_CAT_INVERSE = xp.matmul( + xp_as_float_array(MATRIX_XYZ_TO_HPE, xp=xp, like=XYZ), + xp_as_float_array(CAT_INVERSE_CAT02, xp=xp, like=XYZ), ) - - # Computing full chromatic adaptation. - XYZ_c = full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D) - XYZ_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D) - - # Converting to *Hunt-Pointer-Estevez* colourspace. - LMS = RGB_to_rgb(XYZ_c) - LMS_w = RGB_to_rgb(XYZ_wc) + LMS = vecmul(MATRIX_XYZ_HPE_x_CAT_INVERSE, XYZ_c) + LMS_w = vecmul(MATRIX_XYZ_HPE_x_CAT_INVERSE, XYZ_wc) # Cones absolute response. LMS_n_c = spow(LMS, n_c) @@ -342,9 +364,9 @@ def XYZ_to_Kim2009( LMS_wp = LMS_w_n_c / (LMS_w_n_c + L_A_n_c) # Achromatic signal :math:`A` and :math:`A_w`. - v_A = np.array([40, 20, 1]) - A = np.sum(v_A * LMS_p, axis=-1) / 61 - A_w = np.sum(v_A * LMS_wp, axis=-1) / 61 + v_A = xp_as_float_array([40, 20, 1], xp=xp, like=LMS_wp) + A = xp.sum(v_A * LMS_p, axis=-1) / 61 + A_w = xp.sum(v_A * LMS_wp, axis=-1) / 61 # Perceived *Lightness* :math:`J_p`. a_j, b_j, o_j, n_j = 0.89, 0.24, 0.65, 3.65 @@ -359,25 +381,29 @@ def XYZ_to_Kim2009( Q = J * spow(Y_w, n_q) # Opponent signals :math:`a` and :math:`b`. - a = (1 / 11) * np.sum(np.array([11, -12, 1]) * LMS_p, axis=-1) - b = (1 / 9) * np.sum(np.array([1, 1, -2]) * LMS_p, axis=-1) + a = (1 / 11) * xp.sum( + xp_as_float_array([11, -12, 1], xp=xp, like=LMS_p) * LMS_p, axis=-1 + ) + b = (1 / 9) * xp.sum( + xp_as_float_array([1, 1, -2], xp=xp, like=LMS_p) * LMS_p, axis=-1 + ) # Computing the correlate of *chroma* :math:`C`. a_k, n_k = 456.5, 0.62 - C = a_k * spow(np.hypot(a, b), n_k) + C = a_k * spow(xp.hypot(a, b), n_k) # Computing the correlate of *colourfulness* :math:`M`. a_m, b_m = 0.11, 0.61 - M = C * (a_m * np.log10(Y_w) + b_m) + M = C * (a_m * xp.log10(Y_w) + b_m) # Computing the correlate of *saturation* :math:`s`. - s = 100 * np.sqrt(M / Q) + s = 100 * xp.sqrt(M / Q) # Computing the *hue* angle :math:`h`. - h = np.degrees(np.arctan2(b, a)) % 360 + h = xp_degrees(xp.atan2(b, a)) % 360 # Computing hue :math:`h` quadrature :math:`H`. - H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan) + H = hue_quadrature(h) if compute_H else xp.full_like(h, float("nan")) return CAM_Specification_Kim2009( J=as_float(from_range_100(J)), @@ -472,6 +498,7 @@ def Kim2009_to_XYZ( Examples -------- + >>> import numpy as np >>> specification = CAM_Specification_Kim2009( ... J=28.861908975839647, C=0.5592455924373706, h=219.04806677662953 ... ) @@ -492,30 +519,48 @@ def Kim2009_to_XYZ( M = to_domain_100(M) L_A = as_float_array(L_A) XYZ_w = to_domain_100(XYZ_w) + + xp = array_namespace(J, C, h, M, XYZ_w, L_A) + + J = xp_as_float_array(J, xp=xp) + C = xp_as_float_array(C, xp=xp, like=J) + h = xp_as_float_array(h, xp=xp, like=J) + M = xp_as_float_array(M, xp=xp, like=J) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=J) + L_A = xp_as_float_array(L_A, xp=xp, like=J) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) # Converting *CIE XYZ* tristimulus values to *CMCCAT2000* transform - # sharpened *RGB* values. + # sharpened *RGB* values for the reference white. RGB_w = vecmul(CAT_CAT02, XYZ_w) - # Computing degree of adaptation :math:`D`. - D = ( - degree_of_adaptation(surround.F, L_A) - if not discount_illuminant - else ones(L_A.shape) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*; bypassed entirely when ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=J) + else: + F = xp_as_float_array(surround.F, xp=xp, like=J) + D = F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) + + # Computing full chromatic adaptation for the reference white, + # same formulation as in *CIECAM02*. The :math:`D_{factor}` value + # is reused on the way out. + with sdiv_mode(): + D_factor = Y_w[..., None] * sdiv(D[..., None], RGB_w) + 1 - D[..., None] + XYZ_wc = D_factor * RGB_w + + # Converting to *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` + # colourspace, same transform as in *CIECAM02*. + MATRIX_XYZ_HPE_x_CAT_INVERSE = xp.matmul( + xp_as_float_array(MATRIX_XYZ_TO_HPE, xp=xp, like=J), + xp_as_float_array(CAT_INVERSE_CAT02, xp=xp, like=J), ) + LMS_w = vecmul(MATRIX_XYZ_HPE_x_CAT_INVERSE, XYZ_wc) - # Computing full chromatic adaptation. - XYZ_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D) - - # Converting to *Hunt-Pointer-Estevez* colourspace. - LMS_w = RGB_to_rgb(XYZ_wc) - - # n_q = 0.1308 - # J = Q / spow(Y_w, n_q) if has_only_nan(C) and not has_only_nan(M): a_m, b_m = 0.11, 0.61 - C = M / (a_m * np.log10(Y_w) + b_m) + C = M / (a_m * xp.log10(Y_w) + b_m) elif has_only_nan(C): error = ( 'Either "C" or "M" correlate must be defined in ' @@ -530,8 +575,8 @@ def Kim2009_to_XYZ( LMS_wp = LMS_w_n_c / (LMS_w_n_c + L_A_n_c) # Achromatic signal :math:`A_w` - v_A = np.array([40, 20, 1]) - A_w = np.sum(v_A * LMS_wp, axis=-1) / 61 + v_A = xp_as_float_array([40, 20, 1], xp=xp, like=LMS_wp) + A_w = xp.sum(v_A * LMS_wp, axis=-1) / 61 # Perceived *Lightness* :math:`J_p`. J_p = (J / 100 - 1) / media.E + 1 @@ -544,25 +589,33 @@ def Kim2009_to_XYZ( # Opponent signals :math:`a` and :math:`b`. a_k, n_k = 456.5, 0.62 C_a_k_n_k = spow(C / a_k, 1 / n_k) - hr = np.radians(h) - a, b = np.cos(hr) * C_a_k_n_k, np.sin(hr) * C_a_k_n_k + hr = xp_radians(h) + a, b = xp.cos(hr) * C_a_k_n_k, xp.sin(hr) * C_a_k_n_k # Cones absolute response. - M = np.array( + M = xp_as_float_array( [ [1.0000, 0.3215, 0.2053], [1.0000, -0.6351, -0.1860], [1.0000, -0.1568, -4.4904], - ] + ], + xp=xp, + like=A, ) LMS_p = vecmul(M, tstack([A, a, b])) LMS = spow((-spow(L_A, n_c) * LMS_p) / (LMS_p - 1), 1 / n_c) - # Converting to *Hunt-Pointer-Estevez* colourspace. - RGB_c = rgb_to_RGB(LMS) + # Converting from *Hunt-Pointer-Estevez* :math:`\\rho\\gamma\\beta` + # colourspace back to adapted *RGB*, same transform as in *CIECAM02*. + CAT_x_MATRIX_HPE = xp.matmul( + xp_as_float_array(CAT_CAT02, xp=xp, like=J), + xp_as_float_array(MATRIX_HPE_TO_XYZ, xp=xp, like=J), + ) + RGB_c = vecmul(CAT_x_MATRIX_HPE, LMS) - # Applying inverse full chromatic adaptation. - RGB = full_chromatic_adaptation_inverse(RGB_c, RGB_w, Y_w, D) + # Applying inverse full chromatic adaptation, reusing the + # :math:`D_{factor}` value precomputed on the forward path. + RGB = RGB_c / D_factor XYZ = vecmul(CAT_INVERSE_CAT02, RGB) diff --git a/colour/appearance/llab.py b/colour/appearance/llab.py index 2372f26a22..a69056ed75 100644 --- a/colour/appearance/llab.py +++ b/colour/appearance/llab.py @@ -28,22 +28,30 @@ from __future__ import annotations +import typing from dataclasses import dataclass, field import numpy as np from colour.algebra import polar_to_cartesian, sdiv, sdiv_mode, spow, vecmul -from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat # noqa: TC001 + +if typing.TYPE_CHECKING: + from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat + from colour.utilities import ( CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, from_range_degrees, to_domain_100, tsplit, tstack, + xp_as_float_array, + xp_degrees, + xp_radians, ) __author__ = "Colour Developers" @@ -61,15 +69,6 @@ "CAM_ReferenceSpecification_LLAB", "CAM_Specification_LLAB", "XYZ_to_LLAB", - "XYZ_to_RGB_LLAB", - "chromatic_adaptation", - "f", - "opponent_colour_dimensions", - "hue_angle", - "chroma_correlate", - "colourfulness_correlate", - "saturation_correlate", - "final_opponent_signals", ] @@ -316,421 +315,99 @@ def XYZ_to_LLAB( Examples -------- + *Fairchild (2013)* Table 14.3 Case 4 (chromatic stimulus under illuminant + A reference white, mesopic luminance): + >>> XYZ = np.array([19.01, 20.00, 21.78]) - >>> XYZ_0 = np.array([95.05, 100.00, 108.88]) + >>> XYZ_0 = np.array([109.85, 100.00, 35.58]) >>> Y_b = 20.0 - >>> L = 318.31 + >>> L = 31.83 >>> surround = VIEWING_CONDITIONS_LLAB["ref_average_4_minus"] >>> XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround) # doctest: +ELLIPSIS - CAM_Specification_LLAB(J=np.float64(37.3668650...), C=np.float64(0.0089496...), \ -h=np.float64(270...), s=np.float64(0.0002395...), M=np.float64(0.0190185...), \ -HC=None, a=np.float64(...), b=np.float64(-0.0190185...)) - """ - - _X, Y, _Z = tsplit(to_domain_100(XYZ)) - RGB = XYZ_to_RGB_LLAB(to_domain_100(XYZ)) - RGB_0 = XYZ_to_RGB_LLAB(to_domain_100(XYZ_0)) - - # Reference illuminant *CIE Standard Illuminant D Series* *D65*. - XYZ_0r = np.array([95.05, 100.00, 108.88]) - RGB_0r = XYZ_to_RGB_LLAB(XYZ_0r) - - # Computing chromatic adaptation. - XYZ_r = chromatic_adaptation(RGB, RGB_0, RGB_0r, Y, surround.D) - - # ------------------------------------------------------------------------- - # Computing the correlate of *Lightness* :math:`L_L`. - # ------------------------------------------------------------------------- - # Computing opponent colour dimensions. - L_L, a, b = tsplit( - opponent_colour_dimensions(XYZ_r, Y_b, surround.F_S, surround.F_L) - ) - - # Computing perceptual correlates. - # ------------------------------------------------------------------------- - # Computing the correlate of *chroma* :math:`Ch_L`. - # ------------------------------------------------------------------------- - Ch_L = chroma_correlate(a, b) - - # ------------------------------------------------------------------------- - # Computing the correlate of *colourfulness* :math:`C_L`. - # ------------------------------------------------------------------------- - C_L = colourfulness_correlate(L, L_L, Ch_L, surround.F_C) - - # ------------------------------------------------------------------------- - # Computing the correlate of *saturation* :math:`s_L`. - # ------------------------------------------------------------------------- - s_L = saturation_correlate(Ch_L, L_L) - - # ------------------------------------------------------------------------- - # Computing the *hue* angle :math:`h_L`. - # ------------------------------------------------------------------------- - h_L = hue_angle(a, b) - # TODO: Implement hue composition computation. - - # ------------------------------------------------------------------------- - # Computing final opponent signals. - # ------------------------------------------------------------------------- - A_L, B_L = tsplit(final_opponent_signals(C_L, h_L)) - - return CAM_Specification_LLAB( - J=L_L, - C=Ch_L, - h=as_float(from_range_degrees(h_L)), - s=s_L, - M=C_L, - HC=None, - a=A_L, - b=B_L, - ) - - -def XYZ_to_RGB_LLAB(XYZ: ArrayLike) -> NDArrayFloat: + CAM_Specification_LLAB(J=np.float64(39.81475...), C=np.float64(29.345046...), \ +h=np.float64(271.852666...), s=np.float64(0.737039...), M=np.float64(54.593098...), \ +HC=None, a=np.float64(1.764967...), b=np.float64(-54.564560...)) """ - Convert from *CIE XYZ* tristimulus values to normalised cone responses. - Parameters - ---------- - XYZ - *CIE XYZ* tristimulus values. + XYZ = to_domain_100(XYZ) + XYZ_0 = to_domain_100(XYZ_0) - Returns - ------- - :class:`numpy.ndarray` - Normalised cone responses. + xp = array_namespace(XYZ, XYZ_0, Y_b, L) - Examples - -------- - >>> XYZ = np.array([19.01, 20.00, 21.78]) - >>> XYZ_to_RGB_LLAB(XYZ) # doctest: +ELLIPSIS - array([0.9414279..., 1.0404012..., 1.0897088...]) - """ - - XYZ = as_float_array(XYZ) + _X, Y, _Z = tsplit(XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) + L = xp_as_float_array(L, xp=xp, like=XYZ) + F_S = xp_as_float_array(surround.F_S, xp=xp, like=XYZ) + F_L = xp_as_float_array(surround.F_L, xp=xp, like=XYZ) + F_C = xp_as_float_array(surround.F_C, xp=xp, like=XYZ) + D = xp_as_float_array(surround.D, xp=xp, like=XYZ) + # Computing normalised cone responses for the stimulus, the reference + # white and the *CIE Standard Illuminant D Series* *D65* reference. with sdiv_mode(): - return vecmul(MATRIX_XYZ_TO_RGB_LLAB, sdiv(XYZ, XYZ[..., 1, None])) - - -def chromatic_adaptation( - RGB: ArrayLike, - RGB_0: ArrayLike, - RGB_0r: ArrayLike, - Y: ArrayLike, - D: ArrayLike = 1, -) -> NDArrayFloat: - """ - Apply chromatic adaptation to the specified *RGB* normalised cone - responses array. - - Parameters - ---------- - RGB - *RGB* normalised cone responses array of the test sample / stimulus. - RGB_0 - *RGB* normalised cone responses array of the reference white. - RGB_0r - *RGB* normalised cone responses array of the reference illuminant - *CIE Standard Illuminant D Series* *D65*. - Y - Tristimulus value :math:`Y` of the stimulus. - D - *Discounting-the-Illuminant* factor normalised to domain [0, 1]. - Default is 1. - - Returns - ------- - :class:`numpy.ndarray` - Adapted *CIE XYZ* tristimulus values. - - Examples - -------- - >>> RGB = np.array([0.94142795, 1.04040120, 1.08970885]) - >>> RGB_0 = np.array([0.94146023, 1.04039386, 1.08950293]) - >>> RGB_0r = np.array([0.94146023, 1.04039386, 1.08950293]) - >>> Y = 20.0 - >>> chromatic_adaptation(RGB, RGB_0, RGB_0r, Y) # doctest: +ELLIPSIS - array([19.01, 20. , 21.78]) - """ + RGB = vecmul(MATRIX_XYZ_TO_RGB_LLAB, sdiv(XYZ, XYZ[..., 1, None])) + RGB_0 = vecmul(MATRIX_XYZ_TO_RGB_LLAB, sdiv(XYZ_0, XYZ_0[..., 1, None])) + XYZ_0r = xp_as_float_array([95.05, 100.00, 108.88], xp=xp, like=XYZ) + RGB_0r = vecmul(MATRIX_XYZ_TO_RGB_LLAB, XYZ_0r / 100) + # Computing chromatic adaptation: cone responses are adapted to the D65 + # reference; the blue channel uses a nonlinear power exponent. R, G, B = tsplit(RGB) R_0, G_0, B_0 = tsplit(RGB_0) R_0r, G_0r, B_0r = tsplit(RGB_0r) - Y = as_float_array(Y) - D = as_float_array(D) - beta = spow(B_0 / B_0r, 0.0834) - - R_r = (D * R_0r / R_0 + 1 - D) * R - G_r = (D * G_0r / G_0 + 1 - D) * G - B_r = (D * B_0r / spow(B_0, beta) + 1 - D) * spow(B, beta) - - RGB_r = tstack([R_r, G_r, B_r]) - - Y = tstack([Y, Y, Y]) - - return vecmul(MATRIX_RGB_TO_XYZ_LLAB, RGB_r * Y) - - -def f(x: ArrayLike, F_S: ArrayLike) -> NDArrayFloat: - """ - Model the nonlinear response function of the *:math:`LLAB(l:c)`* colour - appearance model to simulate the nonlinear behaviour of various visual - responses. - - Parameters - ---------- - x - Visual response variable :math:`x`. - F_S - Surround induction factor :math:`F_S`. - - Returns - ------- - :class:`numpy.ndarray` - Modeled visual response variable :math:`x`. - - Examples - -------- - >>> x = np.array([0.23350512, 0.23351103, 0.23355179]) - >>> f(0.200009186234000, 3) # doctest: +ELLIPSIS - np.float64(0.5848125...) - """ - - x = as_float_array(x) - F_S = as_float_array(F_S) - + R_a = (D * R_0r / R_0 + 1 - D) * R + G_a = (D * G_0r / G_0 + 1 - D) * G + B_a = (D * B_0r / spow(B_0, beta) + 1 - D) * spow(B, beta) + Y_stack = tstack([Y, Y, Y]) + XYZ_r = vecmul(MATRIX_RGB_TO_XYZ_LLAB, tstack([R_a, G_a, B_a]) * Y_stack) + X_r, Y_r, Z_r = tsplit(XYZ_r) + + # Computing the nonlinear visual response :math:`f` on the three normalised + # tristimulus components in a single call. one_F_s = 1 / F_S - - x_m = np.where( - x > 0.008856, - spow(x, one_F_s), - ((spow(0.008856, one_F_s) - (16 / 116)) / 0.008856) * x + (16 / 116), + XYZ_n = tstack([X_r / 95.05, Y_r / 100, Z_r / 108.88]) + f_XYZ = xp.where( + XYZ_n > 0.008856, + spow(XYZ_n, one_F_s), + ((spow(0.008856, one_F_s) - (16 / 116)) / 0.008856) * XYZ_n + (16 / 116), ) + f_X, f_Y, f_Z = tsplit(f_XYZ) - return as_float(x_m) - - -def opponent_colour_dimensions( - XYZ: ArrayLike, - Y_b: ArrayLike, - F_S: ArrayLike, - F_L: ArrayLike, -) -> NDArrayFloat: - """ - Compute opponent colour dimensions from the specified adapted *CIE XYZ* - tristimulus values. - - The opponent colour dimensions are based on a modified *CIE L\\*a\\*b\\** - colourspace formulae. - - Parameters - ---------- - XYZ - Adapted *CIE XYZ* tristimulus values. - Y_b - Luminance factor of the background in :math:`cd/m^2`. - F_S - Surround induction factor :math:`F_S`. - F_L - Lightness induction factor :math:`F_L`. - - Returns - ------- - :class:`numpy.ndarray` - Opponent colour dimensions. - - Examples - -------- - >>> XYZ = np.array([19.00999572, 20.00091862, 21.77993863]) - >>> Y_b = 20.0 - >>> F_S = 3.0 - >>> F_L = 1.0 - >>> opponent_colour_dimensions(XYZ, Y_b, F_S, F_L) # doctest: +ELLIPSIS - array([ 3.7368047...e+01, -4.4986443...e-03, -5.2604647...e-03]) - """ - - X, Y, Z = tsplit(XYZ) - Y_b = as_float_array(Y_b) - F_S = as_float_array(F_S) - F_L = as_float_array(F_L) - - # Account for background lightness contrast. + # Computing opponent colour dimensions: modified *CIE L\\*a\\*b\\** with + # background lightness contrast :math:`z`. z = 1 + F_L * spow(Y_b / 100, 0.5) + L_L = as_float_array(116 * spow(f_Y, z) - 16) + a = 500 * (f_X - f_Y) + b = 200 * (f_Y - f_Z) - # Computing modified *CIE L\\*a\\*b\\** colourspace array. - L = 116 * spow(f(Y / 100, F_S), z) - 16 - a = 500 * (f(X / 95.05, F_S) - f(Y / 100, F_S)) - b = 200 * (f(Y / 100, F_S) - f(Z / 108.88, F_S)) - - return tstack([L, a, b]) - - -def hue_angle(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: - """ - Compute the *hue* angle :math:`h_L` in degrees from the specified - opponent colour dimensions. - - Parameters - ---------- - a - Opponent colour dimension :math:`a`. - b - Opponent colour dimension :math:`b`. - - Returns - ------- - :class:`numpy.ndarray` - *Hue* angle :math:`h_L` in degrees. - - Examples - -------- - >>> hue_angle(-4.49864756e-03, -5.26046353e-03) # doctest: +ELLIPSIS - np.float64(229.4635727...) - """ - - a = as_float_array(a) - b = as_float_array(b) - - h_L = np.degrees(np.arctan2(b, a)) % 360 - - return as_float(h_L) - - -def chroma_correlate(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: - """ - Compute the correlate of *chroma* :math:`Ch_L` from the specified - opponent colour dimensions. - - Parameters - ---------- - a - Opponent colour dimension :math:`a`. - b - Opponent colour dimension :math:`b`. - - Returns - ------- - :class:`numpy.ndarray` - Correlate of *chroma* :math:`Ch_L`. - - Examples - -------- - >>> a = -4.49864756e-03 - >>> b = -5.26046353e-03 - >>> chroma_correlate(a, b) # doctest: +ELLIPSIS - np.float64(0.0086506...) - """ - - a = as_float_array(a) - b = as_float_array(b) - + # Computing the correlate of *chroma* :math:`Ch_L`. c = spow(a**2 + b**2, 0.5) - Ch_L = 25 * np.log1p(0.05 * c) - - return as_float(Ch_L) - - -def colourfulness_correlate( - L: ArrayLike, - L_L: ArrayLike, - Ch_L: ArrayLike, - F_C: ArrayLike, -) -> NDArrayFloat: - """ - Compute the correlate of *colourfulness* :math:`C_L`. - - Parameters - ---------- - L - Absolute luminance :math:`L` of the reference white in - :math:`cd/m^2`. - L_L - Correlate of *Lightness* :math:`L_L`. - Ch_L - Correlate of *chroma* :math:`Ch_L`. - F_C - Chroma induction factor :math:`F_C`. + Ch_L = 25 * xp.log1p(0.05 * c) - Returns - ------- - :class:`numpy.ndarray` - Correlate of *colourfulness* :math:`C_L`. - - Examples - -------- - >>> L = 318.31 - >>> L_L = 37.368047493928195 - >>> Ch_L = 0.008650662051714 - >>> F_C = 1.0 - >>> colourfulness_correlate(L, L_L, Ch_L, F_C) # doctest: +ELLIPSIS - np.float64(0.0183832...) - """ - - L = as_float_array(L) - L_L = as_float_array(L_L) - Ch_L = as_float_array(Ch_L) - F_C = as_float_array(F_C) - - S_C = 1 + 0.47 * np.log10(L) - 0.057 * np.log10(L) ** 2 + # Computing the correlate of *colourfulness* :math:`C_L`. + S_C = 1 + 0.47 * xp.log10(L) - 0.057 * xp.log10(L) ** 2 S_M = 0.7 + 0.02 * L_L - 0.0002 * L_L**2 C_L = Ch_L * S_M * S_C * F_C - return as_float(C_L) - - -def saturation_correlate(Ch_L: ArrayLike, L_L: ArrayLike) -> NDArrayFloat: - """ - Compute the correlate of *saturation* :math:`S_L`. - - Parameters - ---------- - Ch_L - Correlate of *chroma* :math:`Ch_L`. - L_L - Correlate of *lightness* :math:`L_L`. - - Returns - ------- - :class:`numpy.ndarray` - Correlate of *saturation* :math:`S_L`. - - Examples - -------- - >>> Ch_L = 0.008650662051714 - >>> L_L = 37.368047493928195 - >>> saturation_correlate(Ch_L, L_L) # doctest: +ELLIPSIS - np.float64(0.0002314...) - """ - - Ch_L = as_float_array(Ch_L) - L_L = as_float_array(L_L) - - return Ch_L / L_L - - -def final_opponent_signals(C_L: ArrayLike, h_L: ArrayLike) -> NDArrayFloat: - """ - Compute the final opponent signals :math:`A_L` and :math:`B_L`. - - Parameters - ---------- - C_L - Correlate of *colourfulness* :math:`C_L`. - h_L - Correlate of *hue* :math:`h_L` in degrees. + # Computing the correlate of *saturation* :math:`s_L`. + s_L = Ch_L / L_L - Returns - ------- - :class:`numpy.ndarray` - Final opponent signals :math:`A_L` and :math:`B_L`. + # Computing the *hue* angle :math:`h_L` in degrees. + h_L = xp_degrees(xp.atan2(b, a)) % 360 + # TODO: Implement hue composition computation. - Examples - -------- - >>> C_L = 0.0183832899143 - >>> h_L = 229.46357270858391 - >>> final_opponent_signals(C_L, h_L) # doctest: +ELLIPSIS - array([-0.0119478..., -0.0139711...]) - """ + # Computing the final opponent signals :math:`A_L`, :math:`B_L` from + # polar coordinates :math:`(C_L, h_L)`. + A_L, B_L = tsplit(polar_to_cartesian(tstack([C_L, xp_radians(h_L)]))) - return polar_to_cartesian(tstack([as_float_array(C_L), np.radians(h_L)])) + return CAM_Specification_LLAB( + J=as_float(L_L), + C=as_float(Ch_L), + h=as_float(from_range_degrees(h_L)), + s=as_float(s_L), + M=as_float(C_L), + HC=None, + a=as_float(A_L), + b=as_float(B_L), + ) diff --git a/colour/appearance/nayatani95.py b/colour/appearance/nayatani95.py index 0e3579fadf..972f2544d9 100644 --- a/colour/appearance/nayatani95.py +++ b/colour/appearance/nayatani95.py @@ -24,8 +24,6 @@ import typing from dataclasses import dataclass, field -import numpy as np - from colour.adaptation.cie1994 import ( MATRIX_XYZ_TO_RGB_CIE1994, beta_1, @@ -41,12 +39,16 @@ from colour.models import XYZ_to_xy from colour.utilities import ( MixinDataclassArithmetic, + array_namespace, as_float, as_float_array, from_range_degrees, to_domain_100, tsplit, - tstack, + xp_as_float_array, + xp_degrees, + xp_radians, + xp_select, ) __author__ = "Colour Developers" @@ -61,24 +63,7 @@ "CAM_ReferenceSpecification_Nayatani95", "CAM_Specification_Nayatani95", "XYZ_to_Nayatani95", - "illuminance_to_luminance", - "XYZ_to_RGB_Nayatani95", - "scaling_coefficient", - "achromatic_response", - "tritanopic_response", - "protanopic_response", - "brightness_correlate", - "ideal_white_brightness_correlate", - "achromatic_lightness_correlate", - "normalised_achromatic_lightness_correlate", - "hue_angle", - "saturation_components", - "saturation_correlate", - "chroma_components", - "chroma_correlate", - "colourfulness_components", - "colourfulness_correlate", - "chromatic_strength_function", + "hue_quadrature", ] MATRIX_XYZ_TO_RGB_NAYATANI95: NDArrayFloat = MATRIX_XYZ_TO_RGB_CIE1994 @@ -194,6 +179,7 @@ def XYZ_to_Nayatani95( E_o: ArrayLike, E_or: ArrayLike, n: ArrayLike = 1, + compute_H: bool = False, ) -> Annotated[CAM_Specification_Nayatani95, 360]: """ Compute the *Nayatani (1995)* colour appearance model correlates from the @@ -216,6 +202,11 @@ def XYZ_to_Nayatani95( domain [1000, 3000]. n Noise term used in the non-linear chromatic adaptation model. + compute_H + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -244,787 +235,218 @@ def XYZ_to_Nayatani95( Examples -------- + *Fairchild (2013)* Table 11.1 Case 1 (near-grey stimulus, photopic): + + >>> import numpy as np >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_n = np.array([95.05, 100.00, 108.88]) >>> Y_o = 20.0 >>> E_o = 5000.0 >>> E_or = 1000.0 - >>> XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or) # doctest: +ELLIPSIS + >>> XYZ_to_Nayatani95( + ... XYZ, XYZ_n, Y_o, E_o, E_or, + ... compute_H=True, + ... ) # doctest: +ELLIPSIS CAM_Specification_Nayatani95(L_star_P=np.float64(49.9998829...), \ C=np.float64(0.0133550...), h=np.float64(257.5232268...), \ s=np.float64(0.0133550...), Q=np.float64(62.6266734...), \ -M=np.float64(0.0167262...), H=None, HC=None, L_star_N=np.float64(50.0039154...)) +M=np.float64(0.0167262...), H=np.float64(317.7841135...), HC=None, \ +L_star_N=np.float64(50.0039154...)) + + *Fairchild (2013)* Table 11.1 Case 2 (chromatic stimulus, lower + illuminance): + + >>> XYZ_to_Nayatani95(np.array([57.06, 43.06, 31.96]), XYZ_n, 20.0, \ +500.0, 1000.0, compute_H=True) # doctest: +ELLIPSIS + CAM_Specification_Nayatani95(L_star_P=np.float64(72.9768964...), \ +C=np.float64(48.3460111...), h=np.float64(21.5766539...), \ +s=np.float64(37.1030727...), Q=np.float64(67.3493717...), \ +M=np.float64(42.9012040...), H=np.float64(2.0564758...), HC=None, \ +L_star_N=np.float64(75.8970185...)) """ XYZ = to_domain_100(XYZ) XYZ_n = to_domain_100(XYZ_n) - Y_o = as_float_array(Y_o) - E_o = as_float_array(E_o) - E_or = as_float_array(E_or) - # Computing adapting luminance :math:`L_o` and normalising luminance - # :math:`L_{or}` in in :math:`cd/m^2`. - # L_o = illuminance_to_luminance(E_o, Y_o) - L_or = illuminance_to_luminance(E_or, Y_o) + xp = array_namespace(XYZ, XYZ_n, Y_o, E_o, E_or) + + Y_o = xp_as_float_array(Y_o, xp=xp, like=XYZ) + E_o = xp_as_float_array(E_o, xp=xp, like=XYZ) + E_or = xp_as_float_array(E_or, xp=xp, like=XYZ) + n = xp_as_float_array(n, xp=xp, like=XYZ) + + # Computing normalising luminance :math:`L_{or}` in :math:`cd/m^2`. + L_or = Y_o * E_or / (100 * xp.pi) - # Computing :math:`\\xi` :math:`\\eta`, :math:`\\zeta` values. + # Computing :math:`\\xi`, :math:`\\eta`, :math:`\\zeta` values from the + # reference white chromaticity (*CIE 1994* chromatic adaptation primitive). xez = intermediate_values(XYZ_to_xy(XYZ_n / 100)) - xi, eta, _zeta = tsplit(xez) + xi, eta, zeta = tsplit(xez) # Computing adapting field cone responses. - RGB_o = ((Y_o[..., None] * E_o[..., None]) / (100 * np.pi)) * xez + RGB_o = ((Y_o[..., None] * E_o[..., None]) / (100 * xp.pi)) * xez # Computing stimulus cone responses. - RGB = XYZ_to_RGB_Nayatani95(XYZ) - R, G, _B = tsplit(RGB) - - # Computing exponential factors of the chromatic adaptation. - bRGB_o = exponential_factors(RGB_o) - bL_or = beta_1(L_or) - - # Computing scaling coefficients :math:`e(R)` and :math:`e(G)` - eR = scaling_coefficient(R, xi) - eG = scaling_coefficient(G, eta) - - # Computing opponent colour dimensions. - # Computing achromatic response :math:`Q`: - Q_response = achromatic_response(RGB, bRGB_o, xez, bL_or, eR, eG, n) - - # Computing tritanopic response :math:`t`: - t_response = tritanopic_response(RGB, bRGB_o, xez, n) + RGB = vecmul(MATRIX_XYZ_TO_RGB_NAYATANI95, XYZ) + R, G, B = tsplit(RGB) - # Computing protanopic response :math:`p`: - p_response = protanopic_response(RGB, bRGB_o, xez, n) + # Computing exponential factors :math:`\\beta_1(R_o)`, + # :math:`\\beta_1(G_o)`, :math:`\\beta_2(B_o)` and the normalising + # :math:`\\beta_1(B_{or})` (*CIE 1994* chromatic adaptation primitives). + # Cast back to the active backend dtype for consistent propagation. + bRGB_o = xp_as_float_array(exponential_factors(RGB_o), xp=xp, like=XYZ) + bR_o, bG_o, bB_o = tsplit(bRGB_o) + bL_or = xp_as_float_array(beta_1(L_or), xp=xp, like=XYZ) + + # Computing scaling coefficients :math:`e(R)` and :math:`e(G)`: 1.758 when + # the cone response exceeds 20 times the intermediate value, otherwise 1. + # ``xp.where`` promotes the *Python* float / int branches to *float64*; + # cast back to keep the active backend dtype. + eR = xp_as_float_array(xp.where((20 * xi) <= R, 1.758, 1), xp=xp, like=XYZ) + eG = xp_as_float_array(xp.where((20 * eta) <= G, 1.758, 1), xp=xp, like=XYZ) + + # Computing the logarithmic cone-response terms shared by the achromatic, + # tritanopic and protanopic opponent responses. + log_R = xp.log10((R + n) / (20 * xi + n)) + log_G = xp.log10((G + n) / (20 * eta + n)) + log_B = xp.log10((B + n) / (20 * zeta + n)) + + # Computing achromatic response :math:`Q` as a weighted combination of the + # logarithmic red and green opponent terms. + Q_response = (2 / 3) * bR_o * eR * log_R + Q_response += (1 / 3) * bG_o * eG * log_G + Q_response *= 41.69 / bL_or + + # Computing tritanopic response :math:`t`. + t_response = bR_o * log_R - (12 / 11) * bG_o * log_G + (1 / 11) * bB_o * log_B + + # Computing protanopic response :math:`p`. + p_response = ( + (1 / 9) * bR_o * log_R + (1 / 9) * bG_o * log_G - (2 / 9) * bB_o * log_B + ) # Computing the correlate of *brightness* :math:`B_r`. - B_r = brightness_correlate(bRGB_o, bL_or, Q_response) + B_r = (50 / bL_or) * ((2 / 3) * bR_o + (1 / 3) * bG_o) + Q_response # Computing *brightness* :math:`B_{rw}` of ideal white. - brightness_ideal_white = ideal_white_brightness_correlate(bRGB_o, xez, bL_or, n) + B_rw = (2 / 3) * bR_o * 1.758 * xp.log10((100 * xi + n) / (20 * xi + n)) + B_rw += (1 / 3) * bG_o * 1.758 * xp.log10((100 * eta + n) / (20 * eta + n)) + B_rw *= 41.69 / bL_or + B_rw += (50 / bL_or) * (2 / 3) * bR_o + B_rw += (50 / bL_or) * (1 / 3) * bG_o # Computing the correlate of achromatic *Lightness* :math:`L_p^\\star`. - L_star_P = achromatic_lightness_correlate(Q_response) + L_star_P = Q_response + 50 # Computing the correlate of normalised achromatic *Lightness* # :math:`L_n^\\star`. - L_star_N = normalised_achromatic_lightness_correlate(B_r, brightness_ideal_white) - - # Computing the *hue* angle :math:`\\theta`. - theta = hue_angle(p_response, t_response) - # TODO: Implement hue quadrature & composition computation. - - # Computing the correlate of *saturation* :math:`S`. - S_RG, S_YB = tsplit(saturation_components(theta, bL_or, t_response, p_response)) - S = saturation_correlate(S_RG, S_YB) + L_star_N = 100 * B_r / B_rw + + # Computing the *hue* angle :math:`\\theta` in degrees from the protanopic + # and tritanopic responses. + theta = xp_degrees(xp.atan2(p_response, t_response)) % 360 + # Computing the *hue* :math:`h` quadrature :math:`H` only when requested + # via ``compute_H``; the bin search delegates to :func:`hue_quadrature`, + # a 400-step linear interpolation between unique-hue angles 20.14, 90.00, + # 164.25, 231.00 per *Fairchild (2013)* p.202. + H = hue_quadrature(theta) if compute_H else xp.full_like(theta, float("nan")) + # TODO: Implement hue composition computation. + + # Computing the chromatic strength function :math:`E_s(\\theta)` used to + # correct the saturation scale as a function of hue angle. + theta_rad = xp_radians(theta) + E_s = cast("NDArrayFloat", 0.9394) + E_s += -0.2478 * xp.sin(1 * theta_rad) + E_s += -0.0743 * xp.sin(2 * theta_rad) + E_s += +0.0666 * xp.sin(3 * theta_rad) + E_s += -0.0186 * xp.sin(4 * theta_rad) + E_s += -0.0055 * xp.cos(1 * theta_rad) + E_s += -0.0521 * xp.cos(2 * theta_rad) + E_s += -0.0573 * xp.cos(3 * theta_rad) + E_s += -0.0061 * xp.cos(4 * theta_rad) + + # Computing *saturation* components :math:`S_{RG}` and :math:`S_{YB}` and + # the *saturation* correlate :math:`S`. + S_RG = 488.93 / bL_or * E_s * t_response + S_YB = 488.93 / bL_or * E_s * p_response + S = xp.hypot(S_RG, S_YB) # Computing the correlate of *chroma* :math:`C`. - # C_RG, C_YB = tsplit(chroma_components(L_star_P, S_RG, S_YB)) - C = chroma_correlate(L_star_P, S) + C = spow(L_star_P / 50, 0.7) * S # Computing the correlate of *colourfulness* :math:`M`. # TODO: Investigate components usage. - # M_RG, M_YB = tsplit(colourfulness_components(C_RG, C_YB, - # brightness_ideal_white)) - M = colourfulness_correlate(C, brightness_ideal_white) + M = C * B_rw / 100 return CAM_Specification_Nayatani95( - L_star_P=L_star_P, - C=C, + L_star_P=as_float(L_star_P), + C=as_float(C), h=as_float(from_range_degrees(theta)), - s=S, - Q=B_r, - M=M, - H=None, + s=as_float(S), + Q=as_float(B_r), + M=as_float(M), + H=as_float(from_range_degrees(H, 400)), HC=None, - L_star_N=L_star_N, - ) - - -def illuminance_to_luminance(E: ArrayLike, Y_f: ArrayLike) -> NDArrayFloat: - """ - Convert the specified *illuminance* :math:`E` value in lux to *luminance* - :math:`Y` in :math:`cd/m^2`. - - Parameters - ---------- - E - *Illuminance* :math:`E` in lux. - Y_f - *Luminance* factor :math:`Y_f` in :math:`cd/m^2`. - - Returns - ------- - :class:`numpy.ndarray` - *Luminance* :math:`Y` in :math:`cd/m^2`. - - Examples - -------- - >>> illuminance_to_luminance(5000.0, 20.0) # doctest: +ELLIPSIS - np.float64(318.3098861...) - """ - - E = as_float_array(E) - Y_f = as_float_array(Y_f) - - return Y_f * E / (100 * np.pi) - - -def XYZ_to_RGB_Nayatani95(XYZ: ArrayLike) -> NDArrayFloat: - """ - Convert *CIE XYZ* tristimulus values to cone responses. - - Parameters - ---------- - XYZ - *CIE XYZ* tristimulus values. - - Returns - ------- - :class:`numpy.ndarray` - Cone responses. - - Examples - -------- - >>> XYZ = np.array([19.01, 20.00, 21.78]) - >>> XYZ_to_RGB_Nayatani95(XYZ) # doctest: +ELLIPSIS - array([20.0005206..., 19.999783..., 19.9988316...]) - """ - - return vecmul(MATRIX_XYZ_TO_RGB_NAYATANI95, XYZ) - - -def scaling_coefficient(x: ArrayLike, y: ArrayLike) -> NDArrayFloat: - """ - Compute the scaling coefficient :math:`e(R)` or :math:`e(G)`. - - Parameters - ---------- - x - Cone response. - y - Intermediate value. - - Returns - ------- - :class:`numpy.ndarray` - Scaling coefficient :math:`e(R)` or :math:`e(G)`. - - Examples - -------- - >>> x = 20.000520600000002 - >>> y = 1.000042192 - >>> scaling_coefficient(x, y) - np.float64(1.0) - """ - - x = as_float_array(x) - y = as_float_array(y) - - return as_float(np.where(x >= (20 * y), 1.758, 1)) - - -def achromatic_response( - RGB: ArrayLike, - bRGB_o: ArrayLike, - xez: ArrayLike, - bL_or: ArrayLike, - eR: ArrayLike, - eG: ArrayLike, - n: ArrayLike = 1, -) -> NDArrayFloat: - """ - Compute the achromatic response :math:`Q` from the specified stimulus - cone responses. - - Parameters - ---------- - RGB - Stimulus cone responses. - bRGB_o - Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`, - :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`. - xez - Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`. - bL_or - Normalising chromatic adaptation exponential factor - :math:`\\beta_1(B_{or})`. - eR - Scaling coefficient :math:`e(R)`. - eG - Scaling coefficient :math:`e(G)`. - n - Noise term used in the non-linear chromatic adaptation model. - - Returns - ------- - :class:`numpy.ndarray` - Achromatic response :math:`Q`. - - Examples - -------- - >>> RGB = np.array([20.00052060, 19.99978300, 19.99883160]) - >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) - >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) - >>> bL_or = 3.681021495604089 - >>> eR = 1.0 - >>> eG = 1.758 - >>> n = 1.0 - >>> achromatic_response(RGB, bRGB_o, xez, bL_or, eR, eG, n) - ... # doctest: +ELLIPSIS - np.float64(-0.0001169...) - """ - - R, G, _B = tsplit(RGB) - bR_o, bG_o, _bB_o = tsplit(bRGB_o) - xi, eta, _zeta = tsplit(xez) - bL_or = as_float_array(bL_or) - eR = as_float_array(eR) - eG = as_float_array(eG) - - Q = (2 / 3) * bR_o * eR * np.log10((R + n) / (20 * xi + n)) - Q += (1 / 3) * bG_o * eG * np.log10((G + n) / (20 * eta + n)) - Q *= 41.69 / bL_or - - return as_float(Q) - - -def tritanopic_response( - RGB: ArrayLike, bRGB_o: ArrayLike, xez: ArrayLike, n: ArrayLike -) -> NDArrayFloat: - """ - Compute the tritanopic response :math:`t` from the specified stimulus cone - responses. - - Parameters - ---------- - RGB - Stimulus cone responses. - bRGB_o - Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`, - :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`. - xez - Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`. - n - Noise term used in the non-linear chromatic adaptation model. - - Returns - ------- - :class:`numpy.ndarray` - Tritanopic response :math:`t`. - - Examples - -------- - >>> RGB = np.array([20.00052060, 19.99978300, 19.99883160]) - >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) - >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) - >>> n = 1.0 - >>> tritanopic_response(RGB, bRGB_o, xez, n) # doctest: +ELLIPSIS - np.float64(-1.7703650...e-05) - """ - - R, G, B = tsplit(RGB) - bR_o, bG_o, bB_o = tsplit(bRGB_o) - xi, eta, zeta = tsplit(xez) - - t = ( - bR_o * np.log10((R + n) / (20 * xi + n)) - - (12 / 11) * bG_o * np.log10((G + n) / (20 * eta + n)) - + (1 / 11) * bB_o * np.log10((B + n) / (20 * zeta + n)) + L_star_N=as_float(L_star_N), ) - return as_float(t) - -def protanopic_response( - RGB: ArrayLike, bRGB_o: ArrayLike, xez: ArrayLike, n: ArrayLike -) -> NDArrayFloat: +def hue_quadrature(h: ArrayLike) -> NDArrayFloat: """ - Compute the protanopic response :math:`p` from the specified stimulus cone - responses. - - Parameters - ---------- - RGB - Stimulus cone responses. - bRGB_o - Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`, - :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`. - xez - Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`. - n - Noise term used in the non-linear chromatic adaptation model. - - Returns - ------- - :class:`numpy.ndarray` - Protanopic response :math:`p`. - - Examples - -------- - >>> RGB = np.array([20.00052060, 19.99978300, 19.99883160]) - >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) - >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) - >>> n = 1.0 - >>> protanopic_response(RGB, bRGB_o, xez, n) # doctest: +ELLIPSIS - np.float64(-8.0021426...e-05) - """ - - R, G, B = tsplit(RGB) - bR_o, bG_o, bB_o = tsplit(bRGB_o) - xi, eta, zeta = tsplit(xez) - - p = (1 / 9) * bR_o * np.log10((R + n) / (20 * xi + n)) - p += (1 / 9) * bG_o * np.log10((G + n) / (20 * eta + n)) - p += -(2 / 9) * bB_o * np.log10((B + n) / (20 * zeta + n)) - - return as_float(p) - - -def brightness_correlate( - bRGB_o: ArrayLike, bL_or: ArrayLike, Q: ArrayLike -) -> NDArrayFloat: - """ - Compute the *brightness* correlate :math:`B_r`. - - Parameters - ---------- - bRGB_o - Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`, - :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`. - bL_or - Normalising chromatic adaptation exponential factor - :math:`\\beta_1(B_{or})`. - Q - Achromatic response :math:`Q`. - - Returns - ------- - :class:`numpy.ndarray` - *Brightness* correlate :math:`B_r`. - - Examples - -------- - >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) - >>> bL_or = 3.681021495604089 - >>> Q = -0.000117024294955 - >>> brightness_correlate(bRGB_o, bL_or, Q) # doctest: +ELLIPSIS - np.float64(62.6266734...) - """ - - bR_o, bG_o, _bB_o = tsplit(bRGB_o) - bL_or = as_float_array(bL_or) - Q = as_float_array(Q) - - B_r = (50 / bL_or) * ((2 / 3) * bR_o + (1 / 3) * bG_o) + Q - - return as_float(B_r) - - -def ideal_white_brightness_correlate( - bRGB_o: ArrayLike, - xez: ArrayLike, - bL_or: ArrayLike, - n: ArrayLike, -) -> NDArrayFloat: - """ - Compute the ideal white *brightness* correlate :math:`B_{rw}`. - - Parameters - ---------- - bRGB_o - Chromatic adaptation exponential factors :math:`\\beta_1(R_o)`, - :math:`\\beta_1(G_o)` and :math:`\\beta_2(B_o)`. - xez - Intermediate values :math:`\\xi`, :math:`\\eta`, :math:`\\zeta`. - bL_or - Normalising chromatic adaptation exponential factor - :math:`\\beta_1(B_{or})`. - n - Noise term used in the non-linear chromatic adaptation model. - - Returns - ------- - :class:`numpy.ndarray` - Ideal white *brightness* correlate :math:`B_{rw}`. - - Examples - -------- - >>> bRGB_o = np.array([4.61062223, 4.61058926, 4.65206986]) - >>> xez = np.array([1.00004219, 0.99998001, 0.99975794]) - >>> bL_or = 3.681021495604089 - >>> n = 1.0 - >>> ideal_white_brightness_correlate(bRGB_o, xez, bL_or, n) - ... # doctest: +ELLIPSIS - np.float64(125.2435392...) - """ - - bR_o, bG_o, _bB_o = tsplit(bRGB_o) - xi, eta, _zeta = tsplit(xez) - bL_or = as_float_array(bL_or) - - B_rw = (2 / 3) * bR_o * 1.758 * np.log10((100 * xi + n) / (20 * xi + n)) - B_rw += (1 / 3) * bG_o * 1.758 * np.log10((100 * eta + n) / (20 * eta + n)) - B_rw *= 41.69 / bL_or - B_rw += (50 / bL_or) * (2 / 3) * bR_o - B_rw += (50 / bL_or) * (1 / 3) * bG_o - - return as_float(B_rw) - - -def achromatic_lightness_correlate( - Q: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *achromatic lightness* correlate :math:`L_p^\\star`. - - Parameters - ---------- - Q - Achromatic response :math:`Q`. - - Returns - ------- - :class:`numpy.ndarray` - *Achromatic lightness* correlate :math:`L_p^\\star`. - - Examples - -------- - >>> Q = -0.000117024294955 - >>> achromatic_lightness_correlate(Q) # doctest: +ELLIPSIS - np.float64(49.9998829...) - """ - - Q = as_float_array(Q) - - return as_float(Q + 50) - - -def normalised_achromatic_lightness_correlate( - B_r: ArrayLike, B_rw: ArrayLike -) -> NDArrayFloat: - """ - Compute the *normalised achromatic lightness* correlate - :math:`L_n^\\star`. - - Parameters - ---------- - B_r - *Brightness* correlate :math:`B_r`. - B_rw - Ideal white *brightness* correlate :math:`B_{rw}`. - - Returns - ------- - :class:`numpy.ndarray` - *Normalised achromatic lightness* correlate :math:`L_n^\\star`. - - Examples - -------- - >>> B_r = 62.626673467230766 - >>> B_rw = 125.24353925846037 - >>> normalised_achromatic_lightness_correlate(B_r, B_rw) - ... # doctest: +ELLIPSIS - np.float64(50.0039154...) - """ - - B_r = as_float_array(B_r) - B_rw = as_float_array(B_rw) - - return as_float(100 * B_r / B_rw) - - -def hue_angle(p: ArrayLike, t: ArrayLike) -> NDArrayFloat: - """ - Compute the *hue* angle :math:`h` in degrees from the specified - protanopic and tritanopic responses. - - Parameters - ---------- - p - Protanopic response :math:`p`. - t - Tritanopic response :math:`t`. - - Returns - ------- - :class:`numpy.ndarray` - *Hue* angle :math:`h` in degrees. - - Examples - -------- - >>> p = -8.002142682085493e-05 - >>> t = -0.000017703650669 - >>> hue_angle(p, t) # doctest: +ELLIPSIS - np.float64(257.5250300...) - """ - - p = as_float_array(p) - t = as_float_array(t) - - h_L = np.degrees(np.arctan2(p, t)) % 360 - - return as_float(h_L) - - -def chromatic_strength_function( - theta: ArrayLike, -) -> NDArrayFloat: - """ - Define the chromatic strength function :math:`E_s(\\theta)` used to - correct saturation scale as a function of hue angle :math:`\\theta` in - degrees. - - Parameters - ---------- - theta - Hue angle :math:`\\theta` in degrees. - - Returns - ------- - :class:`numpy.ndarray` - Corrected saturation scale. - - Examples - -------- - >>> h = 257.52322689806243 - >>> chromatic_strength_function(h) # doctest: +ELLIPSIS - np.float64(1.2267869...) - """ - - theta = np.radians(theta) - - E_s = cast("NDArrayFloat", 0.9394) - E_s += -0.2478 * np.sin(1 * theta) - E_s += -0.0743 * np.sin(2 * theta) - E_s += +0.0666 * np.sin(3 * theta) - E_s += -0.0186 * np.sin(4 * theta) - E_s += -0.0055 * np.cos(1 * theta) - E_s += -0.0521 * np.cos(2 * theta) - E_s += -0.0573 * np.cos(3 * theta) - E_s += -0.0061 * np.cos(4 * theta) - - return as_float(E_s) - - -def saturation_components( - h: ArrayLike, - bL_or: ArrayLike, - t: ArrayLike, - p: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *saturation* components :math:`S_{RG}` and :math:`S_{YB}`. + Compute hue quadrature :math:`H` from the specified *Nayatani (1995)* + hue :math:`\\theta` angle in degrees via linear interpolation between + the four unique-hue angles. Parameters ---------- h - Correlate of *hue* :math:`h` in degrees. - bL_or - Normalising chromatic adaptation exponential factor - :math:`\\beta_1(B_or)`. - t - Tritanopic response :math:`t`. - p - Protanopic response :math:`p`. + Hue :math:`\\theta` angle in degrees. Returns ------- :class:`numpy.ndarray` - *Saturation* components :math:`S_{RG}` and :math:`S_{YB}`. + Hue quadrature :math:`H` in the 400-step *Nayatani (1995)* scale + (R 0, Y 100, G 200, B 300, wrap to R 400). - Examples - -------- - >>> h = 257.52322689806243 - >>> bL_or = 3.681021495604089 - >>> t = -0.000017706764677 - >>> p = -0.000080023561356 - >>> saturation_components(h, bL_or, t, p) # doctest: +ELLIPSIS - array([-0.0028852..., -0.0130396...]) - """ - - h = as_float_array(h) - bL_or = as_float_array(bL_or) - t = as_float_array(t) - p = as_float_array(p) - - E_s = chromatic_strength_function(h) - S_RG = 488.93 / bL_or * E_s * t - S_YB = 488.93 / bL_or * E_s * p - - return tstack([S_RG, S_YB]) - - -def saturation_correlate(S_RG: ArrayLike, S_YB: ArrayLike) -> NDArrayFloat: - """ - Compute the correlate of *saturation* :math:`S`. - - Parameters - ---------- - S_RG - *Saturation* component :math:`S_{RG}`. - S_YB - *Saturation* component :math:`S_{YB}`. - - Returns - ------- - :class:`numpy.ndarray` - Correlate of *saturation* :math:`S`. - - Examples - -------- - >>> S_RG = -0.002885271638197 - >>> S_YB = -0.013039632941332 - >>> saturation_correlate(S_RG, S_YB) # doctest: +ELLIPSIS - np.float64(0.0133550...) - """ - - S_RG = as_float_array(S_RG) - S_YB = as_float_array(S_YB) - - S = np.hypot(S_RG, S_YB) - - return as_float(S) - - -def chroma_components( - L_star_P: ArrayLike, - S_RG: ArrayLike, - S_YB: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *chroma* components :math:`C_{RG}` and :math:`C_{YB}`. - - Parameters - ---------- - L_star_P - *Achromatic lightness* correlate :math:`L_p^\\star`. - S_RG - *Saturation* component :math:`S_{RG}`. - S_YB - *Saturation* component :math:`S_{YB}`. - - Returns - ------- - :class:`numpy.ndarray` - *Chroma* components :math:`C_{RG}` and :math:`C_{YB}`. - - Examples - -------- - >>> L_star_P = 49.99988297570504 - >>> S_RG = -0.002885271638197 - >>> S_YB = -0.013039632941332 - >>> chroma_components(L_star_P, S_RG, S_YB) # doctest: +ELLIPSIS - array([-0.00288527, -0.01303961]) - """ - - L_star_P = as_float_array(L_star_P) - S_RG = as_float_array(S_RG) - S_YB = as_float_array(S_YB) - - C_RG = spow(L_star_P / 50, 0.7) * S_RG - C_YB = spow(L_star_P / 50, 0.7) * S_YB - - return tstack([C_RG, C_YB]) - - -def chroma_correlate(L_star_P: ArrayLike, S: ArrayLike) -> NDArrayFloat: - """ - Compute the correlate of *chroma* :math:`C`. - - Parameters - ---------- - L_star_P - *Achromatic lightness* correlate :math:`L_p^\\star`. - S - Correlate of *saturation* :math:`S`. - - Returns - ------- - :class:`numpy.ndarray` - Correlate of *chroma* :math:`C`. - - Examples - -------- - >>> L_star_P = 49.99988297570504 - >>> S = 0.013355029751778 - >>> chroma_correlate(L_star_P, S) # doctest: +ELLIPSIS - np.float64(0.0133550...) - """ - - L_star_P = as_float_array(L_star_P) - S = as_float_array(S) - - return spow(L_star_P / 50, 0.7) * S - - -def colourfulness_components( - C_RG: ArrayLike, - C_YB: ArrayLike, - B_rw: ArrayLike, -) -> NDArrayFloat: - """ - Compute the *colourfulness* components :math:`M_{RG}` and :math:`M_{YB}`. - - Parameters - ---------- - C_RG - *Chroma* component :math:`C_{RG}`. - C_YB - *Chroma* component :math:`C_{YB}`. - B_rw - Ideal white *brightness* correlate :math:`B_{rw}`. - - Returns - ------- - :class:`numpy.ndarray` - *Colourfulness* components :math:`M_{RG}` and :math:`M_{YB}`. - - Examples - -------- - >>> C_RG = -0.002885271638197 - >>> C_YB = -0.013039632941332 - >>> B_rw = 125.24353925846037 - >>> colourfulness_components(C_RG, C_YB, B_rw) # doctest: +ELLIPSIS - array([-0.0036136..., -0.0163313...]) - """ - - C_RG = as_float_array(C_RG) - C_YB = as_float_array(C_YB) - B_rw = as_float_array(B_rw) - - M_RG = C_RG * B_rw / 100 - M_YB = C_YB * B_rw / 100 - - return tstack([M_RG, M_YB]) - - -def colourfulness_correlate(C: ArrayLike, B_rw: ArrayLike) -> NDArrayFloat: - """ - Compute the correlate of *colourfulness* :math:`M`. - - Parameters + References ---------- - C - Correlate of *chroma* :math:`C`. - B_rw - Ideal white *brightness* correlate :math:`B_{rw}`. - - Returns - ------- - :class:`numpy.ndarray` - Correlate of *colourfulness* :math:`M`. + :cite:`Fairchild2013ba`, :cite:`Nayatani1995a` Examples -------- - >>> C = 0.013355007871689 - >>> B_rw = 125.24353925846037 - >>> colourfulness_correlate(C, B_rw) # doctest: +ELLIPSIS - np.float64(0.0167262...) + >>> hue_quadrature(257.5232268) # doctest: +ELLIPSIS + np.float64(317.7841134...) """ - C = as_float_array(C) - B_rw = as_float_array(B_rw) + h = as_float_array(h) - M = C * B_rw / 100 + xp = array_namespace(h) + + h = as_float_array(xp.where(xp.isnan(h), 0, h)) + + # Unique-hue angles per *Fairchild (2013)* p.202: + # R 20.14, Y 90.00, G 164.25, B 231.00, R 380.14 (wrap). + # Hue quadrature is a 400-step scale obtained via linear interpolation + # between consecutive bin boundaries; no eccentricity weighting (unlike + # *CIECAM02*). + H_0 = (h - 20.14) / (90.00 - 20.14) * 100 + H_1 = 100 + (h - 90.00) / (164.25 - 90.00) * 100 + H_2 = 200 + (h - 164.25) / (231.00 - 164.25) * 100 + H_3 = 300 + (h - 231.00) / (380.14 - 231.00) * 100 + + # ``h < 20.14`` wraps through ``360`` into the B -> R interval. + H_wrap = 300 + (h + 360 - 231.00) / (380.14 - 231.00) * 100 + + H = xp_select( + [ + (h >= 20.14) & (h < 90.00), + (h >= 90.00) & (h < 164.25), + (h >= 164.25) & (h < 231.00), + h >= 231.00, + ], + [H_0, H_1, H_2, H_3], + default=H_wrap, + xp=xp, + ) - return as_float(M) + return as_float(H) diff --git a/colour/appearance/rlab.py b/colour/appearance/rlab.py index c54db76ea9..65f7839609 100644 --- a/colour/appearance/rlab.py +++ b/colour/appearance/rlab.py @@ -21,22 +21,29 @@ from __future__ import annotations +import typing from dataclasses import dataclass, field import numpy as np from colour.algebra import sdiv, sdiv_mode, spow, vecmul from colour.appearance.hunt import MATRIX_XYZ_TO_HPE, XYZ_to_rgb -from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat # noqa: TC001 + +if typing.TYPE_CHECKING: + from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat + from colour.utilities import ( CanonicalMapping, - MixinDataclassArray, + MixinDataclassArithmetic, + array_namespace, as_float, as_float_array, from_range_degrees, row_as_diagonal, to_domain_100, tsplit, + xp_as_float_array, + xp_degrees, ) __author__ = "Colour Developers" @@ -102,7 +109,7 @@ @dataclass -class CAM_ReferenceSpecification_RLAB(MixinDataclassArray): +class CAM_ReferenceSpecification_RLAB(MixinDataclassArithmetic): """ Define the *RLAB* colour appearance model reference specification. @@ -141,7 +148,7 @@ class CAM_ReferenceSpecification_RLAB(MixinDataclassArray): @dataclass -class CAM_Specification_RLAB(MixinDataclassArray): +class CAM_Specification_RLAB(MixinDataclassArithmetic): """ Define the *RLAB* colour appearance model specification. @@ -178,13 +185,13 @@ class CAM_Specification_RLAB(MixinDataclassArray): :cite:`Fairchild1996a`, :cite:`Fairchild2013w` """ - J: NDArrayFloat | None = field(default_factory=lambda: None) - C: NDArrayFloat | None = field(default_factory=lambda: None) - h: NDArrayFloat | None = field(default_factory=lambda: None) - s: NDArrayFloat | None = field(default_factory=lambda: None) - HC: NDArrayFloat | None = field(default_factory=lambda: None) - a: NDArrayFloat | None = field(default_factory=lambda: None) - b: NDArrayFloat | None = field(default_factory=lambda: None) + J: float | NDArrayFloat | None = field(default_factory=lambda: None) + C: float | NDArrayFloat | None = field(default_factory=lambda: None) + h: float | NDArrayFloat | None = field(default_factory=lambda: None) + s: float | NDArrayFloat | None = field(default_factory=lambda: None) + HC: float | NDArrayFloat | None = field(default_factory=lambda: None) + a: float | NDArrayFloat | None = field(default_factory=lambda: None) + b: float | NDArrayFloat | None = field(default_factory=lambda: None) def XYZ_to_RLAB( @@ -257,18 +264,30 @@ def XYZ_to_RLAB( D = as_float_array(D) sigma = as_float_array(sigma) + xp = array_namespace(XYZ, XYZ_n, Y_n, D, sigma) + + Y_n = xp_as_float_array(Y_n, xp=xp, like=XYZ) + D = xp_as_float_array(D, xp=xp, like=XYZ) + sigma = xp_as_float_array(sigma, xp=xp, like=XYZ) + # Converting to cone responses. LMS_n = XYZ_to_rgb(XYZ_n) # Computing the :math:`A` matrix. - LMS_l_E = 3 * LMS_n / np.sum(LMS_n, axis=-1)[..., None] + LMS_l_E = 3 * LMS_n / xp.sum(LMS_n, axis=-1)[..., None] LMS_p_L = (1 + spow(Y_n[..., None], 1 / 3) + LMS_l_E) / ( 1 + spow(Y_n[..., None], 1 / 3) + 1 / LMS_l_E ) LMS_a_L = (LMS_p_L + D[..., None] * (1 - LMS_p_L)) / LMS_n - M = np.matmul(np.matmul(MATRIX_R, row_as_diagonal(LMS_a_L)), MATRIX_XYZ_TO_HPE) + M = xp.matmul( + xp.matmul( + xp_as_float_array(MATRIX_R, xp=xp, like=XYZ), + row_as_diagonal(LMS_a_L), + ), + xp_as_float_array(MATRIX_XYZ_TO_HPE, xp=xp, like=XYZ), + ) XYZ_ref = vecmul(M, XYZ) Y_ref: NDArrayFloat @@ -282,21 +301,21 @@ def XYZ_to_RLAB( bR = 170 * (spow(Y_ref, sigma) - spow(Z_ref, sigma)) # Computing the *hue* angle :math:`h^R`. - hR = np.degrees(np.arctan2(bR, aR)) % 360 + hR = xp_degrees(xp.atan2(bR, aR)) % 360 # TODO: Implement hue composition computation. # Computing the correlate of *chroma* :math:`C^R`. - CR = np.hypot(aR, bR) + CR = xp.hypot(aR, bR) # Computing the correlate of *saturation* :math:`s^R`. with sdiv_mode(): sR = sdiv(CR, LR) return CAM_Specification_RLAB( - J=LR, - C=CR, + J=as_float(LR), + C=as_float(CR), h=as_float(from_range_degrees(hR)), - s=sR, + s=as_float(sR), HC=None, a=as_float(aR), b=as_float(bR), diff --git a/colour/appearance/scam.py b/colour/appearance/scam.py index 2e970f148f..71e4d8dbe8 100644 --- a/colour/appearance/scam.py +++ b/colour/appearance/scam.py @@ -23,19 +23,23 @@ from __future__ import annotations +import typing from dataclasses import astuple, dataclass, field import numpy as np from colour.adaptation import chromatic_adaptation_Li2025 from colour.algebra import sdiv, sdiv_mode, spow -from colour.hints import ( # noqa: TC001 - Annotated, - ArrayLike, - Domain100, - NDArrayFloat, - Range100, -) + +if typing.TYPE_CHECKING: + from colour.hints import ( + Annotated, + ArrayLike, + Domain100, + NDArrayFloat, + Range100, + ) + from colour.models.sucs import ( XYZ_to_sUCS, sUCS_Iab_to_sUCS_ICh, @@ -46,6 +50,7 @@ CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, domain_range_scale, @@ -56,6 +61,9 @@ to_domain_degrees, tsplit, tstack, + xp_as_float_array, + xp_radians, + xp_select, ) __author__ = "Colour Developers" @@ -185,6 +193,7 @@ def XYZ_to_sCAM( Y_b: ArrayLike, surround: InductionFactors_sCAM = VIEWING_CONDITIONS_sCAM["Average"], discount_illuminant: bool = False, + compute_H: bool = False, ) -> Annotated[ CAM_Specification_sCAM, (100, 100, 360, 100, 100, 400, 100, 100, 100, 100) ]: @@ -213,6 +222,11 @@ def XYZ_to_sCAM( Surround viewing conditions induction factors. discount_illuminant Truth value indicating if the illuminant should be discounted. + compute_H + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -266,7 +280,10 @@ def XYZ_to_sCAM( >>> L_A = 318.31 >>> Y_b = 20.0 >>> surround = VIEWING_CONDITIONS_sCAM["Average"] - >>> XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS + >>> XYZ_to_sCAM( + ... XYZ, XYZ_w, L_A, Y_b, surround, + ... compute_H=True, + ... ) # doctest: +ELLIPSIS CAM_Specification_sCAM(J=np.float64(49.9795668...), \ C=np.float64(0.0140531...), h=np.float64(328.2724924...), \ Q=np.float64(195.2302423...), M=np.float64(0.0050244...), \ @@ -277,20 +294,25 @@ def XYZ_to_sCAM( XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) + + xp = array_namespace(XYZ, XYZ_w, L_A, Y_b) + + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) Y_w = XYZ_w[..., 1] if XYZ_w.ndim > 1 else XYZ_w[1] with sdiv_mode(): z = 1.48 + spow(sdiv(Y_b, Y_w), 0.5) - F_L = 0.1710 * spow(L_A, 1 / 3) / (1 - 0.4934 * np.exp(-0.9934 * L_A)) + F_L = 0.1710 * spow(L_A, 1 / 3) / (1 - 0.4934 * xp.exp(-0.9934 * L_A)) with sdiv_mode(): L_A_D65 = sdiv(L_A * 100, Y_b) - XYZ_w_D65 = TVS_D65_sCAM * L_A_D65[..., None] + XYZ_w_D65 = ( + xp_as_float_array(TVS_D65_sCAM, xp=xp, like=L_A_D65) * L_A_D65[..., None] + ) with domain_range_scale("ignore"): XYZ_D65 = chromatic_adaptation_Li2025( @@ -305,7 +327,7 @@ def XYZ_to_sCAM( I_a = 100 * spow(I / 100, surround.c * z) - e_t = 1 + 0.06 * np.cos(np.radians(110 + h)) + e_t = 1 + 0.06 * xp.cos(xp_radians(110 + h)) with sdiv_mode(): M = (C * spow(F_L, 0.1) * sdiv(1, spow(I_a, 0.27)) * e_t) * surround.F @@ -314,13 +336,13 @@ def XYZ_to_sCAM( # After confirmation with the author, 0.1 is the recommended value. Q = sdiv(2, surround.c) * I_a * spow(F_L, 0.1) - H = hue_quadrature(h) + H = hue_quadrature(h) if compute_H else xp.full_like(h, float("nan")) - V = np.sqrt(I_a**2 + 3 * C**2) + V = xp.sqrt(I_a**2 + 3 * C**2) K = 100 - V - D = 1.3 * np.sqrt((100 - I_a) ** 2 + 1.6 * C**2) + D = 1.3 * xp.sqrt((100 - I_a) ** 2 + 1.6 * C**2) W = 100 - D @@ -423,8 +445,12 @@ def sCAM_to_XYZ( M = to_domain_100(M) if not has_only_nan(M) else None XYZ_w = to_domain_100(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) + + xp = array_namespace(I_a, C, h, M, XYZ_w, L_A, Y_b) + + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=I_a) + L_A = xp_as_float_array(L_A, xp=xp, like=I_a) + Y_b = xp_as_float_array(Y_b, xp=xp, like=I_a) if has_only_nan(I_a) or has_only_nan(h): error = ( @@ -448,8 +474,8 @@ def sCAM_to_XYZ( z = 1.48 + spow(sdiv(Y_b, Y_w), 0.5) if C is None and M is not None: - F_L = 0.1710 * spow(L_A, 1 / 3) / (1 - 0.4934 * np.exp(-0.9934 * L_A)) - e_t = 1 + 0.06 * np.cos(np.radians(110 + h)) + F_L = 0.1710 * spow(L_A, 1 / 3) / (1 - 0.4934 * xp.exp(-0.9934 * L_A)) + e_t = 1 + 0.06 * xp.cos(xp_radians(110 + h)) with sdiv_mode(): C = sdiv(M * spow(I_a, 0.27), spow(F_L, 0.1) * e_t * surround.F) @@ -463,7 +489,9 @@ def sCAM_to_XYZ( XYZ_D65 = XYZ_D65 * Y_w[..., None] L_A_D65 = sdiv(L_A * 100, Y_b) - XYZ_w_D65 = TVS_D65_sCAM * L_A_D65[..., None] + XYZ_w_D65 = ( + xp_as_float_array(TVS_D65_sCAM, xp=xp, like=L_A_D65) * L_A_D65[..., None] + ) with domain_range_scale("ignore"): XYZ = chromatic_adaptation_Li2025( @@ -519,32 +547,42 @@ def hue_quadrature(h: ArrayLike) -> NDArrayFloat: """ h = as_float_array(h) - h_n = as_float_array(h % 360) - h_i = HUE_DATA_FOR_HUE_QUADRATURE_sCAM["h_i"] - e_i = HUE_DATA_FOR_HUE_QUADRATURE_sCAM["e_i"] - H_i = HUE_DATA_FOR_HUE_QUADRATURE_sCAM["H_i"] - - h_n = np.where(np.isnan(h_n), 0, h_n) - h_n = np.where(h_n < h_i[0], h_n + 360, h_n) - - i = np.searchsorted(h_i, h_n, side="right") - 1 - i = np.clip(i, 0, len(h_i) - 2) - - h1 = h_i[i] - e1 = e_i[i] - H1 = H_i[i] - - h2_idx = (i + 1) % len(h_i) - h2 = h_i[h2_idx] - e2 = e_i[i + 1] - - h2 = np.where(h2 < h1, h2 + 360, h2) - - with sdiv_mode(): - term1 = sdiv(h_n - h1, e1) - term2 = sdiv(h2 - h_n, e2) - - H = H1 + 100 * sdiv(term1, term1 + term2) + xp = array_namespace(h) + + h_n = h % 360 + h_n = as_float_array(xp.where(xp.isnan(h_n), 0, h_n)) + + # Wrap-around: h_n < 15.6 is treated as h_n + 360. + h_w = as_float_array(xp.where(h_n < 15.6, h_n + 360, h_n)) + + # Hue quadrature table (5 entries, 4 intervals). + # h_i = [15.6, 80.3, 157.8, 219.7, 376.6] + # e_i = [0.7, 0.6, 1.2, 0.9, 0.7 ] + # H_i = [0.0, 100.0, 200.0, 300.0, 400.0] + def _H( + h_k: float, e_k: float, H_k: float, h_k1: float, e_k1: float + ) -> NDArrayFloat: + """Compute hue quadrature for a single bin.""" + + t1 = (h_w - h_k) / e_k + t2 = (h_k1 - h_w) / e_k1 + return H_k + 100 * t1 / (t1 + t2) + + H = xp_select( + [ + (h_w >= 15.6) & (h_w < 80.3), + (h_w >= 80.3) & (h_w < 157.8), + (h_w >= 157.8) & (h_w < 219.7), + (h_w >= 219.7) & (h_w < 376.6), + ], + [ + _H(15.6, 0.7, 0.0, 80.3, 0.6), + _H(80.3, 0.6, 100.0, 157.8, 1.2), + _H(157.8, 1.2, 200.0, 219.7, 0.9), + _H(219.7, 0.9, 300.0, 376.6, 0.7), + ], + xp=xp, + ) return as_float(H) diff --git a/colour/appearance/tests/test_atd95.py b/colour/appearance/tests/test_atd95.py index 5bb65a6eff..56682f32e3 100644 --- a/colour/appearance/tests/test_atd95.py +++ b/colour/appearance/tests/test_atd95.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.appearance import XYZ_to_ATD95 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import as_float_array, domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_float_array, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,7 +40,7 @@ class TestXYZ_to_ATD95: tests methods. """ - def test_XYZ_to_ATD95(self) -> None: + def test_XYZ_to_ATD95(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.atd95.XYZ_to_ATD95` definition. @@ -39,138 +51,130 @@ def test_XYZ_to_ATD95(self) -> None: http://rit-mcsl.org/fairchild//files/AppModEx.xls """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_0 = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_0 = xp_as_array([95.05, 100.00, 108.88], xp=xp) Y_02 = 318.31 K_1 = 0 K_2 = 50 sigma = 300 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), - np.array( - [ - 1.91, - 1.206, - 0.1814, - 0.1788, - 0.0287, - 0.0108, - 0.0192, - 0.0205, - 0.0108, - ] - ), - atol=0.01, + [ + 1.91, + 1.206, + 0.1814, + 0.1788, + 0.0287, + 0.0108, + 0.0192, + 0.0205, + 0.0108, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) Y_02 = 31.83 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), - np.array( - [ - 63.96, - 1.371, - 0.2142, - 0.2031, - 0.068, - 0.0005, - 0.0224, - 0.0308, - 0.0005, - ] - ), - atol=0.01, + [ + 63.96, + 1.371, + 0.2142, + 0.2031, + 0.068, + 0.0005, + 0.0224, + 0.0308, + 0.0005, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_0 = np.array([109.85, 100.00, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_0 = xp_as_array([109.85, 100.00, 35.58], xp=xp) Y_02 = 318.31 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), - np.array( - [ - -0.31, - 0.436, - 0.1075, - 0.1068, - -0.011, - 0.0044, - 0.0106, - -0.0014, - 0.0044, - ] - ), - atol=0.01, + [ + -0.31, + 0.436, + 0.1075, + 0.1068, + -0.011, + 0.0044, + 0.0106, + -0.0014, + 0.0044, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) Y_02 = 31.83 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), - np.array( - [ - 0.79, - 1.091, - 0.1466, - 0.146, - 0.0007, - 0.013, - 0.0152, - 0.0102, - 0.013, - ] - ), - atol=0.01, + [ + 0.79, + 1.091, + 0.1466, + 0.146, + 0.0007, + 0.013, + 0.0152, + 0.0102, + 0.013, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - def test_n_dimensional_XYZ_to_ATD95(self) -> None: + def test_n_dimensional_XYZ_to_ATD95(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.atd95.XYZ_to_ATD95` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_0 = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_0 = xp_as_array([95.05, 100.00, 108.88], xp=xp) Y_02 = 318.31 K_1 = 0 K_2 = 50 sigma = 300 specification = XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_0 = np.tile(XYZ_0, (6, 1)) - np.testing.assert_allclose( + XYZ_0 = xp.tile(xp_as_array(XYZ_0, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_0 = np.reshape(XYZ_0, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 9)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_0 = xp_reshape(xp_as_array(XYZ_0, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 9), xp=xp) + xp_assert_close( XYZ_to_ATD95(XYZ, XYZ_0, Y_02, K_1, K_2, sigma), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_ATD95(self) -> None: + def test_domain_range_scale_XYZ_to_ATD95(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.atd95.XYZ_to_ATD95` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_0 = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_0 = xp_as_array([95.05, 100.00, 108.88], xp=xp) Y_0 = 318.31 k_1 = 0.0 k_2 = 50.0 @@ -183,8 +187,14 @@ def test_domain_range_scale_XYZ_to_ATD95(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_ATD95(XYZ * factor_a, XYZ_0 * factor_a, Y_0, k_1, k_2), + xp_assert_close( + XYZ_to_ATD95( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_0 * xp_as_array(factor_a, xp=xp), + Y_0, + k_1, + k_2, + ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/appearance/tests/test_cam16.py b/colour/appearance/tests/test_cam16.py index 8ef284ac13..5a510ea016 100644 --- a/colour/appearance/tests/test_cam16.py +++ b/colour/appearance/tests/test_cam16.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -17,9 +22,13 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -41,159 +50,150 @@ class TestXYZ_to_CAM16: tests methods. """ - def test_XYZ_to_CAM16(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_XYZ_to_CAM16(self, xp: ModuleType) -> None: """Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition.""" - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CAM16["Average"] - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 41.73120791, - 0.10335574, - 217.06795977, - 2.34501507, - 195.37170899, - 0.10743677, - 275.59498615, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 41.73120791, + 0.10335574, + 217.06795977, + 2.34501507, + 195.37170899, + 0.10743677, + 275.59498615, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_A = 31.83 - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 65.42828069, - 49.67956420, - 17.48659243, - 52.94308868, - 152.06985268, - 42.62473321, - 398.03047943, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 65.42828069, + 49.67956420, + 17.48659243, + 52.94308868, + 152.06985268, + 42.62473321, + 398.03047943, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 21.36052893, - 50.99381895, - 178.86724266, - 61.57953092, - 139.78582768, - 53.00732582, - 223.01823806, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 21.36052893, + 50.99381895, + 178.86724266, + 61.57953092, + 139.78582768, + 53.00732582, + 223.01823806, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 41.36326063, - 52.81154022, - 258.88676291, - 53.12406914, - 194.52011798, - 54.89682038, - 311.24768647, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 41.36326063, + 52.81154022, + 258.88676291, + 53.12406914, + 194.52011798, + 54.89682038, + 311.24768647, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([61.45276998, 7.00421901, 82.2406738]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([61.45276998, 7.00421901, 82.2406738], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 4.074366543152521 - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 21.03801957, - 457.78881613, - 350.06445098, - 241.50642846, - 56.74143988, - 330.94646237, - 376.43915877, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 21.03801957, + 457.78881613, + 350.06445098, + 241.50642846, + 56.74143988, + 330.94646237, + 376.43915877, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_CAM16(self) -> None: + def test_n_dimensional_XYZ_to_CAM16(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CAM16["Average"] - specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 8)) - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 8), xp=xp) + xp_assert_close( + XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_CAM16(self) -> None: + def test_domain_range_scale_XYZ_to_CAM16(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.cam16.XYZ_to_CAM16` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CAM16["Average"] - specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) d_r = ( ("reference", 1, 1), @@ -221,8 +221,15 @@ def test_domain_range_scale_XYZ_to_CAM16(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_CAM16(XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_CAM16( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_A, + Y_b, + surround, + compute_H=True, + ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -237,7 +244,9 @@ def test_nan_XYZ_to_CAM16(self) -> None: cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) surround = InductionFactors_CAM16(cases[0, 0], cases[0, 0], cases[0, 0]) - XYZ_to_CAM16(cases, cases, cases[..., 0], cases[..., 0], surround) + XYZ_to_CAM16( + cases, cases, cases[..., 0], cases[..., 0], surround, compute_H=True + ) class TestCAM16_to_XYZ: @@ -246,80 +255,80 @@ class TestCAM16_to_XYZ: methods. """ - def test_CAM16_to_XYZ(self) -> None: + def test_CAM16_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition.""" specification = CAM_Specification_CAM16(41.73120791, 0.10335574, 217.06795977) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CAM16["Average"] - np.testing.assert_allclose( + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CAM16(65.42828069, 49.67956420, 17.48659243) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([57.06, 43.06, 31.96]), + [57.06, 43.06, 31.96], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CAM16(21.36052893, 50.99381895, 178.86724266) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([3.53, 6.56, 2.14]), + [3.53, 6.56, 2.14], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CAM16(41.36326063, 52.81154022, 258.88676291) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CAM16(21.03801957, 457.78881613, 350.06445098) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 4.074366543152521 - np.testing.assert_allclose( + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([61.45276998, 7.00421901, 82.2406738]), + [61.45276998, 7.00421901, 82.2406738], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CAM16_to_XYZ(self) -> None: + def test_n_dimensional_CAM16_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CAM16["Average"] - specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) specification = CAM_Specification_CAM16( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -328,28 +337,28 @@ def test_n_dimensional_CAM16_to_XYZ(self) -> None: specification = CAM_Specification_CAM16( *tsplit(np.reshape(specification, (2, 3, 8))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_CAM16_to_XYZ(self) -> None: + def test_domain_range_scale_CAM16_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.cam16.CAM16_to_XYZ` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CAM16["Average"] - specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(CAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) d_r = ( ("reference", 1, 1), @@ -377,9 +386,9 @@ def test_domain_range_scale_CAM16_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( CAM16_to_XYZ( - specification * factor_a, + specification * xp_as_array(factor_a, xp=xp), XYZ_w * factor_b, L_A, Y_b, @@ -396,15 +405,14 @@ def test_raise_exception_CAM16_to_XYZ(self) -> None: exception. """ - pytest.raises( - ValueError, - CAM16_to_XYZ, - CAM_Specification_CAM16(41.731207905126638, None, 217.06795976739301), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_CAM16["Average"], - ) + with pytest.raises(ValueError): + CAM16_to_XYZ( + CAM_Specification_CAM16(41.73120790512664, None, 217.067959767393), + np.array([95.05, 100.0, 108.88]), + 318.31, + 20.0, + VIEWING_CONDITIONS_CAM16["Average"], + ) @ignore_numpy_errors def test_nan_CAM16_to_XYZ(self) -> None: diff --git a/colour/appearance/tests/test_ciecam02.py b/colour/appearance/tests/test_ciecam02.py index 87d7571140..cd83ffa215 100644 --- a/colour/appearance/tests/test_ciecam02.py +++ b/colour/appearance/tests/test_ciecam02.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -17,9 +22,13 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -41,7 +50,7 @@ class TestXYZ_to_CIECAM02: tests methods. """ - def test_XYZ_to_CIECAM02(self) -> None: + def test_XYZ_to_CIECAM02(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam02.XYZ_to_CIECAM02` definition. @@ -52,112 +61,110 @@ def test_XYZ_to_CIECAM02(self) -> None: http://rit-mcsl.org/fairchild//files/AppModEx.xls """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = InductionFactors_CIECAM02(1, 0.69, 1) - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), - np.array([41.73, 0.1, 219, 2.36, 195.37, 0.11, 278.1, np.nan]), - atol=0.05, + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [41.73, 0.1, 219, 2.36, 195.37, 0.11, 278.1, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_A = 31.83 - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), - np.array([65.96, 48.57, 19.6, 52.25, 152.67, 41.67, 399.6, np.nan]), - atol=0.05, + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [65.96, 48.57, 19.6, 52.25, 152.67, 41.67, 399.6, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100.00, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100.00, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), - np.array([21.79, 46.94, 177.1, 58.79, 141.17, 48.8, 220.4, np.nan]), - atol=0.05, + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [21.79, 46.94, 177.1, 58.79, 141.17, 48.8, 220.4, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) L_A = 31.83 - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), - np.array([42.53, 51.92, 248.9, 60.22, 122.83, 44.54, 305.8, np.nan]), - atol=0.05, + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [42.53, 51.92, 248.9, 60.22, 122.83, 44.54, 305.8, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([61.45276998, 7.00421901, 82.24067384]) - XYZ_w = np.array([95.05, 100, 108.88]) + XYZ = xp_as_array([61.45276998, 7.00421901, 82.24067384], xp=xp) + XYZ_w = xp_as_array([95.05, 100, 108.88], xp=xp) L_A = 4.074366543152521 - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 21.72630603341673, - 411.5190338631848, - 349.12875710099053, - 227.15081998415593, - 57.657243286322725, - 297.49693233026602, - 375.5788601911363, - np.nan, - ] - ), - atol=0.05, + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 21.72630603341673, + 411.5190338631848, + 349.12875710099053, + 227.15081998415593, + 57.657243286322725, + 297.49693233026602, + 375.5788601911363, + np.nan, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - def test_n_dimensional_XYZ_to_CIECAM02(self) -> None: + def test_n_dimensional_XYZ_to_CIECAM02(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam02.XYZ_to_CIECAM02` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM02["Average"] - specification = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 8)) - np.testing.assert_allclose( - XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 8), xp=xp) + xp_assert_close( + XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_CIECAM02(self) -> None: + def test_domain_range_scale_XYZ_to_CIECAM02(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam02.XYZ_to_CIECAM02` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM02["Average"] - specification = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) d_r = ( ("reference", 1, 1), @@ -185,9 +192,14 @@ def test_domain_range_scale_XYZ_to_CIECAM02(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_CIECAM02( - XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_A, + Y_b, + surround, + compute_H=True, ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -203,7 +215,9 @@ def test_nan_XYZ_to_CIECAM02(self) -> None: cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) surround = InductionFactors_CIECAM02(cases[0, 0], cases[0, 0], cases[0, 0]) - XYZ_to_CIECAM02(cases, cases, cases[..., 0], cases[..., 0], surround) + XYZ_to_CIECAM02( + cases, cases, cases[..., 0], cases[..., 0], surround, compute_H=True + ) class TestCIECAM02_to_XYZ: @@ -212,51 +226,51 @@ class TestCIECAM02_to_XYZ: tests methods. """ - def test_CIECAM02_to_XYZ(self) -> None: + def test_CIECAM02_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.appearance.ciecam02.CIECAM02_to_XYZ` definition.""" specification = CAM_Specification_CIECAM02( 41.73, 0.1, 219, 2.36, 195.37, 0.11, 278.1 ) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = InductionFactors_CIECAM02(1, 0.69, 1) - np.testing.assert_allclose( + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), - atol=0.05, + [19.01, 20.00, 21.78], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) specification = CAM_Specification_CIECAM02( 65.96, 48.57, 19.6, 52.25, 152.67, 41.67, 399.6, np.nan ) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([57.06, 43.06, 31.96]), - atol=0.05, + [57.06, 43.06, 31.96], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) specification = CAM_Specification_CIECAM02( 21.79, 46.94, 177.1, 58.79, 141.17, 48.8, 220.4, np.nan ) - XYZ_w = np.array([109.85, 100.00, 35.58]) + XYZ_w = xp_as_array([109.85, 100.00, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([3.53, 6.56, 2.14]), - atol=0.05, + [3.53, 6.56, 2.14], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) specification = CAM_Specification_CIECAM02( 42.53, 51.92, 248.9, 60.22, 122.83, 44.54, 305.8, np.nan ) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), - atol=0.05, + [19.01, 20.00, 21.78], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) specification = CAM_Specification_CIECAM02( @@ -269,40 +283,40 @@ def test_CIECAM02_to_XYZ(self) -> None: 375.5788601911363, np.nan, ) - XYZ_w = np.array([95.05, 100, 108.88]) + XYZ_w = xp_as_array([95.05, 100, 108.88], xp=xp) L_A = 4.074366543152521 - np.testing.assert_allclose( + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([61.45276998, 7.00421901, 82.24067384]), - atol=0.05, + [61.45276998, 7.00421901, 82.24067384], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - def test_n_dimensional_CIECAM02_to_XYZ(self) -> None: + def test_n_dimensional_CIECAM02_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam02.CIECAM02_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM02["Average"] - specification = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) specification = CAM_Specification_CIECAM02( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -311,28 +325,30 @@ def test_n_dimensional_CIECAM02_to_XYZ(self) -> None: specification = CAM_Specification_CIECAM02( *tsplit(np.reshape(specification, (2, 3, 8))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_CIECAM02_to_XYZ(self) -> None: + def test_domain_range_scale_CIECAM02_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam02.CIECAM02_to_XYZ` definition domain and range scale support. """ - XYZ_i = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_i = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM02["Average"] - specification = XYZ_to_CIECAM02(XYZ_i, XYZ_w, L_A, Y_b, surround) - XYZ = CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM02( + XYZ_i, XYZ_w, L_A, Y_b, surround, compute_H=True + ) + XYZ = as_ndarray(CIECAM02_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) d_r = ( ("reference", 1, 1), @@ -360,9 +376,9 @@ def test_domain_range_scale_CIECAM02_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( CIECAM02_to_XYZ( - specification * factor_a, + specification * xp_as_array(factor_a, xp=xp), XYZ_w * factor_b, L_A, Y_b, @@ -379,15 +395,14 @@ def test_raise_exception_CIECAM02_to_XYZ(self) -> None: raised exception. """ - pytest.raises( - ValueError, - CIECAM02_to_XYZ, - CAM_Specification_CIECAM02(41.731091132513917, None, 219.04843265831178), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_CIECAM02["Average"], - ) + with pytest.raises(ValueError): + CIECAM02_to_XYZ( + CAM_Specification_CIECAM02(41.73109113251392, None, 219.04843265831178), + np.array([95.05, 100.0, 108.88]), + 318.31, + 20.0, + VIEWING_CONDITIONS_CIECAM02["Average"], + ) @ignore_numpy_errors def test_nan_CIECAM02_to_XYZ(self) -> None: diff --git a/colour/appearance/tests/test_ciecam16.py b/colour/appearance/tests/test_ciecam16.py index aa25d0f63e..ed8ccf982e 100644 --- a/colour/appearance/tests/test_ciecam16.py +++ b/colour/appearance/tests/test_ciecam16.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -17,9 +22,13 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -41,182 +50,171 @@ class TestXYZ_to_CIECAM16: tests methods. """ - def test_XYZ_to_CIECAM16(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_XYZ_to_CIECAM16(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam16.XYZ_to_CIECAM16` definition. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM16["Average"] - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 41.73120791, - 0.10335574, - 217.06795977, - 2.34501507, - 195.37170899, - 0.10743677, - 275.59498615, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 41.73120791, + 0.10335574, + 217.06795977, + 2.34501507, + 195.37170899, + 0.10743677, + 275.59498615, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_A = 31.83 - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 65.42828069, - 49.67956420, - 17.48659243, - 52.94308868, - 152.06985268, - 42.62473321, - 398.03047943, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 65.42828069, + 49.67956420, + 17.48659243, + 52.94308868, + 152.06985268, + 42.62473321, + 398.03047943, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 21.36052893, - 50.99381895, - 178.86724266, - 61.57953092, - 139.78582768, - 53.00732582, - 223.01823806, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 21.36052893, + 50.99381895, + 178.86724266, + 61.57953092, + 139.78582768, + 53.00732582, + 223.01823806, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 41.36326063, - 52.81154022, - 258.88676291, - 53.12406914, - 194.52011798, - 54.89682038, - 311.24768647, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 41.36326063, + 52.81154022, + 258.88676291, + 53.12406914, + 194.52011798, + 54.89682038, + 311.24768647, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([61.45276998, 7.00421901, 82.2406738]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([61.45276998, 7.00421901, 82.2406738], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 4.074366543152521 - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 2.212842606688056, - 597.366327557872864, - 352.035143755398565, - 484.428915071471351, - 18.402345804194972, - 431.850377022773955, - 378.267899100834541, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 2.212842606688056, + 597.366327557872864, + 352.035143755398565, + 484.428915071471351, + 18.402345804194972, + 431.850377022773955, + 378.267899100834541, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([60.70, 49.60, 10.29]) - XYZ_w = np.array([96.46, 100.00, 108.62]) + XYZ = xp_as_array([60.70, 49.60, 10.29], xp=xp) + XYZ_w = xp_as_array([96.46, 100.00, 108.62], xp=xp) L_A = 40 Y_b = 16 - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 70.4406, - 58.6035, - 57.9145, - 54.5604, - 172.1555, - 51.2479, - 50.7425, - np.nan, - ] - ), - atol=5e-5, + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 70.4406, + 58.6035, + 57.9145, + 54.5604, + 172.1555, + 51.2479, + 50.7425, + np.nan, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 500, ) - def test_n_dimensional_XYZ_to_CIECAM16(self) -> None: + def test_n_dimensional_XYZ_to_CIECAM16(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam16.XYZ_to_CIECAM16` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM16["Average"] - specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 8)) - np.testing.assert_allclose( - XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 8), xp=xp) + xp_assert_close( + XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_CIECAM16(self) -> None: + def test_domain_range_scale_XYZ_to_CIECAM16(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam16.XYZ_to_CIECAM16` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM16["Average"] - specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) d_r = ( ("reference", 1, 1), @@ -244,9 +242,14 @@ def test_domain_range_scale_XYZ_to_CIECAM16(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_CIECAM16( - XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_A, + Y_b, + surround, + compute_H=True, ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -262,7 +265,9 @@ def test_nan_XYZ_to_CIECAM16(self) -> None: cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) surround = InductionFactors_CIECAM16(cases[0, 0], cases[0, 0], cases[0, 0]) - XYZ_to_CIECAM16(cases, cases, cases[..., 0], cases[..., 0], surround) + XYZ_to_CIECAM16( + cases, cases, cases[..., 0], cases[..., 0], surround, compute_H=True + ) class TestCIECAM16_to_XYZ: @@ -271,7 +276,8 @@ class TestCIECAM16_to_XYZ: tests methods. """ - def test_CIECAM16_to_XYZ(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_CIECAM16_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam16.CIECAM16_to_XYZ` definition. """ @@ -279,13 +285,13 @@ def test_CIECAM16_to_XYZ(self) -> None: specification = CAM_Specification_CIECAM16( 41.73120791, 0.10335574, 217.06795977 ) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM16["Average"] - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -293,20 +299,20 @@ def test_CIECAM16_to_XYZ(self) -> None: 65.42828069, 49.67956420, 17.48659243 ) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([57.06, 43.06, 31.96]), + [57.06, 43.06, 31.96], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CIECAM16( 21.36052893, 50.99381895, 178.86724266 ) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([3.53, 6.56, 2.14]), + [3.53, 6.56, 2.14], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -314,59 +320,59 @@ def test_CIECAM16_to_XYZ(self) -> None: 41.36326063, 52.81154022, 258.88676291 ) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CIECAM16( 2.212842606688056, 597.366327557872864, 352.035143755398565 ) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 4.074366543152521 - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([61.45276998, 7.00421901, 82.2406738]), + [61.45276998, 7.00421901, 82.2406738], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_CIECAM16(70.4406, 58.6035, 57.9145) - XYZ_w = np.array([96.46, 100.00, 108.62]) + XYZ_w = xp_as_array([96.46, 100.00, 108.62], xp=xp) L_A = 40 Y_b = 16 - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([60.70, 49.60, 10.29]), - atol=1e-4, + [60.70, 49.60, 10.29], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - def test_n_dimensional_CIECAM16_to_XYZ(self) -> None: + def test_n_dimensional_CIECAM16_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam16.CIECAM16_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM16["Average"] - specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) specification = CAM_Specification_CIECAM16( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -375,28 +381,28 @@ def test_n_dimensional_CIECAM16_to_XYZ(self) -> None: specification = CAM_Specification_CIECAM16( *tsplit(np.reshape(specification, (2, 3, 8))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_CIECAM16_to_XYZ(self) -> None: + def test_domain_range_scale_CIECAM16_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.ciecam16.CIECAM16_to_XYZ` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_CIECAM16["Average"] - specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) d_r = ( ("reference", 1, 1), @@ -424,9 +430,9 @@ def test_domain_range_scale_CIECAM16_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( CIECAM16_to_XYZ( - specification * factor_a, + specification * xp_as_array(factor_a, xp=xp), XYZ_w * factor_b, L_A, Y_b, @@ -443,15 +449,14 @@ def test_raise_exception_CIECAM16_to_XYZ(self) -> None: raised exception. """ - pytest.raises( - ValueError, - CIECAM16_to_XYZ, - CAM_Specification_CIECAM16(41.731207905126638, None, 217.06795976739301), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_CIECAM16["Average"], - ) + with pytest.raises(ValueError): + CIECAM16_to_XYZ( + CAM_Specification_CIECAM16(41.73120790512664, None, 217.067959767393), + np.array([95.05, 100.0, 108.88]), + 318.31, + 20.0, + VIEWING_CONDITIONS_CIECAM16["Average"], + ) @ignore_numpy_errors def test_nan_CIECAM16_to_XYZ(self) -> None: diff --git a/colour/appearance/tests/test_hellwig2022.py b/colour/appearance/tests/test_hellwig2022.py index 83ca06f556..95b7154012 100644 --- a/colour/appearance/tests/test_hellwig2022.py +++ b/colour/appearance/tests/test_hellwig2022.py @@ -9,6 +9,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -24,9 +29,13 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -48,151 +57,147 @@ class TestXYZ_to_Hellwig2022: unit tests methods. """ - def test_XYZ_to_Hellwig2022(self) -> None: + def test_XYZ_to_Hellwig2022(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hellwig2022.XYZ_to_Hellwig2022` definition. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 41.731, - 0.026, - 217.068, - 0.061, - 55.852, - 0.034, - 275.59498615, - np.nan, - 41.88027828, - 56.05183586, - ] - ), - atol=0.01, + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 41.731, + 0.026, + 217.068, + 0.061, + 55.852, + 0.034, + 275.59498615, + np.nan, + 41.88027828, + 56.05183586, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_A = 31.83 - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 65.428, - 31.330, - 17.487, - 47.200, - 64.077, - 30.245, - 398.03047943, - np.nan, - 70.50187436, - 69.04574688, - ] - ), - atol=0.01, + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 65.428, + 31.330, + 17.487, + 47.200, + 64.077, + 30.245, + 398.03047943, + np.nan, + 70.50187436, + 69.04574688, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 21.361, - 30.603, - 178.867, - 141.223, - 28.590, - 40.376, - 223.01823806, - np.nan, - 29.35191711, - 39.28664523, - ] - ), - atol=0.01, + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 21.361, + 30.603, + 178.867, + 141.223, + 28.590, + 40.376, + 223.01823806, + np.nan, + 29.35191711, + 39.28664523, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([109.85, 100.00, 35.58]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([109.85, 100.00, 35.58], xp=xp) L_A = 31.38 - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 41.064050542871215, - 31.939561618552826, - 259.034056616436715, - 76.668720573462167, - 40.196783565499423, - 30.818359671352116, - 311.329371306428470, - np.nan, - 49.676917719967385, - 48.627748198047854, - ] - ), - atol=0.01, + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 41.064050542871215, + 31.939561618552826, + 259.034056616436715, + 76.668720573462167, + 40.196783565499423, + 30.818359671352116, + 311.329371306428470, + np.nan, + 49.676917719967385, + 48.627748198047854, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - def test_n_dimensional_XYZ_to_Hellwig2022(self) -> None: + def test_n_dimensional_XYZ_to_Hellwig2022(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hellwig2022.XYZ_to_Hellwig2022` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - specification = XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_Hellwig2022( + XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True + ) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 10)) - np.testing.assert_allclose( - XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 10), xp=xp) + xp_assert_close( + XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_Hellwig2022(self) -> None: + def test_domain_range_scale_XYZ_to_Hellwig2022(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hellwig2022.XYZ_to_Hellwig2022` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - specification = XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_Hellwig2022( + XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True + ) d_r = ( ("reference", 1, 1), @@ -222,9 +227,14 @@ def test_domain_range_scale_XYZ_to_Hellwig2022(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hellwig2022( - XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_A, + Y_b, + surround, + compute_H=True, ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -240,7 +250,9 @@ def test_nan_XYZ_to_Hellwig2022(self) -> None: cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) surround = InductionFactors_Hellwig2022(cases[0, 0], cases[0, 0], cases[0, 0]) - XYZ_to_Hellwig2022(cases, cases, cases[..., 0], cases[..., 0], surround) + XYZ_to_Hellwig2022( + cases, cases, cases[..., 0], cases[..., 0], surround, compute_H=True + ) class TestHellwig2022_to_XYZ: @@ -249,7 +261,7 @@ class TestHellwig2022_to_XYZ: unit tests methods. """ - def test_Hellwig2022_to_XYZ(self) -> None: + def test_Hellwig2022_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hellwig2022.Hellwig2022_to_XYZ` definition. @@ -258,13 +270,13 @@ def test_Hellwig2022_to_XYZ(self) -> None: specification = CAM_Specification_Hellwig2022( 41.731207905126638, 0.025763615829912909, 217.06795976739301 ) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - np.testing.assert_allclose( + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -272,20 +284,20 @@ def test_Hellwig2022_to_XYZ(self) -> None: 65.428280687118473, 31.330032520870901, 17.486592427576902 ) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([57.06, 43.06, 31.96]), + [57.06, 43.06, 31.96], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_Hellwig2022( 21.360528925833027, 30.603219780800902, 178.8672426588991 ) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([3.53, 6.56, 2.14]), + [3.53, 6.56, 2.14], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -293,51 +305,53 @@ def test_Hellwig2022_to_XYZ(self) -> None: 41.064050542871215, 31.939561618552826, 259.03405661643671 ) L_A = 31.38 - np.testing.assert_allclose( + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_Hellwig2022( J_HK=41.880278283880095, C=0.025763615829913, h=217.067959767393010 ) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - np.testing.assert_allclose( + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Hellwig2022_to_XYZ(self) -> None: + def test_n_dimensional_Hellwig2022_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hellwig2022.Hellwig2022_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - specification = XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_Hellwig2022( + XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True + ) + XYZ = as_ndarray(Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) specification = CAM_Specification_Hellwig2022( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -346,28 +360,30 @@ def test_n_dimensional_Hellwig2022_to_XYZ(self) -> None: specification = CAM_Specification_Hellwig2022( *tsplit(np.reshape(specification, (2, 3, 10))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_Hellwig2022_to_XYZ(self) -> None: + def test_domain_range_scale_Hellwig2022_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hellwig2022.Hellwig2022_to_XYZ` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_HELLWIG2022["Average"] - specification = XYZ_to_Hellwig2022(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_Hellwig2022( + XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True + ) + XYZ = as_ndarray(Hellwig2022_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) d_r = ( ("reference", 1, 1), @@ -397,9 +413,9 @@ def test_domain_range_scale_Hellwig2022_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Hellwig2022_to_XYZ( - specification * factor_a, + specification * xp_as_array(factor_a, xp=xp), XYZ_w * factor_b, L_A, Y_b, @@ -415,27 +431,27 @@ def test_raise_exception_Hellwig2022_to_XYZ(self) -> None: Test :func:`colour.appearance.hellwig2022.Hellwig2022_to_XYZ` definition raised exception. """ - pytest.raises( - ValueError, - Hellwig2022_to_XYZ, - CAM_Specification_Hellwig2022( - J_HK=None, C=0.025763615829912909, h=217.06795976739301 - ), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_HELLWIG2022["Average"], - ) - - pytest.raises( - ValueError, - Hellwig2022_to_XYZ, - CAM_Specification_Hellwig2022(41.731207905126638, None, 217.06795976739301), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_HELLWIG2022["Average"], - ) + with pytest.raises(ValueError): + Hellwig2022_to_XYZ( + CAM_Specification_Hellwig2022( + J_HK=None, C=0.02576361582991291, h=217.067959767393 + ), + np.array([95.05, 100.0, 108.88]), + 318.31, + 20.0, + VIEWING_CONDITIONS_HELLWIG2022["Average"], + ) + + with pytest.raises(ValueError): + Hellwig2022_to_XYZ( + CAM_Specification_Hellwig2022( + 41.73120790512664, None, 217.067959767393 + ), + np.array([95.05, 100.0, 108.88]), + 318.31, + 20.0, + VIEWING_CONDITIONS_HELLWIG2022["Average"], + ) @ignore_numpy_errors def test_nan_Hellwig2022_to_XYZ(self) -> None: diff --git a/colour/appearance/tests/test_hke.py b/colour/appearance/tests/test_hke.py index ae66d687ff..9d0fa22e94 100644 --- a/colour/appearance/tests/test_hke.py +++ b/colour/appearance/tests/test_hke.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -13,7 +18,13 @@ coefficient_q_Nayatani1997, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Ilia Sibiryakov" __copyright__ = "Copyright 2013 Colour Developers" @@ -36,16 +47,18 @@ class TestHelmholtzKohlrauschEffectObjectNayatani1997: HelmholtzKohlrausch_effect_object_Nayatani1997` definition unit tests methods. """ - def test_HelmholtzKohlrausch_effect_object_Nayatani1997(self) -> None: + def test_HelmholtzKohlrausch_effect_object_Nayatani1997( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.appearance.hke.\ HelmholtzKohlrausch_effect_object_Nayatani1997` definition. """ - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_object_Nayatani1997( - np.array([0.40351010, 0.53933673]), - np.array([0.19783001, 0.46831999]), + xp_as_array([0.40351010, 0.53933673], xp=xp), + xp_as_array([0.19783001, 0.46831999], xp=xp), 63.66, method="VCC", ), @@ -53,10 +66,10 @@ def test_HelmholtzKohlrausch_effect_object_Nayatani1997(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_object_Nayatani1997( - np.array([0.40351010, 0.53933673]), - np.array([0.19783001, 0.46831999]), + xp_as_array([0.40351010, 0.53933673], xp=xp), + xp_as_array([0.19783001, 0.46831999], xp=xp), 63.66, method="VAC", ), @@ -65,7 +78,7 @@ def test_HelmholtzKohlrausch_effect_object_Nayatani1997(self) -> None: ) def test_n_dimensional_HelmholtzKohlrausch_effect_object_Nayatani1997( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.appearance.hke.\ @@ -73,23 +86,27 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_object_Nayatani1997( arrays support. """ - uv_d65 = np.array([0.19783001, 0.46831999]) - uv = np.array([0.40351010, 0.53933673]) + uv_d65 = xp_as_array([0.19783001, 0.46831999], xp=xp) + uv = xp_as_array([0.40351010, 0.53933673], xp=xp) L_a = 63.66 - result_vcc = HelmholtzKohlrausch_effect_object_Nayatani1997( - uv, uv_d65, L_a, method="VCC" + result_vcc = as_ndarray( + HelmholtzKohlrausch_effect_object_Nayatani1997( + uv, uv_d65, L_a, method="VCC" + ) ) - result_vac = HelmholtzKohlrausch_effect_object_Nayatani1997( - uv, uv_d65, L_a, method="VAC" + result_vac = as_ndarray( + HelmholtzKohlrausch_effect_object_Nayatani1997( + uv, uv_d65, L_a, method="VAC" + ) ) - uv_d65 = np.tile(uv_d65, (6, 1)) - uv = np.tile(uv, (6, 1)) - result_vcc = np.tile(result_vcc, 6) - result_vac = np.tile(result_vac, 6) + uv_d65 = xp.tile(xp_as_array(uv_d65, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + result_vcc = xp.tile(xp_as_array(result_vcc, xp=xp), (6,)) + result_vac = xp.tile(xp_as_array(result_vac, xp=xp), (6,)) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_object_Nayatani1997( uv, uv_d65, L_a, method="VCC" ), @@ -97,7 +114,7 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_object_Nayatani1997( atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_object_Nayatani1997( uv, uv_d65, L_a, method="VAC" ), @@ -105,12 +122,12 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_object_Nayatani1997( atol=TOLERANCE_ABSOLUTE_TESTS, ) - uv_d65 = np.reshape(uv_d65, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - result_vcc = np.reshape(result_vcc, (2, 3)) - result_vac = np.reshape(result_vac, (2, 3)) + uv_d65 = xp_reshape(xp_as_array(uv_d65, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + result_vcc = xp_reshape(xp_as_array(result_vcc, xp=xp), (2, 3), xp=xp) + result_vac = xp_reshape(xp_as_array(result_vac, xp=xp), (2, 3), xp=xp) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_object_Nayatani1997( uv, uv_d65, L_a, method="VCC" ), @@ -118,7 +135,7 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_object_Nayatani1997( atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_object_Nayatani1997( uv, uv_d65, L_a, method="VAC" ), @@ -146,16 +163,18 @@ class TestHelmholtzKohlrauschEffectLuminousNayatani1997: methods. """ - def test_HelmholtzKohlrausch_effect_luminous_Nayatani1997(self) -> None: + def test_HelmholtzKohlrausch_effect_luminous_Nayatani1997( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.appearance.hke.\ HelmholtzKohlrausch_effect_luminous_Nayatani1997` definition. """ - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_luminous_Nayatani1997( - np.array([0.40351010, 0.53933673]), - np.array([0.19783001, 0.46831999]), + xp_as_array([0.40351010, 0.53933673], xp=xp), + xp_as_array([0.19783001, 0.46831999], xp=xp), 63.66, method="VCC", ), @@ -163,10 +182,10 @@ def test_HelmholtzKohlrausch_effect_luminous_Nayatani1997(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_luminous_Nayatani1997( - np.array([0.40351010, 0.53933673]), - np.array([0.19783001, 0.46831999]), + xp_as_array([0.40351010, 0.53933673], xp=xp), + xp_as_array([0.19783001, 0.46831999], xp=xp), 63.66, method="VAC", ), @@ -175,7 +194,7 @@ def test_HelmholtzKohlrausch_effect_luminous_Nayatani1997(self) -> None: ) def test_n_dimensional_HelmholtzKohlrausch_effect_luminous_Nayatani1997( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.appearance.hke.\ @@ -183,23 +202,27 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_luminous_Nayatani1997( arrays support. """ - uv_d65 = np.array([0.19783001, 0.46831999]) - uv = np.array([0.40351010, 0.53933673]) + uv_d65 = xp_as_array([0.19783001, 0.46831999], xp=xp) + uv = xp_as_array([0.40351010, 0.53933673], xp=xp) L_a = 63.66 - result_vcc = HelmholtzKohlrausch_effect_luminous_Nayatani1997( - uv, uv_d65, L_a, method="VCC" + result_vcc = as_ndarray( + HelmholtzKohlrausch_effect_luminous_Nayatani1997( + uv, uv_d65, L_a, method="VCC" + ) ) - result_vac = HelmholtzKohlrausch_effect_luminous_Nayatani1997( - uv, uv_d65, L_a, method="VAC" + result_vac = as_ndarray( + HelmholtzKohlrausch_effect_luminous_Nayatani1997( + uv, uv_d65, L_a, method="VAC" + ) ) - uv_d65 = np.tile(uv_d65, (6, 1)) - uv = np.tile(uv, (6, 1)) - result_vcc = np.tile(result_vcc, 6) - result_vac = np.tile(result_vac, 6) + uv_d65 = xp.tile(xp_as_array(uv_d65, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + result_vcc = xp.tile(xp_as_array(result_vcc, xp=xp), (6,)) + result_vac = xp.tile(xp_as_array(result_vac, xp=xp), (6,)) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_luminous_Nayatani1997( uv, uv_d65, L_a, method="VCC" ), @@ -207,7 +230,7 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_luminous_Nayatani1997( atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_luminous_Nayatani1997( uv, uv_d65, L_a, method="VAC" ), @@ -215,12 +238,12 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_luminous_Nayatani1997( atol=TOLERANCE_ABSOLUTE_TESTS, ) - uv_d65 = np.reshape(uv_d65, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - result_vcc = np.reshape(result_vcc, (2, 3)) - result_vac = np.reshape(result_vac, (2, 3)) + uv_d65 = xp_reshape(xp_as_array(uv_d65, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + result_vcc = xp_reshape(xp_as_array(result_vcc, xp=xp), (2, 3), xp=xp) + result_vac = xp_reshape(xp_as_array(result_vac, xp=xp), (2, 3), xp=xp) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_luminous_Nayatani1997( uv, uv_d65, L_a, method="VCC" ), @@ -228,7 +251,7 @@ def test_n_dimensional_HelmholtzKohlrausch_effect_luminous_Nayatani1997( atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HelmholtzKohlrausch_effect_luminous_Nayatani1997( uv, uv_d65, L_a, method="VAC" ), @@ -255,37 +278,37 @@ class TestCoefficient_K_Br_Nayatani1997: definition unit tests methods. """ - def test_coefficient_K_Br_Nayatani1997(self) -> None: + def test_coefficient_K_Br_Nayatani1997(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hke.coefficient_K_Br_Nayatani1997` definition. """ - np.testing.assert_allclose( - coefficient_K_Br_Nayatani1997(10.00000000), + xp_assert_close( + coefficient_K_Br_Nayatani1997(xp_as_array([10.0], xp=xp)), 0.71344817765758839, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - coefficient_K_Br_Nayatani1997(63.66000000), + xp_assert_close( + coefficient_K_Br_Nayatani1997(xp_as_array([63.66], xp=xp)), 1.000128455584031, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - coefficient_K_Br_Nayatani1997(1000.00000000), + xp_assert_close( + coefficient_K_Br_Nayatani1997(xp_as_array([1000.0], xp=xp)), 1.401080840298197, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - coefficient_K_Br_Nayatani1997(10000.00000000), + xp_assert_close( + coefficient_K_Br_Nayatani1997(xp_as_array([10000.0], xp=xp)), 1.592511806930447, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_coefficient_K_Br_Nayatani1997(self) -> None: + def test_n_dimensional_coefficient_K_Br_Nayatani1997(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hke.coefficient_K_Br_Nayatani1997` definition n_dimensional arrays support. @@ -294,25 +317,25 @@ def test_n_dimensional_coefficient_K_Br_Nayatani1997(self) -> None: L_a = 63.66 K_Br = coefficient_K_Br_Nayatani1997(L_a) - L_a = np.tile(L_a, 6) - K_Br = np.tile(K_Br, 6) - np.testing.assert_allclose( + L_a = xp.tile(xp_as_array(L_a, xp=xp), (6,)) + K_Br = xp.tile(xp_as_array(K_Br, xp=xp), (6,)) + xp_assert_close( coefficient_K_Br_Nayatani1997(L_a), K_Br, atol=TOLERANCE_ABSOLUTE_TESTS, ) - L_a = np.reshape(L_a, (2, 3)) - K_Br = np.reshape(K_Br, (2, 3)) - np.testing.assert_allclose( + L_a = xp_reshape(xp_as_array(L_a, xp=xp), (2, 3), xp=xp) + K_Br = xp_reshape(xp_as_array(K_Br, xp=xp), (2, 3), xp=xp) + xp_assert_close( coefficient_K_Br_Nayatani1997(L_a), K_Br, atol=TOLERANCE_ABSOLUTE_TESTS, ) - L_a = np.reshape(L_a, (2, 3, 1)) - K_Br = np.reshape(K_Br, (2, 3, 1)) - np.testing.assert_allclose( + L_a = xp_reshape(xp_as_array(L_a, xp=xp), (2, 3, 1), xp=xp) + K_Br = xp_reshape(xp_as_array(K_Br, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( coefficient_K_Br_Nayatani1997(L_a), K_Br, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -335,37 +358,37 @@ class TestCoefficient_q_Nayatani1997: definition unit tests methods. """ - def test_coefficient_q_Nayatani1997(self) -> None: + def test_coefficient_q_Nayatani1997(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hke.coefficient_q_Nayatani1997` definition. """ - np.testing.assert_allclose( - coefficient_q_Nayatani1997(0.00000000), + xp_assert_close( + coefficient_q_Nayatani1997(xp_as_array([0.0], xp=xp)), -0.121200000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - coefficient_q_Nayatani1997(0.78539816), + xp_assert_close( + coefficient_q_Nayatani1997(xp_as_array([0.78539816], xp=xp)), 0.125211117768464, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - coefficient_q_Nayatani1997(1.57079633), + xp_assert_close( + coefficient_q_Nayatani1997(xp_as_array([1.57079633], xp=xp)), 0.191679999416415, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - coefficient_q_Nayatani1997(2.35619449), + xp_assert_close( + coefficient_q_Nayatani1997(xp_as_array([2.35619449], xp=xp)), 0.028480866426611, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_coefficient_q_Nayatani1997(self) -> None: + def test_n_dimensional_coefficient_q_Nayatani1997(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hke.coefficient_q_Nayatani1997` definition n_dimensional arrays support. @@ -374,22 +397,28 @@ def test_n_dimensional_coefficient_q_Nayatani1997(self) -> None: L_a = 63.66 q = coefficient_q_Nayatani1997(L_a) - L_a = np.tile(L_a, 6) - q = np.tile(q, 6) - np.testing.assert_allclose( - coefficient_q_Nayatani1997(L_a), q, atol=TOLERANCE_ABSOLUTE_TESTS + L_a = xp.tile(xp_as_array(L_a, xp=xp), (6,)) + q = xp.tile(xp_as_array(q, xp=xp), (6,)) + xp_assert_close( + coefficient_q_Nayatani1997(L_a), + q, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - L_a = np.reshape(L_a, (2, 3)) - q = np.reshape(q, (2, 3)) - np.testing.assert_allclose( - coefficient_q_Nayatani1997(L_a), q, atol=TOLERANCE_ABSOLUTE_TESTS + L_a = xp_reshape(xp_as_array(L_a, xp=xp), (2, 3), xp=xp) + q = xp_reshape(xp_as_array(q, xp=xp), (2, 3), xp=xp) + xp_assert_close( + coefficient_q_Nayatani1997(L_a), + q, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - L_a = np.reshape(L_a, (2, 3, 1)) - q = np.reshape(q, (2, 3, 1)) - np.testing.assert_allclose( - coefficient_q_Nayatani1997(L_a), q, atol=TOLERANCE_ABSOLUTE_TESTS + L_a = xp_reshape(xp_as_array(L_a, xp=xp), (2, 3, 1), xp=xp) + q = xp_reshape(xp_as_array(q, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + coefficient_q_Nayatani1997(L_a), + q, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors diff --git a/colour/appearance/tests/test_hunt.py b/colour/appearance/tests/test_hunt.py index f1e5f87c4b..4eb3e6ea4e 100644 --- a/colour/appearance/tests/test_hunt.py +++ b/colour/appearance/tests/test_hunt.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import contextlib from itertools import product @@ -13,7 +18,15 @@ XYZ_to_Hunt, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import as_float_array, domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_float_array, + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -33,7 +46,7 @@ class TestXYZ_to_Hunt: methods. """ - def test_XYZ_to_Hunt(self) -> None: + def test_XYZ_to_Hunt(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hunt.XYZ_to_Hunt` definition. @@ -44,93 +57,93 @@ def test_XYZ_to_Hunt(self) -> None: http://rit-mcsl.org/fairchild//files/AppModEx.xls """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - XYZ_b = XYZ_w * np.array([1, 0.2, 1]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) + XYZ_b = XYZ_w * xp_as_array([1, 0.2, 1], xp=xp) L_A = 318.31 surround = VIEWING_CONDITIONS_HUNT["Normal Scenes"] CCT_w = 6504.0 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), - np.array([42.12, 0.16, 269.3, 0.03, 31.92, 0.16, np.nan, np.nan]), - atol=0.05, + [42.12, 0.16, 269.3, 0.03, 31.92, 0.16, np.nan, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), - np.array([66.76, 63.89, 18.6, 153.36, 31.22, 58.28, np.nan, np.nan]), - atol=0.05, + [66.76, 63.89, 18.6, 153.36, 31.22, 58.28, np.nan, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100.00, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100.00, 35.58], xp=xp) L_A = 318.31 CCT_w = 2856 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), - np.array([19.56, 74.58, 178.3, 245.4, 18.9, 76.33, np.nan, np.nan]), - atol=0.05, + [19.56, 74.58, 178.3, 245.4, 18.9, 76.33, np.nan, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), - np.array([40.27, 73.84, 262.8, 209.29, 22.15, 67.35, np.nan, np.nan]), - atol=0.05, + [40.27, 73.84, 262.8, 209.29, 22.15, 67.35, np.nan, np.nan], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - def test_n_dimensional_XYZ_to_Hunt(self) -> None: + def test_n_dimensional_XYZ_to_Hunt(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hunt.XYZ_to_Hunt` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - XYZ_b = XYZ_w * np.array([1, 0.2, 1]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) + XYZ_b = XYZ_w * xp_as_array([1, 0.2, 1], xp=xp) L_A = 318.31 surround = VIEWING_CONDITIONS_HUNT["Normal Scenes"] CCT_w = 6504.0 specification = XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w) - XYZ = np.tile(XYZ, (6, 1)) - specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + specification = xp_as_array(np.tile(as_ndarray(specification), (6, 1)), xp=xp) + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - XYZ_b = np.tile(XYZ_b, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + XYZ_b = xp.tile(xp_as_array(XYZ_b, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ_b = np.reshape(XYZ_b, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 8)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ_b = xp_reshape(xp_as_array(XYZ_b, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(specification, (2, 3, 8), xp=xp) + xp_assert_close( XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_Hunt(self) -> None: + def test_domain_range_scale_XYZ_to_Hunt(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.hunt.XYZ_to_Hunt` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - XYZ_b = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) + XYZ_b = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 surround = VIEWING_CONDITIONS_HUNT["Normal Scenes"] CCT_w = 6504.0 @@ -143,11 +156,11 @@ def test_domain_range_scale_XYZ_to_Hunt(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunt( - XYZ * factor_a, - XYZ_w * factor_a, - XYZ_b * factor_a, + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + XYZ_b * xp_as_array(factor_a, xp=xp), L_A, surround, CCT_w=CCT_w, @@ -181,20 +194,23 @@ def test_raise_exception_XYZ_to_Hunt(self) -> None: XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w, S_w=S_w) @ignore_numpy_errors - def test_XYZ_p_XYZ_to_Hunt(self) -> None: + def test_XYZ_p_XYZ_to_Hunt(self, xp: ModuleType) -> None: """ - Test :func:`colour.appearance.hunt.XYZ_to_Hunt` definition *XYZ_p* - argument handling. + Test :func:`colour.appearance.hunt.XYZ_to_Hunt` definition *XYZ_p* and + *p* argument handling, exercising the proximal-field adjusted + reference white branch per *Hunt (1991b)* / *Fairchild (2013)* + Equations 12.23-12.28. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - XYZ_b = XYZ_p = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) + XYZ_b = xp_as_array([95.05, 100.00, 108.88], xp=xp) + XYZ_p = xp_as_array([50.00, 30.00, 80.00], xp=xp) L_A = 318.31 surround = VIEWING_CONDITIONS_HUNT["Normal Scenes"] CCT_w = 6504.0 - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunt( XYZ, XYZ_w, @@ -202,20 +218,19 @@ def test_XYZ_p_XYZ_to_Hunt(self) -> None: L_A, surround, XYZ_p=XYZ_p, + p=0.5, CCT_w=CCT_w, ), - np.array( - [ - 30.046267861960700, - 0.121050839936350, - 269.273759446144600, - 0.019909320692942, - 22.209765491265024, - 0.123896438259997, - np.nan, - np.nan, - ] - ), + [ + 28.36030943086153, + 24.97959282007880, + 317.98269937454876, + 46.96217778613629, + 17.71048437831100, + 25.56679971175820, + np.nan, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/appearance/tests/test_kim2009.py b/colour/appearance/tests/test_kim2009.py index 7399c722b3..196c932ba3 100644 --- a/colour/appearance/tests/test_kim2009.py +++ b/colour/appearance/tests/test_kim2009.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -19,9 +24,13 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -43,139 +52,132 @@ class TestXYZ_to_Kim2009: tests methods. """ - def test_XYZ_to_Kim2009(self) -> None: + @pytest.mark.mps_tolerance_absolute(5e-2) + def test_XYZ_to_Kim2009(self, xp: ModuleType) -> None: """Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition.""" - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_a = 318.31 media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] surround = VIEWING_CONDITIONS_KIM2009["Average"] - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - 28.86190898, - 0.55924559, - 219.04806678, - 9.38377973, - 52.71388839, - 0.46417384, - 278.06028246, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), + [ + 28.86190898, + 0.55924559, + 219.04806678, + 9.38377973, + 52.71388839, + 0.46417384, + 278.06028246, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_a = 31.83 - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - 70.15940419, - 57.89295872, - 21.27017200, - 61.23630434, - 128.14034598, - 48.05115573, - 1.41841443, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), + [ + 70.15940419, + 57.89295872, + 21.27017200, + 61.23630434, + 128.14034598, + 48.05115573, + 1.41841443, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100.00, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100.00, 35.58], xp=xp) L_a = 318.31 - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - -4.83430022, - 37.42013921, - 177.12166057, - np.nan, - -8.82944930, - 31.05871555, - 220.36270343, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), + [ + -4.83430022, + 37.42013921, + 177.12166057, + np.nan, + -8.82944930, + 31.05871555, + 220.36270343, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) L_a = 31.83 - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - 47.20460719, - 56.35723637, - 241.04877377, - 73.65830083, - 86.21530880, - 46.77650619, - 301.77516676, - np.nan, - ] - ), + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), + [ + 47.20460719, + 56.35723637, + 241.04877377, + 73.65830083, + 86.21530880, + 46.77650619, + 301.77516676, + np.nan, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Kim2009(self) -> None: + def test_n_dimensional_XYZ_to_Kim2009(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_a = 318.31 media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround) + specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 8)) - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 8), xp=xp) + xp_assert_close( + XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_Kim2009(self) -> None: + def test_domain_range_scale_XYZ_to_Kim2009(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_a = 318.31 media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround) + specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True) d_r = ( ("reference", 1, 1), @@ -203,9 +205,14 @@ def test_domain_range_scale_XYZ_to_Kim2009(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Kim2009( - XYZ * factor_a, XYZ_w * factor_a, L_a, media, surround + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_a, + media, + surround, + compute_H=True, ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -222,7 +229,7 @@ def test_nan_XYZ_to_Kim2009(self) -> None: cases = np.array(list(set(product(cases, repeat=3)))) media = MediaParameters_Kim2009(cases[0, 0]) surround = InductionFactors_Kim2009(cases[0, 0], cases[0, 0], cases[0, 0]) - XYZ_to_Kim2009(cases, cases, cases[0, 0], media, surround) + XYZ_to_Kim2009(cases, cases, cases[0, 0], media, surround, compute_H=True) class TestKim2009_to_XYZ: @@ -231,7 +238,7 @@ class TestKim2009_to_XYZ: tests methods. """ - def test_Kim2009_to_XYZ(self) -> None: + def test_Kim2009_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition.""" specification = CAM_Specification_Kim2009( @@ -244,14 +251,14 @@ def test_Kim2009_to_XYZ(self) -> None: 278.06028246, np.nan, ) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_a = 318.31 media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] surround = VIEWING_CONDITIONS_KIM2009["Average"] - np.testing.assert_allclose( + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([19.01, 20.00, 21.78]), - atol=0.01, + [19.01, 20.00, 21.78], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) specification = CAM_Specification_Kim2009( @@ -265,10 +272,10 @@ def test_Kim2009_to_XYZ(self) -> None: np.nan, ) L_a = 31.83 - np.testing.assert_allclose( + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([57.06, 43.06, 31.96]), - atol=0.01, + [57.06, 43.06, 31.96], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) specification = CAM_Specification_Kim2009( @@ -281,12 +288,12 @@ def test_Kim2009_to_XYZ(self) -> None: 220.36270343, np.nan, ) - XYZ_w = np.array([109.85, 100.00, 35.58]) + XYZ_w = xp_as_array([109.85, 100.00, 35.58], xp=xp) L_a = 318.31 - np.testing.assert_allclose( + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([3.53, 6.56, 2.14]), - atol=0.01, + [3.53, 6.56, 2.14], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) specification = CAM_Specification_Kim2009( @@ -300,38 +307,38 @@ def test_Kim2009_to_XYZ(self) -> None: np.nan, ) L_a = 31.83 - np.testing.assert_allclose( + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([19.01, 20.00, 21.78]), - atol=0.01, + [19.01, 20.00, 21.78], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - def test_n_dimensional_Kim2009_to_XYZ(self) -> None: + def test_n_dimensional_Kim2009_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_a = 318.31 media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround) - XYZ = Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround) + specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround, compute_H=True) + XYZ = as_ndarray(Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround)) specification = CAM_Specification_Kim2009( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -340,28 +347,30 @@ def test_n_dimensional_Kim2009_to_XYZ(self) -> None: specification = CAM_Specification_Kim2009( *tsplit(np.reshape(specification, (2, 3, 8))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_Kim2009_to_XYZ(self) -> None: + def test_domain_range_scale_Kim2009_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition domain and range scale support. """ - XYZ_i = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_i = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_a = 318.31 media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ_i, XYZ_w, L_a, media, surround) - XYZ = Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround) + specification = XYZ_to_Kim2009( + XYZ_i, XYZ_w, L_a, media, surround, compute_H=True + ) + XYZ = as_ndarray(Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround)) d_r = ( ("reference", 1, 1), @@ -389,9 +398,9 @@ def test_domain_range_scale_Kim2009_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Kim2009_to_XYZ( - specification * factor_a, + specification * xp_as_array(factor_a, xp=xp), XYZ_w * factor_b, L_a, media, @@ -408,19 +417,14 @@ def test_raise_exception_Kim2009_to_XYZ(self) -> None: raised exception. """ - pytest.raises( - ValueError, - Kim2009_to_XYZ, - CAM_Specification_Kim2009( - 41.731091132513917, - None, - 219.04843265831178, - ), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_KIM2009["Average"], - ) + with pytest.raises(ValueError): + Kim2009_to_XYZ( + CAM_Specification_Kim2009(41.73109113251392, None, 219.04843265831178), + np.array([95.05, 100.0, 108.88]), + 318.31, + 20.0, # pyright: ignore + VIEWING_CONDITIONS_KIM2009["Average"], + ) @ignore_numpy_errors def test_nan_Kim2009_to_XYZ(self) -> None: diff --git a/colour/appearance/tests/test_llab.py b/colour/appearance/tests/test_llab.py index 7399c722b3..628db6aeda 100644 --- a/colour/appearance/tests/test_llab.py +++ b/colour/appearance/tests/test_llab.py @@ -1,27 +1,31 @@ -"""Define the unit tests for the :mod:`colour.appearance.kim2009` module.""" +"""Define the unit tests for the :mod:`colour.appearance.llab` module.""" from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product +from unittest import mock import numpy as np -import pytest from colour.appearance import ( - MEDIA_PARAMETERS_KIM2009, - VIEWING_CONDITIONS_KIM2009, - CAM_Specification_Kim2009, - InductionFactors_Kim2009, - Kim2009_to_XYZ, - MediaParameters_Kim2009, - XYZ_to_Kim2009, + VIEWING_CONDITIONS_LLAB, + InductionFactors_LLAB, + XYZ_to_LLAB, + llab, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, domain_range_scale, ignore_numpy_errors, - tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -32,413 +36,165 @@ __status__ = "Production" __all__ = [ - "TestXYZ_to_Kim2009", - "TestKim2009_to_XYZ", + "TestXYZ_to_LLAB", ] -class TestXYZ_to_Kim2009: +class TestXYZ_to_LLAB: """ - Define :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition unit - tests methods. + Define :func:`colour.appearance.llab.XYZ_to_LLAB` definition unit tests + methods. """ - def test_XYZ_to_Kim2009(self) -> None: - """Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition.""" - - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - L_a = 318.31 - media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] - surround = VIEWING_CONDITIONS_KIM2009["Average"] - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - 28.86190898, - 0.55924559, - 219.04806678, - 9.38377973, - 52.71388839, - 0.46417384, - 278.06028246, - np.nan, - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - XYZ = np.array([57.06, 43.06, 31.96]) - L_a = 31.83 - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - 70.15940419, - 57.89295872, - 21.27017200, - 61.23630434, - 128.14034598, - 48.05115573, - 1.41841443, - np.nan, - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100.00, 35.58]) - L_a = 318.31 - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - -4.83430022, - 37.42013921, - 177.12166057, - np.nan, - -8.82944930, - 31.05871555, - 220.36270343, - np.nan, - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - XYZ = np.array([19.01, 20.00, 21.78]) - L_a = 31.83 - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), - np.array( - [ - 47.20460719, - 56.35723637, - 241.04877377, - 73.65830083, - 86.21530880, - 46.77650619, - 301.77516676, - np.nan, - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) + def test_XYZ_to_LLAB(self, xp: ModuleType) -> None: + """ + Test :func:`colour.appearance.llab.XYZ_to_LLAB` definition. + + Notes + ----- + - Reference values are taken from *Fairchild (2013)* Table 14.3 for + the *LLAB(l:c)* colour appearance model. The + ``MATRIX_RGB_TO_XYZ_LLAB`` constant is patched to its 4-decimal + rounded form to match the precision of the published reference + data. + """ - def test_n_dimensional_XYZ_to_Kim2009(self) -> None: + with mock.patch( + "colour.appearance.llab.MATRIX_RGB_TO_XYZ_LLAB", + np.around(np.linalg.inv(llab.MATRIX_XYZ_TO_RGB_LLAB), decimals=4), + ): + surround = VIEWING_CONDITIONS_LLAB["ref_average_4_minus"] + + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_0 = xp_as_array([95.05, 100.00, 108.88], xp=xp) + Y_b = 20.0 + L = 318.31 + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), + [37.37, 0.01, 229.5, 0, 0.02, np.nan, -0.01, -0.01], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, + ) + + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) + L = 31.83 + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), + [61.26, 30.51, 22.3, 0.5, 56.55, np.nan, 52.33, 21.43], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, + ) + + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_0 = xp_as_array([109.85, 100.00, 35.58], xp=xp) + L = 318.31 + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), + [16.25, 30.43, 173.8, 1.87, 53.83, np.nan, -53.51, 5.83], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, + ) + + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + L = 31.83 + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), + [39.82, 29.34, 271.9, 0.74, 54.59, np.nan, 1.76, -54.56], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, + ) + + def test_n_dimensional_XYZ_to_LLAB(self, xp: ModuleType) -> None: """ - Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition + Test :func:`colour.appearance.llab.XYZ_to_LLAB` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - L_a = 318.31 - media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] - surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_0 = xp_as_array([95.05, 100.00, 108.88], xp=xp) + Y_b = 20.0 + L = 318.31 + surround = VIEWING_CONDITIONS_LLAB["ref_average_4_minus"] + specification = XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), + XYZ_0 = xp.tile(xp_as_array(XYZ_0, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 8)) - np.testing.assert_allclose( - XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_0 = xp_reshape(xp_as_array(XYZ_0, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 8), xp=xp) + xp_assert_close( + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_Kim2009(self) -> None: - """ - Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition - domain and range scale support. - """ - - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - L_a = 318.31 - media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] - surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround) - - d_r = ( - ("reference", 1, 1), - ( - "1", - 0.01, - np.array( - [ - 1 / 100, - 1 / 100, - 1 / 360, - 1 / 100, - 1 / 100, - 1 / 100, - 1 / 400, - np.nan, - ] - ), - ), - ( - "100", - 1, - np.array([1, 1, 100 / 360, 1, 1, 1, 100 / 400, np.nan]), - ), - ) - for scale, factor_a, factor_b in d_r: - with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_Kim2009( - XYZ * factor_a, XYZ_w * factor_a, L_a, media, surround - ), - as_float_array(specification) * factor_b, - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - @ignore_numpy_errors - def test_nan_XYZ_to_Kim2009(self) -> None: - """ - Test :func:`colour.appearance.kim2009.XYZ_to_Kim2009` definition - nan support. - """ - - cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] - cases = np.array(list(set(product(cases, repeat=3)))) - media = MediaParameters_Kim2009(cases[0, 0]) - surround = InductionFactors_Kim2009(cases[0, 0], cases[0, 0], cases[0, 0]) - XYZ_to_Kim2009(cases, cases, cases[0, 0], media, surround) - - -class TestKim2009_to_XYZ: - """ - Define :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition unit - tests methods. - """ - - def test_Kim2009_to_XYZ(self) -> None: - """Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition.""" - - specification = CAM_Specification_Kim2009( - 28.86190898, - 0.55924559, - 219.04806678, - 9.38377973, - 52.71388839, - 0.46417384, - 278.06028246, - np.nan, - ) - XYZ_w = np.array([95.05, 100.00, 108.88]) - L_a = 318.31 - media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] - surround = VIEWING_CONDITIONS_KIM2009["Average"] - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([19.01, 20.00, 21.78]), - atol=0.01, - ) - - specification = CAM_Specification_Kim2009( - 70.15940419, - 57.89295872, - 21.27017200, - 61.23630434, - 128.14034598, - 48.05115573, - 1.41841443, - np.nan, - ) - L_a = 31.83 - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([57.06, 43.06, 31.96]), - atol=0.01, - ) - - specification = CAM_Specification_Kim2009( - -4.83430022, - 37.42013921, - 177.12166057, - np.nan, - -8.82944930, - 31.05871555, - 220.36270343, - np.nan, - ) - XYZ_w = np.array([109.85, 100.00, 35.58]) - L_a = 318.31 - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([3.53, 6.56, 2.14]), - atol=0.01, - ) - - specification = CAM_Specification_Kim2009( - 47.20460719, - 56.35723637, - 241.04877377, - 73.65830083, - 86.21530880, - 46.77650619, - 301.77516676, - np.nan, - ) - L_a = 31.83 - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - np.array([19.01, 20.00, 21.78]), - atol=0.01, - ) - - def test_n_dimensional_Kim2009_to_XYZ(self) -> None: + def test_colourspace_conversion_matrices_precision(self) -> None: """ - Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition - n-dimensional support. + Test for loss of precision in conversion between *LLAB(l:c)* colour + appearance model *CIE XYZ* tristimulus values and normalised cone + responses matrix. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - L_a = 318.31 - media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] - surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ, XYZ_w, L_a, media, surround) - XYZ = Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround) - - specification = CAM_Specification_Kim2009( - *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() - ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - XYZ, - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - XYZ, - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - specification = CAM_Specification_Kim2009( - *tsplit(np.reshape(specification, (2, 3, 8))).tolist() - ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround), - XYZ, - atol=TOLERANCE_ABSOLUTE_TESTS, - ) + start = np.array([1.0, 1.0, 1.0]) + result = np.array(start) + for _ in range(100000): + result = llab.MATRIX_RGB_TO_XYZ_LLAB @ result + result = llab.MATRIX_XYZ_TO_RGB_LLAB @ result + xp_assert_close(start, result, atol=TOLERANCE_ABSOLUTE_TESTS) - @ignore_numpy_errors - def test_domain_range_scale_Kim2009_to_XYZ(self) -> None: + def test_domain_range_scale_XYZ_to_LLAB(self, xp: ModuleType) -> None: """ - Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition - domain and range scale support. + Test :func:`colour.appearance.llab.XYZ_to_LLAB` definition domain and + range scale support. """ - XYZ_i = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) - L_a = 318.31 - media = MEDIA_PARAMETERS_KIM2009["CRT Displays"] - surround = VIEWING_CONDITIONS_KIM2009["Average"] - specification = XYZ_to_Kim2009(XYZ_i, XYZ_w, L_a, media, surround) - XYZ = Kim2009_to_XYZ(specification, XYZ_w, L_a, media, surround) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_0 = xp_as_array([95.05, 100.00, 108.88], xp=xp) + Y_b = 20.0 + L = 318.31 + surround = VIEWING_CONDITIONS_LLAB["ref_average_4_minus"] + specification = XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround) d_r = ( ("reference", 1, 1), - ( - "1", - np.array( - [ - 1 / 100, - 1 / 100, - 1 / 360, - 1 / 100, - 1 / 100, - 1 / 100, - 1 / 400, - np.nan, - ] - ), - 0.01, - ), - ( - "100", - np.array([1, 1, 100 / 360, 1, 1, 1, 100 / 400, np.nan]), - 1, - ), + ("1", 0.01, np.array([1, 1, 1 / 360, 1, 1, np.nan, 1, 1])), + ("100", 1, np.array([1, 1, 100 / 360, 1, 1, np.nan, 1, 1])), ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - Kim2009_to_XYZ( - specification * factor_a, - XYZ_w * factor_b, - L_a, - media, + xp_assert_close( + XYZ_to_LLAB( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_0 * xp_as_array(factor_a, xp=xp), + Y_b, + L, surround, ), - XYZ * factor_b, + as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_raise_exception_Kim2009_to_XYZ(self) -> None: + def test_nan_XYZ_to_LLAB(self) -> None: """ - Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition - raised exception. - """ - - pytest.raises( - ValueError, - Kim2009_to_XYZ, - CAM_Specification_Kim2009( - 41.731091132513917, - None, - 219.04843265831178, - ), - np.array([95.05, 100.00, 108.88]), - 318.31, - 20.0, - VIEWING_CONDITIONS_KIM2009["Average"], - ) - - @ignore_numpy_errors - def test_nan_Kim2009_to_XYZ(self) -> None: - """ - Test :func:`colour.appearance.kim2009.Kim2009_to_XYZ` definition nan - support. + Test :func:`colour.appearance.llab.XYZ_to_LLAB` definition nan support. """ cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) - media = MediaParameters_Kim2009(cases[0, 0]) - surround = InductionFactors_Kim2009(cases[0, 0], cases[0, 0], cases[0, 0]) - Kim2009_to_XYZ( - CAM_Specification_Kim2009( - cases[..., 0], cases[..., 0], cases[..., 0], M=50 - ), - cases, - cases[0, 0], - media, - surround, - ) + for case in cases: + XYZ = case + XYZ_0 = case + Y_b = case[0] + L = case[0] + surround = InductionFactors_LLAB(1, case[0], case[0], case[0]) + XYZ_to_LLAB(XYZ, XYZ_0, Y_b, L, surround) diff --git a/colour/appearance/tests/test_nayatani95.py b/colour/appearance/tests/test_nayatani95.py index 573de5c9db..e66db7ff23 100644 --- a/colour/appearance/tests/test_nayatani95.py +++ b/colour/appearance/tests/test_nayatani95.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.appearance import XYZ_to_Nayatani95 from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import as_float_array, domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_float_array, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,7 +40,7 @@ class TestXYZ_to_Nayatani95: unit tests methods. """ - def test_XYZ_to_Nayatani95(self) -> None: + def test_XYZ_to_Nayatani95(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition. @@ -40,105 +52,112 @@ def test_XYZ_to_Nayatani95(self) -> None: http://rit-mcsl.org/fairchild//files/AppModEx.xls """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_n = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_n = xp_as_array([95.05, 100.00, 108.88], xp=xp) Y_o = 20 E_o = 5000 E_or = 1000 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([50, 0.01, 257.5, 0.01, 62.6, 0.02, np.nan, np.nan, 50]), - atol=0.05, + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), + [50, 0.01, 257.5, 0.01, 62.6, 0.02, 317.8, np.nan, 50], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) E_o = 500 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([73, 48.3, 21.6, 37.1, 67.3, 42.9, np.nan, np.nan, 75.9]), - atol=0.05, + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), + [73, 48.3, 21.6, 37.1, 67.3, 42.9, 2.1, np.nan, 75.9], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_n = np.array([109.85, 100.00, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_n = xp_as_array([109.85, 100.00, 35.58], xp=xp) E_o = 5000 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([24.5, 49.3, 190.6, 81.3, 37.5, 62.1, np.nan, np.nan, 29.7]), - atol=0.05, + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), + [24.5, 49.3, 190.6, 81.3, 37.5, 62.1, 239.4, np.nan, 29.7], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([19.01, 20.00, 21.78]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) E_o = 500 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([49.4, 39.9, 236.3, 40.2, 44.2, 35.8, np.nan, np.nan, 49.4]), - atol=0.05, + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), + [49.4, 39.9, 236.3, 40.2, 44.2, 35.8, 303.6, np.nan, 49.4], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - def test_n_dimensional_XYZ_to_Nayatani95(self) -> None: + def test_n_dimensional_XYZ_to_Nayatani95(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_n = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_n = xp_as_array([95.05, 100.00, 108.88], xp=xp) Y_o = 20 E_o = 5000 E_or = 1000 - specification = XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or) + specification = XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_n = np.tile(XYZ_n, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 9)) - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 9), xp=xp) + xp_assert_close( + XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_Nayatani95(self) -> None: + def test_domain_range_scale_XYZ_to_Nayatani95(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_n = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_n = xp_as_array([95.05, 100.00, 108.88], xp=xp) Y_o = 20.0 E_o = 5000.0 E_or = 1000.0 - specification = XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or) + specification = XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or, compute_H=True) d_r = ( ("reference", 1, 1), - ("1", 0.01, np.array([1, 1, 1 / 360, 1, 1, 1, np.nan, np.nan, 1])), + ("1", 0.01, np.array([1, 1, 1 / 360, 1, 1, 1, 1 / 400, np.nan, 1])), ( "100", 1, - np.array([1, 1, 100 / 360, 1, 1, 1, np.nan, np.nan, 1]), + np.array([1, 1, 100 / 360, 1, 1, 1, 100 / 400, np.nan, 1]), ), ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ * factor_a, XYZ_n * factor_a, Y_o, E_o, E_or), + xp_assert_close( + XYZ_to_Nayatani95( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_n * xp_as_array(factor_a, xp=xp), + Y_o, + E_o, + E_or, + compute_H=True, + ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/appearance/tests/test_rlab.py b/colour/appearance/tests/test_rlab.py index 573de5c9db..9f6af8e63e 100644 --- a/colour/appearance/tests/test_rlab.py +++ b/colour/appearance/tests/test_rlab.py @@ -1,14 +1,30 @@ -"""Define the unit tests for the :mod:`colour.appearance.nayatani95` module.""" +"""Define the unit tests for the :mod:`colour.appearance.rlab` module.""" from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np -from colour.appearance import XYZ_to_Nayatani95 +from colour.appearance import ( + D_FACTOR_RLAB, + VIEWING_CONDITIONS_RLAB, + XYZ_to_RLAB, +) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import as_float_array, domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_float_array, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -18,138 +34,146 @@ __status__ = "Production" __all__ = [ - "TestXYZ_to_Nayatani95", + "TestXYZ_to_RLAB", ] -class TestXYZ_to_Nayatani95: +class TestXYZ_to_RLAB: """ - Define :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition - unit tests methods. + Define :func:`colour.appearance.rlab.XYZ_to_RLAB` definition unit tests + methods. """ - def test_XYZ_to_Nayatani95(self) -> None: + def test_XYZ_to_RLAB(self, xp: ModuleType) -> None: """ - Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` - definition. + Test :func:`colour.appearance.rlab.XYZ_to_RLAB` definition. Notes ----- - - The test values have been generated from data of the following file - by *Fairchild (2013)*: - http://rit-mcsl.org/fairchild//files/AppModEx.xls + - Reference values are taken from *Fairchild (2013)* Table 13.2 for + the *RLAB* colour appearance model. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_n = np.array([95.05, 100.00, 108.88]) - Y_o = 20 - E_o = 5000 - E_or = 1000 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([50, 0.01, 257.5, 0.01, 62.6, 0.02, np.nan, np.nan, 50]), - atol=0.05, + sigma = 0.4347 + + # Case 1: D65 stimulus, D65 reference white, photopic luminance. + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_n = xp_as_array([95.05, 100.00, 108.88], xp=xp) + Y_n = 318.31 + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), + [49.67, 0.01, 270, 0, np.nan, 0, -0.01], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([57.06, 43.06, 31.96]) - E_o = 500 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([73, 48.3, 21.6, 37.1, 67.3, 42.9, np.nan, np.nan, 75.9]), - atol=0.05, + # Case 2: chromatic stimulus, D65 reference white, mesopic luminance. + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) + Y_n = 31.83 + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), + [69.33, 49.74, 21.3, 0.72, np.nan, 46.33, 18.09], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_n = np.array([109.85, 100.00, 35.58]) - E_o = 5000 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([24.5, 49.3, 190.6, 81.3, 37.5, 62.1, np.nan, np.nan, 29.7]), - atol=0.05, + # Case 3: green stimulus, illuminant A reference white, photopic. + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_n = xp_as_array([109.85, 100.00, 35.58], xp=xp) + Y_n = 318.31 + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), + [30.78, 41.02, 176.9, 1.33, np.nan, -40.96, 2.25], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - XYZ = np.array([19.01, 20.00, 21.78]) - E_o = 500 - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), - np.array([49.4, 39.9, 236.3, 40.2, 44.2, 35.8, np.nan, np.nan, 49.4]), - atol=0.05, + # Case 4: D65 stimulus, illuminant A reference white, mesopic. + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + Y_n = 31.83 + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), + [49.83, 54.87, 286.5, 1.1, np.nan, 15.57, -52.61], + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) - def test_n_dimensional_XYZ_to_Nayatani95(self) -> None: + def test_n_dimensional_XYZ_to_RLAB(self, xp: ModuleType) -> None: """ - Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition + Test :func:`colour.appearance.rlab.XYZ_to_RLAB` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_n = np.array([95.05, 100.00, 108.88]) - Y_o = 20 - E_o = 5000 - E_or = 1000 - specification = XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_n = xp_as_array([95.05, 100.00, 108.88], xp=xp) + Y_n = 318.31 + sigma = 0.4347 + specification = XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_n = np.tile(XYZ_n, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 9)) - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 7), xp=xp) + xp_assert_close( + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_Nayatani95(self) -> None: + def test_domain_range_scale_XYZ_to_RLAB(self, xp: ModuleType) -> None: """ - Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition - domain and range scale support. + Test :func:`colour.appearance.rlab.XYZ_to_RLAB` definition domain and + range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_n = np.array([95.05, 100.00, 108.88]) - Y_o = 20.0 - E_o = 5000.0 - E_or = 1000.0 - specification = XYZ_to_Nayatani95(XYZ, XYZ_n, Y_o, E_o, E_or) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_n = xp_as_array([109.85, 100, 35.58], xp=xp) + Y_n = 31.83 + sigma = VIEWING_CONDITIONS_RLAB["Average"] + D = D_FACTOR_RLAB["Hard Copy Images"] + specification = XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma, D) d_r = ( ("reference", 1, 1), - ("1", 0.01, np.array([1, 1, 1 / 360, 1, 1, 1, np.nan, np.nan, 1])), - ( - "100", - 1, - np.array([1, 1, 100 / 360, 1, 1, 1, np.nan, np.nan, 1]), - ), + ("1", 0.01, np.array([1, 1, 1 / 360, 1, np.nan, 1, 1])), + ("100", 1, np.array([1, 1, 100 / 360, 1, np.nan, 1, 1])), ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_Nayatani95(XYZ * factor_a, XYZ_n * factor_a, Y_o, E_o, E_or), + xp_assert_close( + XYZ_to_RLAB( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_n * xp_as_array(factor_a, xp=xp), + Y_n, + sigma, + D, + ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_nan_XYZ_to_Nayatani95(self) -> None: + def test_nan_XYZ_to_RLAB(self) -> None: """ - Test :func:`colour.appearance.nayatani95.XYZ_to_Nayatani95` definition - nan support. + Test :func:`colour.appearance.rlab.XYZ_to_RLAB` definition nan support. """ cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) - XYZ_to_Nayatani95(cases, cases, cases[..., 0], cases[..., 0], cases[..., 0]) + for case in cases: + XYZ = case + XYZ_n = case + Y_n = case[0] + sigma = case[0] + D = case[0] + XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma, D) diff --git a/colour/appearance/tests/test_scam.py b/colour/appearance/tests/test_scam.py index f8eda174a7..55afa65b33 100644 --- a/colour/appearance/tests/test_scam.py +++ b/colour/appearance/tests/test_scam.py @@ -4,6 +4,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -18,9 +23,13 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers, UltraMo114(Molin Li)" @@ -39,132 +48,127 @@ class TestXYZ_to_sCAM: tests methods. """ - def test_XYZ_to_sCAM(self) -> None: + @pytest.mark.mps_tolerance_absolute(2e-1) + def test_XYZ_to_sCAM(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_sCAM["Average"] - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 49.97956680, - 0.01405311, - 328.27249244, - 195.23024234, - 0.00502448, - 363.60134377, - np.nan, - 49.97957273, - 50.02042727, - 34.97343274, - 65.02656726, - ] - ), + xp_assert_close( + XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 49.97956680, + 0.01405311, + 328.27249244, + 195.23024234, + 0.00502448, + 363.60134377, + np.nan, + 49.97957273, + 50.02042727, + 34.97343274, + 65.02656726, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([57.06, 43.06, 31.96]) + XYZ = xp_as_array([57.06, 43.06, 31.96], xp=xp) L_A = 31.83 - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 71.63079886, - 37.33838127, - 18.75135858, - 259.13174065, - 10.66713872, - 4.20415978, - np.nan, - 96.50614225, - 3.49385775, - 28.37649889, - 71.62350111, - ] - ), + xp_assert_close( + XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 71.63079886, + 37.33838127, + 18.75135858, + 259.13174065, + 10.66713872, + 4.20415978, + np.nan, + 96.50614225, + 3.49385775, + 28.37649889, + 71.62350111, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.array([3.53, 6.56, 2.14]) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ = xp_as_array([3.53, 6.56, 2.14], xp=xp) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), - np.array( - [ - 29.61821869, - 25.97461207, - 178.56952253, - 115.69472052, - 10.76901611, - 227.46922207, - np.nan, - 53.86353400, - 46.13646600, - -0.97480767, - 100.97480767, - ] - ), + xp_assert_close( + XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), + [ + 29.61821869, + 25.97461207, + 178.56952253, + 115.69472052, + 10.76901611, + 227.46922207, + np.nan, + 53.86353400, + 46.13646600, + -0.97480767, + 100.97480767, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_sCAM(self) -> None: + def test_n_dimensional_XYZ_to_sCAM(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_sCAM["Average"] - specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 11)) - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround), + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 11), xp=xp) + xp_assert_close( + XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_sCAM(self) -> None: + def test_domain_range_scale_XYZ_to_sCAM(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.scam.XYZ_to_sCAM` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_sCAM["Average"] - specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) d_r = ( ("reference", 1, 1), @@ -196,8 +200,15 @@ def test_domain_range_scale_XYZ_to_sCAM(self) -> None: for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_sCAM(XYZ * factor_a, XYZ_w * factor_a, L_A, Y_b, surround), + xp_assert_close( + XYZ_to_sCAM( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_A, + Y_b, + surround, + compute_H=True, + ), as_float_array(specification) * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -212,7 +223,9 @@ def test_nan_XYZ_to_sCAM(self) -> None: cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] cases = np.array(list(set(product(cases, repeat=3)))) surround = VIEWING_CONDITIONS_sCAM["Average"] - XYZ_to_sCAM(cases, cases, cases[..., 0], cases[..., 0], surround) + XYZ_to_sCAM( + cases, cases, cases[..., 0], cases[..., 0], surround, compute_H=True + ) class TestsCAM_to_XYZ: @@ -221,65 +234,65 @@ class TestsCAM_to_XYZ: tests methods. """ - def test_sCAM_to_XYZ(self) -> None: + def test_sCAM_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition. """ specification = CAM_Specification_sCAM(49.97956680, 0.01405311, 328.27249244) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_sCAM["Average"] - np.testing.assert_allclose( + xp_assert_close( sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([19.01, 20.00, 21.78]), + [19.01, 20.00, 21.78], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_sCAM(71.63079886, 37.33838127, 18.75135858) L_A = 31.83 - np.testing.assert_allclose( + xp_assert_close( sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([57.06, 43.06, 31.96]), + [57.06, 43.06, 31.96], atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_sCAM(29.61821869, 25.97461207, 178.56952253) - XYZ_w = np.array([109.85, 100, 35.58]) + XYZ_w = xp_as_array([109.85, 100, 35.58], xp=xp) L_A = 318.31 - np.testing.assert_allclose( + xp_assert_close( sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), - np.array([3.53256359, 6.56009775, 2.15585716]), + [3.53256359, 6.56009775, 2.15585716], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sCAM_to_XYZ(self) -> None: + def test_n_dimensional_sCAM_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_sCAM["Average"] - specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) specification = CAM_Specification_sCAM( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_allclose( + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -288,28 +301,28 @@ def test_n_dimensional_sCAM_to_XYZ(self) -> None: specification = CAM_Specification_sCAM( *tsplit(np.reshape(specification, (2, 3, 11))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_sCAM_to_XYZ(self) -> None: + def test_domain_range_scale_sCAM_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.appearance.scam.sCAM_to_XYZ` definition domain and range scale support. """ - XYZ = np.array([19.01, 20.00, 21.78]) - XYZ_w = np.array([95.05, 100.00, 108.88]) + XYZ = xp_as_array([19.01, 20.00, 21.78], xp=xp) + XYZ_w = xp_as_array([95.05, 100.00, 108.88], xp=xp) L_A = 318.31 Y_b = 20 surround = VIEWING_CONDITIONS_sCAM["Average"] - specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround) - XYZ = sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) + specification = XYZ_to_sCAM(XYZ, XYZ_w, L_A, Y_b, surround, compute_H=True) + XYZ = as_ndarray(sCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround)) d_r = ( ("reference", 1, 1), @@ -340,9 +353,9 @@ def test_domain_range_scale_sCAM_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sCAM_to_XYZ( - specification * factor_a, + specification * xp_as_array(factor_a, xp=xp), XYZ_w * factor_b, L_A, Y_b, diff --git a/colour/appearance/tests/test_zcam.py b/colour/appearance/tests/test_zcam.py index 72d3270ba9..d2be5b4b4d 100644 --- a/colour/appearance/tests/test_zcam.py +++ b/colour/appearance/tests/test_zcam.py @@ -4,6 +4,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import permutations import numpy as np @@ -16,11 +21,16 @@ XYZ_to_ZCAM, ZCAM_to_XYZ, ) +from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.utilities import ( as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, tsplit, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -39,57 +49,53 @@ class TestXYZ_to_ZCAM: methods. """ - def test_XYZ_to_ZCAM(self) -> None: + def test_XYZ_to_ZCAM(self, xp: ModuleType) -> None: """ Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition. """ - XYZ = np.array([185, 206, 163]) - XYZ_w = np.array([256, 264, 202]) + XYZ = xp_as_array([185, 206, 163], xp=xp) + XYZ_w = xp_as_array([256, 264, 202], xp=xp) L_a = 264 Y_b = 100 surround = VIEWING_CONDITIONS_ZCAM["Average"] - np.testing.assert_allclose( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), - np.array( - [ - 92.2520, - 3.0216, - 196.3524, - 19.1314, - 321.3464, - 10.5252, - 237.6401, - np.nan, - 34.7022, - 25.2994, - 91.6837, - ] - ), + xp_assert_close( + XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True), + [ + 92.2520, + 3.0216, + 196.3524, + 19.1314, + 321.3464, + 10.5252, + 237.6401, + np.nan, + 34.7022, + 25.2994, + 91.6837, + ], rtol=0.025, - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) - XYZ = np.array([89, 96, 120]) - np.testing.assert_allclose( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), - np.array( - [ - 71.2071, - 6.8539, - 250.6422, - 32.7963, - 248.0394, - 23.8744, - 307.0595, - np.nan, - 18.2796, - 40.4621, - 70.4026, - ] - ), + XYZ = xp_as_array([89, 96, 120], xp=xp) + xp_assert_close( + XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True), + [ + 71.2071, + 6.8539, + 250.6422, + 32.7963, + 248.0394, + 23.8744, + 307.0595, + np.nan, + 18.2796, + 40.4621, + 70.4026, + ], rtol=0.025, - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) # NOTE: Hue quadrature :math:`H_z` is significantly different for this @@ -97,123 +103,123 @@ def test_XYZ_to_ZCAM(self) -> None: # NOTE: :math:`F_L` as reported in the supplemental document has the # same value as for :math:`L_a` = 264 instead of 150. The values seem # to be computed for :math:`L_a` = 264 and :math:`Y_b` = 100. - XYZ = np.array([79, 81, 62]) + XYZ = xp_as_array([79, 81, 62], xp=xp) # L_a = 150 # Y_b = 60 surround = VIEWING_CONDITIONS_ZCAM["Dim"] - np.testing.assert_allclose( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), - np.array( - [ - 68.8890, - 0.9774, - 58.7532, - 12.5916, - 196.7686, - 2.7918, - 43.8258, - np.nan, - 11.0371, - 44.4143, - 68.8737, - ] - ), + xp_assert_close( + XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True), + [ + 68.8890, + 0.9774, + 58.7532, + 12.5916, + 196.7686, + 2.7918, + 43.8258, + np.nan, + 11.0371, + 44.4143, + 68.8737, + ], rtol=0.025, - atol=4, + atol=TOLERANCE_ABSOLUTE_TESTS * 40000000, ) - XYZ = np.array([910, 1114, 500]) - XYZ_w = np.array([2103, 2259, 1401]) + XYZ = xp_as_array([910, 1114, 500], xp=xp) + XYZ_w = xp_as_array([2103, 2259, 1401], xp=xp) L_a = 359 Y_b = 16 surround = VIEWING_CONDITIONS_ZCAM["Dark"] - np.testing.assert_allclose( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), - np.array( - [ - 82.6445, - 13.0838, - 123.9464, - 44.7277, - 114.7431, - 18.1655, - 178.6422, - np.nan, - 34.4874, - 26.8778, - 78.2653, - ] - ), + xp_assert_close( + XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True), + [ + 82.6445, + 13.0838, + 123.9464, + 44.7277, + 114.7431, + 18.1655, + 178.6422, + np.nan, + 34.4874, + 26.8778, + 78.2653, + ], rtol=0.025, - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) - XYZ = np.array([96, 67, 28]) - np.testing.assert_allclose( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), - np.array( - [ - 33.0139, - 19.4070, - 389.7720 % 360, - 86.1882, - 45.8363, - 26.9446, - 397.3301, - np.nan, - 43.6447, - 47.9942, - 30.2593, - ] - ), + XYZ = xp_as_array([96, 67, 28], xp=xp) + xp_assert_close( + XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True), + [ + 33.0139, + 19.4070, + 389.7720 % 360, + 86.1882, + 45.8363, + 26.9446, + 397.3301, + np.nan, + 43.6447, + 47.9942, + 30.2593, + ], rtol=0.025, - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) - def test_n_dimensional_XYZ_to_ZCAM(self) -> None: + def test_n_dimensional_XYZ_to_ZCAM(self, xp: ModuleType) -> None: """ Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition n-dimensional support. """ - XYZ = np.array([185, 206, 163]) - XYZ_w = np.array([256, 264, 202]) + XYZ = xp_as_array([185, 206, 163], xp=xp) + XYZ_w = xp_as_array([256, 264, 202], xp=xp) L_a = 264 Y_b = 100 surround = VIEWING_CONDITIONS_ZCAM["Average"] - specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) + specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True) - XYZ = np.tile(XYZ, (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_almost_equal( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7 + xp_assert_close( + as_ndarray(XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True)), + specification, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_almost_equal( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7 + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + as_ndarray(XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True)), + specification, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 11)) - np.testing.assert_almost_equal( - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround), specification, decimal=7 + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 11), xp=xp) + xp_assert_close( + as_ndarray(XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True)), + as_ndarray(specification), + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_XYZ_to_ZCAM(self) -> None: + def test_domain_range_scale_XYZ_to_ZCAM(self, xp: ModuleType) -> None: """ Tests :func:`colour.appearance.zcam.XYZ_to_ZCAM` definition domain and range scale support. """ - XYZ = np.array([185, 206, 163]) - XYZ_w = np.array([256, 264, 202]) + XYZ = xp_as_array([185, 206, 163], xp=xp) + XYZ_w = xp_as_array([256, 264, 202], xp=xp) L_a = 264 Y_b = 100 surround = VIEWING_CONDITIONS_ZCAM["Average"] - specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) + specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True) d_r = ( ("reference", 1, 1), @@ -240,10 +246,19 @@ def test_domain_range_scale_XYZ_to_ZCAM(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_almost_equal( - XYZ_to_ZCAM(XYZ * factor_a, XYZ_w * factor_a, L_a, Y_b, surround), + xp_assert_close( + as_ndarray( + XYZ_to_ZCAM( + XYZ * xp_as_array(factor_a, xp=xp), + XYZ_w * xp_as_array(factor_a, xp=xp), + L_a, + Y_b, + surround, + compute_H=True, + ) + ), as_float_array(specification) * factor_b, - decimal=7, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -261,7 +276,7 @@ def test_nan_XYZ_to_ZCAM(self) -> None: L_a = case[0] Y_b = 100 surround = InductionFactors_ZCAM(case[0], case[0], case[0], case[0]) - XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) + XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True) class TestZCAM_to_XYZ: @@ -270,7 +285,7 @@ class TestZCAM_to_XYZ: tests methods. """ - def test_ZCAM_to_XYZ(self) -> None: + def test_ZCAM_to_XYZ(self, xp: ModuleType) -> None: """ Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition. """ @@ -288,14 +303,14 @@ def test_ZCAM_to_XYZ(self) -> None: 25.2994, 91.6837, ) - XYZ_w = np.array([256, 264, 202]) + XYZ_w = xp_as_array([256, 264, 202], xp=xp) L_a = 264 Y_b = 100 surround = VIEWING_CONDITIONS_ZCAM["Average"] - np.testing.assert_allclose( + xp_assert_close( ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), - np.array([185, 206, 163]), - atol=0.01, + [185, 206, 163], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, rtol=0.01, ) @@ -312,10 +327,10 @@ def test_ZCAM_to_XYZ(self) -> None: 40.4621, 70.4026, ) - np.testing.assert_allclose( + xp_assert_close( ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), - np.array([89, 96, 120]), - atol=0.01, + [89, 96, 120], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, rtol=0.01, ) @@ -333,10 +348,10 @@ def test_ZCAM_to_XYZ(self) -> None: 68.8737, ) surround = VIEWING_CONDITIONS_ZCAM["Dim"] - np.testing.assert_allclose( + xp_assert_close( ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), - np.array([79, 81, 62]), - atol=0.01, + [79, 81, 62], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, rtol=0.01, ) @@ -353,14 +368,14 @@ def test_ZCAM_to_XYZ(self) -> None: 26.8778, 78.2653, ) - XYZ_w = np.array([2103, 2259, 1401]) + XYZ_w = xp_as_array([2103, 2259, 1401], xp=xp) L_a = 359 Y_b = 16 surround = VIEWING_CONDITIONS_ZCAM["Dark"] - np.testing.assert_allclose( + xp_assert_close( ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), - np.array([910, 1114, 500]), - atol=0.01, + [910, 1114, 500], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, rtol=0.01, ) @@ -377,10 +392,10 @@ def test_ZCAM_to_XYZ(self) -> None: 47.9942, 30.2593, ) - np.testing.assert_allclose( + xp_assert_close( ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), - np.array([96, 67, 28]), - atol=0.01, + [96, 67, 28], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, rtol=0.01, ) @@ -388,67 +403,73 @@ def test_ZCAM_to_XYZ(self) -> None: specification = CAM_Specification_ZCAM( J=82.61980483202505, C=13.194790413382647, h=123.77987744640157 ) - XYZ_w = np.array([2103, 2259, 1401]) + XYZ_w = xp_as_array([2103, 2259, 1401], xp=xp) L_a = 359 Y_b = 16 surround = VIEWING_CONDITIONS_ZCAM["Dark"] - np.testing.assert_allclose( + xp_assert_close( ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), - np.array([910, 1114, 500]), - atol=0.01, + [910, 1114, 500], + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, rtol=0.01, ) - def test_n_dimensional_ZCAM_to_XYZ(self) -> None: + def test_n_dimensional_ZCAM_to_XYZ(self, xp: ModuleType) -> None: """ Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition n-dimensional support. """ - XYZ = np.array([185, 206, 163]) - XYZ_w = np.array([256, 264, 202]) + XYZ = xp_as_array([185, 206, 163], xp=xp) + XYZ_w = xp_as_array([256, 264, 202], xp=xp) L_a = 264 Y_b = 100 surround = VIEWING_CONDITIONS_ZCAM["Average"] - specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround) - XYZ = ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround) + specification = XYZ_to_ZCAM(XYZ, XYZ_w, L_a, Y_b, surround, compute_H=True) + XYZ = as_ndarray(ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)) specification = CAM_Specification_ZCAM( *np.transpose(np.tile(tsplit(specification), (6, 1))).tolist() ) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_almost_equal( - ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7 + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( + as_ndarray(ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)), + as_ndarray(XYZ), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_w = np.tile(XYZ_w, (6, 1)) - np.testing.assert_almost_equal( - ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7 + XYZ_w = xp.tile(xp_as_array(XYZ_w, xp=xp), (6, 1)) + xp_assert_close( + as_ndarray(ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)), + as_ndarray(XYZ), + atol=TOLERANCE_ABSOLUTE_TESTS, ) specification = CAM_Specification_ZCAM( *tsplit(np.reshape(specification, (2, 3, 11))).tolist() ) - XYZ_w = np.reshape(XYZ_w, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_almost_equal( - ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround), XYZ, decimal=7 + XYZ_w = xp_reshape(xp_as_array(XYZ_w, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + as_ndarray(ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)), + as_ndarray(XYZ), + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors - def test_domain_range_scale_ZCAM_to_XYZ(self) -> None: + def test_domain_range_scale_ZCAM_to_XYZ(self, xp: ModuleType) -> None: """ Tests :func:`colour.appearance.zcam.ZCAM_to_XYZ` definition domain and range scale support. """ - XYZ_i = np.array([185, 206, 163]) - XYZ_w = np.array([256, 264, 202]) + XYZ_i = xp_as_array([185, 206, 163], xp=xp) + XYZ_w = xp_as_array([256, 264, 202], xp=xp) L_a = 264 Y_b = 100 surround = VIEWING_CONDITIONS_ZCAM["Average"] - specification = XYZ_to_ZCAM(XYZ_i, XYZ_w, L_a, Y_b, surround) - XYZ = ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround) + specification = XYZ_to_ZCAM(XYZ_i, XYZ_w, L_a, Y_b, surround, compute_H=True) + XYZ = as_ndarray(ZCAM_to_XYZ(specification, XYZ_w, L_a, Y_b, surround)) d_r = ( ("reference", 1, 1), @@ -475,12 +496,18 @@ def test_domain_range_scale_ZCAM_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_almost_equal( - ZCAM_to_XYZ( - specification * factor_a, XYZ_w * factor_b, L_a, Y_b, surround + xp_assert_close( + as_ndarray( + ZCAM_to_XYZ( + specification * xp_as_array(factor_a, xp=xp), + XYZ_w * factor_b, + L_a, + Y_b, + surround, + ) ), XYZ * factor_b, - decimal=7, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -490,19 +517,14 @@ def test_raise_exception_ZCAM_to_XYZ(self) -> None: raised exception. """ - pytest.raises( - ValueError, - ZCAM_to_XYZ, - CAM_Specification_ZCAM( - 41.731091132513917, - None, - 219.04843265831178, - ), - np.array([256, 264, 202]), - 318.31, - 20.0, - VIEWING_CONDITIONS_ZCAM["Average"], - ) + with pytest.raises(ValueError): + ZCAM_to_XYZ( + CAM_Specification_ZCAM(41.73109113251392, None, 219.04843265831178), + np.array([256, 264, 202]), + 318.31, + 20.0, + VIEWING_CONDITIONS_ZCAM["Average"], + ) @ignore_numpy_errors def test_nan_ZCAM_to_XYZ(self) -> None: diff --git a/colour/appearance/zcam.py b/colour/appearance/zcam.py index cf0b21d6cf..ed8ffaf8f0 100644 --- a/colour/appearance/zcam.py +++ b/colour/appearance/zcam.py @@ -27,6 +27,7 @@ from __future__ import annotations +import typing from dataclasses import astuple, dataclass, field import numpy as np @@ -35,25 +36,26 @@ from colour.algebra import sdiv, sdiv_mode, spow from colour.appearance.ciecam02 import ( VIEWING_CONDITIONS_CIECAM02, - degree_of_adaptation, - hue_angle, ) from colour.colorimetry import CCS_ILLUMINANTS -from colour.hints import ( # noqa: TC001 - Annotated, - ArrayLike, - Domain1, - NDArrayFloat, - Range1, -) + +if typing.TYPE_CHECKING: + from colour.hints import ( + Annotated, + ArrayLike, + Domain1, + NDArrayFloat, + Range1, + ) + from colour.models import Izazbz_to_XYZ, XYZ_to_Izazbz, xy_to_XYZ from colour.utilities import ( CanonicalMapping, MixinDataclassArithmetic, MixinDataclassIterable, + array_namespace, as_float, as_float_array, - as_int_array, domain_range_scale, from_range_1, from_range_degrees, @@ -63,6 +65,10 @@ to_domain_degrees, tsplit, tstack, + xp_as_float_array, + xp_degrees, + xp_radians, + xp_select, ) __author__ = "Colour Developers" @@ -337,7 +343,7 @@ def XYZ_to_ZCAM( Y_b: ArrayLike, surround: InductionFactors_ZCAM = VIEWING_CONDITIONS_ZCAM["Average"], discount_illuminant: bool = False, - compute_H: bool = True, + compute_H: bool = False, ) -> Annotated[CAM_Specification_ZCAM, (1, 1, 360, 1, 1, 1, 400, 1, 1, 1)]: """ Compute the *ZCAM* colour appearance model correlates from the specified @@ -366,8 +372,10 @@ def XYZ_to_ZCAM( discount_illuminant Truth value indicating if the illuminant should be discounted. compute_H - Whether to compute *Hue* :math:`h` quadrature :math:`H`. :math:`H` - is rarely used, and expensive to compute. + When *True*, compute the *Hue Quadrature* :math:`H` correlate + via :func:`colour.appearance.hue_quadrature`. Defaults to + *False* because :math:`H` is rarely consumed downstream and + skipping the bin search is a measurable cost saving. Returns ------- @@ -444,7 +452,10 @@ def XYZ_to_ZCAM( >>> L_A = 264 >>> Y_b = 100 >>> surround = VIEWING_CONDITIONS_ZCAM["Average"] - >>> XYZ_to_ZCAM(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS + >>> XYZ_to_ZCAM( + ... XYZ, XYZ_w, L_A, Y_b, surround, + ... compute_H=True, + ... ) # doctest: +ELLIPSIS CAM_Specification_ZCAM(J=np.float64(92.2504437...), \ C=np.float64(3.0216926...), h=np.float64(196.3245737...), \ s=np.float64(19.1319556...), Q=np.float64(321.3408463...), \ @@ -455,16 +466,24 @@ def XYZ_to_ZCAM( XYZ = to_domain_1(XYZ) XYZ_w = to_domain_1(XYZ_w) + + xp = array_namespace(XYZ, XYZ_w, L_A, Y_b) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) + L_A = xp_as_float_array(L_A, xp=xp, like=XYZ) + Y_b = xp_as_float_array(Y_b, xp=xp, like=XYZ) F_s, F, _c, _N_c = surround.values # Step 0 (Forward) - Chromatic adaptation from reference illuminant to # "CIE Standard Illuminant D65" illuminant using "CAT02". - # Computing degree of adaptation :math:`D`. - D = degree_of_adaptation(F, L_A) if not discount_illuminant else ones(L_A.shape) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*; bypassed entirely when ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=XYZ) + else: + F = xp_as_float_array(F, xp=xp, like=XYZ) + D = F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) XYZ_D65 = chromatic_adaptation_Zhai2018( XYZ, XYZ_w, TVS_D65, D, D, transform="CAT02" @@ -473,9 +492,9 @@ def XYZ_to_ZCAM( # Step 1 (Forward) - Computing factors related with viewing conditions and # independent of the test stimulus. # Background factor :math:`F_b` - F_b = np.sqrt(Y_b / Y_w) + F_b = xp.sqrt(Y_b / Y_w) # Luminance level adaptation factor :math:`F_L` - F_L = 0.171 * spow(L_A, 1 / 3) * (1 - np.exp(-48 / 9 * L_A)) + F_L = 0.171 * spow(L_A, 1 / 3) * (1 - xp.exp(-48 / 9 * L_A)) # Step 2 (Forward) - Computing achromatic response (:math:`I_z` and # :math:`I_{z,w}`), redness-greenness (:math:`a_z` and :math:`a_{z,w}`), @@ -484,14 +503,15 @@ def XYZ_to_ZCAM( I_z, a_z, b_z = tsplit(XYZ_to_Izazbz(XYZ_D65, method="Safdar 2021")) I_z_w, _a_z_w, _b_z_w = tsplit(XYZ_to_Izazbz(XYZ_w, method="Safdar 2021")) - # Step 3 (Forward) - Computing hue angle :math:`h_z` - h_z = hue_angle(a_z, b_z) + # Step 3 (Forward) - Computing hue angle :math:`h_z` in degrees in + # :math:`[0, 360)`, same formulation as in *CIECAM02*. + h_z = xp_degrees(xp.atan2(b_z, a_z)) % 360 # Step 4 (Forward) - Computing hue quadrature :math:`H`. - H = hue_quadrature(h_z) if compute_H else np.full(h_z.shape, np.nan) + H = hue_quadrature(h_z) if compute_H else xp.full_like(h_z, float("nan")) # Computing eccentricity factor :math:`e_z`. - e_z = 1.015 + np.cos(np.radians(89.038 + h_z % 360)) + e_z = 1.015 + xp.cos(xp_radians(89.038 + h_z % 360)) # Step 5 (Forward) - Computing brightness :math:`Q_z`, # lightness :math:`J_z`, colourfulness :math`M_z`, and chroma :math:`C_z` @@ -513,13 +533,13 @@ def XYZ_to_ZCAM( # Step 6 (Forward) - Computing saturation :math:`S_z`, # vividness :math:`V_z`, blackness :math:`K_z`, and whiteness :math:`W_z`. with sdiv_mode(): - S_z = 100 * spow(F_L, 0.6) * np.sqrt(sdiv(M_z, Q_z)) + S_z = 100 * spow(F_L, 0.6) * xp.sqrt(sdiv(M_z, Q_z)) - V_z = np.sqrt((J_z - 58) ** 2 + 3.4 * C_z**2) + V_z = xp.sqrt((J_z - 58) ** 2 + 3.4 * C_z**2) - K_z = 100 - 0.8 * np.sqrt(J_z**2 + 8 * C_z**2) + K_z = 100 - 0.8 * xp.sqrt(J_z**2 + 8 * C_z**2) - W_z = 100 - np.sqrt((100 - J_z) ** 2 + C_z**2) + W_z = 100 - xp.sqrt((100 - J_z) ** 2 + C_z**2) return CAM_Specification_ZCAM( J=as_float(from_range_1(J_z)), @@ -676,23 +696,37 @@ def ZCAM_to_XYZ( M_z = to_domain_1(M_z) XYZ_w = to_domain_1(XYZ_w) + + xp = array_namespace(J_z, C_z, h_z, M_z, XYZ_w, L_A, Y_b) + _X_w, Y_w, _Z_w = tsplit(XYZ_w) - L_A = as_float_array(L_A) - Y_b = as_float_array(Y_b) - F_s, F, c, N_c = surround.values + J_z = xp_as_float_array(J_z, xp=xp) + C_z = xp_as_float_array(C_z, xp=xp, like=J_z) + h_z = xp_as_float_array(h_z, xp=xp, like=J_z) + M_z = xp_as_float_array(M_z, xp=xp, like=J_z) + Y_b = xp_as_float_array(Y_b, xp=xp, like=J_z) + XYZ_w = xp_as_float_array(XYZ_w, xp=xp, like=J_z) + L_A = xp_as_float_array(L_A, xp=xp, like=J_z) + + F_s, F, _c, _N_c = surround.values # Step 0 (Forward) - Chromatic adaptation from reference illuminant to # "CIE Standard Illuminant D65" illuminant using "CAT02". - # Computing degree of adaptation :math:`D`. - D = degree_of_adaptation(F, L_A) if not discount_illuminant else ones(L_A.shape) + # Computing degree of adaptation :math:`D`, same formulation as in + # *CIECAM02*; bypassed entirely when ``discount_illuminant`` is set. + if discount_illuminant: + D = xp_as_float_array(ones(L_A.shape), xp=xp, like=J_z) + else: + F = xp_as_float_array(F, xp=xp, like=J_z) + D = F * (1 - (1 / 3.6) * xp.exp((-L_A - 42) / 92)) # Step 1 (Forward) - Computing factors related with viewing conditions and # independent of the test stimulus. # Background factor :math:`F_b` - F_b = np.sqrt(Y_b / Y_w) + F_b = xp.sqrt(Y_b / Y_w) # Luminance level adaptation factor :math:`F_L` - F_L = 0.171 * spow(L_A, 1 / 3) * (1 - np.exp(-48 / 9 * L_A)) + F_L = 0.171 * spow(L_A, 1 / 3) * (1 - xp.exp(-48 / 9 * L_A)) # Step 2 (Forward) - Computing achromatic response (:math:`I_{z,w}`), # redness-greenness (:math:`a_{z,w}`), and yellowness-blueness @@ -725,8 +759,8 @@ def ZCAM_to_XYZ( # :math:`h_z` is currently required as an input. # Computing eccentricity factor :math:`e_z`. - e_z = 1.015 + np.cos(np.radians(89.038 + h_z % 360)) - h_z_r = np.radians(h_z) + e_z = 1.015 + xp.cos(xp_radians(89.038 + h_z % 360)) + h_z_r = xp_radians(h_z) # Step 4 (Inverse) - Computing redness-greenness (:math:`a_z`), and # yellowness-blueness (:math:`b_z`). @@ -737,8 +771,8 @@ def ZCAM_to_XYZ( / (100 * spow(e_z, 0.068) * spow(F_L, 0.2)), C_z_p_e, ) - a_z = C_z_p * np.cos(h_z_r) - b_z = C_z_p * np.sin(h_z_r) + a_z = C_z_p * xp.cos(h_z_r) + b_z = C_z_p * xp.sin(h_z_r) # Step 5 (Inverse) - Computing tristimulus values :math:`XYZ_{D65}`. with domain_range_scale("ignore"): @@ -774,24 +808,40 @@ def hue_quadrature(h: ArrayLike) -> NDArrayFloat: h = as_float_array(h) - h_i = HUE_DATA_FOR_HUE_QUADRATURE["h_i"] - e_i = HUE_DATA_FOR_HUE_QUADRATURE["e_i"] - H_i = HUE_DATA_FOR_HUE_QUADRATURE["H_i"] - - # :math:`h_p` = :math:`h_z` + 360 if :math:`h_z` < :math:`h_1, i.e., h_i[0] - h = np.where(h <= h_i[0], h + 360, h) - # *np.searchsorted* returns an erroneous index if a *nan* is used as input. - h = np.where(np.isnan(h), 0, h) - i = as_int_array(np.searchsorted(h_i, h, side="left") - 1) - - h_ii = h_i[i] - e_ii = e_i[i] - H_ii = H_i[i] - h_ii1 = h_i[i + 1] - e_ii1 = e_i[i + 1] - - h_h_ii = h - h_ii - - H = H_ii + (100 * h_h_ii / e_ii) / (h_h_ii / e_ii + (h_ii1 - h) / e_ii1) + xp = array_namespace(h) + + h = as_float_array(xp.where(xp.isnan(h), 0, h)) + + # Wrap-around: h <= 33.44 is treated as h + 360. + h_w = as_float_array(xp.where(h <= 33.44, h + 360, h)) + + # Hue quadrature table (5 entries, 4 intervals). + # h_i = [33.44, 89.29, 146.30, 238.36, 393.44] + # e_i = [0.68, 0.64, 1.52, 0.77, 0.68 ] + # H_i = [0.0, 100.0, 200.0, 300.0, 400.0 ] + def _H( + h_k: float, e_k: float, H_k: float, h_k1: float, e_k1: float + ) -> NDArrayFloat: + """Compute hue quadrature for a single bin.""" + + t1 = (h_w - h_k) / e_k + t2 = (h_k1 - h_w) / e_k1 + return H_k + 100 * t1 / (t1 + t2) + + H = xp_select( + [ + (h_w >= 33.44) & (h_w < 89.29), + (h_w >= 89.29) & (h_w < 146.30), + (h_w >= 146.30) & (h_w < 238.36), + (h_w >= 238.36) & (h_w < 393.44), + ], + [ + _H(33.44, 0.68, 0.0, 89.29, 0.64), + _H(89.29, 0.64, 100.0, 146.30, 1.52), + _H(146.30, 1.52, 200.0, 238.36, 0.77), + _H(238.36, 0.77, 300.0, 393.44, 0.68), + ], + xp=xp, + ) return as_float(H) diff --git a/colour/biochemistry/michaelis_menten.py b/colour/biochemistry/michaelis_menten.py index d2407a78e3..1398fd13bd 100644 --- a/colour/biochemistry/michaelis_menten.py +++ b/colour/biochemistry/michaelis_menten.py @@ -35,10 +35,12 @@ from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, as_float_array, filter_kwargs, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -98,8 +100,11 @@ def reaction_rate_MichaelisMenten_Michaelis1913( """ S = as_float_array(S) - V_max = as_float_array(V_max) - K_m = as_float_array(K_m) + + xp = array_namespace(S, V_max, K_m) + + V_max = xp_as_float_array(V_max, xp=xp, like=S) + K_m = xp_as_float_array(K_m, xp=xp, like=S) v = (V_max * S) / (K_m + S) @@ -149,9 +154,12 @@ def reaction_rate_MichaelisMenten_Abebe2017( """ S = as_float_array(S) - V_max = as_float_array(V_max) - K_m = as_float_array(K_m) - b_m = as_float_array(b_m) + + xp = array_namespace(S, V_max, K_m, b_m) + + V_max = xp_as_float_array(V_max, xp=xp, like=S) + K_m = xp_as_float_array(K_m, xp=xp, like=S) + b_m = xp_as_float_array(b_m, xp=xp, like=S) v = (V_max * S) / (b_m * S + K_m) @@ -269,8 +277,11 @@ def substrate_concentration_MichaelisMenten_Michaelis1913( """ v = as_float_array(v) - V_max = as_float_array(V_max) - K_m = as_float_array(K_m) + + xp = array_namespace(v, V_max, K_m) + + V_max = xp_as_float_array(V_max, xp=xp, like=v) + K_m = xp_as_float_array(K_m, xp=xp, like=v) S = (v * K_m) / (V_max - v) @@ -320,9 +331,12 @@ def substrate_concentration_MichaelisMenten_Abebe2017( """ v = as_float_array(v) - V_max = as_float_array(V_max) - K_m = as_float_array(K_m) - b_m = as_float_array(b_m) + + xp = array_namespace(v, V_max, K_m, b_m) + + V_max = xp_as_float_array(V_max, xp=xp, like=v) + K_m = xp_as_float_array(K_m, xp=xp, like=v) + b_m = xp_as_float_array(b_m, xp=xp, like=v) S = (v * K_m) / (V_max - b_m * v) diff --git a/colour/biochemistry/tests/test_michaelis_menten.py b/colour/biochemistry/tests/test_michaelis_menten.py index 55fea9841e..3c955e6473 100644 --- a/colour/biochemistry/tests/test_michaelis_menten.py +++ b/colour/biochemistry/tests/test_michaelis_menten.py @@ -5,6 +5,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -18,7 +23,13 @@ substrate_concentration_MichaelisMenten_Michaelis1913, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -41,31 +52,45 @@ class TestReactionRateMichaelisMentenMichaelis1913: reaction_rate_MichaelisMenten_Michaelis1913` definition unit tests methods. """ - def test_reaction_rate_MichaelisMenten_Michaelis1913(self) -> None: + def test_reaction_rate_MichaelisMenten_Michaelis1913(self, xp: ModuleType) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ reaction_rate_MichaelisMenten_Michaelis1913` definition. """ - np.testing.assert_allclose( - reaction_rate_MichaelisMenten_Michaelis1913(0.25, 0.5, 0.25), - 0.250000000000000, + xp_assert_close( + reaction_rate_MichaelisMenten_Michaelis1913( + xp_as_array([0.25], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.250000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - reaction_rate_MichaelisMenten_Michaelis1913(0.5, 0.5, 0.25), - 0.333333333333333, + xp_assert_close( + reaction_rate_MichaelisMenten_Michaelis1913( + xp_as_array([0.5], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.333333333333333], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - reaction_rate_MichaelisMenten_Michaelis1913(0.65, 0.75, 0.35), - 0.487500000000000, + xp_assert_close( + reaction_rate_MichaelisMenten_Michaelis1913( + xp_as_array([0.65], xp=xp), + xp_as_array([0.75], xp=xp), + xp_as_array([0.35], xp=xp), + ), + [0.487500000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_reaction_rate_MichaelisMenten_Michaelis1913(self) -> None: + def test_n_dimensional_reaction_rate_MichaelisMenten_Michaelis1913( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ reaction_rate_MichaelisMenten_Michaelis1913` definition n-dimensional arrays @@ -75,29 +100,29 @@ def test_n_dimensional_reaction_rate_MichaelisMenten_Michaelis1913(self) -> None v = 0.5 V_max = 0.5 K_m = 0.25 - S = reaction_rate_MichaelisMenten_Michaelis1913(v, V_max, K_m) + S = as_ndarray(reaction_rate_MichaelisMenten_Michaelis1913(v, V_max, K_m)) - v = np.tile(v, (6, 1)) - S = np.tile(S, (6, 1)) - np.testing.assert_allclose( + v = xp.tile(xp_as_array(v, xp=xp), (6, 1)) + S = xp.tile(xp_as_array(S, xp=xp), (6, 1)) + xp_assert_close( reaction_rate_MichaelisMenten_Michaelis1913(v, V_max, K_m), S, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V_max = np.tile(V_max, (6, 1)) - K_m = np.tile(K_m, (6, 1)) - np.testing.assert_allclose( + V_max = xp.tile(xp_as_array(V_max, xp=xp), (6, 1)) + K_m = xp.tile(xp_as_array(K_m, xp=xp), (6, 1)) + xp_assert_close( reaction_rate_MichaelisMenten_Michaelis1913(v, V_max, K_m), S, atol=TOLERANCE_ABSOLUTE_TESTS, ) - v = np.reshape(v, (2, 3, 1)) - V_max = np.reshape(V_max, (2, 3, 1)) - K_m = np.reshape(K_m, (2, 3, 1)) - S = np.reshape(S, (2, 3, 1)) - np.testing.assert_allclose( + v = xp_reshape(xp_as_array(v, xp=xp), (2, 3, 1), xp=xp) + V_max = xp_reshape(xp_as_array(V_max, xp=xp), (2, 3, 1), xp=xp) + K_m = xp_reshape(xp_as_array(K_m, xp=xp), (2, 3, 1), xp=xp) + S = xp_reshape(xp_as_array(S, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( reaction_rate_MichaelisMenten_Michaelis1913(v, V_max, K_m), S, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -121,32 +146,46 @@ class TestSubstrateConcentrationMichaelisMentenMichaelis1913: reaction_rate_MichaelisMenten_Michaelis1913` definition unit tests methods. """ - def test_substrate_concentration_MichaelisMenten_Michaelis1913(self) -> None: + def test_substrate_concentration_MichaelisMenten_Michaelis1913( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ substrate_concentration_MichaelisMenten_Michaelis1913` definition. """ - np.testing.assert_allclose( - substrate_concentration_MichaelisMenten_Michaelis1913(0.25, 0.5, 0.25), - 0.250000000000000, + xp_assert_close( + substrate_concentration_MichaelisMenten_Michaelis1913( + xp_as_array([0.25], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.250000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - substrate_concentration_MichaelisMenten_Michaelis1913(1 / 3, 0.5, 0.25), - 0.500000000000000, + xp_assert_close( + substrate_concentration_MichaelisMenten_Michaelis1913( + xp_as_array([1 / 3], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.500000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - substrate_concentration_MichaelisMenten_Michaelis1913(0.4875, 0.75, 0.35), - 0.650000000000000, + xp_assert_close( + substrate_concentration_MichaelisMenten_Michaelis1913( + xp_as_array([0.4875], xp=xp), + xp_as_array([0.75], xp=xp), + xp_as_array([0.35], xp=xp), + ), + [0.650000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_n_dimensional_substrate_concentration_MichaelisMenten_Michaelis1913( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ @@ -157,29 +196,31 @@ def test_n_dimensional_substrate_concentration_MichaelisMenten_Michaelis1913( S = 1 / 3 V_max = 0.5 K_m = 0.25 - v = substrate_concentration_MichaelisMenten_Michaelis1913(S, V_max, K_m) + v = as_ndarray( + substrate_concentration_MichaelisMenten_Michaelis1913(S, V_max, K_m) + ) - S = np.tile(S, (6, 1)) - v = np.tile(v, (6, 1)) - np.testing.assert_allclose( + S = xp.tile(xp_as_array(S, xp=xp), (6, 1)) + v = xp.tile(xp_as_array(v, xp=xp), (6, 1)) + xp_assert_close( substrate_concentration_MichaelisMenten_Michaelis1913(S, V_max, K_m), v, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V_max = np.tile(V_max, (6, 1)) - K_m = np.tile(K_m, (6, 1)) - np.testing.assert_allclose( + V_max = xp.tile(xp_as_array(V_max, xp=xp), (6, 1)) + K_m = xp.tile(xp_as_array(K_m, xp=xp), (6, 1)) + xp_assert_close( substrate_concentration_MichaelisMenten_Michaelis1913(S, V_max, K_m), v, atol=TOLERANCE_ABSOLUTE_TESTS, ) - S = np.reshape(S, (2, 3, 1)) - V_max = np.reshape(V_max, (2, 3, 1)) - K_m = np.reshape(K_m, (2, 3, 1)) - v = np.reshape(v, (2, 3, 1)) - np.testing.assert_allclose( + S = xp_reshape(xp_as_array(S, xp=xp), (2, 3, 1), xp=xp) + V_max = xp_reshape(xp_as_array(V_max, xp=xp), (2, 3, 1), xp=xp) + K_m = xp_reshape(xp_as_array(K_m, xp=xp), (2, 3, 1), xp=xp) + v = xp_reshape(xp_as_array(v, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( substrate_concentration_MichaelisMenten_Michaelis1913(S, V_max, K_m), v, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -203,31 +244,48 @@ class TestReactionRateMichaelisMentenAbebe2017: reaction_rate_MichaelisMenten_Abebe2017` definition unit tests methods. """ - def test_reaction_rate_MichaelisMenten_Abebe2017(self) -> None: + def test_reaction_rate_MichaelisMenten_Abebe2017(self, xp: ModuleType) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ reaction_rate_MichaelisMenten_Abebe2017` definition. """ - np.testing.assert_allclose( - reaction_rate_MichaelisMenten_Abebe2017(0.25, 0.5, 0.25, 0.25), - 0.400000000000000, + xp_assert_close( + reaction_rate_MichaelisMenten_Abebe2017( + xp_as_array([0.25], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.400000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - reaction_rate_MichaelisMenten_Abebe2017(0.5, 0.5, 0.25, 0.25), - 0.666666666666666, + xp_assert_close( + reaction_rate_MichaelisMenten_Abebe2017( + xp_as_array([0.5], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.666666666666666], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - reaction_rate_MichaelisMenten_Abebe2017(0.65, 0.75, 0.35, 0.25), - 0.951219512195122, + xp_assert_close( + reaction_rate_MichaelisMenten_Abebe2017( + xp_as_array([0.65], xp=xp), + xp_as_array([0.75], xp=xp), + xp_as_array([0.35], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.951219512195122], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_reaction_rate_MichaelisMenten_Abebe2017(self) -> None: + def test_n_dimensional_reaction_rate_MichaelisMenten_Abebe2017( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ reaction_rate_MichaelisMenten_Abebe2017` definition n-dimensional arrays @@ -238,31 +296,31 @@ def test_n_dimensional_reaction_rate_MichaelisMenten_Abebe2017(self) -> None: V_max = 0.5 K_m = 0.25 b_m = 0.25 - S = reaction_rate_MichaelisMenten_Abebe2017(v, V_max, K_m, b_m) + S = as_ndarray(reaction_rate_MichaelisMenten_Abebe2017(v, V_max, K_m, b_m)) - v = np.tile(v, (6, 1)) - S = np.tile(S, (6, 1)) - np.testing.assert_allclose( + v = xp.tile(xp_as_array(v, xp=xp), (6, 1)) + S = xp.tile(xp_as_array(S, xp=xp), (6, 1)) + xp_assert_close( reaction_rate_MichaelisMenten_Abebe2017(v, V_max, K_m, b_m), S, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V_max = np.tile(V_max, (6, 1)) - K_m = np.tile(K_m, (6, 1)) - b_m = np.tile(b_m, (6, 1)) - np.testing.assert_allclose( + V_max = xp.tile(xp_as_array(V_max, xp=xp), (6, 1)) + K_m = xp.tile(xp_as_array(K_m, xp=xp), (6, 1)) + b_m = xp.tile(xp_as_array(b_m, xp=xp), (6, 1)) + xp_assert_close( reaction_rate_MichaelisMenten_Abebe2017(v, V_max, K_m, b_m), S, atol=TOLERANCE_ABSOLUTE_TESTS, ) - v = np.reshape(v, (2, 3, 1)) - V_max = np.reshape(V_max, (2, 3, 1)) - K_m = np.reshape(K_m, (2, 3, 1)) - b_m = np.reshape(b_m, (2, 3, 1)) - S = np.reshape(S, (2, 3, 1)) - np.testing.assert_allclose( + v = xp_reshape(xp_as_array(v, xp=xp), (2, 3, 1), xp=xp) + V_max = xp_reshape(xp_as_array(V_max, xp=xp), (2, 3, 1), xp=xp) + K_m = xp_reshape(xp_as_array(K_m, xp=xp), (2, 3, 1), xp=xp) + b_m = xp_reshape(xp_as_array(b_m, xp=xp), (2, 3, 1), xp=xp) + S = xp_reshape(xp_as_array(S, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( reaction_rate_MichaelisMenten_Abebe2017(v, V_max, K_m, b_m), S, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -286,38 +344,49 @@ class TestSubstrateConcentrationMichaelisMentenAbebe2017: reaction_rate_MichaelisMenten_Abebe2017` definition unit tests methods. """ - def test_substrate_concentration_MichaelisMenten_Abebe2017(self) -> None: + def test_substrate_concentration_MichaelisMenten_Abebe2017( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ substrate_concentration_MichaelisMenten_Abebe2017` definition. """ - np.testing.assert_allclose( + xp_assert_close( substrate_concentration_MichaelisMenten_Abebe2017( - 0.400000000000000, 0.5, 0.25, 0.25 + xp_as_array([0.400000000000000], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + xp_as_array([0.25], xp=xp), ), - 0.250000000000000, + [0.250000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( substrate_concentration_MichaelisMenten_Abebe2017( - 0.666666666666666, 0.5, 0.25, 0.25 + xp_as_array([0.666666666666666], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + xp_as_array([0.25], xp=xp), ), - 0.500000000000000, + [0.500000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( substrate_concentration_MichaelisMenten_Abebe2017( - 0.951219512195122, 0.75, 0.35, 0.25 + xp_as_array([0.951219512195122], xp=xp), + xp_as_array([0.75], xp=xp), + xp_as_array([0.35], xp=xp), + xp_as_array([0.25], xp=xp), ), - 0.650000000000000, + [0.650000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_n_dimensional_substrate_concentration_MichaelisMenten_Abebe2017( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ @@ -329,31 +398,33 @@ def test_n_dimensional_substrate_concentration_MichaelisMenten_Abebe2017( V_max = 0.5 K_m = 0.25 b_m = 0.25 - v = substrate_concentration_MichaelisMenten_Abebe2017(S, V_max, K_m, b_m) + v = as_ndarray( + substrate_concentration_MichaelisMenten_Abebe2017(S, V_max, K_m, b_m) + ) - S = np.tile(S, (6, 1)) - v = np.tile(v, (6, 1)) - np.testing.assert_allclose( + S = xp.tile(xp_as_array(S, xp=xp), (6, 1)) + v = xp.tile(xp_as_array(v, xp=xp), (6, 1)) + xp_assert_close( substrate_concentration_MichaelisMenten_Abebe2017(S, V_max, K_m, b_m), v, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V_max = np.tile(V_max, (6, 1)) - K_m = np.tile(K_m, (6, 1)) - b_m = np.tile(b_m, (6, 1)) - np.testing.assert_allclose( + V_max = xp.tile(xp_as_array(V_max, xp=xp), (6, 1)) + K_m = xp.tile(xp_as_array(K_m, xp=xp), (6, 1)) + b_m = xp.tile(xp_as_array(b_m, xp=xp), (6, 1)) + xp_assert_close( substrate_concentration_MichaelisMenten_Abebe2017(S, V_max, K_m, b_m), v, atol=TOLERANCE_ABSOLUTE_TESTS, ) - S = np.reshape(S, (2, 3, 1)) - V_max = np.reshape(V_max, (2, 3, 1)) - K_m = np.reshape(K_m, (2, 3, 1)) - b_m = np.reshape(b_m, (2, 3, 1)) - v = np.reshape(v, (2, 3, 1)) - np.testing.assert_allclose( + S = xp_reshape(xp_as_array(S, xp=xp), (2, 3, 1), xp=xp) + V_max = xp_reshape(xp_as_array(V_max, xp=xp), (2, 3, 1), xp=xp) + K_m = xp_reshape(xp_as_array(K_m, xp=xp), (2, 3, 1), xp=xp) + b_m = xp_reshape(xp_as_array(b_m, xp=xp), (2, 3, 1), xp=xp) + v = xp_reshape(xp_as_array(v, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( substrate_concentration_MichaelisMenten_Abebe2017(S, V_max, K_m, b_m), v, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -377,23 +448,31 @@ class TestReactionRateMichaelisMenten: reaction_rate_MichaelisMenten` wrapper definition unit tests methods. """ - def test_reaction_rate_MichaelisMenten(self) -> None: + def test_reaction_rate_MichaelisMenten(self, xp: ModuleType) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ reaction_rate_MichaelisMenten` wrapper definition. """ - np.testing.assert_allclose( - reaction_rate_MichaelisMenten(0.5, 2.5, 0.8), - 0.961538461538461, + xp_assert_close( + reaction_rate_MichaelisMenten( + xp_as_array([0.5], xp=xp), + xp_as_array([2.5], xp=xp), + xp_as_array([0.8], xp=xp), + ), + [0.961538461538461], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( reaction_rate_MichaelisMenten( - 0.5, 2.5, 0.8, method="Abebe 2017", b_m=0.813 + xp_as_array([0.5], xp=xp), + xp_as_array([2.5], xp=xp), + xp_as_array([0.8], xp=xp), + method="Abebe 2017", + b_m=0.813, ), - 1.036054742705597, + [1.036054742705597], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -404,22 +483,30 @@ class TestSubstrateConcentrationMichaelisMenten: substrate_concentration_MichaelisMenten` wrapper definition unit tests methods. """ - def test_substrate_concentration_MichaelisMenten(self) -> None: + def test_substrate_concentration_MichaelisMenten(self, xp: ModuleType) -> None: """ Test :func:`colour.biochemistry.michaelis_menten.\ substrate_concentration_MichaelisMenten` wrapper definition. """ - np.testing.assert_allclose( - substrate_concentration_MichaelisMenten(0.25, 0.5, 0.25), - 0.250000000000000, + xp_assert_close( + substrate_concentration_MichaelisMenten( + xp_as_array([0.25], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + ), + [0.250000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( substrate_concentration_MichaelisMenten( - 0.400000000000000, 0.5, 0.25, method="Abebe 2017", b_m=0.25 + xp_as_array([0.400000000000000], xp=xp), + xp_as_array([0.5], xp=xp), + xp_as_array([0.25], xp=xp), + method="Abebe 2017", + b_m=0.25, ), - 0.250000000000000, + [0.250000000000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/blindness/machado2009.py b/colour/blindness/machado2009.py index bef65dfca7..532a33a3b8 100644 --- a/colour/blindness/machado2009.py +++ b/colour/blindness/machado2009.py @@ -44,11 +44,15 @@ from colour.hints import ArrayLike, Literal, NDArrayFloat from colour.utilities import ( + array_namespace, as_float_array, + as_float_scalar, as_int_scalar, + as_ndarray, tsplit, tstack, usage_warning, + xp_trapezoid, ) __author__ = "Colour Developers" @@ -114,6 +118,9 @@ def matrix_RGB_to_WSYBRG( wavelengths = cmfs.wavelengths WSYBRG = vecmul(MATRIX_LMS_TO_WSYBRG, cmfs.values) + + xp = array_namespace(WSYBRG) + WS, YB, RG = tsplit(WSYBRG) primaries = reshape_msds( @@ -125,17 +132,17 @@ def matrix_RGB_to_WSYBRG( R, G, B = tsplit(primaries.values) - WS_R = np.trapezoid(R * WS, wavelengths) - WS_G = np.trapezoid(G * WS, wavelengths) - WS_B = np.trapezoid(B * WS, wavelengths) + WS_R = xp_trapezoid(R * WS, x=wavelengths, xp=xp) + WS_G = xp_trapezoid(G * WS, x=wavelengths, xp=xp) + WS_B = xp_trapezoid(B * WS, x=wavelengths, xp=xp) - YB_R = np.trapezoid(R * YB, wavelengths) - YB_G = np.trapezoid(G * YB, wavelengths) - YB_B = np.trapezoid(B * YB, wavelengths) + YB_R = xp_trapezoid(R * YB, x=wavelengths, xp=xp) + YB_G = xp_trapezoid(G * YB, x=wavelengths, xp=xp) + YB_B = xp_trapezoid(B * YB, x=wavelengths, xp=xp) - RG_R = np.trapezoid(R * RG, wavelengths) - RG_G = np.trapezoid(G * RG, wavelengths) - RG_B = np.trapezoid(B * RG, wavelengths) + RG_R = xp_trapezoid(R * RG, x=wavelengths, xp=xp) + RG_G = xp_trapezoid(G * RG, x=wavelengths, xp=xp) + RG_B = xp_trapezoid(B * RG, x=wavelengths, xp=xp) M_G = as_float_array( [ @@ -145,7 +152,7 @@ def matrix_RGB_to_WSYBRG( ] ) - return M_G / np.sum(M_G, axis=-1)[:, None] + return M_G / xp.sum(M_G, axis=-1)[:, None] def msds_cmfs_anomalous_trichromacy_Machado2009( @@ -210,8 +217,14 @@ def msds_cmfs_anomalous_trichromacy_Machado2009( cmfs.extrapolator_kwargs = {"method": "Constant", "left": 0, "right": 0} + xp = array_namespace(cmfs.values) + L, M, _S = tsplit(cmfs.values) - d_L, d_M, d_S = tsplit(d_LMS) + # ``cmfs`` is a host-bound :class:`MultiSpectralDistributions` (its + # ``wavelengths`` are *NumPy*); the shift vector is a per-call host + # parameter, so it is coerced to *NumPy* here to keep the + # ``cmfs.wavelengths - d_S`` arithmetic on a single namespace. + d_L, d_M, d_S = tsplit(as_ndarray(d_LMS)) if d_S != 0: usage_warning( @@ -223,8 +236,8 @@ def msds_cmfs_anomalous_trichromacy_Machado2009( "deuteranomaly simulation." ) - area_L = np.trapezoid(L, cmfs.wavelengths) - area_M = np.trapezoid(M, cmfs.wavelengths) + area_L = xp_trapezoid(L, x=cmfs.wavelengths, xp=xp) + area_M = xp_trapezoid(M, x=cmfs.wavelengths, xp=xp) def alpha(x: NDArrayFloat) -> NDArrayFloat: """Compute :math:`alpha` factor.""" @@ -313,7 +326,9 @@ def matrix_anomalous_trichromacy_Machado2009( cmfs_a = msds_cmfs_anomalous_trichromacy_Machado2009(cmfs, d_LMS) M_a = matrix_RGB_to_WSYBRG(cmfs_a, primaries) - return np.matmul(np.linalg.inv(M_n), M_a) + xp = array_namespace(M_n, M_a) + + return xp.matmul(xp.linalg.inv(M_n), M_a) def matrix_cvd_Machado2009( @@ -369,19 +384,24 @@ def matrix_cvd_Machado2009( "deuteranomaly simulation." ) + severity = as_float_scalar(severity) + matrices = CVD_MATRICES_MACHADO2010[deficiency] - samples = np.array(sorted(matrices.keys())) + keys = sorted(matrices.keys()) + samples = as_float_array(keys) + + xp = array_namespace(samples) + index = as_int_scalar( - np.clip(np.searchsorted(samples, severity), 0, len(samples) - 1) + xp.clip(xp.searchsorted(samples, severity), 0, len(samples) - 1) ) - a = samples[index] - b = samples[min(index + 1, len(samples) - 1)] + a = keys[index] + b = keys[min(index + 1, len(keys) - 1)] m1, m2 = matrices[a], matrices[b] if a == b: - # 1.0 severity colour vision deficiency matrix, returning directly. return m1 - return m1 + (severity - a) * ((m2 - m1) / (b - a)) + return m1 + (float(severity) - a) * ((m2 - m1) / (b - a)) diff --git a/colour/blindness/tests/test_machado2009.py b/colour/blindness/tests/test_machado2009.py index 07b745324e..8f5603b375 100644 --- a/colour/blindness/tests/test_machado2009.py +++ b/colour/blindness/tests/test_machado2009.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.blindness import ( @@ -13,7 +18,7 @@ from colour.characterisation import MSDS_DISPLAY_PRIMARIES from colour.colorimetry import MSDS_CMFS_LMS from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors +from colour.utilities import ignore_numpy_errors, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -35,87 +40,73 @@ class TestMsdsCmfsAnomalousTrichromacyMachado2009: msds_cmfs_anomalous_trichromacy_Machado2009` definition unit tests methods. """ - def test_msds_cmfs_anomalous_trichromacy_Machado2009(self) -> None: + def test_msds_cmfs_anomalous_trichromacy_Machado2009(self, xp: ModuleType) -> None: """ Test :func:`colour.blindness.machado2009.\ msds_cmfs_anomalous_trichromacy_Machado2009` definition. """ cmfs = MSDS_CMFS_LMS["Smith & Pokorny 1975 Normal Trichromats"] - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [0, 0, 0], - ), + xp_as_array([0, 0, 0], xp=xp), )[450], cmfs[450], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [1, 0, 0], - ), + xp_as_array([1, 0, 0], xp=xp), )[450], - np.array([0.03631700, 0.06350000, 0.91000000]), + [0.03631700, 0.06350000, 0.91000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [0, 1, 0], - ), + xp_as_array([0, 1, 0], xp=xp), )[450], - np.array([0.03430000, 0.06178404, 0.91000000]), + [0.03430000, 0.06178404, 0.91000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [0, 0, 1], - ), + xp_as_array([0, 0, 1], xp=xp), )[450], - np.array([0.03430000, 0.06350000, 0.92270240]), + [0.03430000, 0.06350000, 0.92270240], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [10, 0, 0], - ), + xp_as_array([10, 0, 0], xp=xp), )[450], - np.array([0.05447001, 0.06350000, 0.91000000]), + [0.05447001, 0.06350000, 0.91000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [0, 10, 0], - ), + xp_as_array([0, 10, 0], xp=xp), )[450], - np.array([0.03430000, 0.04634036, 0.91000000]), + [0.03430000, 0.04634036, 0.91000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_cmfs_anomalous_trichromacy_Machado2009( cmfs, - np.array( - [0, 0, 10], - ), + xp_as_array([0, 0, 10], xp=xp), )[450], - np.array([0.03430000, 0.06350000, 1.00000000]), + [0.03430000, 0.06350000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -126,7 +117,7 @@ class TestMatrixAnomalousTrichromacyMachado2009: matrix_anomalous_trichromacy_Machado2009` definition unit tests methods. """ - def test_matrix_anomalous_trichromacy_Machado2009(self) -> None: + def test_matrix_anomalous_trichromacy_Machado2009(self, xp: ModuleType) -> None: """ Test :func:`colour.blindness.machado2009.\ matrix_anomalous_trichromacy_Machado2009` definition. @@ -134,84 +125,84 @@ def test_matrix_anomalous_trichromacy_Machado2009(self) -> None: cmfs = MSDS_CMFS_LMS["Smith & Pokorny 1975 Normal Trichromats"] primaries = MSDS_DISPLAY_PRIMARIES["Typical CRT Brainard 1997"] - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 0, 0]) + cmfs, primaries, xp_as_array([0, 0, 0], xp=xp) ), np.identity(3), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([2, 0, 0]) + cmfs, primaries, xp_as_array([2, 0, 0], xp=xp) ), CVD_MATRICES_MACHADO2010["Protanomaly"][0.1], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([10, 0, 0]) + cmfs, primaries, xp_as_array([10, 0, 0], xp=xp) ), CVD_MATRICES_MACHADO2010["Protanomaly"][0.5], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([20, 0, 0]) + cmfs, primaries, xp_as_array([20, 0, 0], xp=xp) ), CVD_MATRICES_MACHADO2010["Protanomaly"][1.0], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 2, 0]) + cmfs, primaries, xp_as_array([0, 2, 0], xp=xp) ), CVD_MATRICES_MACHADO2010["Deuteranomaly"][0.1], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 10, 0]) + cmfs, primaries, xp_as_array([0, 10, 0], xp=xp) ), CVD_MATRICES_MACHADO2010["Deuteranomaly"][0.5], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 20, 0]) + cmfs, primaries, xp_as_array([0, 20, 0], xp=xp) ), CVD_MATRICES_MACHADO2010["Deuteranomaly"][1.0], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 0, 5.00056688094503]) + cmfs, primaries, xp_as_array([0, 0, 5.00056688094503], xp=xp) ), CVD_MATRICES_MACHADO2010["Tritanomaly"][0.1], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 0, 29.002939088780934]) + cmfs, primaries, xp_as_array([0, 0, 29.002939088780934], xp=xp) ), CVD_MATRICES_MACHADO2010["Tritanomaly"][0.5], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_anomalous_trichromacy_Machado2009( - cmfs, primaries, np.array([0, 0, 59.00590434857581]) + cmfs, primaries, xp_as_array([0, 0, 59.00590434857581], xp=xp) ), CVD_MATRICES_MACHADO2010["Tritanomaly"][1.0], - atol=0.001, + atol=TOLERANCE_ABSOLUTE_TESTS * 10000, ) @@ -221,57 +212,49 @@ class TestMatrixCvdMachado2009: definition unit tests methods. """ - def test_matrix_cvd_Machado2009(self) -> None: + def test_matrix_cvd_Machado2009(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.blindness.machado2009.matrix_cvd_Machado2009` definition. """ - np.testing.assert_allclose( + xp_assert_close( matrix_cvd_Machado2009("Protanomaly", 0.0), - np.array( - [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1], - ] - ), + [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_cvd_Machado2009("Deuteranomaly", 0.1), - np.array( - [ - [0.86643500, 0.17770400, -0.04413900], - [0.04956700, 0.93906300, 0.01137000], - [-0.00345300, 0.00723300, 0.99622000], - ] - ), + [ + [0.86643500, 0.17770400, -0.04413900], + [0.04956700, 0.93906300, 0.01137000], + [-0.00345300, 0.00723300, 0.99622000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_cvd_Machado2009("Tritanomaly", 1.0), - np.array( - [ - [1.25552800, -0.07674900, -0.17877900], - [-0.07841100, 0.93080900, 0.14760200], - [0.00473300, 0.69136700, 0.30390000], - ] - ), + [ + [1.25552800, -0.07674900, -0.17877900], + [-0.07841100, 0.93080900, 0.14760200], + [0.00473300, 0.69136700, 0.30390000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_cvd_Machado2009("Tritanomaly", 0.55), - np.array( - [ - [1.06088700, -0.01504350, -0.04584350], - [-0.01895750, 0.96774750, 0.05121150], - [0.00317700, 0.27513700, 0.72168600], - ] - ), + [ + [1.06088700, -0.01504350, -0.04584350], + [-0.01895750, 0.96774750, 0.05121150], + [0.00317700, 0.27513700, 0.72168600], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/characterisation/aces_it.py b/colour/characterisation/aces_it.py index c0b8495000..7b4a35a0bd 100644 --- a/colour/characterisation/aces_it.py +++ b/colour/characterisation/aces_it.py @@ -59,8 +59,6 @@ import os import typing -import numpy as np - from colour.adaptation import matrix_chromatic_adaptation_VonKries from colour.algebra import euclidean_distance, vecmul from colour.characterisation import ( @@ -108,6 +106,7 @@ from colour.temperature import CCT_to_xy_CIE_D from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, as_float_array, as_float_scalar, @@ -116,6 +115,9 @@ required, runtime_warning, tsplit, + xp_as_float_array, + xp_matrix_transpose, + xp_reshape, zeros, ) from colour.utilities.deprecation import handle_arguments_deprecation @@ -243,22 +245,24 @@ def sd_to_aces_relative_exposure_values( s_v = sd.values i_v = illuminant.values + xp = array_namespace(s_v) + r_bar, g_bar, b_bar = tsplit(MSDS_ACES_RICD.values) def k(x: NDArrayFloat, y: NDArrayFloat) -> float: """Compute the :math:`K_r`, :math:`K_g` or :math:`K_b` scale factors.""" - return as_float_scalar(1 / np.sum(x * y)) + return as_float_scalar(1 / xp.sum(x * y)) k_r = k(i_v, r_bar) k_g = k(i_v, g_bar) k_b = k(i_v, b_bar) - E_r = k_r * np.sum(i_v * s_v * r_bar) - E_g = k_g * np.sum(i_v * s_v * g_bar) - E_b = k_b * np.sum(i_v * s_v * b_bar) + E_r = k_r * xp.sum(i_v * s_v * r_bar) + E_g = k_g * xp.sum(i_v * s_v * g_bar) + E_b = k_b * xp.sum(i_v * s_v * b_bar) - E_rgb = np.array([E_r, E_g, E_b]) + E_rgb = xp_as_float_array([E_r, E_g, E_b], xp=xp) # Accounting for flare. E_rgb += FLARE_PERCENTAGE @@ -384,7 +388,7 @@ def generate_illuminants_rawtoaces_v1() -> CanonicalMapping: illuminants = CanonicalMapping() # CIE Illuminants D Series from 4000K to 25000K. - for i in np.arange(4000, 25000 + 500, 500): + for i in range(4000, 25000 + 500, 500): CCT = i * 1.4388 / 1.4380 xy = CCT_to_xy_CIE_D(CCT) sd = sd_CIE_illuminant_D_series(xy) @@ -392,7 +396,7 @@ def generate_illuminants_rawtoaces_v1() -> CanonicalMapping: illuminants[sd.name] = sd.align(SPECTRAL_SHAPE_RAWTOACES) # Blackbody from 1000K to 4000K. - for i in np.arange(1000, 4000, 500): + for i in range(1000, 4000, 500): sd = sd_blackbody(cast("float", i), SPECTRAL_SHAPE_RAWTOACES) illuminants[sd.name] = sd @@ -448,8 +452,10 @@ def white_balance_multipliers( runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape, copy=False) - RGB_w = 1 / np.sum(sensitivities.values * illuminant.values[..., None], axis=0) - RGB_w *= 1 / np.min(RGB_w) + xp = array_namespace(sensitivities.values) + + RGB_w = 1 / xp.sum(sensitivities.values * illuminant.values[..., None], axis=0) + RGB_w *= 1 / xp.min(RGB_w) return RGB_w @@ -497,11 +503,13 @@ def best_illuminant( RGB_w = as_float_array(RGB_w) - sse = np.inf + xp = array_namespace(RGB_w) + + sse = float("inf") illuminant_b = None for illuminant in illuminants.values(): RGB_wi = white_balance_multipliers(sensitivities, illuminant) - sse_c = np.sum((RGB_wi / RGB_w - 1) ** 2) + sse_c = xp.sum((RGB_wi / RGB_w - 1) ** 2) if sse_c < sse: sse = sse_c illuminant_b = illuminant @@ -534,6 +542,7 @@ def normalise_illuminant( Examples -------- + >>> import numpy as np >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", @@ -552,8 +561,10 @@ def normalise_illuminant( runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape) - c_i = np.argmax(np.max(sensitivities.values, axis=0)) - k = 1 / np.sum(illuminant.values * sensitivities.values[..., c_i]) + xp = array_namespace(sensitivities.values) + + c_i = xp.argmax(xp.max(sensitivities.values, axis=0)) + k = 1 / xp.sum(illuminant.values * sensitivities.values[..., c_i]) return illuminant * k @@ -615,8 +626,10 @@ def training_data_sds_to_RGB( RGB_w = white_balance_multipliers(sensitivities, illuminant) - RGB = np.dot( - np.transpose(illuminant.values[..., None] * training_data.values), + xp = array_namespace(sensitivities.values) + + RGB = xp.matmul( + xp_matrix_transpose(illuminant.values[..., None] * training_data.values, xp=xp), sensitivities.values, ) @@ -693,14 +706,16 @@ def training_data_sds_to_XYZ( ) training_data = reshape_msds(training_data, shape, copy=False) - XYZ = np.dot( - np.transpose(illuminant.values[..., None] * training_data.values), + xp = array_namespace(cmfs.values) + + XYZ = xp.matmul( + xp_matrix_transpose(illuminant.values[..., None] * training_data.values, xp=xp), cmfs.values, ) - XYZ *= 1 / np.sum(cmfs.values[..., 1] * illuminant.values) + XYZ *= 1 / xp.sum(cmfs.values[..., 1] * illuminant.values) - XYZ_w = np.dot(np.transpose(cmfs.values), illuminant.values) + XYZ_w = xp.matmul(xp_matrix_transpose(cmfs.values, xp=xp), illuminant.values) XYZ_w *= 1 / XYZ_w[1] if chromatic_adaptation_transform is not None: @@ -736,6 +751,7 @@ def whitepoint_preserving_matrix( Examples -------- + >>> import numpy as np >>> M = np.reshape(np.arange(9), (3, 3)) >>> whitepoint_preserving_matrix(M) array([[ 0., 1., 0.], @@ -744,11 +760,14 @@ def whitepoint_preserving_matrix( """ M = as_float_array(M) - RGB_w = as_float_array(RGB_w) - tail = (RGB_w - np.sum(M[..., :-1], axis=-1))[..., None] + xp = array_namespace(M, RGB_w) + + RGB_w = xp_as_float_array(RGB_w, xp=xp, like=M) - return np.concatenate([M[..., :-1], tail], axis=-1) + tail = (RGB_w - xp.sum(M[..., :-1], axis=-1))[..., None] + + return xp.concat([M[..., :-1], tail], axis=-1) def optimisation_factory_rawtoaces_v1() -> Tuple[ @@ -794,7 +813,9 @@ def objective_function( XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB)) Lab_t = XYZ_to_optimization_colour_model(XYZ_t) - return as_float(np.linalg.norm(Lab_t - Lab)) + xp = array_namespace(Lab_t) + + return as_float(xp.linalg.vector_norm(Lab_t - Lab)) def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: """*CIE XYZ* colourspace to *CIE L\\*a\\*b\\** colourspace function.""" @@ -804,8 +825,12 @@ def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: def finaliser_function(M: ArrayLike) -> NDArrayFloat: """Finaliser function.""" + M = as_float_array(M) + + xp = array_namespace(M) + return whitepoint_preserving_matrix( - np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))]) + xp.concat([xp_reshape(M, (3, 2), xp=xp), zeros((3, 1))], axis=1) ) return ( @@ -856,7 +881,9 @@ def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFlo XYZ_t = vecmul(RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, vecmul(M, RGB)) Jab_t = XYZ_to_optimization_colour_model(XYZ_t) - return as_float(np.sum(euclidean_distance(Jab, Jab_t))) + xp = array_namespace(Jab_t) + + return as_float(xp.sum(euclidean_distance(Jab, Jab_t))) def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: """*CIE XYZ* colourspace to :math:`J_za_zb_z` colourspace function.""" @@ -866,8 +893,12 @@ def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: def finaliser_function(M: ArrayLike) -> NDArrayFloat: """Finaliser function.""" + M = as_float_array(M) + + xp = array_namespace(M) + return whitepoint_preserving_matrix( - np.hstack([np.reshape(M, (3, 2)), zeros((3, 1))]) + xp.concat([xp_reshape(M, (3, 2), xp=xp), zeros((3, 1))], axis=1) ) return ( @@ -922,19 +953,25 @@ def objective_function(M: ArrayLike, RGB: ArrayLike, Jab: ArrayLike) -> DTypeFlo M = finaliser_function(M) - XYZ_t = np.transpose( - np.dot( + xp = array_namespace(M) + + XYZ_t = xp_matrix_transpose( + xp.matmul( RGB_COLOURSPACE_ACES2065_1.matrix_RGB_to_XYZ, - np.dot( + xp.matmul( M, - np.transpose(polynomial_expansion_Finlayson2015(RGB, 2, True)), + xp_matrix_transpose( + polynomial_expansion_Finlayson2015(RGB, 2, True), + xp=xp, + ), ), - ) + ), + xp=xp, ) Jab_t = XYZ_to_optimization_colour_model(XYZ_t) - return as_float(np.sum(euclidean_distance(Jab, Jab_t))) + return as_float(xp.sum(euclidean_distance(Jab, Jab_t))) def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: """*CIE XYZ* colourspace to *Oklab* colourspace function.""" @@ -944,8 +981,12 @@ def XYZ_to_optimization_colour_model(XYZ: ArrayLike) -> NDArrayFloat: def finaliser_function(M: ArrayLike) -> NDArrayFloat: """Finaliser function.""" + M = as_float_array(M) + + xp = array_namespace(M) + return whitepoint_preserving_matrix( - np.hstack([np.reshape(M, (3, 5)), zeros((3, 1))]) + xp.concat([xp_reshape(M, (3, 5), xp=xp), zeros((3, 1))], axis=1) ) return ( @@ -1062,6 +1103,7 @@ def matrix_idt( Computing the IDT matrix for a *CANON EOS 5DMark II* and *CIE Illuminant D Series* *D55* using the method specified in *RAW to ACES* v1: + >>> import numpy as np >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", @@ -1206,6 +1248,7 @@ def camera_RGB_to_ACES2065_1( Examples -------- + >>> import numpy as np >>> path = os.path.join( ... ROOT_RESOURCES_RAWTOACES, ... "CANON_EOS_5DMark_II_RGB_Sensitivities.csv", @@ -1219,12 +1262,15 @@ def camera_RGB_to_ACES2065_1( """ RGB = as_float_array(RGB) - B = as_float_array(B) - b = as_float_array(b) - k = as_float_array(k) - RGB_r = b * RGB / np.min(b) + xp = array_namespace(RGB, B, b, k) + + B = xp_as_float_array(B, xp=xp, like=RGB) + b = xp_as_float_array(b, xp=xp, like=RGB) + k = xp_as_float_array(k, xp=xp, like=RGB) + + RGB_r = b * RGB / xp.min(b) - RGB_r = np.clip(RGB_r, -np.inf, 1) if clip else RGB_r + RGB_r = xp.clip(RGB_r, -float("inf"), 1) if clip else RGB_r return k * vecmul(B, RGB_r) diff --git a/colour/characterisation/correction.py b/colour/characterisation/correction.py index eb3e9a3bea..a80e001dab 100644 --- a/colour/characterisation/correction.py +++ b/colour/characterisation/correction.py @@ -79,6 +79,7 @@ from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, as_float_array, as_int, @@ -88,6 +89,12 @@ tsplit, tstack, validate_method, + xp_as_array, + xp_as_float_array, + xp_lstsq, + xp_matrix_transpose, + xp_reshape, + xp_squeeze, ) __author__ = "Colour Developers" @@ -162,10 +169,14 @@ def matrix_augmented_Cheung2004( RGB = as_float_array(RGB) + xp = array_namespace(RGB) + R, G, B = tsplit(RGB) tail = ones(R.shape) - existing_terms = np.array([3, 4, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22, 35]) + existing_terms = xp_as_float_array( + [3, 4, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22, 35], xp=xp + ) closest_terms = as_int(closest(existing_terms, terms)) if closest_terms != terms: error = ( @@ -465,10 +476,12 @@ def polynomial_expansion_Finlayson2015( RGB = as_float_array(RGB) + xp = array_namespace(RGB) + R, G, B = tsplit(RGB) # TODO: Generalise polynomial expansion. - existing_degrees = np.array([1, 2, 3, 4]) + existing_degrees = xp_as_array([1, 2, 3, 4], xp=xp) closest_degree = as_int(closest(existing_degrees, degree)) if closest_degree != degree: error = ( @@ -652,10 +665,21 @@ def polynomial_expansion_Vandermonde(a: ArrayLike, degree: int = 1) -> NDArrayFl a = as_float_array(a) - a_e = np.transpose(np.vander(np.ravel(a), int(degree) + 1)) - a_e = np.hstack(list(np.reshape(a_e, (a_e.shape[0], -1, 3)))) + xp = array_namespace(a) + + N = int(degree) + 1 + expanded = xp_matrix_transpose( + xp.stack( + [xp_reshape(a, (-1,), xp=xp) ** i for i in range(N - 1, -1, -1)], + axis=-1, + ), + xp=xp, + ) + expanded = xp.concat( + list(xp_reshape(expanded, (expanded.shape[0], -1, 3), xp=xp)), axis=1 + ) - return np.squeeze(a_e[:, 0 : a_e.shape[-1] - a.shape[-1] + 1]) + return xp_squeeze(expanded[:, 0 : expanded.shape[-1] - a.shape[-1] + 1], xp=xp) POLYNOMIAL_EXPANSION_METHODS: CanonicalMapping = CanonicalMapping( @@ -1058,13 +1082,19 @@ def apply_matrix_colour_correction_Cheung2004( """ RGB = as_float_array(RGB) + + xp = array_namespace(RGB, CCM) + + CCM = xp_as_float_array(CCM, xp=xp, like=RGB) shape = RGB.shape - RGB = np.reshape(RGB, (-1, 3)) + RGB = xp_reshape(RGB, (-1, 3), xp=xp) RGB_e = matrix_augmented_Cheung2004(RGB, terms) - return np.reshape(np.transpose(np.dot(CCM, np.transpose(RGB_e))), shape) + return xp_reshape( + xp.squeeze(xp.matmul(CCM, RGB_e[..., None]), axis=-1), shape, xp=xp + ) def apply_matrix_colour_correction_Finlayson2015( @@ -1113,13 +1143,19 @@ def apply_matrix_colour_correction_Finlayson2015( """ RGB = as_float_array(RGB) + + xp = array_namespace(RGB, CCM) + + CCM = xp_as_float_array(CCM, xp=xp, like=RGB) shape = RGB.shape - RGB = np.reshape(RGB, (-1, 3)) + RGB = xp_reshape(RGB, (-1, 3), xp=xp) RGB_e = polynomial_expansion_Finlayson2015(RGB, degree, root_polynomial_expansion) - return np.reshape(np.transpose(np.dot(CCM, np.transpose(RGB_e))), shape) + return xp_reshape( + xp.squeeze(xp.matmul(CCM, RGB_e[..., None]), axis=-1), shape, xp=xp + ) def apply_matrix_colour_correction_Vandermonde( @@ -1163,13 +1199,19 @@ def apply_matrix_colour_correction_Vandermonde( """ RGB = as_float_array(RGB) + + xp = array_namespace(RGB, CCM) + + CCM = xp_as_float_array(CCM, xp=xp, like=RGB) shape = RGB.shape - RGB = np.reshape(RGB, (-1, 3)) + RGB = xp_reshape(RGB, (-1, 3), xp=xp) RGB_e = polynomial_expansion_Vandermonde(RGB, degree) - return np.reshape(np.transpose(np.dot(CCM, np.transpose(RGB_e))), shape) + return xp_reshape( + xp.squeeze(xp.matmul(CCM, RGB_e[..., None]), axis=-1), shape, xp=xp + ) APPLY_MATRIX_COLOUR_CORRECTION_METHODS = CanonicalMapping( @@ -1426,8 +1468,11 @@ def _tps3d_kernel_bookstein(r: np.ndarray, eps: float = 1e-12) -> np.ndarray: ---------- Thin plate spline radial basis kernel: phi(r) = r^2 log r. See e.g. Wikipedia. """ - r2 = np.maximum(r * r, eps) - return r2 * np.log(r2) + + xp = array_namespace(r) + + r2 = xp.clip(r * r, min=eps) + return r2 * xp.log(r2) def _tps3d_kernel_polyharmonic_3d(r: np.ndarray) -> np.ndarray: @@ -1450,9 +1495,12 @@ def _pairwise_distances_euclidean(A: np.ndarray, B: np.ndarray) -> np.ndarray: Compute pairwise Euclidean distances between A (M,3) and B (N,3) without SciPy. Returns (M,N). """ + + xp = array_namespace(A) + # (M,1,3) - (1,N,3) -> (M,N,3) D = A[:, None, :] - B[None, :, :] - return np.sqrt(np.sum(D * D, axis=-1)) + return xp.sqrt(xp.sum(D * D, axis=-1)) def tps3d_parameters( @@ -1489,6 +1537,8 @@ def tps3d_parameters( ctrl = as_float_array(source_points) dest = as_float_array(destination_points) + xp = array_namespace(ctrl) + if ctrl.ndim != 2 or ctrl.shape[1] != 3: message = '"source_points" must be an (N, 3) array!' raise ValueError(message) @@ -1505,35 +1555,43 @@ def tps3d_parameters( kernel = validate_method(kernel, ("Bookstein", "Polyharmonic 3D")) # P: (N,4) -> [1, R, G, B] - P = np.hstack([np.ones((N, 1)), ctrl]) + P = xp.concat([xp_as_float_array(ones((N, 1)), xp=xp, like=ctrl), ctrl], axis=1) # K: (N,N) from pairwise distances r = _pairwise_distances_euclidean(ctrl, ctrl) if kernel == "Bookstein": K = _tps3d_kernel_bookstein(r) - np.fill_diagonal(K, 0.0) + diag_mask = xp.eye(K.shape[0], dtype=bool) + K = xp.where(diag_mask, 0.0, K) else: K = _tps3d_kernel_polyharmonic_3d(r) - np.fill_diagonal(K, 0.0) + diag_mask = xp.eye(K.shape[0], dtype=bool) + K = xp.where(diag_mask, 0.0, K) if smoothing < 0: message = '"smoothing" must be >= 0!' raise ValueError(message) if smoothing > 0: - K = K + np.eye(N) * smoothing - - Z = np.zeros((4, 4)) - L = np.block([[K, P], [P.T, Z]]) + K = K + xp.eye(N) * smoothing + + Z = xp_as_float_array(np.zeros((4, 4)), xp=xp, like=ctrl) + L = xp.concat( + [ + xp.concat([K, P], axis=1), + xp.concat([xp_matrix_transpose(P, xp=xp), Z], axis=1), + ], + axis=0, + ) - V = np.vstack([dest, np.zeros((4, 3))]) + V = xp.concat([dest, xp_as_float_array(np.zeros((4, 3)), xp=xp, like=ctrl)], axis=0) # Solve L * params = V # Use solve when possible; fallback to lstsq for robustness. try: - params = np.linalg.solve(L, V) - except np.linalg.LinAlgError: - params = np.linalg.lstsq(L, V, rcond=None)[0] + params = xp.linalg.solve(L, V) + except (np.linalg.LinAlgError, RuntimeError): + params = xp_lstsq(L, V, xp=xp) W = params[:N, :] A = params[N:, :] @@ -1565,7 +1623,7 @@ def apply_tps3d( clip Whether to clip to [0, 1]. chunk_size - Process pixels in chunks to avoid huge (M,N) temporary arrays for images. + Process pixels in slices to avoid huge (M,N) temporary arrays for images. Returns ------- @@ -1575,24 +1633,27 @@ def apply_tps3d( kernel = validate_method(kernel, ("Bookstein", "Polyharmonic 3D")) RGB = as_float_array(RGB) + + xp = array_namespace(RGB) + shape = RGB.shape if shape[-1] != 3: message = '"RGB" last dimension must be 3!' raise ValueError(message) - pixels = RGB.reshape((-1, 3)) + pixels = xp_reshape(RGB, (-1, 3), xp=xp) M = pixels.shape[0] - out = np.empty_like(pixels) + slices = [] - # Precompute affine input [1, R, G, B] - # Do it chunked to keep memory stable. for start in range(0, M, chunk_size): end = min(start + chunk_size, M) X = pixels[start:end] - P_all = np.hstack([np.ones((X.shape[0], 1)), X]) # (m,4) + P_all = xp.concat( + [xp_as_float_array(ones((X.shape[0], 1)), xp=xp, like=X), X], axis=1 + ) # (m,4) r = _pairwise_distances_euclidean(X, ctrl) # (m,N) if kernel == "Bookstein": @@ -1600,12 +1661,14 @@ def apply_tps3d( else: U = _tps3d_kernel_polyharmonic_3d(r) - out[start:end] = U @ W + P_all @ A + slices.append(xp.matmul(U, W) + xp.matmul(P_all, A)) + + out = xp.concat(slices, axis=0) if clip: - out = np.clip(out, 0.0, 1.0) + out = xp.clip(out, 0.0, 1.0) - return out.reshape(shape) + return xp_reshape(out, shape, xp=xp) def colour_correction_TPS3D( diff --git a/colour/characterisation/tests/test_aces_it.py b/colour/characterisation/tests/test_aces_it.py index 0385543f6d..75bf9ad150 100644 --- a/colour/characterisation/tests/test_aces_it.py +++ b/colour/characterisation/tests/test_aces_it.py @@ -5,8 +5,10 @@ from __future__ import annotations import os +import typing import numpy as np +import pytest from colour.characterisation import ( MSDS_ACES_RICD, @@ -41,7 +43,17 @@ ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import read_sds_from_csv_file -from colour.utilities import domain_range_scale, is_scipy_installed +from colour.utilities import ( + domain_range_scale, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -101,49 +113,50 @@ def test_sd_to_aces_relative_exposure_values(self) -> None: shape = MSDS_ACES_RICD.shape grey_reflector = sd_constant(0.18, shape) - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values(grey_reflector), - np.array([0.18, 0.18, 0.18]), + [0.18, 0.18, 0.18], atol=TOLERANCE_ABSOLUTE_TESTS, ) perfect_reflector = sd_ones(shape) - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values(perfect_reflector), - np.array([0.97783784, 0.97783784, 0.97783784]), + [0.97783784, 0.97783784, 0.97783784], atol=TOLERANCE_ABSOLUTE_TESTS, ) dark_skin = SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"] - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values(dark_skin), - np.array([0.11807796, 0.08690312, 0.05891252]), + [0.11807796, 0.08690312, 0.05891252], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values(dark_skin, SDS_ILLUMINANTS["A"]), - np.array([0.12937082, 0.09120875, 0.06110636]), + [0.12937082, 0.09120875, 0.06110636], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values(dark_skin), - np.array([0.11807796, 0.08690312, 0.05891252]), + [0.11807796, 0.08690312, 0.05891252], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values( dark_skin, chromatic_adaptation_transform="Bradford", ), - np.array([0.11805993, 0.08689013, 0.05900396]), + [0.11805993, 0.08689013, 0.05900396], atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_domain_range_scale_spectral_to_aces_relative_exposure_values( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test :func:`colour.characterisation.aces_it. @@ -158,7 +171,7 @@ def test_domain_range_scale_spectral_to_aces_relative_exposure_values( d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_aces_relative_exposure_values(grey_reflector), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -258,18 +271,18 @@ def test_white_balance_multipliers(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( white_balance_multipliers(MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"]), - np.array([2.34141541, 1.00000000, 1.51633759]), + [2.34141541, 1.00000000, 1.51633759], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( white_balance_multipliers( MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["ISO 7589 Studio Tungsten"], ), - np.array([1.57095278, 1.00000000, 2.43560477]), + [1.57095278, 1.00000000, 2.43560477], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -321,7 +334,7 @@ def test_normalise_illuminant(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( np.sum( normalise_illuminant( SDS_ILLUMINANTS["D55"], MSDS_CANON_EOS_5DMARK_II @@ -349,208 +362,206 @@ def test_training_data_sds_to_RGB(self) -> None: MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"], ) - np.testing.assert_allclose( + xp_assert_close( RGB, - np.array( - [ - [42.00296381, 39.83290349, 43.28842394], - [181.25453293, 180.47486885, 180.30657630], - [1580.35041765, 1578.67251435, 1571.05703787], - [403.67553672, 403.67553672, 403.67553672], - [1193.51958332, 1194.63985124, 1183.92806238], - [862.07824054, 863.30644583, 858.29863779], - [605.42274304, 602.94953701, 596.61414309], - [395.70687930, 394.67167942, 392.97719777], - [227.27502116, 228.33554705, 227.96959477], - [130.97735082, 132.12395139, 131.97239271], - [61.79308820, 61.85572037, 62.40560537], - [592.29430914, 383.93309398, 282.70032306], - [504.67305022, 294.69245978, 193.90976423], - [640.93167741, 494.91914821, 421.68337308], - [356.53952646, 239.77610719, 181.18147755], - [179.58569818, 130.00540238, 109.23999883], - [1061.07297514, 818.29727750, 730.13362169], - [765.75936417, 522.06805938, 456.59355601], - [104.70554060, 80.35106922, 65.75667232], - [694.19925422, 161.06849749, 214.20170991], - [1054.83161580, 709.41713619, 668.10329523], - [697.35479081, 276.20032105, 275.86226833], - [183.26315174, 65.93801513, 74.60775905], - [359.74416854, 59.73576149, 89.81296522], - [1043.53760601, 405.48081521, 376.37298474], - [344.35374209, 111.26727966, 109.10587712], - [215.18064862, 87.41152853, 85.18152727], - [555.37005673, 134.76016985, 111.54658160], - [931.71846961, 210.02605133, 150.65312210], - [211.01186324, 50.73939233, 54.55750662], - [654.45781665, 132.73694874, 107.20009737], - [1193.89772859, 625.60766645, 521.51066476], - [802.65730883, 228.94887565, 178.30864097], - [149.82853589, 44.31839648, 55.29195048], - [80.88083928, 33.78936351, 41.73438243], - [579.50157840, 240.80755019, 188.50864121], - [537.09280420, 80.41714202, 48.28907694], - [777.62363031, 205.11587061, 122.43126732], - [292.65436510, 59.53457252, 44.27126512], - [511.68625012, 134.76897130, 85.73242441], - [903.64947615, 462.49015529, 350.74183199], - [852.95457070, 291.64071698, 151.51871958], - [1427.59841722, 907.54863477, 724.29520203], - [527.68979414, 169.76114596, 89.48561902], - [496.62188809, 317.11827387, 243.77642038], - [554.39017413, 284.77453644, 181.92376325], - [310.50669032, 96.25812545, 41.22765558], - [1246.49891599, 522.05121993, 238.28646123], - [240.19646249, 118.57745244, 82.68426681], - [1005.98836135, 355.93514762, 118.60457241], - [792.31376787, 369.56509398, 143.27388201], - [459.04590557, 315.46594358, 215.53901098], - [806.50918893, 352.20277469, 97.69239677], - [1574.11778922, 1078.61331515, 697.02647383], - [1015.45155837, 598.98507153, 301.94169280], - [479.68722930, 242.23619637, 72.60351059], - [1131.70538515, 628.32510627, 213.67910327], - [185.86573238, 162.55033903, 137.59385867], - [1131.77074807, 603.89218698, 153.83160203], - [638.14148862, 527.18090248, 410.12394346], - [884.58039320, 655.09236879, 329.23967927], - [1172.73094356, 840.43080883, 380.90114088], - [1490.24223350, 1111.18491878, 482.33357611], - [1054.70234779, 513.29967197, 91.55980977], - [1532.99674295, 1035.15868150, 253.21942988], - [662.35328287, 528.52354760, 326.56458987], - [1769.55456145, 1557.58571488, 1155.79098414], - [1196.62083017, 1079.28012658, 888.47017893], - [1578.73591185, 1089.40083172, 314.45691871], - [252.98204345, 206.56788008, 153.62801631], - [973.59975800, 714.51185344, 251.12884859], - [1661.01720988, 1340.46809762, 619.61710815], - [656.66179353, 566.61547800, 322.22788098], - [676.69663303, 571.86743785, 249.62031449], - [1229.28626315, 1020.14702709, 353.11090960], - [390.76190378, 324.36051944, 119.31108035], - [1524.10495708, 1366.72397704, 633.03830849], - [1264.54750712, 1149.12002542, 335.25348483], - [265.96753330, 260.89397210, 130.78590008], - [90.15969432, 90.72350914, 55.12008388], - [298.22463247, 300.48700028, 101.95760063], - [813.34391710, 820.12623357, 313.17818415], - [186.96402165, 190.38042094, 104.27515726], - [230.34939258, 235.91900919, 120.77815429], - [469.57926615, 472.51064145, 256.40912347], - [117.81249486, 129.17019984, 69.78861213], - [133.39581196, 151.50390168, 77.66255652], - [164.19259747, 172.13159331, 80.92295294], - [146.12230124, 149.32536508, 87.48300520], - [201.93215173, 208.89885695, 111.84447436], - [248.41427850, 282.34047722, 122.55482010], - [304.35509339, 377.38986207, 118.66130122], - [381.85533606, 530.40398972, 150.83506876], - [967.19810669, 1161.33086750, 663.54746741], - [613.98437237, 865.41677370, 362.92357557], - [410.21304405, 611.89683658, 284.09389273], - [279.50447144, 416.01646348, 213.18049093], - [334.48807624, 487.46571814, 235.49134434], - [664.04349337, 867.87454943, 549.71146455], - [311.66934673, 431.38058636, 256.13307806], - [110.04404638, 203.88196409, 104.63331585], - [153.35857585, 312.67834716, 149.90942505], - [273.46344514, 462.41992197, 292.50571823], - [184.77058437, 267.46361125, 193.71894670], - [75.79805899, 163.84071881, 95.67465270], - [461.73803707, 668.68797906, 484.77687282], - [523.01992144, 790.69326153, 598.73122243], - [105.89414085, 124.92341127, 113.03925656], - [279.33299507, 446.45128537, 344.73426977], - [340.57250119, 381.28610429, 353.83182947], - [141.00956904, 329.50139051, 228.90179483], - [117.29728945, 156.88993944, 139.49878229], - [565.12438106, 696.52297174, 615.88218349], - [1046.73447319, 1446.22424473, 1277.47338963], - [133.87404291, 253.25944193, 224.75872956], - [586.52626500, 1015.43013448, 885.49907251], - [927.08412116, 1197.93784752, 1140.76612264], - [81.29463446, 202.46201173, 186.35209411], - [350.90699453, 788.82959642, 669.10307704], - [278.88231719, 581.42068355, 526.82554470], - [642.66176703, 990.64038619, 907.64284280], - [689.10344984, 942.49383066, 900.33073076], - [190.62073977, 540.21088595, 523.62573562], - [322.35685764, 676.02683754, 692.94583013], - [896.29532467, 1289.90474463, 1311.34615018], - [204.06785020, 321.83261403, 337.01923114], - [237.10512554, 549.97044011, 646.06486244], - [907.26703197, 1252.44260107, 1309.50173432], - [504.74103065, 728.27088424, 782.27808125], - [470.91049729, 912.49116456, 1059.41083523], - [231.75497961, 539.14727494, 732.41647792], - [624.91135978, 943.51709467, 1086.48492282], - [104.84186738, 398.05825469, 663.96030581], - [100.47632953, 226.41423139, 323.51675153], - [998.19560093, 1168.81108673, 1283.07267859], - [350.74519746, 457.74100518, 552.52270183], - [223.19531677, 560.14850077, 855.05346039], - [66.92044931, 128.18947830, 205.30719728], - [280.63458798, 518.51069955, 784.38948897], - [1071.24122457, 1267.16339790, 1467.81704311], - [271.47257445, 553.57609491, 914.33723598], - [211.86582477, 295.18643027, 418.51776463], - [153.86457460, 342.06625645, 649.82579665], - [179.59188635, 265.25370235, 413.68135787], - [529.77485058, 737.79030218, 1046.29865466], - [208.71936449, 421.30392624, 796.71281168], - [685.50294808, 879.76243717, 1195.00892794], - [85.02189613, 113.33360860, 171.03209018], - [72.06980264, 139.42600347, 315.97906141], - [349.57868286, 426.82308690, 556.49647978], - [726.50329821, 882.48411184, 1163.20130103], - [102.62158777, 177.73895468, 467.26740089], - [208.63097281, 322.84137064, 639.30554347], - [377.19498209, 456.13180268, 706.44272480], - [149.91131672, 218.16462984, 455.15510078], - [556.80606655, 673.96774240, 1020.98785748], - [172.19546054, 181.38617476, 478.69666973], - [494.98572332, 534.88874559, 773.75255591], - [1166.31475206, 1207.81829513, 1411.04368728], - [324.81131421, 298.91188334, 521.96994638], - [731.58631467, 725.95113189, 1192.71141630], - [376.70584074, 352.06184423, 572.37854429], - [421.32413767, 465.07677606, 910.85999527], - [155.65680826, 145.82096629, 282.56390371], - [982.43736509, 991.65710582, 1312.39630323], - [41.37244888, 33.41882583, 59.48460827], - [282.61535563, 188.37255834, 441.62967707], - [182.28936533, 136.29152918, 248.30801310], - [398.28853814, 281.28601665, 641.78038278], - [494.34030557, 393.91395210, 664.96627121], - [579.86630787, 449.57878986, 836.64303806], - [281.30892711, 142.60663373, 309.93723963], - [439.97606151, 345.13329865, 425.68615785], - [887.17712876, 583.53811414, 886.88440975], - [841.97939219, 617.28846790, 810.67002861], - [1280.60242984, 1139.62066080, 1255.46929276], - [336.77846782, 246.82877629, 324.48823631], - [1070.92080733, 527.41599474, 913.93600561], - [676.57753460, 329.48235976, 509.56020035], - [1353.12934453, 1048.28092139, 1227.42851889], - [248.56120754, 78.30056642, 137.39216268], - [675.76876164, 381.60749713, 545.08703142], - [1008.57884369, 704.64042514, 836.94311729], - [1207.19931876, 527.74482440, 737.30284625], - [1157.60714894, 736.24734736, 846.01278626], - [861.62204402, 714.70913295, 747.29294390], - [255.83324360, 94.08214754, 147.60127564], - [1522.93390177, 1017.14491217, 1073.23488749], - [460.59077351, 93.73852735, 210.75844436], - [909.87331348, 498.83253656, 750.09672276], - ] - ), + [ + [42.00296381, 39.83290349, 43.28842394], + [181.25453293, 180.47486885, 180.30657630], + [1580.35041765, 1578.67251435, 1571.05703787], + [403.67553672, 403.67553672, 403.67553672], + [1193.51958332, 1194.63985124, 1183.92806238], + [862.07824054, 863.30644583, 858.29863779], + [605.42274304, 602.94953701, 596.61414309], + [395.70687930, 394.67167942, 392.97719777], + [227.27502116, 228.33554705, 227.96959477], + [130.97735082, 132.12395139, 131.97239271], + [61.79308820, 61.85572037, 62.40560537], + [592.29430914, 383.93309398, 282.70032306], + [504.67305022, 294.69245978, 193.90976423], + [640.93167741, 494.91914821, 421.68337308], + [356.53952646, 239.77610719, 181.18147755], + [179.58569818, 130.00540238, 109.23999883], + [1061.07297514, 818.29727750, 730.13362169], + [765.75936417, 522.06805938, 456.59355601], + [104.70554060, 80.35106922, 65.75667232], + [694.19925422, 161.06849749, 214.20170991], + [1054.83161580, 709.41713619, 668.10329523], + [697.35479081, 276.20032105, 275.86226833], + [183.26315174, 65.93801513, 74.60775905], + [359.74416854, 59.73576149, 89.81296522], + [1043.53760601, 405.48081521, 376.37298474], + [344.35374209, 111.26727966, 109.10587712], + [215.18064862, 87.41152853, 85.18152727], + [555.37005673, 134.76016985, 111.54658160], + [931.71846961, 210.02605133, 150.65312210], + [211.01186324, 50.73939233, 54.55750662], + [654.45781665, 132.73694874, 107.20009737], + [1193.89772859, 625.60766645, 521.51066476], + [802.65730883, 228.94887565, 178.30864097], + [149.82853589, 44.31839648, 55.29195048], + [80.88083928, 33.78936351, 41.73438243], + [579.50157840, 240.80755019, 188.50864121], + [537.09280420, 80.41714202, 48.28907694], + [777.62363031, 205.11587061, 122.43126732], + [292.65436510, 59.53457252, 44.27126512], + [511.68625012, 134.76897130, 85.73242441], + [903.64947615, 462.49015529, 350.74183199], + [852.95457070, 291.64071698, 151.51871958], + [1427.59841722, 907.54863477, 724.29520203], + [527.68979414, 169.76114596, 89.48561902], + [496.62188809, 317.11827387, 243.77642038], + [554.39017413, 284.77453644, 181.92376325], + [310.50669032, 96.25812545, 41.22765558], + [1246.49891599, 522.05121993, 238.28646123], + [240.19646249, 118.57745244, 82.68426681], + [1005.98836135, 355.93514762, 118.60457241], + [792.31376787, 369.56509398, 143.27388201], + [459.04590557, 315.46594358, 215.53901098], + [806.50918893, 352.20277469, 97.69239677], + [1574.11778922, 1078.61331515, 697.02647383], + [1015.45155837, 598.98507153, 301.94169280], + [479.68722930, 242.23619637, 72.60351059], + [1131.70538515, 628.32510627, 213.67910327], + [185.86573238, 162.55033903, 137.59385867], + [1131.77074807, 603.89218698, 153.83160203], + [638.14148862, 527.18090248, 410.12394346], + [884.58039320, 655.09236879, 329.23967927], + [1172.73094356, 840.43080883, 380.90114088], + [1490.24223350, 1111.18491878, 482.33357611], + [1054.70234779, 513.29967197, 91.55980977], + [1532.99674295, 1035.15868150, 253.21942988], + [662.35328287, 528.52354760, 326.56458987], + [1769.55456145, 1557.58571488, 1155.79098414], + [1196.62083017, 1079.28012658, 888.47017893], + [1578.73591185, 1089.40083172, 314.45691871], + [252.98204345, 206.56788008, 153.62801631], + [973.59975800, 714.51185344, 251.12884859], + [1661.01720988, 1340.46809762, 619.61710815], + [656.66179353, 566.61547800, 322.22788098], + [676.69663303, 571.86743785, 249.62031449], + [1229.28626315, 1020.14702709, 353.11090960], + [390.76190378, 324.36051944, 119.31108035], + [1524.10495708, 1366.72397704, 633.03830849], + [1264.54750712, 1149.12002542, 335.25348483], + [265.96753330, 260.89397210, 130.78590008], + [90.15969432, 90.72350914, 55.12008388], + [298.22463247, 300.48700028, 101.95760063], + [813.34391710, 820.12623357, 313.17818415], + [186.96402165, 190.38042094, 104.27515726], + [230.34939258, 235.91900919, 120.77815429], + [469.57926615, 472.51064145, 256.40912347], + [117.81249486, 129.17019984, 69.78861213], + [133.39581196, 151.50390168, 77.66255652], + [164.19259747, 172.13159331, 80.92295294], + [146.12230124, 149.32536508, 87.48300520], + [201.93215173, 208.89885695, 111.84447436], + [248.41427850, 282.34047722, 122.55482010], + [304.35509339, 377.38986207, 118.66130122], + [381.85533606, 530.40398972, 150.83506876], + [967.19810669, 1161.33086750, 663.54746741], + [613.98437237, 865.41677370, 362.92357557], + [410.21304405, 611.89683658, 284.09389273], + [279.50447144, 416.01646348, 213.18049093], + [334.48807624, 487.46571814, 235.49134434], + [664.04349337, 867.87454943, 549.71146455], + [311.66934673, 431.38058636, 256.13307806], + [110.04404638, 203.88196409, 104.63331585], + [153.35857585, 312.67834716, 149.90942505], + [273.46344514, 462.41992197, 292.50571823], + [184.77058437, 267.46361125, 193.71894670], + [75.79805899, 163.84071881, 95.67465270], + [461.73803707, 668.68797906, 484.77687282], + [523.01992144, 790.69326153, 598.73122243], + [105.89414085, 124.92341127, 113.03925656], + [279.33299507, 446.45128537, 344.73426977], + [340.57250119, 381.28610429, 353.83182947], + [141.00956904, 329.50139051, 228.90179483], + [117.29728945, 156.88993944, 139.49878229], + [565.12438106, 696.52297174, 615.88218349], + [1046.73447319, 1446.22424473, 1277.47338963], + [133.87404291, 253.25944193, 224.75872956], + [586.52626500, 1015.43013448, 885.49907251], + [927.08412116, 1197.93784752, 1140.76612264], + [81.29463446, 202.46201173, 186.35209411], + [350.90699453, 788.82959642, 669.10307704], + [278.88231719, 581.42068355, 526.82554470], + [642.66176703, 990.64038619, 907.64284280], + [689.10344984, 942.49383066, 900.33073076], + [190.62073977, 540.21088595, 523.62573562], + [322.35685764, 676.02683754, 692.94583013], + [896.29532467, 1289.90474463, 1311.34615018], + [204.06785020, 321.83261403, 337.01923114], + [237.10512554, 549.97044011, 646.06486244], + [907.26703197, 1252.44260107, 1309.50173432], + [504.74103065, 728.27088424, 782.27808125], + [470.91049729, 912.49116456, 1059.41083523], + [231.75497961, 539.14727494, 732.41647792], + [624.91135978, 943.51709467, 1086.48492282], + [104.84186738, 398.05825469, 663.96030581], + [100.47632953, 226.41423139, 323.51675153], + [998.19560093, 1168.81108673, 1283.07267859], + [350.74519746, 457.74100518, 552.52270183], + [223.19531677, 560.14850077, 855.05346039], + [66.92044931, 128.18947830, 205.30719728], + [280.63458798, 518.51069955, 784.38948897], + [1071.24122457, 1267.16339790, 1467.81704311], + [271.47257445, 553.57609491, 914.33723598], + [211.86582477, 295.18643027, 418.51776463], + [153.86457460, 342.06625645, 649.82579665], + [179.59188635, 265.25370235, 413.68135787], + [529.77485058, 737.79030218, 1046.29865466], + [208.71936449, 421.30392624, 796.71281168], + [685.50294808, 879.76243717, 1195.00892794], + [85.02189613, 113.33360860, 171.03209018], + [72.06980264, 139.42600347, 315.97906141], + [349.57868286, 426.82308690, 556.49647978], + [726.50329821, 882.48411184, 1163.20130103], + [102.62158777, 177.73895468, 467.26740089], + [208.63097281, 322.84137064, 639.30554347], + [377.19498209, 456.13180268, 706.44272480], + [149.91131672, 218.16462984, 455.15510078], + [556.80606655, 673.96774240, 1020.98785748], + [172.19546054, 181.38617476, 478.69666973], + [494.98572332, 534.88874559, 773.75255591], + [1166.31475206, 1207.81829513, 1411.04368728], + [324.81131421, 298.91188334, 521.96994638], + [731.58631467, 725.95113189, 1192.71141630], + [376.70584074, 352.06184423, 572.37854429], + [421.32413767, 465.07677606, 910.85999527], + [155.65680826, 145.82096629, 282.56390371], + [982.43736509, 991.65710582, 1312.39630323], + [41.37244888, 33.41882583, 59.48460827], + [282.61535563, 188.37255834, 441.62967707], + [182.28936533, 136.29152918, 248.30801310], + [398.28853814, 281.28601665, 641.78038278], + [494.34030557, 393.91395210, 664.96627121], + [579.86630787, 449.57878986, 836.64303806], + [281.30892711, 142.60663373, 309.93723963], + [439.97606151, 345.13329865, 425.68615785], + [887.17712876, 583.53811414, 886.88440975], + [841.97939219, 617.28846790, 810.67002861], + [1280.60242984, 1139.62066080, 1255.46929276], + [336.77846782, 246.82877629, 324.48823631], + [1070.92080733, 527.41599474, 913.93600561], + [676.57753460, 329.48235976, 509.56020035], + [1353.12934453, 1048.28092139, 1227.42851889], + [248.56120754, 78.30056642, 137.39216268], + [675.76876164, 381.60749713, 545.08703142], + [1008.57884369, 704.64042514, 836.94311729], + [1207.19931876, 527.74482440, 737.30284625], + [1157.60714894, 736.24734736, 846.01278626], + [861.62204402, 714.70913295, 747.29294390], + [255.83324360, 94.08214754, 147.60127564], + [1522.93390177, 1017.14491217, 1073.23488749], + [460.59077351, 93.73852735, 210.75844436], + [909.87331348, 498.83253656, 750.09672276], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_w, - np.array([2.34141541, 1.00000000, 1.51633759]), + [2.34141541, 1.00000000, 1.51633759], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -560,42 +571,40 @@ def test_training_data_sds_to_RGB(self) -> None: RGB, RGB_w = training_data_sds_to_RGB( training_data, MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"] ) - np.testing.assert_allclose( + xp_assert_close( RGB, - np.array( - [ - [263.80361607, 170.29412869, 132.71463416], - [884.07936328, 628.44083126, 520.43504675], - [324.17856150, 443.95092266, 606.43750890], - [243.82059773, 253.22111395, 144.98600653], - [481.54199203, 527.96925768, 764.50624747], - [628.07015143, 979.73104655, 896.85237907], - [927.63600544, 391.11468312, 150.73047156], - [203.13259862, 317.65395368, 639.54581080], - [686.28955864, 260.78688114, 254.89963998], - [174.05857536, 132.16684952, 230.54054095], - [806.50094411, 817.35481419, 312.91902292], - [1111.20280010, 608.82554576, 194.31984092], - [94.99792206, 185.04148229, 456.53592437], - [340.60457483, 498.62910631, 254.08356415], - [531.53679194, 136.11844274, 109.19876416], - [1387.37113491, 952.84382040, 286.23152122], - [681.97933172, 326.66634506, 526.23078660], - [244.90739217, 554.88866566, 741.21522946], - [1841.80020583, 1834.49277300, 1784.07500285], - [1179.76201558, 1189.84138939, 1182.25520674], - [720.27089899, 726.91855632, 724.84766858], - [382.16849234, 387.41521539, 386.87510339], - [178.43859184, 181.76108810, 182.71062184], - [64.77754952, 64.80020759, 65.45515287], - ] - ), + [ + [263.80361607, 170.29412869, 132.71463416], + [884.07936328, 628.44083126, 520.43504675], + [324.17856150, 443.95092266, 606.43750890], + [243.82059773, 253.22111395, 144.98600653], + [481.54199203, 527.96925768, 764.50624747], + [628.07015143, 979.73104655, 896.85237907], + [927.63600544, 391.11468312, 150.73047156], + [203.13259862, 317.65395368, 639.54581080], + [686.28955864, 260.78688114, 254.89963998], + [174.05857536, 132.16684952, 230.54054095], + [806.50094411, 817.35481419, 312.91902292], + [1111.20280010, 608.82554576, 194.31984092], + [94.99792206, 185.04148229, 456.53592437], + [340.60457483, 498.62910631, 254.08356415], + [531.53679194, 136.11844274, 109.19876416], + [1387.37113491, 952.84382040, 286.23152122], + [681.97933172, 326.66634506, 526.23078660], + [244.90739217, 554.88866566, 741.21522946], + [1841.80020583, 1834.49277300, 1784.07500285], + [1179.76201558, 1189.84138939, 1182.25520674], + [720.27089899, 726.91855632, 724.84766858], + [382.16849234, 387.41521539, 386.87510339], + [178.43859184, 181.76108810, 182.71062184], + [64.77754952, 64.80020759, 65.45515287], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_w, - np.array([2.34141541, 1.00000000, 1.51633759]), + [2.34141541, 1.00000000, 1.51633759], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -612,206 +621,204 @@ def test_training_data_sds_to_XYZ(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( training_data_sds_to_XYZ( read_training_data_rawtoaces_v1(), MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], SDS_ILLUMINANTS["D55"], ), - np.array( - [ - [0.01743541, 0.01795040, 0.01961110], - [0.08556071, 0.08957352, 0.09017032], - [0.74558770, 0.78175495, 0.78343383], - [0.19005289, 0.19950000, 0.20126062], - [0.56263167, 0.59145443, 0.58944868], - [0.40708229, 0.42774653, 0.42813199], - [0.28533739, 0.29945717, 0.29732644], - [0.18670375, 0.19575576, 0.19612855], - [0.10734487, 0.11290543, 0.11381239], - [0.06188310, 0.06524694, 0.06594260], - [0.02905436, 0.03045954, 0.03111642], - [0.25031624, 0.22471846, 0.12599982], - [0.20848487, 0.18072652, 0.08216289], - [0.28173081, 0.26937432, 0.19943363], - [0.15129458, 0.13765872, 0.08086671], - [0.07854243, 0.07274480, 0.05123870], - [0.46574583, 0.43948749, 0.34501135], - [0.33111608, 0.29368033, 0.21379720], - [0.04596029, 0.04443836, 0.03115443], - [0.28422646, 0.15495892, 0.11586479], - [0.47490187, 0.41497780, 0.33505853], - [0.29452546, 0.20003225, 0.13705453], - [0.06905269, 0.04421818, 0.03449201], - [0.13040440, 0.06239791, 0.04175606], - [0.43838730, 0.29962261, 0.18439668], - [0.13390118, 0.08356608, 0.04956679], - [0.08356733, 0.05794634, 0.03910007], - [0.21637988, 0.12469189, 0.04842559], - [0.37899204, 0.22130821, 0.07365608], - [0.07733610, 0.04256869, 0.02300063], - [0.25696432, 0.14119282, 0.04740500], - [0.51960474, 0.41409496, 0.25643556], - [0.32241564, 0.19954021, 0.08051276], - [0.05811798, 0.03389661, 0.02553745], - [0.03192572, 0.02139972, 0.01894908], - [0.24605476, 0.17854962, 0.09147038], - [0.20624731, 0.10555152, 0.01675508], - [0.31255107, 0.19334840, 0.05143990], - [0.11006219, 0.06057155, 0.01700794], - [0.20509764, 0.12555310, 0.03594860], - [0.38058683, 0.30396093, 0.16256996], - [0.34354473, 0.23964048, 0.06111316], - [0.62251344, 0.54770879, 0.34634977], - [0.21294652, 0.14470338, 0.03492000], - [0.22064317, 0.19656587, 0.11907643], - [0.23955073, 0.19768225, 0.08595970], - [0.12377361, 0.08353105, 0.01434151], - [0.52378659, 0.40757502, 0.10242337], - [0.09732322, 0.07735501, 0.03254246], - [0.41081884, 0.30127969, 0.04240016], - [0.32946008, 0.27129095, 0.05232655], - [0.19870991, 0.18701769, 0.09764509], - [0.31867743, 0.25717029, 0.02158054], - [0.67745549, 0.64283785, 0.31268426], - [0.43182429, 0.39425828, 0.13198410], - [0.19075096, 0.16573196, 0.01845293], - [0.47578930, 0.43714747, 0.07974541], - [0.08420865, 0.08615579, 0.06605406], - [0.47306132, 0.43488423, 0.05262924], - [0.28242654, 0.28638349, 0.19186089], - [0.37367384, 0.38524079, 0.13498637], - [0.49536547, 0.51027091, 0.15645211], - [0.63680942, 0.67272132, 0.19642820], - [0.43790684, 0.39093965, 0.02518505], - [0.63216527, 0.66425603, 0.07124985], - [0.28682848, 0.29807036, 0.14308787], - [0.78666095, 0.83181391, 0.53110094], - [0.54475049, 0.57280425, 0.43240766], - [0.65555915, 0.68992930, 0.10030198], - [0.10560623, 0.10992647, 0.06863885], - [0.40588908, 0.43345904, 0.08589490], - [0.69824760, 0.76446843, 0.23843395], - [0.27951451, 0.30869595, 0.13310650], - [0.28351930, 0.32278417, 0.09130925], - [0.51144946, 0.58985649, 0.11409286], - [0.16769668, 0.19357639, 0.04824163], - [0.64027510, 0.74864980, 0.24145602], - [0.51533750, 0.64418491, 0.09390029], - [0.10903312, 0.13420204, 0.04403235], - [0.03916991, 0.04755109, 0.02410291], - [0.12726285, 0.16825903, 0.03705646], - [0.34079923, 0.44119883, 0.10621489], - [0.08299513, 0.10226271, 0.04607974], - [0.10117617, 0.12690940, 0.05211600], - [0.20673305, 0.25456362, 0.11244267], - [0.05040081, 0.06702198, 0.02944861], - [0.05809758, 0.07896803, 0.03312583], - [0.07202711, 0.09383365, 0.03453490], - [0.06392748, 0.07896740, 0.03860393], - [0.08851258, 0.11174080, 0.04873213], - [0.09821259, 0.13743849, 0.03901353], - [0.12253000, 0.18989034, 0.03327101], - [0.15082798, 0.25948217, 0.03805919], - [0.41476613, 0.56455709, 0.26988900], - [0.25043710, 0.40869656, 0.12211755], - [0.17536685, 0.28765326, 0.10166502], - [0.12038544, 0.19242328, 0.07754636], - [0.14661345, 0.23524743, 0.09334793], - [0.29469553, 0.41056592, 0.23093160], - [0.13015693, 0.19492122, 0.09333495], - [0.04081181, 0.08280292, 0.03122401], - [0.06569736, 0.13553353, 0.05266408], - [0.12177383, 0.20160583, 0.11621774], - [0.08354206, 0.11970984, 0.08207175], - [0.02834645, 0.06259404, 0.03135058], - [0.20884161, 0.29927365, 0.20553553], - [0.23180119, 0.33870071, 0.24267407], - [0.04413521, 0.05398934, 0.04862030], - [0.13068910, 0.19470885, 0.15073584], - [0.16108644, 0.18484544, 0.17474649], - [0.06206737, 0.12873462, 0.09368693], - [0.05126858, 0.06722639, 0.05961970], - [0.25534374, 0.31335090, 0.27780291], - [0.48369629, 0.63319069, 0.57347864], - [0.06066266, 0.09712274, 0.09253437], - [0.27940216, 0.41909220, 0.39351159], - [0.44664100, 0.54665344, 0.55342931], - [0.03590889, 0.06959304, 0.07535965], - [0.16621092, 0.30339106, 0.29722885], - [0.12909138, 0.22008859, 0.22690521], - [0.31015553, 0.42498221, 0.42044232], - [0.33970423, 0.42779997, 0.43883150], - [0.10000582, 0.19440825, 0.23393750], - [0.16694758, 0.26056864, 0.32541934], - [0.43598087, 0.55484571, 0.63089871], - [0.10305166, 0.13633951, 0.16650820], - [0.12725465, 0.19404057, 0.30068226], - [0.44450660, 0.54666776, 0.64220554], - [0.25312549, 0.31346831, 0.38485942], - [0.24557618, 0.34698805, 0.51328941], - [0.13585660, 0.18761687, 0.36302217], - [0.32288492, 0.39652004, 0.54579104], - [0.08400465, 0.11889755, 0.34519851], - [0.06038029, 0.07936884, 0.16393180], - [0.47840043, 0.53070661, 0.64043584], - [0.16727376, 0.19048161, 0.27055547], - [0.14740952, 0.19227205, 0.44545300], - [0.03953792, 0.04540593, 0.10766386], - [0.16200092, 0.18995251, 0.41003367], - [0.53147895, 0.57554326, 0.74787983], - [0.17107460, 0.19285623, 0.48157477], - [0.11394187, 0.12139868, 0.21928748], - [0.10838799, 0.11193347, 0.34884682], - [0.10390937, 0.10854555, 0.22459293], - [0.28493924, 0.30349174, 0.54832107], - [0.13572090, 0.13988801, 0.43412229], - [0.36141619, 0.37929776, 0.62919317], - [0.04527113, 0.04612919, 0.09028801], - [0.05164102, 0.04505136, 0.17732932], - [0.18148861, 0.19085005, 0.29528314], - [0.37792382, 0.39238764, 0.61357669], - [0.08148672, 0.06054619, 0.27321036], - [0.13431208, 0.12118937, 0.35762939], - [0.19932157, 0.19328547, 0.37878896], - [0.09456787, 0.08094285, 0.25785832], - [0.29868476, 0.28967149, 0.54786550], - [0.09582629, 0.06156148, 0.27163852], - [0.25053785, 0.23630807, 0.40751054], - [0.56821117, 0.57452018, 0.72419232], - [0.16116009, 0.13379410, 0.28760107], - [0.37816205, 0.32564214, 0.64945876], - [0.19440630, 0.16599850, 0.31684298], - [0.24229817, 0.19698372, 0.51538353], - [0.08104904, 0.06295569, 0.15738669], - [0.48808364, 0.46372832, 0.69336648], - [0.01983575, 0.01538929, 0.03252398], - [0.13468770, 0.08473328, 0.25136965], - [0.08762890, 0.06560340, 0.13804375], - [0.20192043, 0.12939477, 0.36343630], - [0.24231283, 0.19018859, 0.36604686], - [0.28784724, 0.21105155, 0.46114703], - [0.12549222, 0.07471177, 0.17126268], - [0.20910983, 0.18235419, 0.22475458], - [0.43032307, 0.32727171, 0.49574549], - [0.39105442, 0.32475758, 0.42885925], - [0.60567491, 0.57928897, 0.64030251], - [0.15645417, 0.12986348, 0.17171885], - [0.50025055, 0.32646202, 0.51899239], - [0.29822363, 0.19839451, 0.27397060], - [0.63136923, 0.55375993, 0.63816664], - [0.10261977, 0.05754107, 0.07473368], - [0.30325538, 0.21976283, 0.29171854], - [0.46794841, 0.39368920, 0.44286306], - [0.54326558, 0.36319029, 0.41127862], - [0.52355493, 0.42261205, 0.43529051], - [0.39852212, 0.37568122, 0.37825751], - [0.10892106, 0.06698290, 0.07939788], - [0.68780223, 0.58022018, 0.54422258], - [0.18984448, 0.09051898, 0.12104133], - [0.41991006, 0.29457037, 0.40780639], - ] - ), + [ + [0.01743541, 0.01795040, 0.01961110], + [0.08556071, 0.08957352, 0.09017032], + [0.74558770, 0.78175495, 0.78343383], + [0.19005289, 0.19950000, 0.20126062], + [0.56263167, 0.59145443, 0.58944868], + [0.40708229, 0.42774653, 0.42813199], + [0.28533739, 0.29945717, 0.29732644], + [0.18670375, 0.19575576, 0.19612855], + [0.10734487, 0.11290543, 0.11381239], + [0.06188310, 0.06524694, 0.06594260], + [0.02905436, 0.03045954, 0.03111642], + [0.25031624, 0.22471846, 0.12599982], + [0.20848487, 0.18072652, 0.08216289], + [0.28173081, 0.26937432, 0.19943363], + [0.15129458, 0.13765872, 0.08086671], + [0.07854243, 0.07274480, 0.05123870], + [0.46574583, 0.43948749, 0.34501135], + [0.33111608, 0.29368033, 0.21379720], + [0.04596029, 0.04443836, 0.03115443], + [0.28422646, 0.15495892, 0.11586479], + [0.47490187, 0.41497780, 0.33505853], + [0.29452546, 0.20003225, 0.13705453], + [0.06905269, 0.04421818, 0.03449201], + [0.13040440, 0.06239791, 0.04175606], + [0.43838730, 0.29962261, 0.18439668], + [0.13390118, 0.08356608, 0.04956679], + [0.08356733, 0.05794634, 0.03910007], + [0.21637988, 0.12469189, 0.04842559], + [0.37899204, 0.22130821, 0.07365608], + [0.07733610, 0.04256869, 0.02300063], + [0.25696432, 0.14119282, 0.04740500], + [0.51960474, 0.41409496, 0.25643556], + [0.32241564, 0.19954021, 0.08051276], + [0.05811798, 0.03389661, 0.02553745], + [0.03192572, 0.02139972, 0.01894908], + [0.24605476, 0.17854962, 0.09147038], + [0.20624731, 0.10555152, 0.01675508], + [0.31255107, 0.19334840, 0.05143990], + [0.11006219, 0.06057155, 0.01700794], + [0.20509764, 0.12555310, 0.03594860], + [0.38058683, 0.30396093, 0.16256996], + [0.34354473, 0.23964048, 0.06111316], + [0.62251344, 0.54770879, 0.34634977], + [0.21294652, 0.14470338, 0.03492000], + [0.22064317, 0.19656587, 0.11907643], + [0.23955073, 0.19768225, 0.08595970], + [0.12377361, 0.08353105, 0.01434151], + [0.52378659, 0.40757502, 0.10242337], + [0.09732322, 0.07735501, 0.03254246], + [0.41081884, 0.30127969, 0.04240016], + [0.32946008, 0.27129095, 0.05232655], + [0.19870991, 0.18701769, 0.09764509], + [0.31867743, 0.25717029, 0.02158054], + [0.67745549, 0.64283785, 0.31268426], + [0.43182429, 0.39425828, 0.13198410], + [0.19075096, 0.16573196, 0.01845293], + [0.47578930, 0.43714747, 0.07974541], + [0.08420865, 0.08615579, 0.06605406], + [0.47306132, 0.43488423, 0.05262924], + [0.28242654, 0.28638349, 0.19186089], + [0.37367384, 0.38524079, 0.13498637], + [0.49536547, 0.51027091, 0.15645211], + [0.63680942, 0.67272132, 0.19642820], + [0.43790684, 0.39093965, 0.02518505], + [0.63216527, 0.66425603, 0.07124985], + [0.28682848, 0.29807036, 0.14308787], + [0.78666095, 0.83181391, 0.53110094], + [0.54475049, 0.57280425, 0.43240766], + [0.65555915, 0.68992930, 0.10030198], + [0.10560623, 0.10992647, 0.06863885], + [0.40588908, 0.43345904, 0.08589490], + [0.69824760, 0.76446843, 0.23843395], + [0.27951451, 0.30869595, 0.13310650], + [0.28351930, 0.32278417, 0.09130925], + [0.51144946, 0.58985649, 0.11409286], + [0.16769668, 0.19357639, 0.04824163], + [0.64027510, 0.74864980, 0.24145602], + [0.51533750, 0.64418491, 0.09390029], + [0.10903312, 0.13420204, 0.04403235], + [0.03916991, 0.04755109, 0.02410291], + [0.12726285, 0.16825903, 0.03705646], + [0.34079923, 0.44119883, 0.10621489], + [0.08299513, 0.10226271, 0.04607974], + [0.10117617, 0.12690940, 0.05211600], + [0.20673305, 0.25456362, 0.11244267], + [0.05040081, 0.06702198, 0.02944861], + [0.05809758, 0.07896803, 0.03312583], + [0.07202711, 0.09383365, 0.03453490], + [0.06392748, 0.07896740, 0.03860393], + [0.08851258, 0.11174080, 0.04873213], + [0.09821259, 0.13743849, 0.03901353], + [0.12253000, 0.18989034, 0.03327101], + [0.15082798, 0.25948217, 0.03805919], + [0.41476613, 0.56455709, 0.26988900], + [0.25043710, 0.40869656, 0.12211755], + [0.17536685, 0.28765326, 0.10166502], + [0.12038544, 0.19242328, 0.07754636], + [0.14661345, 0.23524743, 0.09334793], + [0.29469553, 0.41056592, 0.23093160], + [0.13015693, 0.19492122, 0.09333495], + [0.04081181, 0.08280292, 0.03122401], + [0.06569736, 0.13553353, 0.05266408], + [0.12177383, 0.20160583, 0.11621774], + [0.08354206, 0.11970984, 0.08207175], + [0.02834645, 0.06259404, 0.03135058], + [0.20884161, 0.29927365, 0.20553553], + [0.23180119, 0.33870071, 0.24267407], + [0.04413521, 0.05398934, 0.04862030], + [0.13068910, 0.19470885, 0.15073584], + [0.16108644, 0.18484544, 0.17474649], + [0.06206737, 0.12873462, 0.09368693], + [0.05126858, 0.06722639, 0.05961970], + [0.25534374, 0.31335090, 0.27780291], + [0.48369629, 0.63319069, 0.57347864], + [0.06066266, 0.09712274, 0.09253437], + [0.27940216, 0.41909220, 0.39351159], + [0.44664100, 0.54665344, 0.55342931], + [0.03590889, 0.06959304, 0.07535965], + [0.16621092, 0.30339106, 0.29722885], + [0.12909138, 0.22008859, 0.22690521], + [0.31015553, 0.42498221, 0.42044232], + [0.33970423, 0.42779997, 0.43883150], + [0.10000582, 0.19440825, 0.23393750], + [0.16694758, 0.26056864, 0.32541934], + [0.43598087, 0.55484571, 0.63089871], + [0.10305166, 0.13633951, 0.16650820], + [0.12725465, 0.19404057, 0.30068226], + [0.44450660, 0.54666776, 0.64220554], + [0.25312549, 0.31346831, 0.38485942], + [0.24557618, 0.34698805, 0.51328941], + [0.13585660, 0.18761687, 0.36302217], + [0.32288492, 0.39652004, 0.54579104], + [0.08400465, 0.11889755, 0.34519851], + [0.06038029, 0.07936884, 0.16393180], + [0.47840043, 0.53070661, 0.64043584], + [0.16727376, 0.19048161, 0.27055547], + [0.14740952, 0.19227205, 0.44545300], + [0.03953792, 0.04540593, 0.10766386], + [0.16200092, 0.18995251, 0.41003367], + [0.53147895, 0.57554326, 0.74787983], + [0.17107460, 0.19285623, 0.48157477], + [0.11394187, 0.12139868, 0.21928748], + [0.10838799, 0.11193347, 0.34884682], + [0.10390937, 0.10854555, 0.22459293], + [0.28493924, 0.30349174, 0.54832107], + [0.13572090, 0.13988801, 0.43412229], + [0.36141619, 0.37929776, 0.62919317], + [0.04527113, 0.04612919, 0.09028801], + [0.05164102, 0.04505136, 0.17732932], + [0.18148861, 0.19085005, 0.29528314], + [0.37792382, 0.39238764, 0.61357669], + [0.08148672, 0.06054619, 0.27321036], + [0.13431208, 0.12118937, 0.35762939], + [0.19932157, 0.19328547, 0.37878896], + [0.09456787, 0.08094285, 0.25785832], + [0.29868476, 0.28967149, 0.54786550], + [0.09582629, 0.06156148, 0.27163852], + [0.25053785, 0.23630807, 0.40751054], + [0.56821117, 0.57452018, 0.72419232], + [0.16116009, 0.13379410, 0.28760107], + [0.37816205, 0.32564214, 0.64945876], + [0.19440630, 0.16599850, 0.31684298], + [0.24229817, 0.19698372, 0.51538353], + [0.08104904, 0.06295569, 0.15738669], + [0.48808364, 0.46372832, 0.69336648], + [0.01983575, 0.01538929, 0.03252398], + [0.13468770, 0.08473328, 0.25136965], + [0.08762890, 0.06560340, 0.13804375], + [0.20192043, 0.12939477, 0.36343630], + [0.24231283, 0.19018859, 0.36604686], + [0.28784724, 0.21105155, 0.46114703], + [0.12549222, 0.07471177, 0.17126268], + [0.20910983, 0.18235419, 0.22475458], + [0.43032307, 0.32727171, 0.49574549], + [0.39105442, 0.32475758, 0.42885925], + [0.60567491, 0.57928897, 0.64030251], + [0.15645417, 0.12986348, 0.17171885], + [0.50025055, 0.32646202, 0.51899239], + [0.29822363, 0.19839451, 0.27397060], + [0.63136923, 0.55375993, 0.63816664], + [0.10261977, 0.05754107, 0.07473368], + [0.30325538, 0.21976283, 0.29171854], + [0.46794841, 0.39368920, 0.44286306], + [0.54326558, 0.36319029, 0.41127862], + [0.52355493, 0.42261205, 0.43529051], + [0.39852212, 0.37568122, 0.37825751], + [0.10892106, 0.06698290, 0.07939788], + [0.68780223, 0.58022018, 0.54422258], + [0.18984448, 0.09051898, 0.12104133], + [0.41991006, 0.29457037, 0.40780639], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -819,78 +826,74 @@ def test_training_data_sds_to_XYZ(self) -> None: SDS_COLOURCHECKERS["BabelColor Average"].values() ) - np.testing.assert_allclose( + xp_assert_close( training_data_sds_to_XYZ( training_data, MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], SDS_ILLUMINANTS["D55"], ), - np.array( - [ - [0.11386016, 0.10184316, 0.06318332], - [0.38043230, 0.34842093, 0.23582246], - [0.17359019, 0.18707491, 0.31848244], - [0.10647823, 0.13300376, 0.06486355], - [0.24658643, 0.23417740, 0.40546447], - [0.30550003, 0.42171110, 0.41928361], - [0.38409200, 0.30325611, 0.05955461], - [0.13149767, 0.11720378, 0.35673016], - [0.28717811, 0.19215580, 0.12514286], - [0.08401031, 0.06423349, 0.12782115], - [0.33990604, 0.44124555, 0.10834694], - [0.46443889, 0.42686462, 0.07340585], - [0.07650085, 0.06051409, 0.26167301], - [0.14598990, 0.23185071, 0.09380297], - [0.20642710, 0.12162691, 0.04673088], - [0.57371755, 0.59896814, 0.08930486], - [0.30208819, 0.19714705, 0.28492050], - [0.14184323, 0.19554336, 0.36653731], - [0.86547610, 0.91241348, 0.88583082], - [0.55802432, 0.58852191, 0.59042758], - [0.34102067, 0.35951875, 0.36251375], - [0.18104441, 0.19123509, 0.19353380], - [0.08461047, 0.08944605, 0.09150081], - [0.03058273, 0.03200953, 0.03277947], - ] - ), + [ + [0.11386016, 0.10184316, 0.06318332], + [0.38043230, 0.34842093, 0.23582246], + [0.17359019, 0.18707491, 0.31848244], + [0.10647823, 0.13300376, 0.06486355], + [0.24658643, 0.23417740, 0.40546447], + [0.30550003, 0.42171110, 0.41928361], + [0.38409200, 0.30325611, 0.05955461], + [0.13149767, 0.11720378, 0.35673016], + [0.28717811, 0.19215580, 0.12514286], + [0.08401031, 0.06423349, 0.12782115], + [0.33990604, 0.44124555, 0.10834694], + [0.46443889, 0.42686462, 0.07340585], + [0.07650085, 0.06051409, 0.26167301], + [0.14598990, 0.23185071, 0.09380297], + [0.20642710, 0.12162691, 0.04673088], + [0.57371755, 0.59896814, 0.08930486], + [0.30208819, 0.19714705, 0.28492050], + [0.14184323, 0.19554336, 0.36653731], + [0.86547610, 0.91241348, 0.88583082], + [0.55802432, 0.58852191, 0.59042758], + [0.34102067, 0.35951875, 0.36251375], + [0.18104441, 0.19123509, 0.19353380], + [0.08461047, 0.08944605, 0.09150081], + [0.03058273, 0.03200953, 0.03277947], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( training_data_sds_to_XYZ( training_data, MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], SDS_ILLUMINANTS["D55"], chromatic_adaptation_transform="Bradford", ), - np.array( - [ - [0.11386557, 0.10185906, 0.06306965], - [0.38044920, 0.34846911, 0.23548776], - [0.17349711, 0.18690409, 0.31901794], - [0.10656174, 0.13314825, 0.06450454], - [0.24642109, 0.23388536, 0.40625776], - [0.30564803, 0.42194543, 0.41894818], - [0.38414010, 0.30337780, 0.05881558], - [0.13128440, 0.11682332, 0.35780551], - [0.28707604, 0.19200780, 0.12518610], - [0.08392779, 0.06409174, 0.12816180], - [0.34028525, 0.44190577, 0.10665985], - [0.46462806, 0.42722924, 0.07207641], - [0.07631823, 0.06018898, 0.26258457], - [0.14620929, 0.23222248, 0.09296807], - [0.20635082, 0.12152088, 0.04669974], - [0.57410962, 0.59968182, 0.08713069], - [0.30185180, 0.19675858, 0.28565273], - [0.14177898, 0.19541060, 0.36711242], - [0.86550834, 0.91247072, 0.88567193], - [0.55803077, 0.58853268, 0.59040518], - [0.34102300, 0.35952246, 0.36250826], - [0.18104563, 0.19123690, 0.19353274], - [0.08461039, 0.08944568, 0.09150425], - [0.03058222, 0.03200864, 0.03278183], - ] - ), + [ + [0.11386557, 0.10185906, 0.06306965], + [0.38044920, 0.34846911, 0.23548776], + [0.17349711, 0.18690409, 0.31901794], + [0.10656174, 0.13314825, 0.06450454], + [0.24642109, 0.23388536, 0.40625776], + [0.30564803, 0.42194543, 0.41894818], + [0.38414010, 0.30337780, 0.05881558], + [0.13128440, 0.11682332, 0.35780551], + [0.28707604, 0.19200780, 0.12518610], + [0.08392779, 0.06409174, 0.12816180], + [0.34028525, 0.44190577, 0.10665985], + [0.46462806, 0.42722924, 0.07207641], + [0.07631823, 0.06018898, 0.26258457], + [0.14620929, 0.23222248, 0.09296807], + [0.20635082, 0.12152088, 0.04669974], + [0.57410962, 0.59968182, 0.08713069], + [0.30185180, 0.19675858, 0.28565273], + [0.14177898, 0.19541060, 0.36711242], + [0.86550834, 0.91247072, 0.88567193], + [0.55803077, 0.58853268, 0.59040518], + [0.34102300, 0.35952246, 0.36250826], + [0.18104563, 0.19123690, 0.19353274], + [0.08461039, 0.08944568, 0.09150425], + [0.03058222, 0.03200864, 0.03278183], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -901,27 +904,28 @@ class TestWhitepointPreservingMatrix: definition unit tests methods. """ - def test_whitepoint_preserving_matrix(self) -> None: + def test_whitepoint_preserving_matrix(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.aces_it.\ whitepoint_preserving_matrix` definition. """ - np.testing.assert_array_equal( - whitepoint_preserving_matrix(np.reshape(np.arange(9), (3, 3))), - np.array([[0, 1, 0], [3, 4, -6], [6, 7, -12]]), + xp_assert_equal( + whitepoint_preserving_matrix(xp_reshape(xp.arange(9), (3, 3), xp=xp)), + [[0, 1, 0], [3, 4, -6], [6, 7, -12]], ) - np.testing.assert_array_equal( - whitepoint_preserving_matrix(np.reshape(np.arange(12), (3, 4))), - np.array([[0, 1, 2, -2], [4, 5, 6, -14], [8, 9, 10, -26]]), + xp_assert_equal( + whitepoint_preserving_matrix(xp_reshape(xp.arange(12), (3, 4), xp=xp)), + [[0, 1, 2, -2], [4, 5, 6, -14], [8, 9, 10, -26]], ) - np.testing.assert_array_equal( + xp_assert_equal( whitepoint_preserving_matrix( - np.reshape(np.arange(9), (3, 3)), np.array([1, 2, 3]) + xp_reshape(xp.arange(9), (3, 3), xp=xp), + xp_as_array([1, 2, 3], xp=xp), ), - np.array([[0, 1, 0], [3, 4, -5], [6, 7, -10]]), + [[0, 1, 0], [3, 4, -5], [6, 7, -10]], ) @@ -990,16 +994,14 @@ def test_matrix_idt(self) -> None: # 0.864994 -0.026302 0.161308 # 0.056527 1.122997 -0.179524 # 0.023683 -0.202547 1.178864 - np.testing.assert_allclose( + xp_assert_close( matrix_idt(MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"])[0], - np.array( - [ - [0.86498930, -0.02627499, 0.16128570], - [0.05654629, 1.12305999, -0.17960629], - [0.02369089, -0.20253026, 1.17883937], - ] - ), - atol=0.0001, + [ + [0.86498930, -0.02627499, 0.16128570], + [0.05654629, 1.12305999, -0.17960629], + [0.02369089, -0.20253026, 1.17883937], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) # The *RAW to ACES* v1 matrix for the same camera and optimized by @@ -1008,16 +1010,14 @@ def test_matrix_idt(self) -> None: # 0.888492 -0.077505 0.189014 # 0.021805 1.066614 -0.088418 # -0.019718 -0.206664 1.226381 - np.testing.assert_allclose( + xp_assert_close( matrix_idt(MSDS_CANON_EOS_5DMARK_II, SD_AMPAS_ISO7589_STUDIO_TUNGSTEN)[0], - np.array( - [ - [0.88849143, -0.07750529, 0.18901385], - [0.02180460, 1.06661379, -0.08841839], - [-0.01971799, -0.20666347, 1.22638146], - ] - ), - atol=0.0001, + [ + [0.88849143, -0.07750529, 0.18901385], + [0.02180460, 1.06661379, -0.08841839], + [-0.01971799, -0.20666347, 1.22638146], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) M, RGB_w = matrix_idt( @@ -1026,21 +1026,19 @@ def test_matrix_idt(self) -> None: optimisation_factory=optimisation_factory_Jzazbz, additional_data=False, ) - np.testing.assert_allclose( + xp_assert_close( M, - np.array( - [ - [0.85154529, -0.00930079, 0.15775549], - [0.05413281, 1.12208831, -0.17622112], - [0.02327675, -0.22372411, 1.20044737], - ] - ), - atol=0.0001, + [ + [0.85154529, -0.00930079, 0.15775549], + [0.05413281, 1.12208831, -0.17622112], + [0.02327675, -0.22372411, 1.20044737], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_w, - np.array([2.34141541, 1.00000000, 1.51633759]), - atol=0.0001, + [2.34141541, 1.00000000, 1.51633759], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) M, RGB_w = matrix_idt( MSDS_CANON_EOS_5DMARK_II, @@ -1048,42 +1046,40 @@ def test_matrix_idt(self) -> None: optimisation_factory=optimisation_factory_Oklab_15, additional_data=False, ) - np.testing.assert_allclose( + xp_assert_close( M, - np.array( + [ [ - [ - 0.64535942, - -0.61130888, - 0.10668827, - 0.73619966, - 0.39808135, - -0.27501982, - ], - [ - -0.15942100, - 0.72812052, - -0.09069782, - 0.65082426, - 0.01006055, - -0.13888651, - ], - [ - -0.17183392, - -0.40291315, - 1.39402532, - 0.51025076, - -0.29541153, - -0.03411748, - ], - ] - ), - atol=0.0001, + 0.64535942, + -0.61130888, + 0.10668827, + 0.73619966, + 0.39808135, + -0.27501982, + ], + [ + -0.15942100, + 0.72812052, + -0.09069782, + 0.65082426, + 0.01006055, + -0.13888651, + ], + [ + -0.17183392, + -0.40291315, + 1.39402532, + 0.51025076, + -0.29541153, + -0.03411748, + ], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_w, - np.array([2.34141541, 1.00000000, 1.51633759]), - atol=0.0001, + [2.34141541, 1.00000000, 1.51633759], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) M, RGB_w = matrix_idt( @@ -1092,28 +1088,26 @@ def test_matrix_idt(self) -> None: optimisation_kwargs={"method": "Nelder-Mead"}, additional_data=False, ) - np.testing.assert_allclose( + xp_assert_close( M, - np.array( - [ - [0.883387, 0.002254, 0.114359], - [0.082968, 1.134324, -0.217291], - [0.015048, -0.150215, 1.135168], - ] - ), - atol=0.0001, + [ + [0.883387, 0.002254, 0.114359], + [0.082968, 1.134324, -0.217291], + [0.015048, -0.150215, 1.135168], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_w, - np.array([2.34141541, 1.00000000, 1.51633759]), - atol=0.0001, + [2.34141541, 1.00000000, 1.51633759], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) training_data = sds_and_msds_to_msds( SDS_COLOURCHECKERS["BabelColor Average"].values() ) - np.testing.assert_allclose( + xp_assert_close( matrix_idt( reshape_msds( MSDS_CAMERA_SENSITIVITIES["Nikon 5100 (NPL)"], @@ -1122,30 +1116,26 @@ def test_matrix_idt(self) -> None: SD_AMPAS_ISO7589_STUDIO_TUNGSTEN, training_data=training_data, )[0], - np.array( - [ - [0.75804117, 0.10318202, 0.13877681], - [-0.00117867, 1.09993170, -0.09875304], - [0.06964389, -0.31098445, 1.24134056], - ] - ), - atol=0.0001, + [ + [0.75804117, 0.10318202, 0.13877681], + [-0.00117867, 1.09993170, -0.09875304], + [0.06964389, -0.31098445, 1.24134056], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( matrix_idt( MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"], chromatic_adaptation_transform="Bradford", )[0], - np.array( - [ - [0.86507763, -0.02407809, 0.15900045], - [0.05633306, 1.12612394, -0.18245700], - [0.02450723, -0.20931423, 1.18480700], - ] - ), - atol=0.0001, + [ + [0.86507763, -0.02407809, 0.15900045], + [0.05633306, 1.12612394, -0.18245700], + [0.02450723, -0.20931423, 1.18480700], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) _M, RGB_w, XYZ, RGB = matrix_idt( @@ -1154,35 +1144,33 @@ def test_matrix_idt(self) -> None: additional_data=True, ) - np.testing.assert_allclose( - RGB_w, np.array([2.34141541, 1.00000000, 1.51633759]) + xp_assert_close( + RGB_w, + [2.34141541, 1.00000000, 1.51633759], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ[:5, ...], - np.array( - [ - [0.01743160, 0.01794927, 0.01960625], - [0.08556139, 0.08957352, 0.09017387], - [0.74560311, 0.78175547, 0.78350814], - [0.19005289, 0.19950000, 0.20126062], - [0.56264334, 0.59145486, 0.58950505], - ] - ), + [ + [0.01743160, 0.01794927, 0.01960625], + [0.08556139, 0.08957352, 0.09017387], + [0.74560311, 0.78175547, 0.78350814], + [0.19005289, 0.19950000, 0.20126062], + [0.56264334, 0.59145486, 0.58950505], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB[:5, ...], - np.array( - [ - [0.02075823, 0.01968577, 0.02139352], - [0.08957758, 0.08919227, 0.08910910], - [0.78102307, 0.78019384, 0.77643020], - [0.19950000, 0.19950000, 0.19950000], - [0.58984787, 0.59040152, 0.58510766], - ] - ), + [ + [0.02075823, 0.01968577, 0.02139352], + [0.08957758, 0.08919227, 0.08910910], + [0.78102307, 0.78019384, 0.77643020], + [0.19950000, 0.19950000, 0.19950000], + [0.58984787, 0.59040152, 0.58510766], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1193,7 +1181,8 @@ class TestCamera_RGB_to_ACES2065_1: definition unit tests methods. """ - def test_camera_RGB_to_ACES2065_1(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_camera_RGB_to_ACES2065_1(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.aces_it.camera_RGB_to_ACES2065_1` definition. @@ -1203,20 +1192,20 @@ def test_camera_RGB_to_ACES2065_1(self) -> None: return B, b = matrix_idt(MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"]) # pyright: ignore - np.testing.assert_allclose( - camera_RGB_to_ACES2065_1(np.array([0.1, 0.2, 0.3]), B, b), - np.array([0.27064400, 0.15614871, 0.50129650]), + xp_assert_close( + camera_RGB_to_ACES2065_1(xp_as_array([0.1, 0.2, 0.3], xp=xp), B, b), + [0.27064400, 0.15614871, 0.50129650], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - camera_RGB_to_ACES2065_1(np.array([1.5, 1.5, 1.5]), B, b), - np.array([3.36538176, 1.47467189, 2.46068761]), + xp_assert_close( + camera_RGB_to_ACES2065_1(xp_as_array([1.5, 1.5, 1.5], xp=xp), B, b), + [3.36538176, 1.47467189, 2.46068761], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - camera_RGB_to_ACES2065_1(np.array([1.0, 1.0, 1.0]), B, b, True), - np.array([2.24358784, 0.98311459, 1.64045840]), + xp_assert_close( + camera_RGB_to_ACES2065_1(xp_as_array([1.0, 1.0, 1.0], xp=xp), B, b, True), + [2.24358784, 0.98311459, 1.64045840], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/characterisation/tests/test_correction.py b/colour/characterisation/tests/test_correction.py index af905f02e5..d08775da7f 100644 --- a/colour/characterisation/tests/test_correction.py +++ b/colour/characterisation/tests/test_correction.py @@ -31,9 +31,14 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat + from colour.hints import NDArrayFloat, ModuleType -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -124,13 +129,13 @@ class TestMatrixAugmentedCheung2004: matrix_augmented_Cheung2004` definition unit tests methods. """ - def test_matrix_augmented_Cheung2004(self) -> None: + def test_matrix_augmented_Cheung2004(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ matrix_augmented_Cheung2004` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) polynomials = [ np.array([0.17224810, 0.09170660, 0.06416938]), @@ -362,7 +367,7 @@ def test_matrix_augmented_Cheung2004(self) -> None: ] for i, terms in enumerate([3, 4, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22, 35]): - np.testing.assert_allclose( + xp_assert_close( matrix_augmented_Cheung2004(RGB, terms), polynomials[i], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -374,12 +379,8 @@ def test_raise_exception_matrix_augmented_Cheung2004(self) -> None: matrix_augmented_Cheung2004` definition raised exception. """ - pytest.raises( - ValueError, - matrix_augmented_Cheung2004, - np.array([0.17224810, 0.09170660, 0.06416938]), - 6, - ) + with pytest.raises(ValueError): + matrix_augmented_Cheung2004(np.array([0.1722481, 0.0917066, 0.06416938]), 6) @ignore_numpy_errors def test_nan_matrix_augmented_Cheung2004(self) -> None: @@ -399,13 +400,13 @@ class TestPolynomialExpansionFinlayson2015: polynomial_expansion_Finlayson2015` definition unit tests methods. """ - def test_polynomial_expansion_Finlayson2015(self) -> None: + def test_polynomial_expansion_Finlayson2015(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ polynomial_expansion_Finlayson2015` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) polynomials = [ [ @@ -548,12 +549,12 @@ def test_polynomial_expansion_Finlayson2015(self) -> None: ] for i in range(4): - np.testing.assert_allclose( + xp_assert_close( polynomial_expansion_Finlayson2015(RGB, i + 1, False), polynomials[i][0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( polynomial_expansion_Finlayson2015(RGB, i + 1, True), polynomials[i][1], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -565,12 +566,10 @@ def test_raise_exception_polynomial_expansion_Finlayson2015(self) -> None: polynomial_expansion_Finlayson2015` definition raised exception. """ - pytest.raises( - ValueError, - polynomial_expansion_Finlayson2015, - np.array([0.17224810, 0.09170660, 0.06416938]), - 5, - ) + with pytest.raises(ValueError): + polynomial_expansion_Finlayson2015( + np.array([0.1722481, 0.0917066, 0.06416938]), 5 + ) @ignore_numpy_errors def test_nan_polynomial_expansion_Finlayson2015(self) -> None: @@ -590,13 +589,13 @@ class TestPolynomialExpansionVandermonde: polynomial_expansion_Vandermonde` definition unit tests methods. """ - def test_polynomial_expansion_Vandermonde(self) -> None: + def test_polynomial_expansion_Vandermonde(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ polynomial_expansion_Vandermonde` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) polynomials = [ np.array([0.17224810, 0.09170660, 0.06416938, 1.00000000]), @@ -645,7 +644,7 @@ def test_polynomial_expansion_Vandermonde(self) -> None: ] for i in range(4): - np.testing.assert_allclose( + xp_assert_close( polynomial_expansion_Vandermonde(RGB, i + 1), polynomials[i], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -669,57 +668,59 @@ class TestMatrixColourCorrectionCheung2004: matrix_colour_correction_Cheung2004` definition unit tests methods. """ - def test_matrix_colour_correction_Cheung2004(self) -> None: + def test_matrix_colour_correction_Cheung2004(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ matrix_colour_correction_Cheung2004` definition. """ - np.testing.assert_allclose( - matrix_colour_correction_Cheung2004(MATRIX_TEST, MATRIX_REFERENCE), - np.array( - [ - [0.69822661, 0.03071629, 0.16210422], - [0.06893498, 0.67579611, 0.16430385], - [-0.06314956, 0.09212471, 0.97134152], - ] + xp_assert_close( + matrix_colour_correction_Cheung2004( + xp_as_array(MATRIX_TEST, xp=xp), xp_as_array(MATRIX_REFERENCE, xp=xp) ), + [ + [0.69822661, 0.03071629, 0.16210422], + [0.06893498, 0.67579611, 0.16430385], + [-0.06314956, 0.09212471, 0.97134152], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - matrix_colour_correction_Cheung2004(MATRIX_TEST, MATRIX_REFERENCE, terms=7), - np.array( - [ - [ - 0.80512769, - 0.04001012, - -0.01255261, - -0.41056170, - -0.28052094, - 0.68417697, - 0.02251728, - ], - [ - 0.03270288, - 0.71452384, - 0.17581905, - -0.00897913, - 0.04900199, - -0.17162742, - 0.01688472, - ], - [ - -0.03973098, - -0.07164767, - 1.16401636, - 0.29017859, - -0.88909018, - 0.26675507, - 0.02345109, - ], - ] + xp_assert_close( + matrix_colour_correction_Cheung2004( + xp_as_array(MATRIX_TEST, xp=xp), + xp_as_array(MATRIX_REFERENCE, xp=xp), + terms=7, ), + [ + [ + 0.80512769, + 0.04001012, + -0.01255261, + -0.41056170, + -0.28052094, + 0.68417697, + 0.02251728, + ], + [ + 0.03270288, + 0.71452384, + 0.17581905, + -0.00897913, + 0.04900199, + -0.17162742, + 0.01688472, + ], + [ + -0.03973098, + -0.07164767, + 1.16401636, + 0.29017859, + -0.88909018, + 0.26675507, + 0.02345109, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -750,77 +751,78 @@ class TestMatrixColourCorrectionFinlayson2015: matrix_colour_correction_Finlayson2015` definition unit tests methods. """ - def test_matrix_colour_correction_Finlayson2015(self) -> None: + @pytest.mark.mps_tolerance_absolute(2e-1) + def test_matrix_colour_correction_Finlayson2015(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ matrix_colour_correction_Finlayson2015` definition. """ - np.testing.assert_allclose( - matrix_colour_correction_Finlayson2015(MATRIX_TEST, MATRIX_REFERENCE), - np.array( - [ - [0.69822661, 0.03071629, 0.16210422], - [0.06893498, 0.67579611, 0.16430385], - [-0.06314956, 0.09212471, 0.97134152], - ] + xp_assert_close( + matrix_colour_correction_Finlayson2015( + xp_as_array(MATRIX_TEST, xp=xp), xp_as_array(MATRIX_REFERENCE, xp=xp) ), + [ + [0.69822661, 0.03071629, 0.16210422], + [0.06893498, 0.67579611, 0.16430385], + [-0.06314956, 0.09212471, 0.97134152], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_colour_correction_Finlayson2015( - MATRIX_TEST, MATRIX_REFERENCE, degree=3 + xp_as_array(MATRIX_TEST, xp=xp), + xp_as_array(MATRIX_REFERENCE, xp=xp), + degree=3, ), - np.array( + [ [ - [ - 2.87796213, - 9.85720054, - 2.99863978, - 76.97227806, - 73.73571500, - -49.37563169, - -48.70879206, - -47.53280959, - 29.88241815, - -39.82871801, - -37.11388282, - 23.30393209, - 3.81579802, - ], - [ - -0.78448243, - 5.63631335, - 0.95306110, - 14.19762287, - 20.60124427, - -18.05512861, - -14.52994195, - -13.10606336, - 10.53666341, - -3.63132534, - -12.49672335, - 8.17401039, - 3.37995231, - ], - [ - -2.39092600, - 10.57193455, - 4.16361285, - 23.41748866, - 58.26902059, - -39.39669827, - -26.63805785, - -35.98397757, - 21.25508558, - -4.12726077, - -34.31995017, - 18.72796247, - 7.33531009, - ], - ] - ), + 2.87796213, + 9.85720054, + 2.99863978, + 76.97227806, + 73.73571500, + -49.37563169, + -48.70879206, + -47.53280959, + 29.88241815, + -39.82871801, + -37.11388282, + 23.30393209, + 3.81579802, + ], + [ + -0.78448243, + 5.63631335, + 0.95306110, + 14.19762287, + 20.60124427, + -18.05512861, + -14.52994195, + -13.10606336, + 10.53666341, + -3.63132534, + -12.49672335, + 8.17401039, + 3.37995231, + ], + [ + -2.39092600, + 10.57193455, + 4.16361285, + 23.41748866, + 58.26902059, + -39.39669827, + -26.63805785, + -35.98397757, + 21.25508558, + -4.12726077, + -34.31995017, + 18.72796247, + 7.33531009, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -853,68 +855,68 @@ class TestMatrixColourCorrectionVandermonde: matrix_colour_correction_Vandermonde` definition unit tests methods. """ - def test_matrix_colour_correction_Vandermonde(self) -> None: + def test_matrix_colour_correction_Vandermonde(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ matrix_colour_correction_Vandermonde` definition. """ - np.testing.assert_allclose( - matrix_colour_correction_Vandermonde(MATRIX_TEST, MATRIX_REFERENCE), - np.array( - [ - [0.66770040, 0.02514036, 0.12745797, 0.02485425], - [0.03155494, 0.66896825, 0.12187874, 0.03043460], - [-0.14502258, 0.07716975, 0.87841836, 0.06666049], - ] + xp_assert_close( + matrix_colour_correction_Vandermonde( + xp_as_array(MATRIX_TEST, xp=xp), xp_as_array(MATRIX_REFERENCE, xp=xp) ), + [ + [0.66770040, 0.02514036, 0.12745797, 0.02485425], + [0.03155494, 0.66896825, 0.12187874, 0.03043460], + [-0.14502258, 0.07716975, 0.87841836, 0.06666049], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_colour_correction_Vandermonde( - MATRIX_TEST, MATRIX_REFERENCE, degree=3 + xp_as_array(MATRIX_TEST, xp=xp), + xp_as_array(MATRIX_REFERENCE, xp=xp), + degree=3, ), - np.array( + [ [ - [ - -0.04328223, - -1.87886146, - 1.83369170, - -0.10798116, - 1.06608177, - -0.87495813, - 0.75525839, - -0.08558123, - 0.15919076, - 0.02404598, - ], - [ - 0.00998152, - 0.44525275, - -0.53192490, - 0.00904507, - -0.41034458, - 0.36173334, - 0.02904178, - 0.78362950, - 0.07894900, - 0.01986479, - ], - [ - -1.66921744, - 3.62954420, - -2.96789849, - 2.31451409, - -3.10767297, - 1.85975390, - -0.98795093, - 0.85962796, - 0.63591240, - 0.07302317, - ], - ] - ), + -0.04328223, + -1.87886146, + 1.83369170, + -0.10798116, + 1.06608177, + -0.87495813, + 0.75525839, + -0.08558123, + 0.15919076, + 0.02404598, + ], + [ + 0.00998152, + 0.44525275, + -0.53192490, + 0.00904507, + -0.41034458, + 0.36173334, + 0.02904178, + 0.78362950, + 0.07894900, + 0.01986479, + ], + [ + -1.66921744, + 3.62954420, + -2.96789849, + 2.31451409, + -3.10767297, + 1.85975390, + -0.98795093, + 0.85962796, + 0.63591240, + 0.07302317, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -947,56 +949,57 @@ class TestApplyMatrixColourCorrectionCheung2004: apply_matrix_colour_correction_Cheung2004` definition unit tests methods. """ - def test_apply_matrix_colour_correction_Cheung2004(self) -> None: + def test_apply_matrix_colour_correction_Cheung2004(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ apply_matrix_colour_correction_Cheung2004` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) - np.testing.assert_allclose( + xp_assert_close( apply_matrix_colour_correction_Cheung2004( RGB, - np.array( - [ - [0.69822661, 0.03071629, 0.16210422], - [0.06893498, 0.67579611, 0.16430385], - [-0.06314956, 0.09212471, 0.97134152], - ] - ), + [ + [0.69822661, 0.03071629, 0.16210422], + [0.06893498, 0.67579611, 0.16430385], + [-0.06314956, 0.09212471, 0.97134152], + ], ), - np.array([0.13348722, 0.08439216, 0.05990144]), + [0.13348722, 0.08439216, 0.05990144], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_apply_matrix_colour_correction_Cheung2004(self) -> None: + def test_n_dimensional_apply_matrix_colour_correction_Cheung2004( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.characterisation.correction.\ apply_matrix_colour_correction_Cheung2004` definition n-dimensional support. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) - CCM = np.array( + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) + CCM = xp_as_array( [ [0.69822661, 0.03071629, 0.16210422], [0.06893498, 0.67579611, 0.16430385], [-0.06314956, 0.09212471, 0.97134152], - ] + ], + xp=xp, ) RGB_c = apply_matrix_colour_correction_Cheung2004(RGB, CCM) - RGB = np.tile(RGB, (6, 1)) - RGB_c = np.tile(RGB_c, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + RGB_c = xp.tile(xp_as_array(RGB_c, xp=xp), (6, 1)) + xp_assert_close( apply_matrix_colour_correction_Cheung2004(RGB, CCM), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - RGB_c = np.reshape(RGB_c, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + RGB_c = xp_reshape(xp_as_array(RGB_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( apply_matrix_colour_correction_Cheung2004(RGB, CCM), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1031,56 +1034,57 @@ class TestApplyMatrixColourCorrectionFinlayson2015: apply_matrix_colour_correction_Finlayson2015` definition unit tests methods. """ - def test_apply_matrix_colour_correction_Finlayson2015(self) -> None: + def test_apply_matrix_colour_correction_Finlayson2015(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ apply_matrix_colour_correction_Finlayson2015` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) - np.testing.assert_allclose( + xp_assert_close( apply_matrix_colour_correction_Finlayson2015( RGB, - np.array( - [ - [0.69822661, 0.03071629, 0.16210422], - [0.06893498, 0.67579611, 0.16430385], - [-0.06314956, 0.09212471, 0.97134152], - ] - ), + [ + [0.69822661, 0.03071629, 0.16210422], + [0.06893498, 0.67579611, 0.16430385], + [-0.06314956, 0.09212471, 0.97134152], + ], ), - np.array([0.13348722, 0.08439216, 0.05990144]), + [0.13348722, 0.08439216, 0.05990144], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_apply_matrix_colour_correction_Finlayson2015(self) -> None: + def test_n_dimensional_apply_matrix_colour_correction_Finlayson2015( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.characterisation.correction.\ apply_matrix_colour_correction_Finlayson2015` definition n-dimensional support. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) - CCM = np.array( + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) + CCM = xp_as_array( [ [0.69822661, 0.03071629, 0.16210422], [0.06893498, 0.67579611, 0.16430385], [-0.06314956, 0.09212471, 0.97134152], - ] + ], + xp=xp, ) RGB_c = apply_matrix_colour_correction_Finlayson2015(RGB, CCM) - RGB = np.tile(RGB, (6, 1)) - RGB_c = np.tile(RGB_c, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + RGB_c = xp.tile(xp_as_array(RGB_c, xp=xp), (6, 1)) + xp_assert_close( apply_matrix_colour_correction_Finlayson2015(RGB, CCM), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - RGB_c = np.reshape(RGB_c, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + RGB_c = xp_reshape(xp_as_array(RGB_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( apply_matrix_colour_correction_Finlayson2015(RGB, CCM), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1115,56 +1119,57 @@ class TestApplyMatrixColourCorrectionVandermonde: apply_matrix_colour_correction_Vandermonde` definition unit tests methods. """ - def test_apply_matrix_colour_correction_Vandermonde(self) -> None: + def test_apply_matrix_colour_correction_Vandermonde(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ apply_matrix_colour_correction_Vandermonde` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) - np.testing.assert_allclose( + xp_assert_close( apply_matrix_colour_correction_Vandermonde( RGB, - np.array( - [ - [0.66770040, 0.02514036, 0.12745797, 0.02485425], - [0.03155494, 0.66896825, 0.12187874, 0.03043460], - [-0.14502258, 0.07716975, 0.87841836, 0.06666049], - ] - ), + [ + [0.66770040, 0.02514036, 0.12745797, 0.02485425], + [0.03155494, 0.66896825, 0.12187874, 0.03043460], + [-0.14502258, 0.07716975, 0.87841836, 0.06666049], + ], ), - np.array([0.15034881, 0.10503956, 0.10512517]), + [0.15034881, 0.10503956, 0.10512517], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_apply_matrix_colour_correction_Vandermonde(self) -> None: + def test_n_dimensional_apply_matrix_colour_correction_Vandermonde( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.characterisation.correction.\ apply_matrix_colour_correction_Vandermonde` definition n-dimensional support. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) - CCM = np.array( + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) + CCM = xp_as_array( [ [0.66770040, 0.02514036, 0.12745797, 0.02485425], [0.03155494, 0.66896825, 0.12187874, 0.03043460], [-0.14502258, 0.07716975, 0.87841836, 0.06666049], - ] + ], + xp=xp, ) RGB_c = apply_matrix_colour_correction_Vandermonde(RGB, CCM) - RGB = np.tile(RGB, (6, 1)) - RGB_c = np.tile(RGB_c, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + RGB_c = xp.tile(xp_as_array(RGB_c, xp=xp), (6, 1)) + xp_assert_close( apply_matrix_colour_correction_Vandermonde(RGB, CCM), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - RGB_c = np.reshape(RGB_c, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + RGB_c = xp_reshape(xp_as_array(RGB_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( apply_matrix_colour_correction_Vandermonde(RGB, CCM), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1199,46 +1204,46 @@ class TestColourCorrectionCheung2004: colour_correction_Cheung2004` definition unit tests methods. """ - def test_colour_correction_Cheung2004(self) -> None: + def test_colour_correction_Cheung2004(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ colour_correction_Cheung2004` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) - np.testing.assert_allclose( + xp_assert_close( colour_correction_Cheung2004(RGB, MATRIX_TEST, MATRIX_REFERENCE), - np.array([0.13348722, 0.08439216, 0.05990144]), + [0.13348722, 0.08439216, 0.05990144], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_correction_Cheung2004(RGB, MATRIX_TEST, MATRIX_REFERENCE, terms=7), - np.array([0.15850295, 0.09871628, 0.08105752]), + [0.15850295, 0.09871628, 0.08105752], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_colour_correction_Cheung2004(self) -> None: + def test_n_dimensional_colour_correction_Cheung2004(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ colour_correction_Cheung2004` definition n-dimensional support. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) RGB_c = colour_correction_Cheung2004(RGB, MATRIX_TEST, MATRIX_REFERENCE) - RGB = np.tile(RGB, (6, 1)) - RGB_c = np.tile(RGB_c, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + RGB_c = xp.tile(xp_as_array(RGB_c, xp=xp), (6, 1)) + xp_assert_close( colour_correction_Cheung2004(RGB, MATRIX_TEST, MATRIX_REFERENCE), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - RGB_c = np.reshape(RGB_c, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + RGB_c = xp_reshape(xp_as_array(RGB_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( colour_correction_Cheung2004(RGB, MATRIX_TEST, MATRIX_REFERENCE), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1272,48 +1277,50 @@ class TestColourCorrectionFinlayson2015: colour_correction_Finlayson2015` definition unit tests methods. """ - def test_colour_correction_Finlayson2015(self) -> None: + def test_colour_correction_Finlayson2015(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ colour_correction_Finlayson2015` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) - np.testing.assert_allclose( + xp_assert_close( colour_correction_Finlayson2015(RGB, MATRIX_TEST, MATRIX_REFERENCE), - np.array([0.13348722, 0.08439216, 0.05990144]), + [0.13348722, 0.08439216, 0.05990144], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_correction_Finlayson2015( RGB, MATRIX_TEST, MATRIX_REFERENCE, degree=3 ), - np.array([0.13914542, 0.08602124, 0.06422973]), + [0.13914542, 0.08602124, 0.06422973], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_colour_correction_Finlayson2015(self) -> None: + def test_n_dimensional_colour_correction_Finlayson2015( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.characterisation.correction.\ colour_correction_Finlayson2015` definition n-dimensional support. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) RGB_c = colour_correction_Finlayson2015(RGB, MATRIX_TEST, MATRIX_REFERENCE) - RGB = np.tile(RGB, (6, 1)) - RGB_c = np.tile(RGB_c, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + RGB_c = xp.tile(xp_as_array(RGB_c, xp=xp), (6, 1)) + xp_assert_close( colour_correction_Finlayson2015(RGB, MATRIX_TEST, MATRIX_REFERENCE), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - RGB_c = np.reshape(RGB_c, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + RGB_c = xp_reshape(xp_as_array(RGB_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( colour_correction_Finlayson2015(RGB, MATRIX_TEST, MATRIX_REFERENCE), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1347,46 +1354,46 @@ class TestColourCorrectionVandermonde: colour_correction_Vandermonde` definition unit tests methods. """ - def test_colour_correction_Vandermonde(self) -> None: + def test_colour_correction_Vandermonde(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ colour_correction_Vandermonde` definition. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) - np.testing.assert_allclose( + xp_assert_close( colour_correction_Vandermonde(RGB, MATRIX_TEST, MATRIX_REFERENCE), - np.array([0.15034881, 0.10503956, 0.10512517]), + [0.15034881, 0.10503956, 0.10512517], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_correction_Vandermonde(RGB, MATRIX_TEST, MATRIX_REFERENCE, degree=3), - np.array([0.15747814, 0.10035799, 0.06616709]), + [0.15747814, 0.10035799, 0.06616709], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_colour_correction_Vandermonde(self) -> None: + def test_n_dimensional_colour_correction_Vandermonde(self, xp: ModuleType) -> None: """ Test :func:`colour.characterisation.correction.\ colour_correction_Vandermonde` definition n-dimensional support. """ - RGB = np.array([0.17224810, 0.09170660, 0.06416938]) + RGB = xp_as_array([0.17224810, 0.09170660, 0.06416938], xp=xp) RGB_c = colour_correction_Vandermonde(RGB, MATRIX_TEST, MATRIX_REFERENCE) - RGB = np.tile(RGB, (6, 1)) - RGB_c = np.tile(RGB_c, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + RGB_c = xp.tile(xp_as_array(RGB_c, xp=xp), (6, 1)) + xp_assert_close( colour_correction_Vandermonde(RGB, MATRIX_TEST, MATRIX_REFERENCE), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - RGB_c = np.reshape(RGB_c, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + RGB_c = xp_reshape(xp_as_array(RGB_c, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( colour_correction_Vandermonde(RGB, MATRIX_TEST, MATRIX_REFERENCE), RGB_c, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/characterisation/tests/test_correction_tps3d.py b/colour/characterisation/tests/test_correction_tps3d.py index 28aff7f3e6..8bdea84c36 100644 --- a/colour/characterisation/tests/test_correction_tps3d.py +++ b/colour/characterisation/tests/test_correction_tps3d.py @@ -2,45 +2,58 @@ Unit tests for the TPS-3D colour correction method. """ +from __future__ import annotations + +import typing + import numpy as np +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.characterisation.correction import ( apply_tps3d, colour_correction_TPS3D, tps3d_parameters, ) +from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import as_ndarray, xp_as_array -def test_tps3d_maps_control_points() -> None: +def test_tps3d_maps_control_points(xp: ModuleType) -> None: """Control points should map exactly to their targets.""" rng = np.random.default_rng(42) - M_T = rng.random((24, 3)) - M_R = np.clip(M_T * 0.85 + 0.05, 0, 1) + M_T_np = rng.random((24, 3)) + M_R_np = np.clip(M_T_np * 0.85 + 0.05, 0, 1) + + M_T = xp_as_array(M_T_np, xp=xp) + M_R = xp_as_array(M_R_np, xp=xp) W, A, ctrl = tps3d_parameters(M_T, M_R, smoothing=1e-10) mapped = apply_tps3d(M_T, W, A, ctrl, clip=False, chunk_size=1024) - assert np.max(np.abs(mapped - M_R)) < 1e-6 + assert np.max(np.abs(as_ndarray(mapped) - M_R_np)) < TOLERANCE_ABSOLUTE_TESTS -def test_tps3d_identity_is_identity() -> None: +def test_tps3d_identity_is_identity(xp: ModuleType) -> None: """Identity mapping should leave data unchanged.""" rng = np.random.default_rng(123) - M_T = rng.random((24, 3)) + M_T = xp_as_array(rng.random((24, 3)), xp=xp) W, A, ctrl = tps3d_parameters(M_T, M_T, smoothing=1e-12) - img = rng.random((16, 17, 3)) + img_np = rng.random((16, 17, 3)) + img = xp_as_array(img_np, xp=xp) out = apply_tps3d(img, W, A, ctrl, clip=False, chunk_size=1024) - assert np.max(np.abs(out - img)) < 1e-6 + assert np.max(np.abs(as_ndarray(out) - img_np)) < TOLERANCE_ABSOLUTE_TESTS -def test_colour_correction_tps3d_shape() -> None: +def test_colour_correction_tps3d_shape(xp: ModuleType) -> None: """Colour correction should preserve the input shape.""" rng = np.random.default_rng(7) - M_T = rng.random((24, 3)) - M_R = rng.random((24, 3)) - img = rng.random((10, 11, 3)) + M_T = xp_as_array(rng.random((24, 3)), xp=xp) + M_R = xp_as_array(rng.random((24, 3)), xp=xp) + img = xp_as_array(rng.random((10, 11, 3)), xp=xp) out = colour_correction_TPS3D(img, M_T, M_R, smoothing=1e-8, chunk_size=1024) - assert out.shape == img.shape + assert as_ndarray(out).shape == (10, 11, 3) diff --git a/colour/colorimetry/__init__.py b/colour/colorimetry/__init__.py index 21ac0f9d6c..fee8e73939 100644 --- a/colour/colorimetry/__init__.py +++ b/colour/colorimetry/__init__.py @@ -3,16 +3,21 @@ MultiSpectralDistributions, SpectralDistribution, SpectralShape, + extrapolate_signal, + interpolate_signal, reshape_msds, reshape_sd, sds_and_msds_to_msds, sds_and_msds_to_sds, + trim_signal, ) # isort: split from .blackbody import ( blackbody_spectral_radiance, + msds_blackbody, + msds_rayleigh_jeans, planck_law, rayleigh_jeans_law, sd_blackbody, @@ -68,6 +73,7 @@ msds_to_XYZ, msds_to_XYZ_ASTME308, msds_to_XYZ_integration, + msds_to_XYZ_tristimulus_weighting_factors_ASTME308, sd_to_XYZ, sd_to_XYZ_ASTME308, sd_to_XYZ_integration, @@ -89,7 +95,9 @@ # isort: split from .illuminants import ( + CIE_illuminant_D_series, daylight_locus_function, + msds_CIE_illuminant_D_series, sd_CIE_illuminant_D_series, sd_CIE_standard_illuminant_A, ) @@ -160,13 +168,18 @@ "MultiSpectralDistributions", "SpectralDistribution", "SpectralShape", + "extrapolate_signal", + "interpolate_signal", "reshape_msds", "reshape_sd", "sds_and_msds_to_msds", "sds_and_msds_to_sds", + "trim_signal", ] __all__ += [ "blackbody_spectral_radiance", + "msds_blackbody", + "msds_rayleigh_jeans", "planck_law", "rayleigh_jeans_law", "sd_blackbody", @@ -222,6 +235,7 @@ "msds_to_XYZ", "msds_to_XYZ_ASTME308", "msds_to_XYZ_integration", + "msds_to_XYZ_tristimulus_weighting_factors_ASTME308", "sd_to_XYZ", "sd_to_XYZ_ASTME308", "sd_to_XYZ_integration", @@ -239,7 +253,9 @@ "bandpass_correction_Stearns1988", ] __all__ += [ + "CIE_illuminant_D_series", "daylight_locus_function", + "msds_CIE_illuminant_D_series", "sd_CIE_illuminant_D_series", "sd_CIE_standard_illuminant_A", ] diff --git a/colour/colorimetry/blackbody.py b/colour/colorimetry/blackbody.py index 9d76dc1f7f..64499a5119 100644 --- a/colour/colorimetry/blackbody.py +++ b/colour/colorimetry/blackbody.py @@ -18,19 +18,29 @@ import typing -import numpy as np - from colour.colorimetry import ( SPECTRAL_SHAPE_DEFAULT, + MultiSpectralDistributions, SpectralDistribution, SpectralShape, ) -from colour.constants import CONSTANT_BOLTZMANN, CONSTANT_LIGHT_SPEED +from colour.constants import ( + CONSTANT_BOLTZMANN, + CONSTANT_LIGHT_SPEED, +) if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import as_float, as_float_array +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + as_ndarray, + xp_as_float_array, + xp_reshape, + xp_squeeze, +) from colour.utilities.common import attest __author__ = "Colour Developers" @@ -47,8 +57,10 @@ "planck_law", "blackbody_spectral_radiance", "sd_blackbody", + "msds_blackbody", "rayleigh_jeans_law", "sd_rayleigh_jeans", + "msds_rayleigh_jeans", ] # 2 * math.pi * CONSTANT_PLANCK * CONSTANT_LIGHT_SPEED ** 2 @@ -131,15 +143,20 @@ def planck_law( l = as_float_array(wavelength) # noqa: E741 t = as_float_array(temperature) - attest(np.all(l > 0), "Wavelengths must be positive real numbers!") + xp = array_namespace(l, t) + + l = xp_as_float_array(l, xp=xp, like=t) # noqa: E741 + t = xp_as_float_array(t, xp=xp, like=l) - l = np.ravel(l)[..., None] # noqa: E741 - t = np.ravel(t)[None, ...] + attest(xp.all(l > 0), "Wavelengths must be positive real numbers!") - d = 1 / np.expm1(c2 / (n * l * t)) - p = ((c1 * n**-2 * l**-5) / np.pi) * d + l = xp_reshape(l, (-1,), xp=xp)[..., None] # noqa: E741 + t = xp_reshape(t, (-1,), xp=xp)[None, ...] - return as_float(np.squeeze(p)) + d = 1 / xp.expm1(c2 / (n * l * t)) + p = ((c1 * n**-2 * l**-5) / xp.pi) * d + + return as_float(xp_squeeze(p, xp=xp)) blackbody_spectral_radiance = planck_law @@ -222,6 +239,64 @@ def sd_blackbody( ) +def msds_blackbody( + temperatures: ArrayLike, + shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, + c1: float = CONSTANT_C1, + c2: float = CONSTANT_C2, + n: float = CONSTANT_N, +) -> MultiSpectralDistributions: + """ + Generate the multi-spectral distributions of the planckian radiator + for the specified ``N`` temperatures :math:`T[K]` with values in + *watts per steradian per square metre per nanometre* + (:math:`W/sr/m^2/nm`). + + Parameters + ---------- + temperatures + Temperatures :math:`T[K]` in kelvins. + shape + Spectral shape used to create the spectral distributions of the + planckian radiators. + c1, c2, n + See :func:`colour.sd_blackbody`. + + Returns + ------- + :class:`colour.MultiSpectralDistributions` + Blackbody multi-spectral distributions of shape + ``(n_wavelengths, N)`` with values in *watts per steradian per + square metre per nanometre* (:math:`W/sr/m^2/nm`). + + Examples + -------- + >>> import numpy as np + >>> from colour.utilities import numpy_print_options + >>> with numpy_print_options(suppress=True): + ... msds_blackbody( # doctest: +ELLIPSIS + ... np.array([3000.0, 5000.0]), shape=SpectralShape(400, 700, 100) + ... ) + MultiSpectralDistributions([[ 400. , 72.1837..., 8742.5713...], + [ 500. , 260.2281..., 12106.0645...], + [ 600. , 517.4366..., 12761.3938...], + [ 700. , 750.5147..., 11811.1793...]], + [...], + SpragueInterpolator, + {}, + Extrapolator, + {'method': 'Constant', 'left': None, 'right': None}) + """ + + temperatures = as_float_array(temperatures) + + return MultiSpectralDistributions( + planck_law(shape.wavelengths * 1e-9, temperatures, c1, c2, n) * 1e-9, + shape.wavelengths, + labels=[f"{T}K Blackbody" for T in as_ndarray(temperatures)], + ) + + def rayleigh_jeans_law(wavelength: ArrayLike, temperature: ArrayLike) -> NDArrayFloat: """ Approximate the spectral radiance of a blackbody as a function of @@ -277,15 +352,20 @@ def rayleigh_jeans_law(wavelength: ArrayLike, temperature: ArrayLike) -> NDArray l = as_float_array(wavelength) # noqa: E741 t = as_float_array(temperature) - l = np.ravel(l)[..., None] # noqa: E741 - t = np.ravel(t)[None, ...] + xp = array_namespace(l, t) + + l = xp_as_float_array(l, xp=xp, like=t) # noqa: E741 + t = xp_as_float_array(t, xp=xp, like=l) + + l = xp_reshape(l, (-1,), xp=xp)[..., None] # noqa: E741 + t = xp_reshape(t, (-1,), xp=xp)[None, ...] c = CONSTANT_LIGHT_SPEED k_B = CONSTANT_BOLTZMANN B = (2 * c * k_B * t) / (l**4) - return as_float(np.squeeze(B)) + return as_float(xp_squeeze(B, xp=xp)) def sd_rayleigh_jeans( @@ -353,3 +433,64 @@ def sd_rayleigh_jeans( shape.wavelengths, name=f"{temperature}K Rayleigh-Jeans", ) + + +def msds_rayleigh_jeans( + temperatures: ArrayLike, + shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, +) -> MultiSpectralDistributions: + """ + Generate the multi-spectral distributions of the planckian radiator + for the specified ``N`` temperatures :math:`T[K]` with values in + *watts per steradian per square metre per nanometre* + (:math:`W/sr/m^2/nm`) according to the *Rayleigh-Jeans* law. + + Parameters + ---------- + temperatures + Temperatures :math:`T[K]` in kelvins. + shape + Spectral shape used to create the spectral distributions of the + planckian radiators. + + Returns + ------- + :class:`colour.MultiSpectralDistributions` + Rayleigh-Jeans multi-spectral distributions of shape + ``(n_wavelengths, N)`` with values in *watts per steradian per + square metre per nanometre* (:math:`W/sr/m^2/nm`). + + Notes + ----- + - The *Rayleigh-Jeans* law agrees with experimental results at large + wavelengths (low frequencies) but strongly disagrees at short + wavelengths (high frequencies). This inconsistency between + observations and the predictions of classical physics is commonly + known as the *ultraviolet catastrophe*. + + Examples + -------- + >>> import numpy as np + >>> from colour.utilities import numpy_print_options + >>> with numpy_print_options(suppress=True): + ... msds_rayleigh_jeans( # doctest: +ELLIPSIS + ... np.array([3000.0, 5000.0]), shape=SpectralShape(400, 700, 100) + ... ) + MultiSpectralDistributions([[ 400. , 970097.946..., 1616829.910...], + [ 500. , 397352.118..., 662253.531...], + [ 600. , 191624.285..., 319373.809...], + [ 700. , 103434.016..., 172390.027...]], + [...], + SpragueInterpolator, + {}, + Extrapolator, + {'method': 'Constant', 'left': None, 'right': None}) + """ + + temperatures = as_float_array(temperatures) + + return MultiSpectralDistributions( + rayleigh_jeans_law(shape.wavelengths * 1e-9, temperatures) * 1e-9, + shape.wavelengths, + labels=[f"{T}K Rayleigh-Jeans" for T in as_ndarray(temperatures)], + ) diff --git a/colour/colorimetry/dominant.py b/colour/colorimetry/dominant.py index 71d3c2220d..4cfb28628f 100644 --- a/colour/colorimetry/dominant.py +++ b/colour/colorimetry/dominant.py @@ -25,8 +25,6 @@ import typing -import numpy as np - from colour.algebra import euclidean_distance, sdiv, sdiv_mode from colour.colorimetry import MultiSpectralDistributions, handle_spectral_arguments from colour.geometry import extend_line_segment, intersect_line_segments @@ -35,7 +33,16 @@ from colour.hints import ArrayLike, NDArrayFloat, NDArrayInt, Tuple from colour.models import XYZ_to_xy -from colour.utilities import as_float_array, required +from colour.utilities import ( + array_namespace, + as_float_array, + as_ndarray, + required, + xp_as_float_array, + xp_reshape, + xp_resize, + xp_squeeze, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -88,6 +95,7 @@ def closest_spectral_locus_wavelength( Examples -------- + >>> import numpy as np >>> from colour.colorimetry import MSDS_CMFS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> xy = np.array([0.54369557, 0.32107944]) @@ -103,25 +111,35 @@ def closest_spectral_locus_wavelength( import scipy.spatial.distance # noqa: PLC0415 xy = as_float_array(xy) - xy_n = np.resize(xy_n, xy.shape) - xy_s = as_float_array(xy_s) + xp = array_namespace(xy, xy_s) + + xy_n = xp_resize(xp_as_float_array(xy_n, xp=xp, like=xy), xy.shape, xp=xp) xy_e = extend_line_segment(xy, xy_n) if inverse else extend_line_segment(xy_n, xy) # Closing horse-shoe shape to handle line of purples intersections. - xy_s = np.vstack([xy_s, xy_s[0, :]]) + xy_s = xp_as_float_array(xy_s, xp=xp, like=xy) + xy_s = xp.concat([xy_s, xy_s[0:1, :]], axis=0) xy_wl = intersect_line_segments( - np.concatenate((xy_n, xy_e), -1), - np.hstack([xy_s, np.roll(xy_s, 1, axis=0)]), + xp.concat((xy_n, xy_e), axis=-1), + xp.concat([xy_s, xp.roll(xy_s, 1, axis=0)], axis=-1), ).xy # Extracting the first intersection per-wavelength. - xy_wl = np.sort(xy_wl, 1)[:, 0, :] + xy_wl = xp.sort(xy_wl, axis=1)[:, 0, :] - i_wl = np.argmin(scipy.spatial.distance.cdist(xy_wl, xy_s), axis=-1) + # scipy requires numpy arrays. + i_wl = xp.argmin( + xp_as_float_array( + scipy.spatial.distance.cdist(as_ndarray(xy_wl), as_ndarray(xy_s)), + xp=xp, + like=xy_wl, + ), + axis=-1, + ) - i_wl = np.reshape(i_wl, xy.shape[0:-1]) - xy_wl = np.reshape(xy_wl, xy.shape) + i_wl = xp_reshape(i_wl, xy.shape[0:-1], xp=xp) + xy_wl = xp_reshape(xy_wl, xy.shape, xp=xp) return i_wl, xy_wl @@ -175,6 +193,7 @@ def dominant_wavelength( -------- *Dominant wavelength* computation: + >>> import numpy as np >>> from colour.colorimetry import MSDS_CMFS >>> from pprint import pprint >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] @@ -198,27 +217,35 @@ def dominant_wavelength( cmfs, _illuminant = handle_spectral_arguments(cmfs) xy = as_float_array(xy) - xy_n = np.resize(xy_n, xy.shape) + + xp = array_namespace(xy) + + xy_n = xp_resize(xp_as_float_array(xy_n, xp=xp, like=xy), xy.shape, xp=xp) xy_s = XYZ_to_xy(cmfs.values) + wavelengths = xp_as_float_array(cmfs.wavelengths, xp=xp, like=xy) i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, xy_s, inverse) xy_cwl = xy_wl - wl = cmfs.wavelengths[i_wl] + wl = wavelengths[i_wl] + xy_s = xp_as_float_array(xy_s, xp=xp, like=xy) xy_e = extend_line_segment(xy, xy_n) if inverse else extend_line_segment(xy_n, xy) intersect = intersect_line_segments( - np.concatenate((xy_n, xy_e), -1), np.hstack([xy_s[0], xy_s[-1]]) + xp.concat((xy_n, xy_e), axis=-1), xp.concat([xy_s[0], xy_s[-1]], axis=0) ).intersect - intersect = np.reshape(intersect, wl.shape) + intersect = xp_reshape(intersect, wl.shape, xp=xp) i_wl_r, xy_cwl_r = closest_spectral_locus_wavelength(xy, xy_n, xy_s, not inverse) - wl_r = -cmfs.wavelengths[i_wl_r] + wl_r = -wavelengths[i_wl_r] + + wl = xp.where(intersect, wl_r, wl) + xy_cwl = xp.where(intersect[..., None], xy_cwl_r, xy_cwl) - wl = np.where(intersect, wl_r, wl) - xy_cwl = np.where(intersect[..., None], xy_cwl_r, xy_cwl) + xy_wl = xp_squeeze(xy_wl, xp=xp) + xy_cwl = xp_squeeze(xy_cwl, xp=xp) - return wl, np.squeeze(xy_wl), np.squeeze(xy_cwl) + return wl, xy_wl, xy_cwl def complementary_wavelength( @@ -267,6 +294,7 @@ def complementary_wavelength( -------- *Complementary wavelength* computation: + >>> import numpy as np >>> from colour.colorimetry import MSDS_CMFS >>> from pprint import pprint >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] @@ -318,6 +346,7 @@ def excitation_purity( Examples -------- + >>> import numpy as np >>> from colour.colorimetry import MSDS_CMFS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> xy = np.array([0.54369557, 0.32107944]) @@ -365,6 +394,7 @@ def colorimetric_purity( Examples -------- + >>> import numpy as np >>> from colour.colorimetry import MSDS_CMFS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> xy = np.array([0.54369557, 0.32107944]) diff --git a/colour/colorimetry/generation.py b/colour/colorimetry/generation.py index 9972867c0b..36076c5ae0 100644 --- a/colour/colorimetry/generation.py +++ b/colour/colorimetry/generation.py @@ -36,10 +36,9 @@ from __future__ import annotations +import math import typing -import numpy as np - from colour.algebra.interpolation import LinearInterpolator from colour.colorimetry import ( SPECTRAL_SHAPE_DEFAULT, @@ -59,10 +58,12 @@ from colour.utilities import ( CanonicalMapping, + array_namespace, as_float_array, full, ones, validate_method, + xp_resize, ) __author__ = "Colour Developers" @@ -429,7 +430,11 @@ def sd_gaussian_normal( settings = {"name": f"{mu}nm - {sigma} Sigma - Gaussian"} settings.update(kwargs) - values = np.exp(-((shape.wavelengths - mu) ** 2) / (2 * sigma**2)) + wavelengths = as_float_array(shape.wavelengths) + + xp = array_namespace(wavelengths) + + values = xp.exp(-((wavelengths - mu) ** 2) / (2 * sigma**2)) return SpectralDistribution(values, shape.wavelengths, **settings) @@ -485,8 +490,12 @@ def sd_gaussian_fwhm( settings = {"name": f"{peak_wavelength}nm - {fwhm} FWHM - Gaussian"} settings.update(kwargs) - mu, sigma = peak_wavelength, fwhm / (2 * np.sqrt(2 * np.log(2))) - values = np.exp(-((shape.wavelengths - mu) ** 2) / (2 * sigma**2)) + wavelengths = as_float_array(shape.wavelengths) + + xp = array_namespace(wavelengths) + + mu, sigma = peak_wavelength, fwhm / (2 * math.sqrt(2 * math.log(2))) + values = xp.exp(-((wavelengths - mu) ** 2) / (2 * sigma**2)) return SpectralDistribution(values, shape.wavelengths, **settings) @@ -556,11 +565,14 @@ def sd_gaussian_super_clamped( settings = {"name": f"{peak_wavelength}nm - {fwhm} FWHM - Super-Gaussian Clamped"} settings.update(kwargs) - wavelengths = shape.wavelengths + wavelengths = as_float_array(shape.wavelengths) + + xp = array_namespace(wavelengths) + # Convert FWHM to sigma: FWHM = 2 * sigma * (2 * ln(2))^(1/exponent) - sigma = fwhm / (2 * (2 * np.log(2)) ** (1 / exponent)) + sigma = fwhm / (2 * (2 * math.log(2)) ** (1 / exponent)) # Super-Gaussian: exp(-|x/sigma|^exponent) - values = np.exp(-(np.abs((wavelengths - peak_wavelength) / sigma) ** exponent)) + values = xp.exp(-(xp.abs((wavelengths - peak_wavelength) / sigma) ** exponent)) sd = SpectralDistribution(values, wavelengths, **settings) sd.range = sd.range / sd.range.max() # Normalize peak to 1 @@ -714,9 +726,11 @@ def sd_single_led_Ohno2005( } settings.update(kwargs) - values = np.exp( - -(((shape.wavelengths - peak_wavelength) / half_spectral_width) ** 2) - ) + wavelengths = as_float_array(shape.wavelengths) + + xp = array_namespace(wavelengths) + + values = xp.exp(-(((wavelengths - peak_wavelength) / half_spectral_width) ** 2)) values = (values + 2 * values**5) / 3 return SpectralDistribution(values, shape.wavelengths, **settings) @@ -840,6 +854,7 @@ def sd_multi_leds_Ohno2005( Examples -------- + >>> import numpy as np >>> sd = sd_multi_leds_Ohno2005( ... np.array([457, 530, 615]), ... np.array([20, 30, 20]), @@ -852,20 +867,29 @@ def sd_multi_leds_Ohno2005( """ peak_wavelengths = as_float_array(peak_wavelengths) - half_spectral_widths = np.resize(half_spectral_widths, peak_wavelengths.shape) + + xp = array_namespace(peak_wavelengths) + + half_spectral_widths = xp_resize( + half_spectral_widths, peak_wavelengths.shape, xp=xp + ) if peak_power_ratios is None: peak_power_ratios = ones(peak_wavelengths.shape) else: - peak_power_ratios = np.resize(peak_power_ratios, peak_wavelengths.shape) + peak_power_ratios = xp_resize(peak_power_ratios, peak_wavelengths.shape, xp=xp) sd = sd_zeros(shape) for peak_wavelength, half_spectral_width, peak_power_ratio in zip( peak_wavelengths, half_spectral_widths, peak_power_ratios, strict=True ): + # ``.item()`` works uniformly across NumPy, JAX, and PyTorch element + # slices where ``float(...)`` is JAX-strict for ``(1,)``-shaped slices. sd += ( - sd_single_led_Ohno2005(peak_wavelength, half_spectral_width, **kwargs) - * peak_power_ratio + sd_single_led_Ohno2005( + peak_wavelength.item(), half_spectral_width.item(), **kwargs + ) + * peak_power_ratio.item() ) def _format_array(a: NDArrayFloat) -> str: @@ -936,6 +960,7 @@ def sd_multi_leds( Examples -------- + >>> import numpy as np >>> sd = sd_multi_leds( ... np.array([457, 530, 615]), ... half_spectral_widths=np.array([20, 30, 20]), diff --git a/colour/colorimetry/illuminants.py b/colour/colorimetry/illuminants.py index 8813afa61a..3b5fd91072 100644 --- a/colour/colorimetry/illuminants.py +++ b/colour/colorimetry/illuminants.py @@ -7,6 +7,8 @@ - :func:`colour.sd_CIE_standard_illuminant_A` - :func:`colour.sd_CIE_illuminant_D_series` +- :func:`colour.msds_CIE_illuminant_D_series` +- :func:`colour.CIE_illuminant_D_series` - :func:`colour.daylight_locus_function` References @@ -29,21 +31,26 @@ import typing -import numpy as np - from colour.algebra import LinearInterpolator from colour.colorimetry import ( SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES, SPECTRAL_SHAPE_DEFAULT, + MultiSpectralDistributions, SpectralDistribution, SpectralShape, - reshape_sd, ) if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import as_float, as_float_array, tsplit +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + as_ndarray, + xp_as_float_array, + xp_round, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -54,7 +61,9 @@ __all__ = [ "sd_CIE_standard_illuminant_A", + "CIE_illuminant_D_series", "sd_CIE_illuminant_D_series", + "msds_CIE_illuminant_D_series", "daylight_locus_function", ] @@ -129,12 +138,16 @@ def sd_CIE_standard_illuminant_A( {'method': 'Constant', 'left': None, 'right': None}) """ + wavelengths = as_float_array(shape.wavelengths) + + xp = array_namespace(wavelengths) + values = ( 100 - * (560 / shape.wavelengths) ** 5 + * (560 / wavelengths) ** 5 * ( - np.expm1((1.435 * 10**7) / (2848 * 560)) - / np.expm1((1.435 * 10**7) / (2848 * shape.wavelengths)) + xp.expm1((1.435 * 10**7) / (2848 * 560)) + / xp.expm1((1.435 * 10**7) / (2848 * wavelengths)) ) ) @@ -143,6 +156,99 @@ def sd_CIE_standard_illuminant_A( ) +def CIE_illuminant_D_series( + xy: ArrayLike, + M1_M2_rounding: bool = True, + shape: SpectralShape | None = None, +) -> NDArrayFloat: + """ + Return the spectral values of the *CIE Illuminant D Series* for the + specified *CIE xy* chromaticity coordinates of shape ``(N, 2)``. + + Array kernel underlying :func:`colour.sd_CIE_illuminant_D_series` and + :func:`colour.msds_CIE_illuminant_D_series`; computation dispatches + through the *Array API* and follows the input backend. + + Parameters + ---------- + xy + *CIE xy* chromaticity coordinates of shape ``(N, 2)``. + M1_M2_rounding + Whether to round :math:`M1` and :math:`M2` variables to 3 decimal + places in order to yield the internationally agreed values. + shape + Optional target spectral shape. When provided the natural + ``(300, 830, 10)`` basis is linearly interpolated to + ``shape.wavelengths`` along the wavelength axis. + + Returns + ------- + :class:`numpy.ndarray` or backend tensor + Spectral values of shape ``(n_wavelengths, N)``; the output stays + in the namespace and device of the input :math:`xy` array. + + Notes + ----- + - See :func:`colour.sd_CIE_illuminant_D_series` for the *CIE + 015:2004* :math:`xy` recommendation. + + References + ---------- + :cite:`CIETC1-482004`, :cite:`Wyszecki2000z` + + Examples + -------- + >>> import numpy as np + >>> from colour.utilities import numpy_print_options + >>> from colour.temperature import CCT_to_xy_CIE_D + >>> CCTs = np.array([5000.0, 6500.0]) * 1.4388 / 1.4380 + >>> xy = CCT_to_xy_CIE_D(CCTs) + >>> with numpy_print_options(suppress=True): + ... CIE_illuminant_D_series( # doctest: +ELLIPSIS + ... xy, shape=SpectralShape(400, 700, 100) + ... ) + array([[ 49.3081..., 82.7549...], + [ 95.7237..., 109.3545...], + [ 97.6878..., 90.0062...], + [ 91.6035..., 71.6091...]]) + """ + + xy = as_float_array(xy) + + xp = array_namespace(xy) + + x, y = xy[..., 0], xy[..., 1] + M = 0.0241 + 0.2562 * x - 0.7341 * y + M1 = (-1.3515 - 1.7703 * x + 5.9114 * y) / M + M2 = (0.0300 - 31.4424 * x + 30.0717 * y) / M + + if M1_M2_rounding: + M1 = xp_round(M1, decimals=3, xp=xp) + M2 = xp_round(M2, decimals=3, xp=xp) + + S0 = SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S0"] + S1 = SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S1"] + S2 = SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S2"] + + S0_values = xp_as_float_array(S0.values, xp=xp, like=xy) + S1_values = xp_as_float_array(S1.values, xp=xp, like=xy) + S2_values = xp_as_float_array(S2.values, xp=xp, like=xy) + + distribution = ( + S0_values[:, None] + + M1[None, :] * S1_values[:, None] + + M2[None, :] * S2_values[:, None] + ) + + if shape is None: + return distribution + + basis_wavelengths = xp_as_float_array(S0.wavelengths, xp=xp, like=M1) + target_wavelengths = xp_as_float_array(shape.wavelengths, xp=xp, like=M1) + + return LinearInterpolator(basis_wavelengths, distribution)(target_wavelengths) + + def sd_CIE_illuminant_D_series( xy: ArrayLike, M1_M2_rounding: bool = True, @@ -155,7 +261,7 @@ def sd_CIE_illuminant_D_series( Parameters ---------- xy - *CIE xy* chromaticity coordinates. + *CIE xy* chromaticity coordinates of shape ``(2,)``. M1_M2_rounding Whether to round :math:`M1` and :math:`M2` variables to 3 decimal places in order to yield the internationally agreed values. @@ -304,31 +410,99 @@ def sd_CIE_illuminant_D_series( xy = as_float_array(xy) - x, y = tsplit(xy) - - M = 0.0241 + 0.2562 * x - 0.7341 * y - M1 = (-1.3515 - 1.7703 * x + 5.9114 * y) / M - M2 = (0.0300 - 31.4424 * x + 30.0717 * y) / M + xp = array_namespace(xy) - if M1_M2_rounding: - M1 = np.around(M1, 3) - M2 = np.around(M2, 3) + # The single chromaticity is expanded to the kernel's batched ``(1, 2)`` + # form and squeezed back; ``shape`` is applied below via ``align`` rather + # than the kernel's interpolation-only path. + distribution = xp.squeeze( + CIE_illuminant_D_series(xy[None, :], M1_M2_rounding), axis=-1 + ) - S0 = SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S0"] - S1 = SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S1"] - S2 = SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S2"] + sd = SpectralDistribution( + distribution, + SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S0"].wavelengths, + name=f"CIE xy ({xy[0]}, {xy[1]}) - CIE Illuminant D Series", + interpolator=LinearInterpolator, + ) if shape is not None: - S0 = reshape_sd(S0, shape=shape, copy=False) - S1 = reshape_sd(S1, shape=shape, copy=False) - S2 = reshape_sd(S2, shape=shape, copy=False) + sd.align(shape) - distribution = S0.values + M1 * S1.values + M2 * S2.values + return sd - return SpectralDistribution( - distribution, - S0.wavelengths, - name=f"CIE xy ({xy[0]}, {xy[1]}) - CIE Illuminant D Series", + +def msds_CIE_illuminant_D_series( + xy: ArrayLike, + M1_M2_rounding: bool = True, + shape: SpectralShape | None = None, +) -> MultiSpectralDistributions: + """ + Return the multi-spectral distributions of the specified ``N`` *CIE + Illuminant D Series* using the specified *CIE xy* chromaticity + coordinates. + + Parameters + ---------- + xy + *CIE xy* chromaticity coordinates of shape ``(N, 2)``. + M1_M2_rounding + Whether to round :math:`M1` and :math:`M2` variables to 3 decimal + places in order to yield the internationally agreed values. + shape + Specifies the shape of the returned MultiSpectralDistributions. + Optional, default None. + + Returns + ------- + :class:`colour.MultiSpectralDistributions` + *CIE Illuminant D Series* multi-spectral distributions of shape + ``(n_wavelengths, N)``. + + Notes + ----- + - See :func:`colour.sd_CIE_illuminant_D_series` for the *CIE + 015:2004* :math:`xy` recommendation. + + References + ---------- + :cite:`CIETC1-482004`, :cite:`Wyszecki2000z` + + Examples + -------- + >>> import numpy as np + >>> from colour.utilities import numpy_print_options + >>> from colour.temperature import CCT_to_xy_CIE_D + >>> CCTs = np.array([5000.0, 6500.0]) * 1.4388 / 1.4380 + >>> xy = CCT_to_xy_CIE_D(CCTs) + >>> with numpy_print_options(suppress=True): + ... msds_CIE_illuminant_D_series( # doctest: +ELLIPSIS + ... xy, shape=SpectralShape(400, 700, 100) + ... ) + MultiSpectralDistributions([[400. , 49.3081, 82.7549], + [500. , 95.7237, 109.3545], + [600. , 97.6878, 90.0062], + [700. , 91.6035, 71.6091]], + [...], + LinearInterpolator, + {}, + Extrapolator, + {'method': 'Constant', 'left': None, 'right': None}) + """ + + xy = as_float_array(xy) + + xy_labels = as_ndarray(xy) + + return MultiSpectralDistributions( + CIE_illuminant_D_series(xy, M1_M2_rounding, shape), + SDS_BASIS_FUNCTIONS_CIE_ILLUMINANT_D_SERIES["S0"].wavelengths + if shape is None + else shape.wavelengths, + labels=[ + f"CIE xy ({xy_labels[i, 0]}, {xy_labels[i, 1]}) - CIE Illuminant D Series" + for i in range(xy_labels.shape[0]) + ], interpolator=LinearInterpolator, ) diff --git a/colour/colorimetry/lightness.py b/colour/colorimetry/lightness.py index 94808103d0..641064ec4c 100644 --- a/colour/colorimetry/lightness.py +++ b/colour/colorimetry/lightness.py @@ -64,8 +64,6 @@ import typing -import numpy as np - from colour.algebra import spow from colour.biochemistry import ( reaction_rate_MichaelisMenten_Abebe2017, @@ -84,6 +82,7 @@ ) from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, as_float_array, filter_kwargs, @@ -94,6 +93,7 @@ to_domain_100, usage_warning, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -205,7 +205,9 @@ def lightness_Wyszecki1963( Y = to_domain_100(Y) - if np.any(Y < 1) or np.any(Y > 98): + xp = array_namespace(Y) + + if xp.any(Y < 1) or xp.any(Y > 98): usage_warning( '"W*" Lightness computation is only applicable for ' '1% < "Y" < 98%, unpredictable results may occur!' @@ -265,11 +267,14 @@ def intermediate_lightness_function_CIE1976( """ Y = as_float_array(Y) - Y_n = as_float_array(Y_n) + + xp = array_namespace(Y, Y_n) + + Y_n = xp_as_float_array(Y_n, xp=xp, like=Y) Y_Y_n = Y / Y_n - f_Y_Y_n = np.where( + f_Y_Y_n = xp.where( Y_Y_n > (24 / 116) ** 3, spow(Y_Y_n, 1 / 3), (841 / 108) * Y_Y_n + 16 / 116, @@ -512,18 +517,21 @@ def lightness_Abebe2017( """ Y = as_float_array(Y) - Y_n = as_float_array(optional(Y_n, 100)) + + xp = array_namespace(Y) + + Y_n = xp_as_float_array(optional(Y_n, 100), xp=xp, like=Y) method = validate_method(method, ("Michaelis-Menten", "Stevens")) Y_Y_n = Y / Y_n if method == "stevens": - L = np.where( + L = xp.where( Y_n <= 100, 1.226 * spow(Y_Y_n, 0.266) - 0.226, 1.127 * spow(Y_Y_n, 0.230) - 0.127, ) else: - L = np.where( + L = xp.where( Y_n <= 100, reaction_rate_MichaelisMenten_Abebe2017( spow(Y_Y_n, 0.582), 1.448, 0.635, 0.813 @@ -665,7 +673,7 @@ def lightness( Y = Y / 100 # Abebe uses absolute luminance, scale inputs to cd/m² in scale 1. - if function in (lightness_Abebe2017,) and domain_range_1: + if function == lightness_Abebe2017 and domain_range_1: Y = Y * 100 if "Y_n" in kwargs: kwargs["Y_n"] = kwargs["Y_n"] * 100 @@ -673,7 +681,7 @@ def lightness( L = function(Y, **filter_kwargs(function, **kwargs)) # Scale Abebe output to [0, 100] for comparability (not in scale 1). - if function in (lightness_Abebe2017,) and not domain_range_1: + if function == lightness_Abebe2017 and not domain_range_1: return L * 100 return L diff --git a/colour/colorimetry/luminance.py b/colour/colorimetry/luminance.py index c241baddd1..d7d5990c4e 100644 --- a/colour/colorimetry/luminance.py +++ b/colour/colorimetry/luminance.py @@ -63,8 +63,6 @@ import typing -import numpy as np - from colour.algebra import spow from colour.biochemistry import ( substrate_concentration_MichaelisMenten_Abebe2017, @@ -84,6 +82,7 @@ ) from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, as_float_array, filter_kwargs, @@ -94,6 +93,7 @@ to_domain_10, to_domain_100, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -269,9 +269,12 @@ def intermediate_luminance_function_CIE1976( """ f_Y_Y_n = as_float_array(f_Y_Y_n) - Y_n = as_float_array(Y_n) - Y = np.where( + xp = array_namespace(f_Y_Y_n, Y_n) + + Y_n = xp_as_float_array(Y_n, xp=xp, like=f_Y_Y_n) + + Y = xp.where( f_Y_Y_n > 24 / 116, Y_n * f_Y_Y_n**3, Y_n * (f_Y_Y_n - 16 / 116) * (108 / 841), @@ -380,8 +383,11 @@ def luminance_Fairchild2010(L_hdr: Domain100, epsilon: ArrayLike = 1.836) -> Ran L_hdr = to_domain_100(L_hdr) - Y = np.exp( - np.log( + xp = array_namespace(L_hdr, epsilon) + + epsilon = xp_as_float_array(epsilon, xp=xp, like=L_hdr) + Y = xp.exp( + xp.log( substrate_concentration_MichaelisMenten_Michaelis1913( L_hdr - 0.02, 100, spow(0.184, epsilon) ) @@ -444,12 +450,16 @@ def luminance_Fairchild2011( """ L_hdr = to_domain_100(L_hdr) + + xp = array_namespace(L_hdr, epsilon) + + epsilon = xp_as_float_array(epsilon, xp=xp, like=L_hdr) method = validate_method(method, ("hdr-CIELAB", "hdr-IPT")) maximum_perception = 247 if method == "hdr-cielab" else 246 - Y = np.exp( - np.log( + Y = xp.exp( + xp.log( substrate_concentration_MichaelisMenten_Michaelis1913( L_hdr - 0.02, maximum_perception, spow(2, epsilon) ) @@ -520,17 +530,20 @@ def luminance_Abebe2017( """ L = as_float_array(L) - Y_n = as_float_array(optional(Y_n, 100)) + + xp = array_namespace(L) + + Y_n = xp_as_float_array(optional(Y_n, 100), xp=xp, like=L) method = validate_method(method, ("Michaelis-Menten", "Stevens")) if method == "stevens": - Y = np.where( + Y = xp.where( Y_n <= 100, spow((L + 0.226) / 1.226, 1 / 0.266), spow((L + 0.127) / 1.127, 1 / 0.230), ) else: - Y = np.where( + Y = xp.where( Y_n <= 100, spow( substrate_concentration_MichaelisMenten_Abebe2017( @@ -679,7 +692,7 @@ def luminance( LV = LV / 10 # Abebe expects L in [0, 1] and Y_n in cd/m². - if function in (luminance_Abebe2017,): + if function == luminance_Abebe2017: if domain_range_reference or domain_range_100: LV = LV / 100 if domain_range_1 and "Y_n" in kwargs: @@ -695,7 +708,7 @@ def luminance( Y_V = Y_V * 100 # Abebe outputs absolute cd/m², scale to [0, 1] in scale 1. - if function in (luminance_Abebe2017,) and domain_range_1: + if function == luminance_Abebe2017 and domain_range_1: Y_V = Y_V / 100 return Y_V diff --git a/colour/colorimetry/photometry.py b/colour/colorimetry/photometry.py index e6353c5746..abdf53fde7 100644 --- a/colour/colorimetry/photometry.py +++ b/colour/colorimetry/photometry.py @@ -15,11 +15,15 @@ from __future__ import annotations -import numpy as np - from colour.colorimetry import SDS_LEFS_PHOTOPIC, SpectralDistribution, reshape_sd from colour.constants import CONSTANT_K_M -from colour.utilities import as_float_scalar, optional +from colour.utilities import ( + array_namespace, + as_float_scalar, + optional, + xp_as_float_array, + xp_trapezoid, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -80,7 +84,12 @@ def luminous_flux( extrapolator_kwargs={"method": "Constant", "left": 0, "right": 0}, ) - flux = K_m * np.trapezoid(lef.values * sd.values, sd.wavelengths) + xp = array_namespace(sd.values) + + sd_values = xp_as_float_array(sd.values, xp=xp) + lef_values = xp_as_float_array(lef.values, xp=xp, like=sd_values) + + flux = K_m * xp_trapezoid(lef_values * sd_values, x=sd.wavelengths, xp=xp) return as_float_scalar(flux) @@ -131,9 +140,14 @@ def luminous_efficiency( extrapolator_kwargs={"method": "Constant", "left": 0, "right": 0}, ) - efficiency = np.trapezoid(lef.values * sd.values, sd.wavelengths) / np.trapezoid( - sd.values, sd.wavelengths - ) + xp = array_namespace(sd.values) + + sd_values = xp_as_float_array(sd.values, xp=xp) + lef_values = xp_as_float_array(lef.values, xp=xp, like=sd_values) + + efficiency = xp_trapezoid( + lef_values * sd_values, x=sd.wavelengths, xp=xp + ) / xp_trapezoid(sd_values, x=sd.wavelengths, xp=xp) return as_float_scalar(efficiency) diff --git a/colour/colorimetry/spectrum.py b/colour/colorimetry/spectrum.py index 72214b8f2a..99c28320df 100644 --- a/colour/colorimetry/spectrum.py +++ b/colour/colorimetry/spectrum.py @@ -62,21 +62,27 @@ from colour.hints import Any, TypeVar, cast from colour.utilities import ( CACHE_REGISTRY, + array_namespace, as_float_array, as_int, + as_ndarray, attest, filter_kwargs, - first_item, interval, is_caching_enabled, is_iterable, is_numeric, + is_numpy_namespace, is_pandas_installed, is_uniform, optional, runtime_warning, tstack, validate_method, + xp_as_float_array, + xp_isin, + xp_linspace, + xp_round, ) if typing.TYPE_CHECKING or is_pandas_installed(): @@ -99,6 +105,9 @@ "SPECTRAL_SHAPE_DEFAULT", "SpectralDistribution", "MultiSpectralDistributions", + "interpolate_signal", + "extrapolate_signal", + "trim_signal", "reshape_sd", "reshape_msds", "sds_and_msds_to_sds", @@ -289,7 +298,7 @@ def boundaries(self) -> tuple: def boundaries(self, value: ArrayLike) -> None: """Setter for the **self.boundaries** property.""" - value = np.asarray(value) + value = as_float_array(value) attest( value.size == 2, @@ -409,19 +418,26 @@ def __contains__(self, wavelength: ArrayLike) -> bool: False """ + wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) + decimals = np.finfo(cast("Any", DTYPE_FLOAT_DEFAULT)).precision return bool( - np.all( - np.isin( - np.around( - wavelength, # pyright: ignore - decimals, + xp.all( + xp_isin( + xp_round( + wavelength, + decimals=decimals, + xp=xp, ), - np.around( + xp_round( self.wavelengths, - decimals, + decimals=decimals, + xp=xp, ), + xp=xp, ) ) ) @@ -467,7 +483,9 @@ def __eq__(self, other: object) -> bool: """ if isinstance(other, SpectralShape): - return np.array_equal(self.wavelengths, other.wavelengths) + return self.wavelengths.shape == other.wavelengths.shape and bool( + np.all(self.wavelengths == other.wavelengths) + ) return False @@ -541,8 +559,13 @@ def range(self, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat: ) samples = as_int(round((interval + end - start) / interval)) - range_, interval_effective = np.linspace( - start, end, samples, retstep=True, dtype=dtype + range_, interval_effective = xp_linspace( + start, + end, + num=int(samples), + xp=np, + retstep=True, + dtype=dtype, ) _CACHE_SHAPE_RANGE[hash_key] = range_ @@ -561,6 +584,262 @@ def range(self, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat: """Default spectral shape according to *ASTM E308-15* practise shape.""" +def interpolate_signal( + signal: SpectralDistribution | MultiSpectralDistributions, + shape: SpectralShape, + interpolator: Type[ProtocolInterpolator] | None = None, + interpolator_kwargs: dict | None = None, +) -> SpectralDistribution | MultiSpectralDistributions: + """ + Interpolate the specified spectral distribution or multi-spectral + distributions in-place according to *CIE 167:2005* recommendation or + specified interpolation arguments. + + This is the shared implementation backing the + :meth:`colour.SpectralDistribution.interpolate` and + :meth:`colour.MultiSpectralDistributions.interpolate` methods; the values + are interpolated vectorised across the wavelength axis (and channels, for + multi-spectral distributions). + + Parameters + ---------- + signal + Spectral distribution or multi-spectral distributions to interpolate. + shape + Spectral shape used for interpolation. + interpolator + Interpolator class type to use as interpolating function. + interpolator_kwargs + Arguments to use when instantiating the interpolating function. + + Returns + ------- + :class:`colour.SpectralDistribution` or \ +:class:`colour.MultiSpectralDistributions` + Interpolated spectral distribution or multi-spectral distributions. + + References + ---------- + :cite:`CIETC1-382005e` + + Examples + -------- + >>> data = { + ... 500: 0.0651, + ... 520: 0.0705, + ... 540: 0.0772, + ... 560: 0.0870, + ... 580: 0.1128, + ... 600: 0.1360, + ... } + >>> sd = SpectralDistribution(data) + >>> interpolate_signal(sd, SpectralShape(500, 600, 1)).shape + SpectralShape(500.0, 600.0, 1.0) + """ + + shape_start, shape_end, shape_interval = as_float_array( + [signal.shape.start, signal.shape.end, signal.shape.interval] + ) + + shape = SpectralShape( + *[ + x[0] if x[0] is not None else x[1] + for x in zip( + (shape.start, shape.end, shape.interval), + (shape_start, shape_end, shape_interval), + strict=True, + ) + ] + ) + + shape.start = max([shape.start, shape_start]) + shape.end = min([shape.end, shape_end]) + + if interpolator is None: + if signal.interpolator not in (SpragueInterpolator, CubicSplineInterpolator): + interpolator = signal.interpolator + elif signal.is_uniform(): + interpolator = SpragueInterpolator + else: + interpolator = CubicSplineInterpolator + + if interpolator_kwargs is None: + if signal.interpolator not in (SpragueInterpolator, CubicSplineInterpolator): + interpolator_kwargs = signal.interpolator_kwargs + else: + interpolator_kwargs = {} + + signal_interpolator, signal.interpolator = signal.interpolator, interpolator + signal_interpolator_kwargs, signal.interpolator_kwargs = ( + signal.interpolator_kwargs, + interpolator_kwargs, + ) + + values = signal[shape.wavelengths] + + signal.wavelengths = shape.wavelengths + signal.values = values + + signal.interpolator = signal_interpolator + signal.interpolator_kwargs = signal_interpolator_kwargs + + return signal + + +def extrapolate_signal( + signal: SpectralDistribution | MultiSpectralDistributions, + shape: SpectralShape, + extrapolator: Type[ProtocolExtrapolator] | None = None, + extrapolator_kwargs: dict | None = None, +) -> SpectralDistribution | MultiSpectralDistributions: + """ + Extrapolate the specified spectral distribution or multi-spectral + distributions in-place according to *CIE 15:2004* and *CIE 167:2005* + recommendations or specified extrapolation arguments. + + This is the shared implementation backing the + :meth:`colour.SpectralDistribution.extrapolate` and + :meth:`colour.MultiSpectralDistributions.extrapolate` methods. + + Parameters + ---------- + signal + Spectral distribution or multi-spectral distributions to extrapolate. + shape + Spectral shape used for extrapolation. + extrapolator + Extrapolator class type to use as extrapolating function. + extrapolator_kwargs + Arguments to use when instantiating the extrapolating function. + + Returns + ------- + :class:`colour.SpectralDistribution` or \ +:class:`colour.MultiSpectralDistributions` + Extrapolated spectral distribution or multi-spectral distributions. + + References + ---------- + :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` + + Examples + -------- + >>> data = { + ... 500: 0.0651, + ... 520: 0.0705, + ... 540: 0.0772, + ... 560: 0.0870, + ... 580: 0.1128, + ... 600: 0.1360, + ... } + >>> sd = SpectralDistribution(data) + >>> extrapolate_signal(sd, SpectralShape(400, 700, 20)).shape + SpectralShape(400.0, 700.0, 20.0) + """ + + shape_start, shape_end, shape_interval = as_float_array( + [signal.shape.start, signal.shape.end, signal.shape.interval] + ) + + xp = array_namespace(shape_start) + + wavelengths = xp.concat( + [ + xp.arange(shape.start, shape_start, shape_interval), + xp.arange(shape_end, shape.end, shape_interval) + shape_interval, + ], + axis=0, + ) + + extrapolator = optional(extrapolator, Extrapolator) + extrapolator_kwargs = optional( + extrapolator_kwargs, + {"method": "Constant", "left": None, "right": None}, + ) + + signal_extrapolator = signal.extrapolator + signal_extrapolator_kwargs = signal.extrapolator_kwargs + + signal.extrapolator = extrapolator + signal.extrapolator_kwargs = extrapolator_kwargs + + # The following self-assignment is written as intended and triggers the + # extrapolation. + signal[wavelengths] = signal[wavelengths] + + signal.extrapolator = signal_extrapolator + signal.extrapolator_kwargs = signal_extrapolator_kwargs + + return signal + + +def trim_signal( + signal: SpectralDistribution | MultiSpectralDistributions, + shape: SpectralShape, +) -> SpectralDistribution | MultiSpectralDistributions: + """ + Trim the specified spectral distribution or multi-spectral distributions + wavelengths in-place to the specified spectral shape. + + This is the shared implementation backing the + :meth:`colour.SpectralDistribution.trim` and + :meth:`colour.MultiSpectralDistributions.trim` methods. + + Parameters + ---------- + signal + Spectral distribution or multi-spectral distributions to trim. + shape + Spectral shape used for trimming. + + Returns + ------- + :class:`colour.SpectralDistribution` or \ +:class:`colour.MultiSpectralDistributions` + Trimmed spectral distribution or multi-spectral distributions. + + Examples + -------- + >>> data = { + ... 500: 0.0651, + ... 520: 0.0705, + ... 540: 0.0772, + ... 560: 0.0870, + ... 580: 0.1128, + ... 600: 0.1360, + ... } + >>> sd = SpectralDistribution(data) + >>> trim_signal(sd, SpectralShape(520, 580, 20)).shape + SpectralShape(520.0, 580.0, 20.0) + """ + + start = max([shape.start, signal.shape.start]) + end = min([shape.end, signal.shape.end]) + + domain = as_float_array(signal.domain) + + xp = array_namespace(domain) + + indexes = xp.nonzero(xp.logical_and(domain >= start, domain <= end)) + + # Both arrays are gathered before either is assigned: setting + # ``wavelengths`` first would resize ``values`` to the trimmed domain + # length and corrupt it. ``signal.range`` is the array backing the + # ``values`` property. + wavelengths = signal.wavelengths[indexes] + values = signal.range[indexes] + + signal.wavelengths = wavelengths + signal.values = values + + if signal.shape.boundaries != shape.boundaries: + runtime_warning( + f'"{shape}" shape could not be honoured, using "{signal.shape}"!' + ) + + return signal + + class SpectralDistribution(Signal): """ Define the spectral distribution: the base object for spectral @@ -884,7 +1163,7 @@ def shape(self) -> SpectralShape: """ if self._shape is None: - wavelengths = self.wavelengths + wavelengths = as_ndarray(self.wavelengths) wavelengths_interval = interval(wavelengths) if wavelengths_interval.size != 1: runtime_warning( @@ -1200,61 +1479,7 @@ def interpolate( [600. 0.136 ...]] """ - shape_start, shape_end, shape_interval = as_float_array( - [ - self.shape.start, - self.shape.end, - self.shape.interval, - ] - ) - - shape = SpectralShape( - *[ - x[0] if x[0] is not None else x[1] - for x in zip( - (shape.start, shape.end, shape.interval), - (shape_start, shape_end, shape_interval), - strict=True, - ) - ] - ) - - shape.start = max([shape.start, shape_start]) - shape.end = min([shape.end, shape_end]) - - if interpolator is None: - if self.interpolator not in ( - SpragueInterpolator, - CubicSplineInterpolator, - ): - interpolator = self.interpolator - elif self.is_uniform(): - interpolator = SpragueInterpolator - else: - interpolator = CubicSplineInterpolator - - if interpolator_kwargs is None: - if self.interpolator not in ( - SpragueInterpolator, - CubicSplineInterpolator, - ): - interpolator_kwargs = self.interpolator_kwargs - else: - interpolator_kwargs = {} - - self_interpolator, self.interpolator = self.interpolator, interpolator - self_interpolator_kwargs, self.interpolator_kwargs = ( - self.interpolator_kwargs, - interpolator_kwargs, - ) - - values = self[shape.wavelengths] - - self.domain = shape.wavelengths - self.values = values - - self.interpolator = self_interpolator - self.interpolator_kwargs = self_interpolator_kwargs + interpolate_signal(self, shape, interpolator, interpolator_kwargs) return self @@ -1321,39 +1546,7 @@ def extrapolate( [700. 0.136 ]] """ - shape_start, shape_end, shape_interval = as_float_array( - [ - self.shape.start, - self.shape.end, - self.shape.interval, - ] - ) - - wavelengths = np.hstack( - [ - np.arange(shape.start, shape_start, shape_interval), - np.arange(shape_end, shape.end, shape_interval) + shape_interval, - ] - ) - - extrapolator = optional(extrapolator, Extrapolator) - extrapolator_kwargs = optional( - extrapolator_kwargs, - {"method": "Constant", "left": None, "right": None}, - ) - - self_extrapolator = self.extrapolator - self_extrapolator_kwargs = self.extrapolator_kwargs - - self.extrapolator = extrapolator - self.extrapolator_kwargs = extrapolator_kwargs - - # The following self-assignment is written as intended and triggers the - # extrapolation. - self[wavelengths] = self[wavelengths] - - self.extrapolator = self_extrapolator - self.extrapolator_kwargs = self_extrapolator_kwargs + extrapolate_signal(self, shape, extrapolator, extrapolator_kwargs) return self @@ -1595,21 +1788,7 @@ def trim(self, shape: SpectralShape) -> Self: [580. 0.1128 ...]] """ - start = max([shape.start, self.shape.start]) - end = min([shape.end, self.shape.end]) - - indexes = np.where(np.logical_and(self.domain >= start, self.domain <= end)) - - wavelengths = self.wavelengths[indexes] - values = self.values[indexes] - - self.wavelengths = wavelengths - self.values = values - - if self.shape.boundaries != shape.boundaries: - runtime_warning( - f'"{shape}" shape could not be honoured, using "{self.shape}"!' - ) + trim_signal(self, shape) return self @@ -1845,10 +2024,11 @@ def __init__( **kwargs: Any, ) -> None: domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain - signals = self.multi_signals_unpack_data(data, domain, labels) + domain_unpacked, range_unpacked, labels_unpacked = ( + self.multi_signals_unpack_data(data, domain, labels) + ) - domain = signals[next(iter(signals.keys()))].domain if signals else None - uniform = is_uniform(domain) if domain is not None and len(domain) > 0 else True + uniform = is_uniform(domain_unpacked) if len(domain_unpacked) > 0 else True # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations # defaults. @@ -1864,13 +2044,56 @@ def __init__( {"method": "Constant", "left": None, "right": None}, ) - super().__init__(signals, domain, signal_type=SpectralDistribution, **kwargs) + super().__init__( + range_unpacked, + domain_unpacked, + labels=labels_unpacked, + signal_type=SpectralDistribution, + **kwargs, + ) self._display_name: str = self.name self.display_name = kwargs.get("display_name", self._display_name) - self._display_labels: list = list(self.signals.keys()) + self._display_labels: list = list(self.labels) self.display_labels = kwargs.get("display_labels", self._display_labels) + self._shape: SpectralShape | None = None + + self.register_callback("_domain", "on_domain_changed", self._on_domain_changed) + + @staticmethod + def _on_domain_changed( + msds: MultiSpectralDistributions, name: str, value: NDArrayFloat + ) -> NDArrayFloat: + """ + Invalidate the cached spectral shape when the multi-spectral + distributions domain is modified. + + Mirrors :meth:`SpectralDistribution._on_domain_changed`: any + assignment to ``self._domain`` (the canonical 1-D domain owned by + :class:`MultiSignals`) fires the callback and clears the cached + :attr:`shape`. + + Parameters + ---------- + msds + Multi-spectral distributions instance whose domain has changed. + name + Name of the modified attribute (expected to be "_domain"). + value + New domain values that triggered the callback. + + Returns + ------- + :class:`numpy.ndarray` + The specified domain values, unchanged. + """ + + if name == "_domain": + msds._shape = None + + return value + @property def display_name(self) -> str: """ @@ -1948,8 +2171,21 @@ def display_labels(self, value: Sequence) -> None: ) self._display_labels = [str(label) for label in value] - for i, signal in enumerate(self.signals.values()): - cast("SpectralDistribution", signal).display_name = self._display_labels[i] + + @property + def signals(self) -> Mapping[str, SpectralDistribution]: # pyright: ignore + """ + Materialise per-column :class:`SpectralDistribution` views and + propagate ``display_name`` from :attr:`display_labels`. + """ + + # MSDS passes ``signal_type=SpectralDistribution`` so the parent + # builds :class:`SpectralDistribution` instances at runtime; the + # cast tightens the static type to match. + signals = cast("Mapping[str, SpectralDistribution]", super().signals) + for label, display_label in zip(signals, self._display_labels, strict=False): + signals[label].display_name = display_label + return signals @property def wavelengths(self) -> NDArrayFloat: @@ -2045,7 +2281,20 @@ def shape(self) -> SpectralShape: SpectralShape(500.0, 560.0, 1.0) """ - return first_item(self._signals.values()).shape + if self._shape is None: + wavelengths = as_ndarray(self.wavelengths) + wavelengths_interval = interval(wavelengths) + if wavelengths_interval.size != 1: + runtime_warning( + f'"{self.name}" multi-spectral distributions is not ' + "uniform, using minimum interval!" + ) + + self._shape = SpectralShape( + wavelengths[0], wavelengths[-1], min(wavelengths_interval) + ) + + return self._shape def interpolate( self, @@ -2266,10 +2515,7 @@ def interpolate( [560. 0.5945 ... 0.995 ... 0.0039 ...]] """ - for signal in self.signals.values(): - cast("SpectralDistribution", signal).interpolate( - shape, interpolator, interpolator_kwargs - ) + interpolate_signal(self, shape, interpolator, interpolator_kwargs) return self @@ -2352,10 +2598,7 @@ def extrapolate( [700. 0.5945 0.995 0.0039 ]] """ - for signal in self.signals.values(): - cast("SpectralDistribution", signal).extrapolate( - shape, extrapolator, extrapolator_kwargs - ) + extrapolate_signal(self, shape, extrapolator, extrapolator_kwargs) return self @@ -2500,14 +2743,8 @@ def align( [565. 0.5945 ... 0.995 ... 0.0039 ...]] """ - for signal in self.signals.values(): - cast("SpectralDistribution", signal).align( - shape, - interpolator, - interpolator_kwargs, - extrapolator, - extrapolator_kwargs, - ) + self.interpolate(shape, interpolator, interpolator_kwargs) + self.extrapolate(shape, extrapolator, extrapolator_kwargs) return self @@ -2585,8 +2822,7 @@ def trim(self, shape: SpectralShape) -> Self: [560. 0.5945 ... 0.995 ... 0.0039 ...]] """ - for signal in self.signals.values(): - cast("SpectralDistribution", signal).trim(shape) + trim_signal(self, shape) return self @@ -2634,8 +2870,10 @@ def normalise(self, factor: Real = 1) -> Self: [560. 1. ... 1. ... 0.0143382...]] """ - for signal in self.signals.values(): - cast("SpectralDistribution", signal).normalise(factor) + xp = array_namespace(self.values) + + with sdiv_mode(): + self *= sdiv(1, xp.max(self.values, axis=0)) * factor return self @@ -2688,10 +2926,7 @@ def to_sds(self) -> List[SpectralDistribution]: [560. 0.0039 ...]] """ - return [ - cast("SpectralDistribution", signal.copy()) - for signal in self.signals.values() - ] + return [signal.copy() for signal in self.signals.values()] _CACHE_RESHAPED_SDS_AND_MSDS: dict = CACHE_REGISTRY.register_cache( @@ -2757,17 +2992,30 @@ def reshape_sd( if isinstance(value, Mapping): kwargs_items[i] = (keyword, tuple(value.items())) - hash_key = hash((sd, shape, method, tuple(kwargs_items))) + hash_key = hash( + (sd, shape, method, tuple(kwargs_items), type(sd.values).__module__) + ) if is_caching_enabled() and hash_key in _CACHE_RESHAPED_SDS_AND_MSDS: reshaped_sd = _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] return reshaped_sd.copy() if copy else reshaped_sd + # ``Align`` / ``Interpolate`` / ``Extrapolate`` go through scipy and + # downgrade backend tensors to *NumPy*; re-promote only when that + # actually happened so ``Trim`` does not pay the setter cost. + R = as_float_array(sd.values) + xp = array_namespace(R) + function = getattr(sd, method) reshaped_sd = getattr(sd.copy(), method)(shape, **filter_kwargs(function, **kwargs)) + if not is_numpy_namespace(xp) and is_numpy_namespace( + array_namespace(reshaped_sd.values) + ): + reshaped_sd.values = xp_as_float_array(reshaped_sd.values, xp=xp, like=R) + _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] = reshaped_sd return reshaped_sd diff --git a/colour/colorimetry/tests/test_blackbody.py b/colour/colorimetry/tests/test_blackbody.py index b287518fcd..c7bbf92809 100644 --- a/colour/colorimetry/tests/test_blackbody.py +++ b/colour/colorimetry/tests/test_blackbody.py @@ -9,7 +9,11 @@ import pytest from colour.colorimetry import ( + MultiSpectralDistributions, + SpectralDistribution, SpectralShape, + msds_blackbody, + msds_rayleigh_jeans, planck_law, rayleigh_jeans_law, sd_blackbody, @@ -18,9 +22,15 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat + from colour.hints import NDArrayFloat, ModuleType -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -36,6 +46,7 @@ "DATA_RAYLEIGH_JEANS", "TestPlanckLaw", "TestSdBlackbody", + "TestMsdsBlackbody", "TestRayleighJeansLaw", "TestSdRayleighJeans", ] @@ -1206,49 +1217,49 @@ class TestPlanckLaw: tests methods. """ - def test_planck_law(self) -> None: + @pytest.mark.mps_xfail("MPS float32 epsilon on large-magnitude radiance") + def test_planck_law(self, xp: ModuleType) -> None: """Test :func:`colour.colorimetry.blackbody.planck_law` definition.""" - wavelengths = 2 ** np.arange(0, 16, 1) * 1e-9 + wavelengths = xp_as_array(2 ** np.arange(0, 16, 1) * 1e-9, xp=xp) for temperature, radiance in sorted(DATA_PLANCK_LAW.items()): - np.testing.assert_allclose( + xp_assert_close( planck_law(wavelengths, temperature), radiance, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_planck_law(self) -> None: + @pytest.mark.mps_xfail("MPS float32 epsilon on large-magnitude radiance") + def test_n_dimensional_planck_law(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.blackbody.planck_law` definition n-dimensional arrays support. """ wl = 500 * 1e-9 - p = planck_law(wl, 5500) + p = as_ndarray(planck_law(wl, 5500)) - wl = np.tile(wl, 6) - p = np.tile(p, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + p = xp.tile(xp_as_array(p, xp=xp), (6,)) + xp_assert_close( planck_law(wl, 5500), p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) # The "colour.colorimetry.planck_law" definition behaviour with # n-dimensional arrays is unusual. - # p = np.np.reshape(p, (2, 3)) - np.testing.assert_allclose( + xp_assert_close( planck_law(wl, 5500), p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) # The "colour.colorimetry.planck_law" definition behaviour with # n-dimensional arrays is unusual. - # p = np.reshape(p, (2, 3, 1)) - np.testing.assert_allclose( + xp_assert_close( planck_law(wl, 5500), p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1256,9 +1267,9 @@ def test_n_dimensional_planck_law(self) -> None: # The "colour.colorimetry.planck_law" definition behaviour with # n-dimensional arrays is unusual. - p = planck_law(500 * 1e-9, [5000, 5500, 6000]) - p = np.tile(p, (6, 1)) - np.testing.assert_allclose( + p = as_ndarray(planck_law(500 * 1e-9, [5000, 5500, 6000])) + p = xp.tile(xp_as_array(p, xp=xp), (6, 1)) + xp_assert_close( planck_law(wl, [5000, 5500, 6000]), p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1271,7 +1282,8 @@ def test_raise_exception_planck_law(self) -> None: """ for wavelength in [-1.0, 0.0, -np.inf, np.nan]: - pytest.raises(AssertionError, planck_law, wavelength, 5500) + with pytest.raises(AssertionError): + planck_law(wavelength, 5500) @ignore_numpy_errors def test_nan_planck_law(self) -> None: @@ -1295,65 +1307,90 @@ class TestSdBlackbody: def test_sd_blackbody(self) -> None: """Test :func:`colour.colorimetry.blackbody.sd_blackbody` definition.""" - np.testing.assert_allclose( - sd_blackbody(5000, SpectralShape(360, 830, 1)).values, + sd = sd_blackbody(5000, SpectralShape(360, 830, 1)) + assert isinstance(sd, SpectralDistribution) + xp_assert_close( + sd.values, DATA_BLACKBODY, atol=TOLERANCE_ABSOLUTE_TESTS, ) +class TestMsdsBlackbody: + """ + Define :func:`colour.colorimetry.blackbody.msds_blackbody` definition + unit tests methods. + """ + + def test_msds_blackbody(self) -> None: + """ + Test :func:`colour.colorimetry.blackbody.msds_blackbody` definition. + """ + + temperatures = np.array([3000.0, 5000.0, 7000.0]) + msds = msds_blackbody(temperatures, SpectralShape(360, 830, 1)) + assert isinstance(msds, MultiSpectralDistributions) + assert msds.values.shape == (471, 3) + for i, T in enumerate(temperatures): + xp_assert_close( + msds.values[:, i], + sd_blackbody(T, SpectralShape(360, 830, 1)).values, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + class TestRayleighJeansLaw: """ Define :func:`colour.colorimetry.blackbody.rayleigh_jeans_law` definition unit tests methods. """ - def test_rayleigh_jeans_law(self) -> None: + @pytest.mark.mps_xfail("MPS float32 epsilon on large-magnitude radiance") + def test_rayleigh_jeans_law(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.blackbody.rayleigh_jeans_law` definition. """ - wavelengths = 2 ** np.arange(0, 16, 1) * 1e-9 + wavelengths = xp_as_array(2 ** np.arange(0, 16, 1) * 1e-9, xp=xp) for temperature, radiance in sorted(DATA_RAYLEIGH_JEANS_LAW.items()): - np.testing.assert_allclose( + xp_assert_close( rayleigh_jeans_law(wavelengths, temperature), radiance, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_rayleigh_jeans_law(self) -> None: + @pytest.mark.mps_xfail("MPS float32 epsilon on large-magnitude radiance") + def test_n_dimensional_rayleigh_jeans_law(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.blackbody.rayleigh_jeans_law` definition n-dimensional arrays support. """ wl = 500 * 1e-9 - p = rayleigh_jeans_law(wl, 5500) + p = as_ndarray(rayleigh_jeans_law(wl, 5500)) - wl = np.tile(wl, 6) - p = np.tile(p, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + p = xp.tile(xp_as_array(p, xp=xp), (6,)) + xp_assert_close( rayleigh_jeans_law(wl, 5500), p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) # The "colour.colorimetry.rayleigh_jeans_law" definition behaviour with # n-dimensional arrays is unusual. - # p = np.np.reshape(p, (2, 3)) - np.testing.assert_allclose( + xp_assert_close( rayleigh_jeans_law(wl, 5500), p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) # The "colour.colorimetry.rayleigh_jeans_law" definition behaviour with # n-dimensional arrays is unusual. - # p = np.reshape(p, (2, 3, 1)) - np.testing.assert_allclose( + xp_assert_close( rayleigh_jeans_law(wl, 5500), p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1361,9 +1398,9 @@ def test_n_dimensional_rayleigh_jeans_law(self) -> None: # The "colour.colorimetry.rayleigh_jeans_law" definition behaviour with # n-dimensional arrays is unusual. - p = rayleigh_jeans_law(500 * 1e-9, [5000, 5500, 6000]) - p = np.tile(p, (6, 1)) - np.testing.assert_allclose( + p = as_ndarray(rayleigh_jeans_law(500 * 1e-9, [5000, 5500, 6000])) + p = xp.tile(xp_as_array(p, xp=xp), (6, 1)) + xp_assert_close( rayleigh_jeans_law(wl, [5000, 5500, 6000]), p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1393,8 +1430,32 @@ def test_sd_rayleigh_jeans(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( sd_rayleigh_jeans(5000, SpectralShape(360, 830, 1)).values, DATA_RAYLEIGH_JEANS, atol=TOLERANCE_ABSOLUTE_TESTS, ) + + +class TestMsdsRayleighJeans: + """ + Define :func:`colour.colorimetry.blackbody.msds_rayleigh_jeans` + definition unit tests methods. + """ + + def test_msds_rayleigh_jeans(self) -> None: + """ + Test :func:`colour.colorimetry.blackbody.msds_rayleigh_jeans` + definition. + """ + + temperatures = np.array([3000.0, 5000.0, 7000.0]) + msds = msds_rayleigh_jeans(temperatures, SpectralShape(360, 830, 1)) + assert isinstance(msds, MultiSpectralDistributions) + assert msds.values.shape == (471, 3) + for i, T in enumerate(temperatures): + xp_assert_close( + msds.values[:, i], + sd_rayleigh_jeans(T, SpectralShape(360, 830, 1)).values, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) diff --git a/colour/colorimetry/tests/test_correction.py b/colour/colorimetry/tests/test_correction.py index c712025f44..cc2a728e48 100644 --- a/colour/colorimetry/tests/test_correction.py +++ b/colour/colorimetry/tests/test_correction.py @@ -2,10 +2,9 @@ from __future__ import annotations -import numpy as np - from colour.colorimetry import SpectralDistribution, bandpass_correction_Stearns1988 from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -81,7 +80,7 @@ def test_bandpass_correction_Stearns1988(self) -> None: ) ) - np.testing.assert_allclose( + xp_assert_close( bandpass_correction_Stearns1988(sd).values, DATA_BANDPASS_CORRECTED, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/tests/test_dominant.py b/colour/colorimetry/tests/test_dominant.py index e777a99e35..ae4f1c7583 100644 --- a/colour/colorimetry/tests/test_dominant.py +++ b/colour/colorimetry/tests/test_dominant.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -17,7 +22,14 @@ from colour.colorimetry.dominant import closest_spectral_locus_wavelength from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import XYZ_to_xy -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -48,7 +60,7 @@ def setup_method(self) -> None: self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] - def test_closest_spectral_locus_wavelength(self) -> None: + def test_closest_spectral_locus_wavelength(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.\ closest_spectral_locus_wavelength` definition. @@ -57,28 +69,30 @@ def test_closest_spectral_locus_wavelength(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, self._xy_s) assert i_wl == np.array(256) - np.testing.assert_allclose( + xp_assert_close( xy_wl, - np.array([0.68354746, 0.31628409]), + [0.68354746, 0.31628409], atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.array([0.37605506, 0.24452225]) + xy = xp_as_array([0.37605506, 0.24452225], xp=xp) i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, self._xy_s) assert i_wl == np.array(248) - np.testing.assert_allclose( + xp_assert_close( xy_wl, - np.array([0.45723147, 0.13628148]), + [0.45723147, 0.13628148], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_closest_spectral_locus_wavelength(self) -> None: + def test_n_dimensional_closest_spectral_locus_wavelength( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.dominant.\ closest_spectral_locus_wavelength` definition n-dimensional arrays support. @@ -87,28 +101,31 @@ def test_n_dimensional_closest_spectral_locus_wavelength(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, self._xy_s) - i_wl_r, xy_wl_r = np.array(256), np.array([0.68354746, 0.31628409]) - np.testing.assert_allclose(i_wl, i_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + i_wl_r, xy_wl_r = ( + xp_as_array(256, xp=xp), + xp_as_array([0.68354746, 0.31628409], xp=xp), + ) + xp_assert_close(i_wl, i_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.tile(xy, (6, 1)) - xy_n = np.tile(xy_n, (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xy_n = xp.tile(xp_as_array(xy_n, xp=xp), (6, 1)) i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, self._xy_s) - i_wl_r = np.tile(i_wl_r, 6) - xy_wl_r = np.tile(xy_wl_r, (6, 1)) - np.testing.assert_allclose(i_wl, i_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + i_wl_r = xp.tile(xp_as_array(i_wl_r, xp=xp), (6,)) + xy_wl_r = xp.tile(xp_as_array(xy_wl_r, xp=xp), (6, 1)) + xp_assert_close(i_wl, i_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - xy_n = np.reshape(xy_n, (2, 3, 2)) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xy_n = xp_reshape(xp_as_array(xy_n, xp=xp), (2, 3, 2), xp=xp) i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, self._xy_s) - i_wl_r = np.reshape(i_wl_r, (2, 3)) - xy_wl_r = np.reshape(xy_wl_r, (2, 3, 2)) - np.testing.assert_allclose(i_wl, i_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + i_wl_r = xp_reshape(xp_as_array(i_wl_r, xp=xp), (2, 3), xp=xp) + xy_wl_r = xp_reshape(xp_as_array(xy_wl_r, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(i_wl, i_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_closest_spectral_locus_wavelength(self) -> None: @@ -137,7 +154,7 @@ def setup_method(self) -> None: self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] - def test_dominant_wavelength(self) -> None: + def test_dominant_wavelength(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.dominant_wavelength` definition. @@ -146,38 +163,38 @@ def test_dominant_wavelength(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n) assert wl == np.array(616.0) - np.testing.assert_allclose( + xp_assert_close( xy_wl, - np.array([0.68354746, 0.31628409]), + [0.68354746, 0.31628409], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_cwl, - np.array([0.68354746, 0.31628409]), + [0.68354746, 0.31628409], atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.array([0.37605506, 0.24452225]) + xy = xp_as_array([0.37605506, 0.24452225], xp=xp) i_wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n) assert i_wl == np.array(-509.0) - np.testing.assert_allclose( + xp_assert_close( xy_wl, - np.array([0.45723147, 0.13628148]), + [0.45723147, 0.13628148], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_cwl, - np.array([0.01040962, 0.73207453]), + [0.01040962, 0.73207453], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_dominant_wavelength(self) -> None: + def test_n_dimensional_dominant_wavelength(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.dominant_wavelength` definition n-dimensional arrays support. @@ -186,7 +203,7 @@ def test_n_dimensional_dominant_wavelength(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n) wl_r, xy_wl_r, xy_cwl_r = ( @@ -194,29 +211,29 @@ def test_n_dimensional_dominant_wavelength(self) -> None: np.array([0.68354746, 0.31628409]), np.array([0.68354746, 0.31628409]), ) - np.testing.assert_allclose(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.tile(xy, (6, 1)) - xy_n = np.tile(xy_n, (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xy_n = xp.tile(xp_as_array(xy_n, xp=xp), (6, 1)) wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n) - wl_r = np.tile(wl_r, 6) - xy_wl_r = np.tile(xy_wl_r, (6, 1)) - xy_cwl_r = np.tile(xy_cwl_r, (6, 1)) - np.testing.assert_allclose(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - - xy = np.reshape(xy, (2, 3, 2)) - xy_n = np.reshape(xy_n, (2, 3, 2)) + wl_r = xp.tile(xp_as_array(wl_r, xp=xp), (6,)) + xy_wl_r = xp.tile(xp_as_array(xy_wl_r, xp=xp), (6, 1)) + xy_cwl_r = xp.tile(xp_as_array(xy_cwl_r, xp=xp), (6, 1)) + xp_assert_close(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xy_n = xp_reshape(xp_as_array(xy_n, xp=xp), (2, 3, 2), xp=xp) wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n) - wl_r = np.reshape(wl_r, (2, 3)) - xy_wl_r = np.reshape(xy_wl_r, (2, 3, 2)) - xy_cwl_r = np.reshape(xy_cwl_r, (2, 3, 2)) - np.testing.assert_allclose(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + wl_r = xp_reshape(xp_as_array(wl_r, xp=xp), (2, 3), xp=xp) + xy_wl_r = xp_reshape(xp_as_array(xy_wl_r, xp=xp), (2, 3, 2), xp=xp) + xy_cwl_r = xp_reshape(xp_as_array(xy_cwl_r, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_dominant_wavelength(self) -> None: @@ -245,7 +262,7 @@ def setup_method(self) -> None: self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] - def test_complementary_wavelength(self) -> None: + def test_complementary_wavelength(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.complementary_wavelength` definition. @@ -254,38 +271,38 @@ def test_complementary_wavelength(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 wl, xy_wl, xy_cwl = complementary_wavelength(xy, xy_n) assert wl == np.array(492.0) - np.testing.assert_allclose( + xp_assert_close( xy_wl, - np.array([0.03647950, 0.33847127]), + [0.03647950, 0.33847127], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_cwl, - np.array([0.03647950, 0.33847127]), + [0.03647950, 0.33847127], atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.array([0.37605506, 0.24452225]) + xy = xp_as_array([0.37605506, 0.24452225], xp=xp) i_wl, xy_wl, xy_cwl = complementary_wavelength(xy, xy_n) assert i_wl == np.array(509.0) - np.testing.assert_allclose( + xp_assert_close( xy_wl, - np.array([0.01040962, 0.73207453]), + [0.01040962, 0.73207453], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_cwl, - np.array([0.01040962, 0.73207453]), + [0.01040962, 0.73207453], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_complementary_wavelength(self) -> None: + def test_n_dimensional_complementary_wavelength(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.complementary_wavelength` definition n-dimensional arrays support. @@ -294,7 +311,7 @@ def test_n_dimensional_complementary_wavelength(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 wl, xy_wl, xy_cwl = complementary_wavelength(xy, xy_n) wl_r, xy_wl_r, xy_cwl_r = ( @@ -302,29 +319,29 @@ def test_n_dimensional_complementary_wavelength(self) -> None: np.array([0.03647950, 0.33847127]), np.array([0.03647950, 0.33847127]), ) - np.testing.assert_allclose(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.tile(xy, (6, 1)) - xy_n = np.tile(xy_n, (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xy_n = xp.tile(xp_as_array(xy_n, xp=xp), (6, 1)) wl, xy_wl, xy_cwl = complementary_wavelength(xy, xy_n) - wl_r = np.tile(wl_r, 6) - xy_wl_r = np.tile(xy_wl_r, (6, 1)) - xy_cwl_r = np.tile(xy_cwl_r, (6, 1)) - np.testing.assert_allclose(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - - xy = np.reshape(xy, (2, 3, 2)) - xy_n = np.reshape(xy_n, (2, 3, 2)) + wl_r = xp.tile(xp_as_array(wl_r, xp=xp), (6,)) + xy_wl_r = xp.tile(xp_as_array(xy_wl_r, xp=xp), (6, 1)) + xy_cwl_r = xp.tile(xp_as_array(xy_cwl_r, xp=xp), (6, 1)) + xp_assert_close(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xy_n = xp_reshape(xp_as_array(xy_n, xp=xp), (2, 3, 2), xp=xp) wl, xy_wl, xy_cwl = complementary_wavelength(xy, xy_n) - wl_r = np.reshape(wl_r, (2, 3)) - xy_wl_r = np.reshape(xy_wl_r, (2, 3, 2)) - xy_cwl_r = np.reshape(xy_cwl_r, (2, 3, 2)) - np.testing.assert_allclose(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + wl_r = xp_reshape(xp_as_array(wl_r, xp=xp), (2, 3), xp=xp) + xy_wl_r = xp_reshape(xp_as_array(xy_wl_r, xp=xp), (2, 3, 2), xp=xp) + xy_cwl_r = xp_reshape(xp_as_array(xy_cwl_r, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(wl, wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_wl, xy_wl_r, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_cwl, xy_cwl_r, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_complementary_wavelength(self) -> None: @@ -353,29 +370,29 @@ def setup_method(self) -> None: self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] - def test_excitation_purity(self) -> None: + def test_excitation_purity(self, xp: ModuleType) -> None: """Test :func:`colour.colorimetry.dominant.excitation_purity` definition.""" if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 - np.testing.assert_allclose( + xp_assert_close( excitation_purity(xy, xy_n), 0.622885671878446, atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.array([0.37605506, 0.24452225]) - np.testing.assert_allclose( + xy = xp_as_array([0.37605506, 0.24452225], xp=xp) + xp_assert_close( excitation_purity(xy, xy_n), 0.438347859215887, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_excitation_purity(self) -> None: + def test_n_dimensional_excitation_purity(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.excitation_purity` definition n-dimensional arrays support. @@ -384,23 +401,19 @@ def test_n_dimensional_excitation_purity(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 - P_e = excitation_purity(xy, xy_n) + P_e = as_ndarray(excitation_purity(xy, xy_n)) - xy = np.tile(xy, (6, 1)) - xy_n = np.tile(xy_n, (6, 1)) - P_e = np.tile(P_e, 6) - np.testing.assert_allclose( - excitation_purity(xy, xy_n), P_e, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xy_n = xp.tile(xp_as_array(xy_n, xp=xp), (6, 1)) + P_e = xp.tile(xp_as_array(P_e, xp=xp), (6,)) + xp_assert_close(excitation_purity(xy, xy_n), P_e, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - xy_n = np.reshape(xy_n, (2, 3, 2)) - P_e = np.reshape(P_e, (2, 3)) - np.testing.assert_allclose( - excitation_purity(xy, xy_n), P_e, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xy_n = xp_reshape(xp_as_array(xy_n, xp=xp), (2, 3, 2), xp=xp) + P_e = xp_reshape(xp_as_array(P_e, xp=xp), (2, 3), xp=xp) + xp_assert_close(excitation_purity(xy, xy_n), P_e, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_excitation_purity(self) -> None: @@ -429,7 +442,7 @@ def setup_method(self) -> None: self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] - def test_colorimetric_purity(self) -> None: + def test_colorimetric_purity(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.colorimetric_purity` definition. @@ -438,23 +451,23 @@ def test_colorimetric_purity(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 - np.testing.assert_allclose( + xp_assert_close( colorimetric_purity(xy, xy_n), 0.613582813175483, atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.array([0.37605506, 0.24452225]) - np.testing.assert_allclose( + xy = xp_as_array([0.37605506, 0.24452225], xp=xp) + xp_assert_close( colorimetric_purity(xy, xy_n), 0.244307811178847, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_colorimetric_purity(self) -> None: + def test_n_dimensional_colorimetric_purity(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.dominant.colorimetric_purity` definition n-dimensional arrays support. @@ -463,22 +476,26 @@ def test_n_dimensional_colorimetric_purity(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.54369557, 0.32107944]) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) xy_n = self._xy_D65 - P_e = colorimetric_purity(xy, xy_n) + P_e = as_ndarray(colorimetric_purity(xy, xy_n)) - xy = np.tile(xy, (6, 1)) - xy_n = np.tile(xy_n, (6, 1)) - P_e = np.tile(P_e, 6) - np.testing.assert_allclose( - colorimetric_purity(xy, xy_n), P_e, atol=TOLERANCE_ABSOLUTE_TESTS + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xy_n = xp.tile(xp_as_array(xy_n, xp=xp), (6, 1)) + P_e = xp.tile(xp_as_array(P_e, xp=xp), (6,)) + xp_assert_close( + colorimetric_purity(xy, xy_n), + P_e, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.reshape(xy, (2, 3, 2)) - xy_n = np.reshape(xy_n, (2, 3, 2)) - P_e = np.reshape(P_e, (2, 3)) - np.testing.assert_allclose( - colorimetric_purity(xy, xy_n), P_e, atol=TOLERANCE_ABSOLUTE_TESTS + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xy_n = xp_reshape(xp_as_array(xy_n, xp=xp), (2, 3, 2), xp=xp) + P_e = xp_reshape(xp_as_array(P_e, xp=xp), (2, 3), xp=xp) + xp_assert_close( + colorimetric_purity(xy, xy_n), + P_e, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors diff --git a/colour/colorimetry/tests/test_generation.py b/colour/colorimetry/tests/test_generation.py index 842cbab6b0..af5d9786d3 100644 --- a/colour/colorimetry/tests/test_generation.py +++ b/colour/colorimetry/tests/test_generation.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.colorimetry.generation import ( @@ -21,6 +26,7 @@ sd_zeros, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import xp_as_array, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -58,11 +64,11 @@ def test_sd_constant(self) -> None: sd = sd_constant(np.pi) - np.testing.assert_allclose(sd[360], np.pi, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(float(sd[360]), np.pi, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[555], np.pi, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(float(sd[555]), np.pi, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[780], np.pi, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(float(sd[780]), np.pi, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdZeros: @@ -79,11 +85,11 @@ def test_sd_zeros(self) -> None: sd = sd_zeros() - assert sd[360] == 0 + assert float(sd[360]) == 0 - assert sd[555] == 0 + assert float(sd[555]) == 0 - assert sd[780] == 0 + assert float(sd[780]) == 0 class TestSdOnes: @@ -97,11 +103,11 @@ def test_sd_ones(self) -> None: sd = sd_ones() - assert sd[360] == 1 + assert float(sd[360]) == 1 - assert sd[555] == 1 + assert float(sd[555]) == 1 - assert sd[780] == 1 + assert float(sd[780]) == 1 class TestMsdsConstant: @@ -115,21 +121,21 @@ def test_msds_constant(self) -> None: msds = msds_constant(np.pi, labels=["a", "b", "c"]) - np.testing.assert_allclose( + xp_assert_close( msds[360], - np.array([np.pi, np.pi, np.pi]), + [np.pi, np.pi, np.pi], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds[555], - np.array([np.pi, np.pi, np.pi]), + [np.pi, np.pi, np.pi], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds[780], - np.array([np.pi, np.pi, np.pi]), + [np.pi, np.pi, np.pi], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -148,11 +154,11 @@ def test_msds_zeros(self) -> None: msds = msds_zeros(labels=["a", "b", "c"]) - np.testing.assert_equal(msds[360], np.array([0, 0, 0])) + xp_assert_equal(msds[360], [0, 0, 0]) - np.testing.assert_equal(msds[555], np.array([0, 0, 0])) + xp_assert_equal(msds[555], [0, 0, 0]) - np.testing.assert_equal(msds[780], np.array([0, 0, 0])) + xp_assert_equal(msds[780], [0, 0, 0]) class TestMsdsOnes: @@ -166,11 +172,11 @@ def test_msds_ones(self) -> None: msds = msds_ones(labels=["a", "b", "c"]) - np.testing.assert_equal(msds[360], np.array([1, 1, 1])) + xp_assert_equal(msds[360], [1, 1, 1]) - np.testing.assert_equal(msds[555], np.array([1, 1, 1])) + xp_assert_equal(msds[555], [1, 1, 1]) - np.testing.assert_equal(msds[780], np.array([1, 1, 1])) + xp_assert_equal(msds[780], [1, 1, 1]) class TestSdGaussianNormal: @@ -187,15 +193,11 @@ def test_sd_gaussian_normal(self) -> None: sd = sd_gaussian_normal(555, 25) - np.testing.assert_allclose( - sd[530], 0.606530659712633, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[530], 0.606530659712633, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[580], 0.606530659712633, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[580], 0.606530659712633, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdGaussianFwhm: @@ -211,15 +213,13 @@ def test_sd_gaussian_fwhm(self) -> None: sd = sd_gaussian_fwhm(555, 25) - np.testing.assert_allclose(sd[530], 0.0625, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[530], 0.0625, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[580], 0.062499999999999, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[580], 0.062499999999999, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[555 - 25 / 2], 0.5, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[555 - 25 / 2], 0.5, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdGaussianSuperClamped: @@ -236,23 +236,23 @@ def test_sd_gaussian_super_clamped(self) -> None: # Test without clamping (symmetric) sd = sd_gaussian_super_clamped(555, 25, clamp="none") - np.testing.assert_allclose(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) # Test right clamping sd = sd_gaussian_super_clamped(600, 50, clamp="right") - np.testing.assert_allclose(sd[600], 1, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[700], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[600], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[700], 1, atol=TOLERANCE_ABSOLUTE_TESTS) assert sd[500] < 1 # Test left clamping sd = sd_gaussian_super_clamped(450, 40, clamp="left") - np.testing.assert_allclose(sd[450], 1, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[400], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[450], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[400], 1, atol=TOLERANCE_ABSOLUTE_TESTS) assert sd[500] < 1 # Test super-Gaussian exponent (flatter peak) sd = sd_gaussian_super_clamped(555, 25, exponent=4.0) - np.testing.assert_allclose(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) # With higher exponent, the falloff is steeper but the peak is flatter @@ -267,16 +267,14 @@ def test_sd_gaussian(self) -> None: # Test default method (Normal) sd = sd_gaussian(555, 25) - np.testing.assert_allclose( - sd[530], 0.606530659712633, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[530], 0.606530659712633, atol=TOLERANCE_ABSOLUTE_TESTS) sd = sd_gaussian(555, 25, method="FWHM") - np.testing.assert_allclose(sd[530], 0.0625, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[530], 0.0625, atol=TOLERANCE_ABSOLUTE_TESTS) sd = sd_gaussian(600, 50, method="Super-Gaussian Clamped", clamp="right") - np.testing.assert_allclose(sd[600], 1, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[700], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[600], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[700], 1, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdSingleLedOhno2005: @@ -293,15 +291,11 @@ def test_sd_single_led_Ohno2005(self) -> None: sd = sd_single_led_Ohno2005(555, 25) - np.testing.assert_allclose( - sd[530], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[530], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd[555], 1, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[580], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[580], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdSingleLed: @@ -315,15 +309,11 @@ def test_sd_single_led(self) -> None: # Test default method (Ohno 2005) sd = sd_single_led(555, half_spectral_width=25) - np.testing.assert_allclose( - sd[530], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[530], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS) # Test explicit Ohno 2005 method sd = sd_single_led(555, method="Ohno 2005", half_spectral_width=25) - np.testing.assert_allclose( - sd[530], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[530], 0.127118445056538, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdMultiLedsOhno2005: @@ -332,46 +322,34 @@ class TestSdMultiLedsOhno2005: definition unit tests methods. """ - def test_sd_multi_leds_Ohno2005(self) -> None: + def test_sd_multi_leds_Ohno2005(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.generation.sd_multi_leds_Ohno2005` definition. """ sd = sd_multi_leds_Ohno2005( - np.array([457, 530, 615]), - np.array([20, 30, 20]), - np.array([0.731, 1.000, 1.660]), + xp_as_array([457, 530, 615], xp=xp), + xp_as_array([20, 30, 20], xp=xp), + xp_as_array([0.731, 1.000, 1.660], xp=xp), ) - np.testing.assert_allclose( - sd[500], 0.129513248576116, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[500], 0.129513248576116, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[570], 0.059932156222703, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[570], 0.059932156222703, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[640], 0.116433257970624, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[640], 0.116433257970624, atol=TOLERANCE_ABSOLUTE_TESTS) sd = sd_multi_leds_Ohno2005( - np.array([457, 530, 615]), - np.array([20, 30, 20]), + xp_as_array([457, 530, 615], xp=xp), + xp_as_array([20, 30, 20], xp=xp), ) - np.testing.assert_allclose( - sd[500], 0.130394510062799, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[500], 0.130394510062799, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[570], 0.058539618824187, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[570], 0.058539618824187, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose( - sd[640], 0.070140708922879, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(sd[640], 0.070140708922879, atol=TOLERANCE_ABSOLUTE_TESTS) class TestSdMultiLeds: @@ -380,26 +358,22 @@ class TestSdMultiLeds: tests methods. """ - def test_sd_multi_leds(self) -> None: + def test_sd_multi_leds(self, xp: ModuleType) -> None: """Test :func:`colour.colorimetry.generation.sd_multi_leds` definition.""" # Test default method (Ohno 2005) sd = sd_multi_leds( - np.array([457, 530, 615]), - half_spectral_widths=np.array([20, 30, 20]), - peak_power_ratios=np.array([0.731, 1.000, 1.660]), - ) - np.testing.assert_allclose( - sd[500], 0.129513248576116, atol=TOLERANCE_ABSOLUTE_TESTS + xp_as_array([457, 530, 615], xp=xp), + half_spectral_widths=xp_as_array([20, 30, 20], xp=xp), + peak_power_ratios=xp_as_array([0.731, 1.000, 1.660], xp=xp), ) + xp_assert_close(sd[500], 0.129513248576116, atol=TOLERANCE_ABSOLUTE_TESTS) # Test explicit Ohno 2005 method sd = sd_multi_leds( - np.array([457, 530, 615]), + xp_as_array([457, 530, 615], xp=xp), method="Ohno 2005", - half_spectral_widths=np.array([20, 30, 20]), - peak_power_ratios=np.array([0.731, 1.000, 1.660]), - ) - np.testing.assert_allclose( - sd[500], 0.129513248576116, atol=TOLERANCE_ABSOLUTE_TESTS + half_spectral_widths=xp_as_array([20, 30, 20], xp=xp), + peak_power_ratios=xp_as_array([0.731, 1.000, 1.660], xp=xp), ) + xp_assert_close(sd[500], 0.129513248576116, atol=TOLERANCE_ABSOLUTE_TESTS) diff --git a/colour/colorimetry/tests/test_illuminants.py b/colour/colorimetry/tests/test_illuminants.py index 1124cd44bc..cda87bff67 100644 --- a/colour/colorimetry/tests/test_illuminants.py +++ b/colour/colorimetry/tests/test_illuminants.py @@ -8,18 +8,28 @@ from colour.colorimetry import ( SDS_ILLUMINANTS, + CIE_illuminant_D_series, + MultiSpectralDistributions, + SpectralDistribution, SpectralShape, daylight_locus_function, + msds_CIE_illuminant_D_series, sd_CIE_illuminant_D_series, sd_CIE_standard_illuminant_A, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat + from colour.hints import NDArrayFloat, ModuleType from colour.temperature import CCT_to_xy_CIE_D -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -32,6 +42,7 @@ "DATA_A", "TestSdCIEStandardIlluminantA", "TestSdCIEIlluminantDSeries", + "TestMsdsCIEIlluminantDSeries", "TestDaylightLocusFunction", ] @@ -148,13 +159,42 @@ def test_sd_CIE_standard_illuminant_A(self) -> None: sd_CIE_standard_illuminant_A` definition. """ - np.testing.assert_allclose( + xp_assert_close( sd_CIE_standard_illuminant_A(SpectralShape(360, 830, 5)).values, DATA_A, atol=TOLERANCE_ABSOLUTE_TESTS, ) +class TestCIEIlluminantDSeries: + """ + Define :func:`colour.colorimetry.illuminants.CIE_illuminant_D_series` + definition unit tests methods. + """ + + def test_CIE_illuminant_D_series(self) -> None: + """ + Test :func:`colour.colorimetry.illuminants.CIE_illuminant_D_series` + definition. + """ + + CCTs = np.array([5000.0, 6500.0, 7500.0]) * 1.4388 / 1.4380 + xy_batch = CCT_to_xy_CIE_D(CCTs) + + xp_assert_close( + CIE_illuminant_D_series(xy_batch), + msds_CIE_illuminant_D_series(xy_batch).values, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + shape = SpectralShape(380, 780, 5) + xp_assert_close( + CIE_illuminant_D_series(xy_batch, shape=shape), + msds_CIE_illuminant_D_series(xy_batch, shape=shape).values, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + class TestSdCIEIlluminantDSeries: """ Define :func:`colour.colorimetry.illuminants.sd_CIE_illuminant_D_series` @@ -177,62 +217,88 @@ def test_sd_CIE_illuminant_D_series(self) -> None: xy = CCT_to_xy_CIE_D(CCT) sd_r = SDS_ILLUMINANTS[name] sd_t = sd_CIE_illuminant_D_series(xy) + assert isinstance(sd_t, SpectralDistribution) - np.testing.assert_allclose( + xp_assert_close( sd_r.values, sd_t[sd_r.wavelengths], atol=tolerance, ) +class TestMsdsCIEIlluminantDSeries: + """ + Define :func:`colour.colorimetry.illuminants.\ +msds_CIE_illuminant_D_series` definition unit tests methods. + """ + + def test_msds_CIE_illuminant_D_series(self) -> None: + """ + Test :func:`colour.colorimetry.illuminants.\ +msds_CIE_illuminant_D_series` definition. + """ + + CCTs = np.array([5000.0, 6500.0, 7500.0]) * 1.4388 / 1.4380 + xy_batch = CCT_to_xy_CIE_D(CCTs) + msds = msds_CIE_illuminant_D_series(xy_batch) + assert isinstance(msds, MultiSpectralDistributions) + assert msds.values.shape[1] == xy_batch.shape[0] + for i in range(xy_batch.shape[0]): + xp_assert_close( + msds.values[:, i], + sd_CIE_illuminant_D_series(xy_batch[i]).values, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + class TestDaylightLocusFunction: """ Define :func:`colour.colorimetry.illuminants.daylight_locus_function` definition unit tests methods. """ - def test_daylight_locus_function(self) -> None: + def test_daylight_locus_function(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.illuminants.daylight_locus_function` definition. """ - np.testing.assert_allclose( - daylight_locus_function(0.31270), + xp_assert_close( + daylight_locus_function(xp_as_array(0.31270, xp=xp)), 0.329105129999999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - daylight_locus_function(0.34570), + xp_assert_close( + daylight_locus_function(xp_as_array(0.34570, xp=xp)), 0.358633529999999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - daylight_locus_function(0.44758), + xp_assert_close( + daylight_locus_function(xp_as_array(0.44758, xp=xp)), 0.408571030799999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_daylight_locus_function(self) -> None: + def test_n_dimensional_daylight_locus_function(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.illuminants.daylight_locus_function` definition n-dimensional support. """ - x_D = np.array([0.31270]) - y_D = daylight_locus_function(x_D) + x_D = xp_as_array([0.31270], xp=xp) + y_D = as_ndarray(daylight_locus_function(x_D)) - x_D = np.tile(x_D, (6, 1)) - y_D = np.tile(y_D, (6, 1)) - np.testing.assert_allclose( + x_D = xp.tile(xp_as_array(x_D, xp=xp), (6, 1)) + y_D = xp.tile(xp_as_array(y_D, xp=xp), (6, 1)) + xp_assert_close( daylight_locus_function(x_D), y_D, atol=TOLERANCE_ABSOLUTE_TESTS ) - x_D = np.reshape(x_D, (2, 3, 1)) - y_D = np.reshape(y_D, (2, 3, 1)) - np.testing.assert_allclose( + x_D = xp_reshape(xp_as_array(x_D, xp=xp), (2, 3, 1), xp=xp) + y_D = xp_reshape(xp_as_array(y_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( daylight_locus_function(x_D), y_D, atol=TOLERANCE_ABSOLUTE_TESTS ) diff --git a/colour/colorimetry/tests/test_lefs.py b/colour/colorimetry/tests/test_lefs.py index 9345cbd720..49728630de 100644 --- a/colour/colorimetry/tests/test_lefs.py +++ b/colour/colorimetry/tests/test_lefs.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.colorimetry import ( @@ -9,7 +14,13 @@ sd_mesopic_luminous_efficiency_function, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -441,52 +452,52 @@ def test_mesopic_weighting_function(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( mesopic_weighting_function(500, 0.2), 0.70522000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( mesopic_weighting_function(500, 0.2, source="Red Heavy", method="LRC"), 0.90951000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( mesopic_weighting_function(700, 10, source="Red Heavy", method="LRC"), 0.00410200, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_mesopic_weighting_function(self) -> None: + def test_n_dimensional_mesopic_weighting_function(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lefs.mesopic_weighting_function` definition n-dimensional arrays support. """ wl = 500 - Vm = mesopic_weighting_function(wl, 0.2) + Vm = as_ndarray(mesopic_weighting_function(wl, 0.2)) - wl = np.tile(wl, 6) - Vm = np.tile(Vm, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + Vm = xp.tile(xp_as_array(Vm, xp=xp), (6,)) + xp_assert_close( mesopic_weighting_function(wl, 0.2), Vm, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - Vm = np.reshape(Vm, (2, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + Vm = xp_reshape(xp_as_array(Vm, xp=xp), (2, 3), xp=xp) + xp_assert_close( mesopic_weighting_function(wl, 0.2), Vm, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - Vm = np.reshape(Vm, (2, 3, 1)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + Vm = xp_reshape(xp_as_array(Vm, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( mesopic_weighting_function(wl, 0.2), Vm, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -518,7 +529,7 @@ def test_sd_mesopic_luminous_efficiency_function(self) -> None: sd_mesopic_luminous_efficiency_function` definition. """ - np.testing.assert_allclose( + xp_assert_close( sd_mesopic_luminous_efficiency_function(0.2).values, DATA_MESOPIC_LEF, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/tests/test_lightness.py b/colour/colorimetry/tests/test_lightness.py index e8d2140ae8..193e436f87 100644 --- a/colour/colorimetry/tests/test_lightness.py +++ b/colour/colorimetry/tests/test_lightness.py @@ -2,8 +2,13 @@ from __future__ import annotations +import typing + import numpy as np +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.colorimetry import ( intermediate_lightness_function_CIE1976, lightness_Abebe2017, @@ -15,7 +20,14 @@ ) from colour.colorimetry.lightness import lightness from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -42,31 +54,31 @@ class TestLightnessGlasser1958: definition unit tests methods. """ - def test_lightness_Glasser1958(self) -> None: + def test_lightness_Glasser1958(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Glasser1958` definition. """ - np.testing.assert_allclose( - lightness_Glasser1958(12.19722535), + xp_assert_close( + lightness_Glasser1958(xp_as_array([12.19722535], xp=xp)), 39.83512646492521, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Glasser1958(23.04276781), + xp_assert_close( + lightness_Glasser1958(xp_as_array([23.04276781], xp=xp)), 53.585946877480623, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Glasser1958(6.15720079), + xp_assert_close( + lightness_Glasser1958(xp_as_array([6.15720079], xp=xp)), 27.972867038082629, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_lightness_Glasser1958(self) -> None: + def test_n_dimensional_lightness_Glasser1958(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Glasser1958` definition n-dimensional arrays support. @@ -75,36 +87,30 @@ def test_n_dimensional_lightness_Glasser1958(self) -> None: Y = 12.19722535 L = lightness_Glasser1958(Y) - Y = np.tile(Y, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( - lightness_Glasser1958(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(lightness_Glasser1958(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( - lightness_Glasser1958(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(lightness_Glasser1958(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( - lightness_Glasser1958(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(lightness_Glasser1958(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_lightness_Glasser1958(self) -> None: + def test_domain_range_scale_lightness_Glasser1958(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.lightness.lightness_Glasser1958` definition domain and range scale support. """ - L = lightness_Glasser1958(12.19722535) + L = as_ndarray(lightness_Glasser1958(12.19722535)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( lightness_Glasser1958(12.19722535 * factor), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -126,31 +132,31 @@ class TestLightnessWyszecki1963: definition unit tests methods. """ - def test_lightness_Wyszecki1963(self) -> None: + def test_lightness_Wyszecki1963(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Wyszecki1963` definition. """ - np.testing.assert_allclose( - lightness_Wyszecki1963(12.19722535), + xp_assert_close( + lightness_Wyszecki1963(xp_as_array([12.19722535], xp=xp)), 40.547574599570197, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Wyszecki1963(23.04276781), + xp_assert_close( + lightness_Wyszecki1963(xp_as_array([23.04276781], xp=xp)), 54.140714588256841, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Wyszecki1963(6.15720079), + xp_assert_close( + lightness_Wyszecki1963(xp_as_array([6.15720079], xp=xp)), 28.821339499883976, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_lightness_Wyszecki1963(self) -> None: + def test_n_dimensional_lightness_Wyszecki1963(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Wyszecki1963` definition n-dimensional arrays support. @@ -159,36 +165,30 @@ def test_n_dimensional_lightness_Wyszecki1963(self) -> None: Y = 12.19722535 W = lightness_Wyszecki1963(Y) - Y = np.tile(Y, 6) - W = np.tile(W, 6) - np.testing.assert_allclose( - lightness_Wyszecki1963(Y), W, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + W = xp.tile(xp_as_array(W, xp=xp), (6,)) + xp_assert_close(lightness_Wyszecki1963(Y), W, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3)) - W = np.reshape(W, (2, 3)) - np.testing.assert_allclose( - lightness_Wyszecki1963(Y), W, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + W = xp_reshape(xp_as_array(W, xp=xp), (2, 3), xp=xp) + xp_assert_close(lightness_Wyszecki1963(Y), W, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3, 1)) - W = np.reshape(W, (2, 3, 1)) - np.testing.assert_allclose( - lightness_Wyszecki1963(Y), W, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + W = xp_reshape(xp_as_array(W, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(lightness_Wyszecki1963(Y), W, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_lightness_Wyszecki1963(self) -> None: + def test_domain_range_scale_lightness_Wyszecki1963(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.lightness.lightness_Wyszecki1963` definition domain and range scale support. """ - W = lightness_Wyszecki1963(12.19722535) + W = as_ndarray(lightness_Wyszecki1963(12.19722535)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( lightness_Wyszecki1963(12.19722535 * factor), W * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -210,31 +210,33 @@ class TestIntermediateLightnessFunctionCIE1976: intermediate_lightness_function_CIE1976` definition unit tests methods. """ - def test_intermediate_lightness_function_CIE1976(self) -> None: + def test_intermediate_lightness_function_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.\ intermediate_lightness_function_CIE1976` definition. """ - np.testing.assert_allclose( - intermediate_lightness_function_CIE1976(12.19722535), + xp_assert_close( + intermediate_lightness_function_CIE1976(xp_as_array([12.19722535], xp=xp)), 0.495929964178047, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - intermediate_lightness_function_CIE1976(23.04276781), + xp_assert_close( + intermediate_lightness_function_CIE1976(xp_as_array([23.04276781], xp=xp)), 0.613072093530391, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - intermediate_lightness_function_CIE1976(6.15720079), + xp_assert_close( + intermediate_lightness_function_CIE1976(xp_as_array([6.15720079], xp=xp)), 0.394876333449113, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_intermediate_lightness_function_CIE1976(self) -> None: + def test_n_dimensional_intermediate_lightness_function_CIE1976( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.lightness.\ intermediate_lightness_function_CIE1976` definition n-dimensional arrays @@ -244,42 +246,45 @@ def test_n_dimensional_intermediate_lightness_function_CIE1976(self) -> None: Y = 12.19722535 f_Y_Y_n = intermediate_lightness_function_CIE1976(Y) - Y = np.tile(Y, 6) - f_Y_Y_n = np.tile(f_Y_Y_n, 6) - np.testing.assert_allclose( + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + f_Y_Y_n = xp.tile(xp_as_array(f_Y_Y_n, xp=xp), (6,)) + xp_assert_close( intermediate_lightness_function_CIE1976(Y), f_Y_Y_n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y = np.reshape(Y, (2, 3)) - f_Y_Y_n = np.reshape(f_Y_Y_n, (2, 3)) - np.testing.assert_allclose( + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + f_Y_Y_n = xp_reshape(xp_as_array(f_Y_Y_n, xp=xp), (2, 3), xp=xp) + xp_assert_close( intermediate_lightness_function_CIE1976(Y), f_Y_Y_n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y = np.reshape(Y, (2, 3, 1)) - f_Y_Y_n = np.reshape(f_Y_Y_n, (2, 3, 1)) - np.testing.assert_allclose( + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + f_Y_Y_n = xp_reshape(xp_as_array(f_Y_Y_n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( intermediate_lightness_function_CIE1976(Y), f_Y_Y_n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_intermediate_lightness_function_CIE1976(self) -> None: + def test_domain_range_scale_intermediate_lightness_function_CIE1976( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.colorimetry.lightness.\ intermediate_lightness_function_CIE1976` definition domain and range scale support. """ - f_Y_Y_n = intermediate_lightness_function_CIE1976(12.19722535, 100) + f_Y_Y_n = as_ndarray(intermediate_lightness_function_CIE1976(12.19722535, 100)) for scale in ("reference", "1", "100"): with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( intermediate_lightness_function_CIE1976(12.19722535, 100), f_Y_Y_n, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -303,49 +308,49 @@ class TestLightnessCIE1976: unit tests methods. """ - def test_lightness_CIE1976(self) -> None: + def test_lightness_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_CIE1976` definition. """ - np.testing.assert_allclose( - lightness_CIE1976(12.19722535), + xp_assert_close( + lightness_CIE1976(xp_as_array([12.19722535], xp=xp)), 41.527875844653451, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_CIE1976(23.04276781), + xp_assert_close( + lightness_CIE1976(xp_as_array([23.04276781], xp=xp)), 55.116362849525402, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_CIE1976(6.15720079), + xp_assert_close( + lightness_CIE1976(xp_as_array([6.15720079], xp=xp)), 29.805654680097106, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_CIE1976(12.19722535, 50), + xp_assert_close( + lightness_CIE1976(xp_as_array([12.19722535], xp=xp), 50), 56.480581732417676, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_CIE1976(12.19722535, 75), + xp_assert_close( + lightness_CIE1976(xp_as_array([12.19722535], xp=xp), 75), 47.317620274162735, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_CIE1976(12.19722535, 95), + xp_assert_close( + lightness_CIE1976(xp_as_array([12.19722535], xp=xp), 95), 42.519930728120940, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_lightness_CIE1976(self) -> None: + def test_n_dimensional_lightness_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_CIE1976` definition n-dimensional arrays support. @@ -354,36 +359,30 @@ def test_n_dimensional_lightness_CIE1976(self) -> None: Y = 12.19722535 L_star = lightness_CIE1976(Y) - Y = np.tile(Y, 6) - L_star = np.tile(L_star, 6) - np.testing.assert_allclose( - lightness_CIE1976(Y), L_star, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + L_star = xp.tile(xp_as_array(L_star, xp=xp), (6,)) + xp_assert_close(lightness_CIE1976(Y), L_star, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3)) - L_star = np.reshape(L_star, (2, 3)) - np.testing.assert_allclose( - lightness_CIE1976(Y), L_star, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + L_star = xp_reshape(xp_as_array(L_star, xp=xp), (2, 3), xp=xp) + xp_assert_close(lightness_CIE1976(Y), L_star, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3, 1)) - L_star = np.reshape(L_star, (2, 3, 1)) - np.testing.assert_allclose( - lightness_CIE1976(Y), L_star, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + L_star = xp_reshape(xp_as_array(L_star, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(lightness_CIE1976(Y), L_star, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_lightness_CIE1976(self) -> None: + def test_domain_range_scale_lightness_CIE1976(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.lightness.lightness_CIE1976` definition domain and range scale support. """ - L_star = lightness_CIE1976(12.19722535, 100) + L_star = as_ndarray(lightness_CIE1976(12.19722535, 100)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( lightness_CIE1976(12.19722535 * factor, 100 * factor), L_star * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -405,49 +404,49 @@ class TestLightnessFairchild2010: definition unit tests methods. """ - def test_lightness_Fairchild2010(self) -> None: + def test_lightness_Fairchild2010(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Fairchild2010` definition. """ - np.testing.assert_allclose( - lightness_Fairchild2010(12.19722535 / 100), + xp_assert_close( + lightness_Fairchild2010(xp_as_array([12.19722535 / 100], xp=xp)), 31.996390226262736, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2010(23.04276781 / 100), + xp_assert_close( + lightness_Fairchild2010(xp_as_array([23.04276781 / 100], xp=xp)), 60.203153682783302, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2010(6.15720079 / 100), + xp_assert_close( + lightness_Fairchild2010(xp_as_array([6.15720079 / 100], xp=xp)), 11.836517240976489, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2010(12.19722535 / 100, 2.75), + xp_assert_close( + lightness_Fairchild2010(xp_as_array([12.19722535 / 100], xp=xp), 2.75), 24.424283249379986, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2010(1008), + xp_assert_close( + lightness_Fairchild2010(xp_as_array([1008.0], xp=xp)), 100.019986327374240, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2010(100800), + xp_assert_close( + lightness_Fairchild2010(xp_as_array([100800.0], xp=xp)), 100.019999997090270, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_lightness_Fairchild2010(self) -> None: + def test_n_dimensional_lightness_Fairchild2010(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Fairchild2010` definition n-dimensional arrays support. @@ -456,36 +455,36 @@ def test_n_dimensional_lightness_Fairchild2010(self) -> None: Y = 12.19722535 / 100 L_hdr = lightness_Fairchild2010(Y) - Y = np.tile(Y, 6) - L_hdr = np.tile(L_hdr, 6) - np.testing.assert_allclose( + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + L_hdr = xp.tile(xp_as_array(L_hdr, xp=xp), (6,)) + xp_assert_close( lightness_Fairchild2010(Y), L_hdr, atol=TOLERANCE_ABSOLUTE_TESTS ) - Y = np.reshape(Y, (2, 3)) - L_hdr = np.reshape(L_hdr, (2, 3)) - np.testing.assert_allclose( + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3), xp=xp) + xp_assert_close( lightness_Fairchild2010(Y), L_hdr, atol=TOLERANCE_ABSOLUTE_TESTS ) - Y = np.reshape(Y, (2, 3, 1)) - L_hdr = np.reshape(L_hdr, (2, 3, 1)) - np.testing.assert_allclose( + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( lightness_Fairchild2010(Y), L_hdr, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_lightness_Fairchild2010(self) -> None: + def test_domain_range_scale_lightness_Fairchild2010(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.lightness.lightness_Fairchild2010` definition domain and range scale support. """ - L_hdr = lightness_Fairchild2010(12.19722535 / 100) + L_hdr = as_ndarray(lightness_Fairchild2010(12.19722535 / 100)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( lightness_Fairchild2010(12.19722535 / 100 * factor_a), L_hdr * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -507,49 +506,49 @@ class TestLightnessFairchild2011: definition unit tests methods. """ - def test_lightness_Fairchild2011(self) -> None: + def test_lightness_Fairchild2011(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Fairchild2011` definition. """ - np.testing.assert_allclose( - lightness_Fairchild2011(12.19722535 / 100), + xp_assert_close( + lightness_Fairchild2011(xp_as_array([12.19722535 / 100], xp=xp)), 51.852958445912506, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2011(23.04276781 / 100), + xp_assert_close( + lightness_Fairchild2011(xp_as_array([23.04276781 / 100], xp=xp)), 65.275207956353853, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2011(6.15720079 / 100), + xp_assert_close( + lightness_Fairchild2011(xp_as_array([6.15720079 / 100], xp=xp)), 39.818935510715917, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2011(12.19722535 / 100, 2.75), + xp_assert_close( + lightness_Fairchild2011(xp_as_array([12.19722535 / 100], xp=xp), 2.75), 0.13268968410139345, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2011(1008), + xp_assert_close( + lightness_Fairchild2011(xp_as_array([1008.0], xp=xp)), 234.72925682, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Fairchild2011(100800), + xp_assert_close( + lightness_Fairchild2011(xp_as_array([100800.0], xp=xp)), 245.5705978, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_lightness_Fairchild2011(self) -> None: + def test_n_dimensional_lightness_Fairchild2011(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Fairchild2011` definition n-dimensional arrays support. @@ -558,36 +557,36 @@ def test_n_dimensional_lightness_Fairchild2011(self) -> None: Y = 12.19722535 / 100 L_hdr = lightness_Fairchild2011(Y) - Y = np.tile(Y, 6) - L_hdr = np.tile(L_hdr, 6) - np.testing.assert_allclose( + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + L_hdr = xp.tile(xp_as_array(L_hdr, xp=xp), (6,)) + xp_assert_close( lightness_Fairchild2011(Y), L_hdr, atol=TOLERANCE_ABSOLUTE_TESTS ) - Y = np.reshape(Y, (2, 3)) - L_hdr = np.reshape(L_hdr, (2, 3)) - np.testing.assert_allclose( + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3), xp=xp) + xp_assert_close( lightness_Fairchild2011(Y), L_hdr, atol=TOLERANCE_ABSOLUTE_TESTS ) - Y = np.reshape(Y, (2, 3, 1)) - L_hdr = np.reshape(L_hdr, (2, 3, 1)) - np.testing.assert_allclose( + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( lightness_Fairchild2011(Y), L_hdr, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_lightness_Fairchild2011(self) -> None: + def test_domain_range_scale_lightness_Fairchild2011(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.lightness.lightness_Fairchild2011` definition domain and range scale support. """ - L_hdr = lightness_Fairchild2011(12.19722535 / 100) + L_hdr = as_ndarray(lightness_Fairchild2011(12.19722535 / 100)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( lightness_Fairchild2011(12.19722535 / 100 * factor_a), L_hdr * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -609,43 +608,45 @@ class TestLightnessAbebe2017: definition unit tests methods. """ - def test_lightness_Abebe2017(self) -> None: + def test_lightness_Abebe2017(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Abebe2017` definition. """ - np.testing.assert_allclose( - lightness_Abebe2017(12.19722535), + xp_assert_close( + lightness_Abebe2017(xp_as_array([12.19722535], xp=xp)), 0.486955571109229, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Abebe2017(12.19722535, method="Stevens"), + xp_assert_close( + lightness_Abebe2017(xp_as_array([12.19722535], xp=xp), method="Stevens"), 0.474544792145434, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Abebe2017(12.19722535, 1000), + xp_assert_close( + lightness_Abebe2017(xp_as_array([12.19722535], xp=xp), 1000), 0.286847428534793, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Abebe2017(12.19722535, 4000), + xp_assert_close( + lightness_Abebe2017(xp_as_array([12.19722535], xp=xp), 4000), 0.192145492588158, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - lightness_Abebe2017(12.19722535, 4000, method="Stevens"), + xp_assert_close( + lightness_Abebe2017( + xp_as_array([12.19722535], xp=xp), 4000, method="Stevens" + ), 0.170365211220992, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_lightness_Abebe2017(self) -> None: + def test_n_dimensional_lightness_Abebe2017(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.lightness.lightness_Abebe2017` definition n-dimensional arrays support. @@ -654,23 +655,17 @@ def test_n_dimensional_lightness_Abebe2017(self) -> None: Y = 12.19722535 L = lightness_Abebe2017(Y) - Y = np.tile(Y, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( - lightness_Abebe2017(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(lightness_Abebe2017(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( - lightness_Abebe2017(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(lightness_Abebe2017(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS) - Y = np.reshape(Y, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( - lightness_Abebe2017(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(lightness_Abebe2017(Y), L, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_lightness_Abebe2017(self) -> None: @@ -689,7 +684,7 @@ class TestLightness: tests methods. """ - def test_domain_range_scale_lightness(self) -> None: + def test_domain_range_scale_lightness(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.lightness.lightness` definition domain and range scale support. @@ -703,13 +698,13 @@ def test_domain_range_scale_lightness(self) -> None: "Fairchild 2011", "Abebe 2017", ) - v = [lightness(12.19722535, method, Y_n=100) for method in m] + v = [as_ndarray(lightness(12.19722535, method, Y_n=100)) for method in m] d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for method, value in zip(m, v, strict=True): for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( lightness(12.19722535 * factor, method, Y_n=100 * factor), value * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/tests/test_luminance.py b/colour/colorimetry/tests/test_luminance.py index f0a9d1e57a..6df2307c7b 100644 --- a/colour/colorimetry/tests/test_luminance.py +++ b/colour/colorimetry/tests/test_luminance.py @@ -2,7 +2,13 @@ from __future__ import annotations +import typing + import numpy as np +import pytest + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType from colour.colorimetry import ( intermediate_luminance_function_CIE1976, @@ -15,7 +21,14 @@ ) from colour.colorimetry.luminance import luminance from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -42,31 +55,31 @@ class TestLuminanceNewhall1943: definition unit tests methods. """ - def test_luminance_Newhall1943(self) -> None: + def test_luminance_Newhall1943(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Newhall1943` definition. """ - np.testing.assert_allclose( - luminance_Newhall1943(4.08244375), + xp_assert_close( + luminance_Newhall1943(xp_as_array([4.08244375], xp=xp)), 12.550078816731881, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Newhall1943(5.39132685), + xp_assert_close( + luminance_Newhall1943(xp_as_array([5.39132685], xp=xp)), 23.481252371310738, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Newhall1943(2.97619312), + xp_assert_close( + luminance_Newhall1943(xp_as_array([2.97619312], xp=xp)), 6.4514266875601924, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_luminance_Newhall1943(self) -> None: + def test_n_dimensional_luminance_Newhall1943(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Newhall1943` definition n-dimensional arrays support. @@ -75,36 +88,30 @@ def test_n_dimensional_luminance_Newhall1943(self) -> None: V = 4.08244375 Y = luminance_Newhall1943(V) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - luminance_Newhall1943(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close(luminance_Newhall1943(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - luminance_Newhall1943(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close(luminance_Newhall1943(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - luminance_Newhall1943(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(luminance_Newhall1943(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_luminance_Newhall1943(self) -> None: + def test_domain_range_scale_luminance_Newhall1943(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.luminance.luminance_Newhall1943` definition domain and range scale support. """ - Y = luminance_Newhall1943(4.08244375) + Y = as_ndarray(luminance_Newhall1943(4.08244375)) d_r = (("reference", 1, 1), ("1", 0.1, 0.01), ("100", 10, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( luminance_Newhall1943(4.08244375 * factor_a), Y * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -126,31 +133,31 @@ class TestLuminanceASTMD1535: definition unit tests methods. """ - def test_luminance_ASTMD1535(self) -> None: + def test_luminance_ASTMD1535(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_ASTMD1535` definition. """ - np.testing.assert_allclose( - luminance_ASTMD1535(4.08244375), + xp_assert_close( + luminance_ASTMD1535(xp_as_array([4.08244375], xp=xp)), 12.236342675366036, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_ASTMD1535(5.39132685), + xp_assert_close( + luminance_ASTMD1535(xp_as_array([5.39132685], xp=xp)), 22.893999867280378, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_ASTMD1535(2.97619312), + xp_assert_close( + luminance_ASTMD1535(xp_as_array([2.97619312], xp=xp)), 6.2902253509053132, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_luminance_ASTMD1535(self) -> None: + def test_n_dimensional_luminance_ASTMD1535(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_ASTMD1535` definition n-dimensional arrays support. @@ -159,36 +166,30 @@ def test_n_dimensional_luminance_ASTMD1535(self) -> None: V = 4.08244375 Y = luminance_ASTMD1535(V) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - luminance_ASTMD1535(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close(luminance_ASTMD1535(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - luminance_ASTMD1535(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close(luminance_ASTMD1535(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - luminance_ASTMD1535(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(luminance_ASTMD1535(V), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_luminance_ASTMD1535(self) -> None: + def test_domain_range_scale_luminance_ASTMD1535(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.luminance.luminance_ASTMD1535` definition domain and range scale support. """ - Y = luminance_ASTMD1535(4.08244375) + Y = as_ndarray(luminance_ASTMD1535(4.08244375)) d_r = (("reference", 1, 1), ("1", 0.1, 0.01), ("100", 10, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( luminance_ASTMD1535(4.08244375 * factor_a), Y * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -210,31 +211,39 @@ class TestIntermediateLuminanceFunctionCIE1976: intermediate_luminance_function_CIE1976` definition unit tests methods. """ - def test_intermediate_luminance_function_CIE1976(self) -> None: + def test_intermediate_luminance_function_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.\ intermediate_luminance_function_CIE1976` definition. """ - np.testing.assert_allclose( - intermediate_luminance_function_CIE1976(0.495929964178047), + xp_assert_close( + intermediate_luminance_function_CIE1976( + xp_as_array([0.495929964178047], xp=xp) + ), 12.197225350000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - intermediate_luminance_function_CIE1976(0.613072093530391), + xp_assert_close( + intermediate_luminance_function_CIE1976( + xp_as_array([0.613072093530391], xp=xp) + ), 23.042767810000004, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - intermediate_luminance_function_CIE1976(0.394876333449113), + xp_assert_close( + intermediate_luminance_function_CIE1976( + xp_as_array([0.394876333449113], xp=xp) + ), 6.157200790000001, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_intermediate_luminance_function_CIE1976(self) -> None: + def test_n_dimensional_intermediate_luminance_function_CIE1976( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.luminance.\ intermediate_luminance_function_CIE1976` definition n-dimensional arrays @@ -244,42 +253,45 @@ def test_n_dimensional_intermediate_luminance_function_CIE1976(self) -> None: f_Y_Y_n = 0.495929964178047 Y = intermediate_luminance_function_CIE1976(f_Y_Y_n) - f_Y_Y_n = np.tile(f_Y_Y_n, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( + f_Y_Y_n = xp.tile(xp_as_array(f_Y_Y_n, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( intermediate_luminance_function_CIE1976(f_Y_Y_n), Y, atol=TOLERANCE_ABSOLUTE_TESTS, ) - f_Y_Y_n = np.reshape(f_Y_Y_n, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( + f_Y_Y_n = xp_reshape(xp_as_array(f_Y_Y_n, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( intermediate_luminance_function_CIE1976(f_Y_Y_n), Y, atol=TOLERANCE_ABSOLUTE_TESTS, ) - f_Y_Y_n = np.reshape(f_Y_Y_n, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( + f_Y_Y_n = xp_reshape(xp_as_array(f_Y_Y_n, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( intermediate_luminance_function_CIE1976(f_Y_Y_n), Y, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_intermediate_luminance_function_CIE1976(self) -> None: + def test_domain_range_scale_intermediate_luminance_function_CIE1976( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.colorimetry.luminance.\ intermediate_luminance_function_CIE1976` definition domain and range scale support. """ - Y = intermediate_luminance_function_CIE1976(41.527875844653451, 100) + Y = as_ndarray(intermediate_luminance_function_CIE1976(41.527875844653451, 100)) for scale in ("reference", "1", "100"): with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( intermediate_luminance_function_CIE1976(41.527875844653451, 100), Y, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -303,49 +315,49 @@ class TestLuminanceCIE1976: unit tests methods. """ - def test_luminance_CIE1976(self) -> None: + def test_luminance_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_CIE1976` definition. """ - np.testing.assert_allclose( - luminance_CIE1976(41.527875844653451), + xp_assert_close( + luminance_CIE1976(xp_as_array([41.527875844653451], xp=xp)), 12.197225350000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_CIE1976(55.116362849525402), + xp_assert_close( + luminance_CIE1976(xp_as_array([55.116362849525402], xp=xp)), 23.042767810000004, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_CIE1976(29.805654680097106), + xp_assert_close( + luminance_CIE1976(xp_as_array([29.805654680097106], xp=xp)), 6.157200790000001, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_CIE1976(56.480581732417676, 50), + xp_assert_close( + luminance_CIE1976(xp_as_array([56.480581732417676], xp=xp), 50), 12.197225349999998, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_CIE1976(47.317620274162735, 75), + xp_assert_close( + luminance_CIE1976(xp_as_array([47.317620274162735], xp=xp), 75), 12.197225350000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_CIE1976(42.519930728120940, 95), + xp_assert_close( + luminance_CIE1976(xp_as_array([42.519930728120940], xp=xp), 95), 12.197225350000005, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_luminance_CIE1976(self) -> None: + def test_n_dimensional_luminance_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_CIE1976` definition n-dimensional arrays support. @@ -354,36 +366,30 @@ def test_n_dimensional_luminance_CIE1976(self) -> None: L_star = 41.527875844653451 Y = luminance_CIE1976(L_star) - L_star = np.tile(L_star, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - luminance_CIE1976(L_star), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_star = xp.tile(xp_as_array(L_star, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close(luminance_CIE1976(L_star), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - L_star = np.reshape(L_star, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - luminance_CIE1976(L_star), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_star = xp_reshape(xp_as_array(L_star, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close(luminance_CIE1976(L_star), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - L_star = np.reshape(L_star, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - luminance_CIE1976(L_star), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_star = xp_reshape(xp_as_array(L_star, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(luminance_CIE1976(L_star), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_luminance_CIE1976(self) -> None: + def test_domain_range_scale_luminance_CIE1976(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.luminance.luminance_CIE1976` definition domain and range scale support. """ - Y = luminance_CIE1976(41.527875844653451, 100) + Y = as_ndarray(luminance_CIE1976(41.527875844653451, 100)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( luminance_CIE1976(41.527875844653451 * factor, 100 * factor), Y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -405,49 +411,50 @@ class TestLuminanceFairchild2010: definition unit tests methods. """ - def test_luminance_Fairchild2010(self) -> None: + @pytest.mark.mps_xfail("MPS float32 precision divergence") + def test_luminance_Fairchild2010(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Fairchild2010` definition. """ - np.testing.assert_allclose( - luminance_Fairchild2010(31.996390226262736), + xp_assert_close( + luminance_Fairchild2010(xp_as_array([31.996390226262736], xp=xp)), 0.12197225350000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2010(60.203153682783302), + xp_assert_close( + luminance_Fairchild2010(xp_as_array([60.203153682783302], xp=xp)), 0.23042767809999998, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2010(11.836517240976489), + xp_assert_close( + luminance_Fairchild2010(xp_as_array([11.836517240976489], xp=xp)), 0.06157200790000001, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2010(24.424283249379986, 2.75), + xp_assert_close( + luminance_Fairchild2010(xp_as_array([24.424283249379986], xp=xp), 2.75), 0.12197225350000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2010(100.019986327374240), + xp_assert_close( + luminance_Fairchild2010(xp_as_array([100.019986327374240], xp=xp)), 1008.00000024, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2010(100.019999997090270), + xp_assert_close( + luminance_Fairchild2010(xp_as_array([100.019999997090270], xp=xp)), 100799.92312466, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_luminance_Fairchild2010(self) -> None: + def test_n_dimensional_luminance_Fairchild2010(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Fairchild2010` definition n-dimensional arrays support. @@ -456,36 +463,36 @@ def test_n_dimensional_luminance_Fairchild2010(self) -> None: L_hdr = 31.996390226262736 Y = luminance_Fairchild2010(L_hdr) - L_hdr = np.tile(L_hdr, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( + L_hdr = xp.tile(xp_as_array(L_hdr, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( luminance_Fairchild2010(L_hdr), Y, atol=TOLERANCE_ABSOLUTE_TESTS ) - L_hdr = np.reshape(L_hdr, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( luminance_Fairchild2010(L_hdr), Y, atol=TOLERANCE_ABSOLUTE_TESTS ) - L_hdr = np.reshape(L_hdr, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( luminance_Fairchild2010(L_hdr), Y, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_luminance_Fairchild2010(self) -> None: + def test_domain_range_scale_luminance_Fairchild2010(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.luminance.luminance_Fairchild2010` definition domain and range scale support. """ - Y = luminance_Fairchild2010(31.996390226262736) + Y = as_ndarray(luminance_Fairchild2010(31.996390226262736)) d_r = (("reference", 1, 1), ("1", 0.01, 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( luminance_Fairchild2010(31.996390226262736 * factor_a), Y * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -507,49 +514,50 @@ class TestLuminanceFairchild2011: definition unit tests methods. """ - def test_luminance_Fairchild2011(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_luminance_Fairchild2011(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Fairchild2011` definition. """ - np.testing.assert_allclose( - luminance_Fairchild2011(51.852958445912506), + xp_assert_close( + luminance_Fairchild2011(xp_as_array([51.852958445912506], xp=xp)), 0.12197225350000007, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2011(65.275207956353853), + xp_assert_close( + luminance_Fairchild2011(xp_as_array([65.275207956353853], xp=xp)), 0.23042767809999998, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2011(39.818935510715917), + xp_assert_close( + luminance_Fairchild2011(xp_as_array([39.818935510715917], xp=xp)), 0.061572007900000038, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2011(0.13268968410139345, 2.75), + xp_assert_close( + luminance_Fairchild2011(xp_as_array([0.13268968410139345], xp=xp), 2.75), 0.12197225350000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2011(234.72925681957565), + xp_assert_close( + luminance_Fairchild2011(xp_as_array([234.72925681957565], xp=xp)), 1008.00000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Fairchild2011(245.57059778237573), + xp_assert_close( + luminance_Fairchild2011(xp_as_array([245.57059778237573], xp=xp)), 100800.00000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_luminance_Fairchild2011(self) -> None: + def test_n_dimensional_luminance_Fairchild2011(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Fairchild2011` definition n-dimensional arrays support. @@ -558,36 +566,36 @@ def test_n_dimensional_luminance_Fairchild2011(self) -> None: L_hdr = 51.852958445912506 Y = luminance_Fairchild2011(L_hdr) - L_hdr = np.tile(L_hdr, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( + L_hdr = xp.tile(xp_as_array(L_hdr, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( luminance_Fairchild2011(L_hdr), Y, atol=TOLERANCE_ABSOLUTE_TESTS ) - L_hdr = np.reshape(L_hdr, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( luminance_Fairchild2011(L_hdr), Y, atol=TOLERANCE_ABSOLUTE_TESTS ) - L_hdr = np.reshape(L_hdr, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( + L_hdr = xp_reshape(xp_as_array(L_hdr, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( luminance_Fairchild2011(L_hdr), Y, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_luminance_Fairchild2011(self) -> None: + def test_domain_range_scale_luminance_Fairchild2011(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.luminance.luminance_Fairchild2011` definition domain and range scale support. """ - Y = luminance_Fairchild2011(26.459509817572265) + Y = as_ndarray(luminance_Fairchild2011(26.459509817572265)) d_r = (("reference", 1, 1), ("1", 0.01, 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( luminance_Fairchild2011(26.459509817572265 * factor_a), Y * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -609,43 +617,47 @@ class TestLuminanceAbebe2017: definition unit tests methods. """ - def test_luminance_Abebe2017(self) -> None: + def test_luminance_Abebe2017(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Abebe2017` definition. """ - np.testing.assert_allclose( - luminance_Abebe2017(0.486955571109229), + xp_assert_close( + luminance_Abebe2017(xp_as_array([0.486955571109229], xp=xp)), 12.197225350000004, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Abebe2017(0.474544792145434, method="Stevens"), + xp_assert_close( + luminance_Abebe2017( + xp_as_array([0.474544792145434], xp=xp), method="Stevens" + ), 12.197225350000025, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Abebe2017(0.286847428534793, 1000), + xp_assert_close( + luminance_Abebe2017(xp_as_array([0.286847428534793], xp=xp), 1000), 12.197225350000046, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Abebe2017(0.192145492588158, 4000), + xp_assert_close( + luminance_Abebe2017(xp_as_array([0.192145492588158], xp=xp), 4000), 12.197225350000121, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminance_Abebe2017(0.170365211220992, 4000, method="Stevens"), + xp_assert_close( + luminance_Abebe2017( + xp_as_array([0.170365211220992], xp=xp), 4000, method="Stevens" + ), 12.197225349999933, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_luminance_Abebe2017(self) -> None: + def test_n_dimensional_luminance_Abebe2017(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.luminance.luminance_Abebe2017` definition n-dimensional arrays support. @@ -654,23 +666,17 @@ def test_n_dimensional_luminance_Abebe2017(self) -> None: L = 0.486955571109229 Y = luminance_Abebe2017(L) - L = np.tile(L, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - luminance_Abebe2017(L), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close(luminance_Abebe2017(L), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - luminance_Abebe2017(L), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close(luminance_Abebe2017(L), Y, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - luminance_Abebe2017(L), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(luminance_Abebe2017(L), Y, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_luminance_Abebe2017(self) -> None: @@ -689,7 +695,7 @@ class TestLuminance: tests methods. """ - def test_domain_range_scale_luminance(self) -> None: + def test_domain_range_scale_luminance(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.luminance.luminance` definition domain and range scale support. @@ -703,15 +709,17 @@ def test_domain_range_scale_luminance(self) -> None: "Fairchild 2011", "Abebe 2017", ) - v = [luminance(41.527875844653451, method, Y_n=100) for method in m] + v = [as_ndarray(luminance(41.527875844653451, method, Y_n=100)) for method in m] d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for method, value in zip(m, v, strict=True): for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( luminance( - 41.527875844653451 * factor, method, Y_n=100 * factor + 41.527875844653451 * factor, + method, + Y_n=100 * factor, ), value * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/tests/test_photometry.py b/colour/colorimetry/tests/test_photometry.py index cae9fcf3fc..5d5ce34cd3 100644 --- a/colour/colorimetry/tests/test_photometry.py +++ b/colour/colorimetry/tests/test_photometry.py @@ -2,8 +2,6 @@ from __future__ import annotations -import numpy as np - from colour.colorimetry import ( SDS_ILLUMINANTS, SDS_LIGHT_SOURCES, @@ -13,6 +11,7 @@ sd_zeros, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -37,20 +36,20 @@ class TestLuminousFlux: def test_luminous_flux(self) -> None: """Test :func:`colour.colorimetry.photometry.luminous_flux` definition.""" - np.testing.assert_allclose( - luminous_flux(SDS_ILLUMINANTS["FL2"].copy().normalise()), + xp_assert_close( + float(luminous_flux(SDS_ILLUMINANTS["FL2"].copy().normalise())), 28588.73612977, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminous_flux(SDS_LIGHT_SOURCES["Neodimium Incandescent"]), + xp_assert_close( + float(luminous_flux(SDS_LIGHT_SOURCES["Neodimium Incandescent"])), 23807.65552737, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminous_flux(SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"]), + xp_assert_close( + float(luminous_flux(SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"])), 13090.06759053, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -68,20 +67,20 @@ def test_luminous_efficiency(self) -> None: definition. """ - np.testing.assert_allclose( - luminous_efficiency(SDS_ILLUMINANTS["FL2"].copy().normalise()), + xp_assert_close( + float(luminous_efficiency(SDS_ILLUMINANTS["FL2"].copy().normalise())), 0.49317624, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminous_efficiency(SDS_LIGHT_SOURCES["Neodimium Incandescent"]), + xp_assert_close( + float(luminous_efficiency(SDS_LIGHT_SOURCES["Neodimium Incandescent"])), 0.19943936, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminous_efficiency(SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"]), + xp_assert_close( + float(luminous_efficiency(SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"])), 0.51080919, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -99,26 +98,26 @@ def test_luminous_efficacy(self) -> None: definition. """ - np.testing.assert_allclose( - luminous_efficacy(SDS_ILLUMINANTS["FL2"].copy().normalise()), + xp_assert_close( + float(luminous_efficacy(SDS_ILLUMINANTS["FL2"].copy().normalise())), 336.83937176, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminous_efficacy(SDS_LIGHT_SOURCES["Neodimium Incandescent"]), + xp_assert_close( + float(luminous_efficacy(SDS_LIGHT_SOURCES["Neodimium Incandescent"])), 136.21708032, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - luminous_efficacy(SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"]), + xp_assert_close( + float(luminous_efficacy(SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"])), 348.88267549, atol=TOLERANCE_ABSOLUTE_TESTS, ) sd = sd_zeros() sd[555] = 1 - np.testing.assert_allclose( - luminous_efficacy(sd), 683.00000000, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + float(luminous_efficacy(sd)), 683.00000000, atol=TOLERANCE_ABSOLUTE_TESTS ) diff --git a/colour/colorimetry/tests/test_spectrum.py b/colour/colorimetry/tests/test_spectrum.py index 584da69ae0..120e9d60c3 100644 --- a/colour/colorimetry/tests/test_spectrum.py +++ b/colour/colorimetry/tests/test_spectrum.py @@ -3,6 +3,10 @@ from __future__ import annotations import pickle +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType import numpy as np import pytest @@ -20,7 +24,14 @@ sds_and_msds_to_sds, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import is_caching_enabled, is_scipy_installed, tstack +from colour.utilities import ( + as_ndarray, + is_caching_enabled, + is_scipy_installed, + tstack, + xp_assert_close, + xp_assert_equal, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -1308,18 +1319,22 @@ def test_start(self) -> None: assert SpectralShape(360, 830, 1).start == 360 - pytest.raises(AssertionError, lambda: SpectralShape(360, 360, 1)) + with pytest.raises(AssertionError): + SpectralShape(360, 360, 1) - pytest.raises(AssertionError, lambda: SpectralShape(360, 0, 1)) + with pytest.raises(AssertionError): + SpectralShape(360, 0, 1) def test_end(self) -> None: """Test :attr:`colour.colorimetry.spectrum.SpectralShape.end` property.""" assert SpectralShape(360, 830, 1).end == 830 - pytest.raises(AssertionError, lambda: SpectralShape(830, 830, 1)) + with pytest.raises(AssertionError): + SpectralShape(830, 830, 1) - pytest.raises(AssertionError, lambda: SpectralShape(830, 0, 1)) + with pytest.raises(AssertionError): + SpectralShape(830, 0, 1) def test_interval(self) -> None: """ @@ -1347,7 +1362,7 @@ def test_wavelengths(self) -> None: property. """ - np.testing.assert_array_equal( + xp_assert_equal( SpectralShape(0, 10, 0.1).wavelengths, np.arange(0, 10 + 0.1, 0.1), ) @@ -1366,7 +1381,7 @@ def test__iter__(self) -> None: method. """ - np.testing.assert_array_equal( + xp_assert_equal( list(SpectralShape(0, 10, 0.1)), np.arange(0, 10 + 0.1, 0.1), ) @@ -1413,7 +1428,7 @@ def test__ne__(self) -> None: def test_range(self) -> None: """Test :func:`colour.colorimetry.spectrum.SpectralShape.range` method.""" - np.testing.assert_array_equal( + xp_assert_equal( list(SpectralShape(0, 10, 0.1)), np.arange(0, 10 + 0.1, 0.1), ) @@ -1476,63 +1491,75 @@ def test_pickling(self) -> None: data = pickle.loads(data) # noqa: S301 assert self._sd == data - def test_display_name(self) -> None: + def test_display_name(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.SpectralDistribution.display_name` property. """ - assert self._sd.display_name == "Sample" - assert self._non_uniform_sd.display_name == "Display Non Uniform Sample" + sd = self._sd.copy(xp=xp) + assert sd.display_name == "Sample" + assert ( + self._non_uniform_sd.copy(xp=xp).display_name + == "Display Non Uniform Sample" + ) - def test_wavelengths(self) -> None: + def test_wavelengths(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.SpectralDistribution.wavelengths` property. """ - np.testing.assert_array_equal(self._sd.wavelengths, self._sd.domain) + sd = self._sd.copy(xp=xp) + + xp_assert_equal(sd.wavelengths, sd.domain) - sd = self._sd.copy() - sd.wavelengths = sd.wavelengths + 10 - np.testing.assert_array_equal(sd.wavelengths, sd.domain) + sd_c = sd.copy() + sd_c.wavelengths = sd_c.wavelengths + 10 + xp_assert_equal(sd_c.wavelengths, sd_c.domain) - def test_values(self) -> None: + def test_values(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.SpectralDistribution.values` property. """ - np.testing.assert_array_equal(self._sd.values, self._sd.range) + sd = self._sd.copy(xp=xp) - sd = self._sd.copy() - sd.values = sd.values + 10 - np.testing.assert_array_equal(sd.values, sd.range) + xp_assert_equal(sd.values, as_ndarray(sd.range)) - def test_shape(self) -> None: + sd_c = sd.copy() + sd_c.values = sd_c.values + 10 + xp_assert_equal(sd_c.values, as_ndarray(sd_c.range)) + + def test_shape(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.SpectralDistribution.shape` property. """ - assert self._sd.shape == SpectralShape(340, 820, 20) + sd = self._sd.copy(xp=xp) + assert sd.shape == SpectralShape(340, 820, 20) - def test__init__(self) -> None: + def test__init__(self, xp: ModuleType) -> None: """ Test :meth:`colour.colorimetry.spectrum.SpectralDistribution.__init__` method. """ - np.testing.assert_allclose( - SpectralDistribution(DATA_SAMPLE).wavelengths, + xp_assert_close( + SpectralDistribution(DATA_SAMPLE).copy(xp=xp).wavelengths, SpectralDistribution( DATA_SAMPLE.values(), SpectralShape(340, 820, 20), - ).wavelengths, + ) + .copy(xp=xp) + .wavelengths, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_interpolate(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_interpolate(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ SpectralDistribution.interpolate` method. @@ -1542,9 +1569,11 @@ def test_interpolate(self) -> None: return shape = SpectralShape(self._sd.shape.start, self._sd.shape.end, 1) - sd = reshape_sd(self._sd, shape, "Interpolate") - np.testing.assert_allclose( - sd.values, DATA_SAMPLE_INTERPOLATED, atol=TOLERANCE_ABSOLUTE_TESTS + sd = reshape_sd(self._sd.copy(xp=xp), shape, "Interpolate") + xp_assert_close( + sd.values, + DATA_SAMPLE_INTERPOLATED, + atol=TOLERANCE_ABSOLUTE_TESTS, ) assert sd.shape == shape @@ -1554,11 +1583,11 @@ def test_interpolate(self) -> None: 1, ) sd = reshape_sd( - self._non_uniform_sd, + self._non_uniform_sd.copy(xp=xp), shape, "Interpolate", ) - np.testing.assert_allclose( + xp_assert_close( sd.values, DATA_SAMPLE_INTERPOLATED_NON_UNIFORM, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1569,7 +1598,7 @@ def test_interpolate(self) -> None: 1, ) - def test_extrapolate(self) -> None: + def test_extrapolate(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ SpectralDistribution.extrapolate` method. @@ -1579,13 +1608,15 @@ def test_extrapolate(self) -> None: return data = dict(zip(range(25, 35), [0] * 5 + [1] * 5, strict=True)) - sd = SpectralDistribution(data) + sd = SpectralDistribution(data).copy(xp=xp) sd.extrapolate(SpectralShape(10, 50, 5)) - np.testing.assert_allclose(sd[10], 0, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(sd[50], 1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(float(sd[10]), 0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(float(sd[50]), 1, atol=TOLERANCE_ABSOLUTE_TESTS) - sd = SpectralDistribution(np.linspace(0, 1, 10), np.linspace(25, 35, 10)) + sd = SpectralDistribution(np.linspace(0, 1, 10), np.linspace(25, 35, 10)).copy( + xp=xp + ) shape = SpectralShape(10, 50, 10) sd.extrapolate( shape, @@ -1596,56 +1627,56 @@ def test_extrapolate(self) -> None: }, ) - np.testing.assert_allclose( - sd[10], -1.5000000000000004, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + float(sd[10]), -1.5000000000000004, atol=TOLERANCE_ABSOLUTE_TESTS ) - np.testing.assert_allclose( - sd[50], 2.4999999999999964, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + float(sd[50]), 2.4999999999999964, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_align(self) -> None: + def test_align(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ SpectralDistribution.align` method. """ shape = SpectralShape(100, 900, 5) - assert self._sd.copy().align(shape).shape == shape + assert self._sd.copy(xp=xp).align(shape).shape == shape shape = SpectralShape(600, 650, 1) - assert self._sd.copy().align(shape).shape == shape + assert self._sd.copy(xp=xp).align(shape).shape == shape - def test_trim(self) -> None: + def test_trim(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ SpectralDistribution.trim` method. """ shape = SpectralShape(400, 700, 20) - assert self._sd.copy().trim(shape).shape == shape + assert self._sd.copy(xp=xp).trim(shape).shape == shape shape = SpectralShape(200, 900, 1) - assert self._sd.copy().trim(shape).shape == self._sd.shape + assert self._sd.copy(xp=xp).trim(shape).shape == self._sd.shape - def test_normalise(self) -> None: + def test_normalise(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ SpectralDistribution.normalise` method. """ - np.testing.assert_allclose( - self._sd.copy().normalise(100).values, + xp_assert_close( + self._sd.copy(xp=xp).normalise(100).values, DATA_SAMPLE_NORMALISED, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_callback_on_domain_changed(self) -> None: + def test_callback_on_domain_changed(self, xp: ModuleType) -> None: """ Test :class:`colour.colorimetry.spectrum.\ SpectralDistribution` *on_domain_changed* callback. """ - sd = self._sd.copy() + sd = self._sd.copy(xp=xp) assert sd.shape == SpectralShape(340, 820, 20) sd[840] = 0 assert sd.shape == SpectralShape(340, 840, 20) @@ -1737,41 +1768,46 @@ def test_pickling(self) -> None: data = pickle.loads(data) # noqa: S301 assert self._msds == data - def test_display_name(self) -> None: + def test_display_name(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.MultiSpectralDistributions.display_name` property. """ - assert self._sample_msds.display_name == "Sample Observer" + msds = self._sample_msds.copy(xp=xp) + assert msds.display_name == "Sample Observer" assert ( - self._non_uniform_sample_msds.display_name + self._non_uniform_sample_msds.copy(xp=xp).display_name == "Display Non Uniform Sample Observer" ) - def test_wavelengths(self) -> None: + def test_wavelengths(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.MultiSpectralDistributions.wavelengths` property. """ - np.testing.assert_array_equal(self._msds.wavelengths, self._msds.domain) + msds = self._msds.copy(xp=xp) + + xp_assert_equal(msds.wavelengths, msds.domain) - msds = self._msds.copy() - msds.wavelengths = msds.wavelengths + 10 - np.testing.assert_array_equal(msds.wavelengths, msds.domain) + msds_c = msds.copy() + msds_c.wavelengths = msds_c.wavelengths + 10 + xp_assert_equal(msds_c.wavelengths, msds_c.domain) - def test_values(self) -> None: + def test_values(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.MultiSpectralDistributions.values` property. """ - np.testing.assert_array_equal(self._msds.values, self._msds.range) + msds = self._msds.copy(xp=xp) - msds = self._msds.copy() - msds.values = msds.values + 10 - np.testing.assert_array_equal(msds.values, msds.range) + xp_assert_equal(msds.values, as_ndarray(msds.range)) + + msds_c = msds.copy() + msds_c.values = msds_c.values + 10 + xp_assert_equal(msds_c.values, as_ndarray(msds_c.range)) def test_display_labels(self) -> None: """ @@ -1786,30 +1822,34 @@ def test_display_labels(self) -> None: "Display z_bar", ) - def test_shape(self) -> None: + def test_shape(self, xp: ModuleType) -> None: """ Test :attr:`colour.colorimetry.spectrum.MultiSpectralDistributions.shape` property. """ - assert self._msds.shape == SpectralShape(380, 780, 5) + msds = self._msds.copy(xp=xp) + assert msds.shape == SpectralShape(380, 780, 5) - def test__init__(self) -> None: + def test__init__(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ MultiSpectralDistributions.__init__` method. """ - np.testing.assert_allclose( - MultiSpectralDistributions(DATA_CMFS).wavelengths, + xp_assert_close( + MultiSpectralDistributions(DATA_CMFS).copy(xp=xp).wavelengths, MultiSpectralDistributions( DATA_CMFS.values(), SpectralShape(380, 780, 5), - ).wavelengths, + ) + .copy(xp=xp) + .wavelengths, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_interpolate(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_interpolate(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ MultiSpectralDistributions.interpolate` method. @@ -1821,10 +1861,10 @@ def test_interpolate(self) -> None: shape = SpectralShape( self._sample_msds.shape.start, self._sample_msds.shape.end, 1 ) - msds = reshape_msds(self._sample_msds, shape, "Interpolate") + msds = reshape_msds(self._sample_msds.copy(xp=xp), shape, "Interpolate") for signal in msds.signals.values(): - np.testing.assert_allclose( - signal.values, # pyright: ignore + xp_assert_close( + signal.values, DATA_SAMPLE_INTERPOLATED, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1836,13 +1876,13 @@ def test_interpolate(self) -> None: 1, ) msds = reshape_msds( - self._non_uniform_sample_msds, + self._non_uniform_sample_msds.copy(xp=xp), shape, "Interpolate", ) for signal in msds.signals.values(): - np.testing.assert_allclose( - signal.values, # pyright: ignore + xp_assert_close( + signal.values, DATA_SAMPLE_INTERPOLATED_NON_UNIFORM, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1852,7 +1892,7 @@ def test_interpolate(self) -> None: 1, ) - def test_extrapolate(self) -> None: + def test_extrapolate(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ MultiSpectralDistributions.extrapolate` method. @@ -1862,19 +1902,23 @@ def test_extrapolate(self) -> None: return data = dict(zip(range(25, 35), tstack([[0] * 5 + [1] * 5] * 3), strict=True)) - msds = MultiSpectralDistributions(data) + msds = MultiSpectralDistributions(data).copy(xp=xp) msds.extrapolate(SpectralShape(10, 50, 5)) - np.testing.assert_allclose( - msds[10], np.array([0.0, 0.0, 0.0]), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + msds[10], + [0.0, 0.0, 0.0], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - msds[50], np.array([1.0, 1.0, 1.0]), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + msds[50], + [1.0, 1.0, 1.0], + atol=TOLERANCE_ABSOLUTE_TESTS, ) msds = MultiSpectralDistributions( tstack([np.linspace(0, 1, 10)] * 3), np.linspace(25, 35, 10) - ) + ).copy(xp=xp) msds.extrapolate( SpectralShape(10, 50, 10), extrapolator_kwargs={ @@ -1883,22 +1927,24 @@ def test_extrapolate(self) -> None: "right": None, }, ) - np.testing.assert_allclose( + xp_assert_close( msds[10], - np.array([-1.5, -1.5, -1.5]), + [-1.5, -1.5, -1.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - msds[50], np.array([2.5, 2.5, 2.5]), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + msds[50], + [2.5, 2.5, 2.5], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_align(self) -> None: + def test_align(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ MultiSpectralDistributions.align` method. """ - msds = self._sample_msds.copy() + msds = self._sample_msds.copy(xp=xp) shape = SpectralShape(100, 900, 5) assert msds.align(shape).shape == shape @@ -1906,31 +1952,31 @@ def test_align(self) -> None: shape = SpectralShape(600, 650, 1) assert msds.align(shape).shape == shape - def test_trim(self) -> None: + def test_trim(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ MultiSpectralDistributions.trim` method. """ shape = SpectralShape(400, 700, 5) - assert self._msds.copy().trim(shape).shape == shape + assert self._msds.copy(xp=xp).trim(shape).shape == shape shape = SpectralShape(200, 900, 1) - assert self._msds.copy().trim(shape).shape == self._msds.shape + assert self._msds.copy(xp=xp).trim(shape).shape == self._msds.shape - def test_normalise(self) -> None: + def test_normalise(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum. MultiSpectralDistributions.normalise` method. """ - np.testing.assert_allclose( - self._sample_msds.copy().normalise(100).values, + xp_assert_close( + self._sample_msds.copy(xp=xp).normalise(100).values, tstack([DATA_SAMPLE_NORMALISED] * 3), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_to_sds(self) -> None: + def test_to_sds(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.spectrum.\ MultiSpectralDistributions.to_sds` method. @@ -1939,21 +1985,21 @@ def test_to_sds(self) -> None: if not is_scipy_installed(): # pragma: no cover return - sds = self._non_uniform_sample_msds.to_sds() + sds = self._non_uniform_sample_msds.copy(xp=xp).to_sds() assert len(sds) == 3 for i, sd in enumerate(sds): assert sd.name == self._labels[i] assert sd.display_name == self._display_labels[i] - def test_callback_on_domain_changed(self) -> None: + def test_callback_on_domain_changed(self, xp: ModuleType) -> None: """ Test underlying :class:`colour.colorimetry.spectrum.\ SpectralDistribution` *on_domain_changed* callback when used with :class:`colour.colorimetry.spectrum.MultiSpectralDistributions` class. """ - msds = self._msds.copy() + msds = self._msds.copy(xp=xp) assert msds.shape == SpectralShape(380, 780, 5) msds[785] = 0 assert msds.shape == SpectralShape(380, 785, 5) @@ -2073,7 +2119,7 @@ def test_sds_and_msds_to_msds(self) -> None: assert sds_and_msds_to_msds(multi_sds_1) == multi_sds_1 multi_sds_0 = sds_and_msds_to_msds([multi_sds_1]) - np.testing.assert_array_equal(multi_sds_0.range, multi_sds_1.range) + xp_assert_equal(multi_sds_0.range, multi_sds_1.range) assert sds_and_msds_to_msds([multi_sds_1]) == multi_sds_1 shape = SpectralShape(500, 560, 10) @@ -2081,13 +2127,13 @@ def test_sds_and_msds_to_msds(self) -> None: sds_and_msds_to_msds([sd_1, sd_2, multi_sds_1, multi_sds_2]).shape == shape ) - np.testing.assert_allclose( + xp_assert_close( sds_and_msds_to_msds([sd_1, sd_2, multi_sds_1, multi_sds_2]).wavelengths, shape.wavelengths, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sds_and_msds_to_msds([sd_1, sd_2, multi_sds_1, multi_sds_2]).values, tstack( [sd_1.align(shape).values, sd_2.align(shape).values] diff --git a/colour/colorimetry/tests/test_transformations.py b/colour/colorimetry/tests/test_transformations.py index 99292f6c25..ac3cc3ae21 100644 --- a/colour/colorimetry/tests/test_transformations.py +++ b/colour/colorimetry/tests/test_transformations.py @@ -5,8 +5,13 @@ from __future__ import annotations +import typing + import numpy as np +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.colorimetry import ( MSDS_CMFS, LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs, @@ -16,7 +21,13 @@ RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -47,19 +58,27 @@ def test_RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(self) -> None: """ cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] - np.testing.assert_allclose( - RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(435), cmfs[435], atol=0.0025 + xp_assert_close( + RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(435), + cmfs[435], + atol=TOLERANCE_ABSOLUTE_TESTS * 25000, ) - np.testing.assert_allclose( - RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(545), cmfs[545], atol=0.0025 + xp_assert_close( + RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(545), + cmfs[545], + atol=TOLERANCE_ABSOLUTE_TESTS * 25000, ) - np.testing.assert_allclose( - RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(700), cmfs[700], atol=0.0025 + xp_assert_close( + RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(700), + cmfs[700], + atol=TOLERANCE_ABSOLUTE_TESTS * 25000, ) - def test_n_dimensional_RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(self) -> None: + def test_n_dimensional_RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.transformations.\ RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs` definition n-dimensional arrays @@ -67,27 +86,27 @@ def test_n_dimensional_RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(self) -> None: """ wl = 700 - XYZ = RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl) + XYZ = as_ndarray(RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl)) - wl = np.tile(wl, 6) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - XYZ = np.reshape(XYZ, (2, 3, 1, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1, 3), xp=xp) + xp_assert_close( RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -118,25 +137,27 @@ def test_RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(self) -> None: """ cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(435), cmfs[435], - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(545), cmfs[545], - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(700), cmfs[700], - atol=0.025, + atol=TOLERANCE_ABSOLUTE_TESTS * 250000, ) - def test_n_dimensional_RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(self) -> None: + def test_n_dimensional_RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.transformations.\ RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs` definition n-dimensional arrays @@ -144,27 +165,27 @@ def test_n_dimensional_RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(self) -> None: """ wl = 700 - XYZ = RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl) + XYZ = as_ndarray(RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl)) - wl = np.tile(wl, 6) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - XYZ = np.reshape(XYZ, (2, 3, 1, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1, 3), xp=xp) + xp_assert_close( RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -195,25 +216,27 @@ def test_RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(self) -> None: """ cmfs = MSDS_CMFS["Stockman & Sharpe 10 Degree Cone Fundamentals"] - np.testing.assert_allclose( + xp_assert_close( RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(435), cmfs[435], - atol=0.0025, + atol=TOLERANCE_ABSOLUTE_TESTS * 25000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(545), cmfs[545], - atol=0.0025, + atol=TOLERANCE_ABSOLUTE_TESTS * 25000, ) - np.testing.assert_allclose( + xp_assert_close( RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(700), cmfs[700], - atol=0.0025, + atol=TOLERANCE_ABSOLUTE_TESTS * 25000, ) - def test_n_dimensional_RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(self) -> None: + def test_n_dimensional_RGB_10_degree_cmfs_to_LMS_10_degree_cmfs( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.transformations.\ RGB_10_degree_cmfs_to_LMS_10_degree_cmfs` definition n-dimensional arrays @@ -221,27 +244,27 @@ def test_n_dimensional_RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(self) -> None: """ wl = 700 - LMS = RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(wl) + LMS = as_ndarray(RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(wl)) - wl = np.tile(wl, 6) - LMS = np.tile(LMS, (6, 1)) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + LMS = xp.tile(xp_as_array(LMS, xp=xp), (6, 1)) + xp_assert_close( RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(wl), LMS, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - LMS = np.reshape(LMS, (2, 3, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + LMS = xp_reshape(xp_as_array(LMS, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(wl), LMS, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - LMS = np.reshape(LMS, (2, 3, 1, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + LMS = xp_reshape(xp_as_array(LMS, xp=xp), (2, 3, 1, 3), xp=xp) + xp_assert_close( RGB_10_degree_cmfs_to_LMS_10_degree_cmfs(wl), LMS, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -272,25 +295,27 @@ def test_LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(self) -> None: """ cmfs = MSDS_CMFS["CIE 2015 2 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(435), cmfs[435], - atol=0.00015, + atol=TOLERANCE_ABSOLUTE_TESTS * 1500, ) - np.testing.assert_allclose( + xp_assert_close( LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(545), cmfs[545], - atol=0.00015, + atol=TOLERANCE_ABSOLUTE_TESTS * 1500, ) - np.testing.assert_allclose( + xp_assert_close( LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(700), cmfs[700], - atol=0.00015, + atol=TOLERANCE_ABSOLUTE_TESTS * 1500, ) - def test_n_dimensional_LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(self) -> None: + def test_n_dimensional_LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.transformations.\ LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs` definition n-dimensional arrays @@ -298,27 +323,27 @@ def test_n_dimensional_LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(self) -> None: """ wl = 700 - XYZ = LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl) + XYZ = as_ndarray(LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl)) - wl = np.tile(wl, 6) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - XYZ = np.reshape(XYZ, (2, 3, 1, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1, 3), xp=xp) + xp_assert_close( LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -349,25 +374,27 @@ def test_LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(self) -> None: """ cmfs = MSDS_CMFS["CIE 2015 10 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(435), cmfs[435], - atol=0.00015, + atol=TOLERANCE_ABSOLUTE_TESTS * 1500, ) - np.testing.assert_allclose( + xp_assert_close( LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(545), cmfs[545], - atol=0.00015, + atol=TOLERANCE_ABSOLUTE_TESTS * 1500, ) - np.testing.assert_allclose( + xp_assert_close( LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(700), cmfs[700], - atol=0.00015, + atol=TOLERANCE_ABSOLUTE_TESTS * 1500, ) - def test_n_dimensional_LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(self) -> None: + def test_n_dimensional_LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.transformations.\ LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs` definition n-dimensional arrays @@ -375,27 +402,27 @@ def test_n_dimensional_LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(self) -> None: """ wl = 700 - XYZ = LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl) + XYZ = as_ndarray(LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl)) - wl = np.tile(wl, 6) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - XYZ = np.reshape(XYZ, (2, 3, 1, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1, 3), xp=xp) + xp_assert_close( LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs(wl), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/tests/test_tristimulus_values.py b/colour/colorimetry/tests/test_tristimulus_values.py index 748cb5d6aa..a218d2b2df 100644 --- a/colour/colorimetry/tests/test_tristimulus_values.py +++ b/colour/colorimetry/tests/test_tristimulus_values.py @@ -30,6 +30,7 @@ msds_to_XYZ, msds_to_XYZ_ASTME308, msds_to_XYZ_integration, + msds_to_XYZ_tristimulus_weighting_factors_ASTME308, reshape_msds, reshape_sd, sd_CIE_standard_illuminant_A, @@ -47,9 +48,14 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat + from colour.hints import NDArrayFloat, ModuleType -from colour.utilities import domain_range_scale +from colour.utilities import ( + domain_range_scale, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -719,13 +725,13 @@ def test_lagrange_coefficients_ASTME2022(self) -> None: lagrange_coefficients_ASTME2022` definition. """ - np.testing.assert_allclose( + xp_assert_close( lagrange_coefficients_ASTME2022(10, "inner"), LAGRANGE_COEFFICIENTS_A, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( lagrange_coefficients_ASTME2022(10, "boundary"), LAGRANGE_COEFFICIENTS_B, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -734,7 +740,7 @@ def test_lagrange_coefficients_ASTME2022(self) -> None: # Testing that the cache returns a copy of the data. lagrange_coefficients = lagrange_coefficients_ASTME2022(10) - np.testing.assert_allclose( + xp_assert_close( lagrange_coefficients, LAGRANGE_COEFFICIENTS_A, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -742,7 +748,7 @@ def test_lagrange_coefficients_ASTME2022(self) -> None: lagrange_coefficients *= 10 - np.testing.assert_allclose( + xp_assert_close( lagrange_coefficients_ASTME2022(10), LAGRANGE_COEFFICIENTS_A, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -777,12 +783,16 @@ def test_tristimulus_weighting_factors_ASTME2022(self) -> None: twf = tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 10) ) - np.testing.assert_allclose(np.round(twf, 3), TWF_A_CIE_1964_10_10, atol=1e-5) + xp_assert_close( + np.around(twf, 3), TWF_A_CIE_1964_10_10, atol=TOLERANCE_ABSOLUTE_TESTS * 100 + ) twf = tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 20) ) - np.testing.assert_allclose(np.round(twf, 3), TWF_A_CIE_1964_10_20, atol=1e-5) + xp_assert_close( + np.around(twf, 3), TWF_A_CIE_1964_10_20, atol=TOLERANCE_ABSOLUTE_TESTS * 100 + ) cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] D65 = reshape_sd( @@ -791,8 +801,8 @@ def test_tristimulus_weighting_factors_ASTME2022(self) -> None: twf = tristimulus_weighting_factors_ASTME2022( cmfs, D65, SpectralShape(360, 830, 20) ) - np.testing.assert_allclose( - np.round(twf, 3), + xp_assert_close( + np.around(twf, 3), TWF_D65_CIE_1931_2_20, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -800,23 +810,21 @@ def test_tristimulus_weighting_factors_ASTME2022(self) -> None: twf = tristimulus_weighting_factors_ASTME2022( cmfs, D65, SpectralShape(360, 830, 20), k=1 ) - np.testing.assert_allclose( - twf, TWF_D65_CIE_1931_2_20_K1, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(twf, TWF_D65_CIE_1931_2_20_K1, atol=TOLERANCE_ABSOLUTE_TESTS) # Testing that the cache returns a copy of the data. cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] twf = tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 10) ) - np.testing.assert_allclose( - np.round(twf, 3), + xp_assert_close( + np.around(twf, 3), TWF_A_CIE_1964_10_10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.round( + xp_assert_close( + np.around( tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 10) ), @@ -838,21 +846,11 @@ def test_raise_exception_tristimulus_weighting_factors_ASTME2022(self) -> None: A_1 = sd_CIE_standard_illuminant_A(cmfs_1.shape) A_2 = sd_CIE_standard_illuminant_A(cmfs_2.shape) - pytest.raises( - ValueError, - tristimulus_weighting_factors_ASTME2022, - cmfs_1, - A_2, - shape, - ) + with pytest.raises(ValueError): + tristimulus_weighting_factors_ASTME2022(cmfs_1, A_2, shape) - pytest.raises( - ValueError, - tristimulus_weighting_factors_ASTME2022, - cmfs_2, - A_1, - shape, - ) + with pytest.raises(ValueError): + tristimulus_weighting_factors_ASTME2022(cmfs_2, A_1, shape) class TestTristimulusWeightingFactorsIntegration: @@ -873,8 +871,8 @@ def test_tristimulus_weighting_factors_integration(self) -> None: twf = tristimulus_weighting_factors_integration( cmfs, A, SpectralShape(360, 830, 20) ) - np.testing.assert_allclose( - np.round(twf, 3), + xp_assert_close( + np.around(twf, 3), TWF_INTEGRATION_A_CIE_1964_10_20, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -886,8 +884,8 @@ def test_tristimulus_weighting_factors_integration(self) -> None: twf = tristimulus_weighting_factors_integration( cmfs, D65, SpectralShape(360, 830, 20) ) - np.testing.assert_allclose( - np.round(twf, 3), + xp_assert_close( + np.around(twf, 3), TWF_INTEGRATION_D65_CIE_1931_2_20, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -895,7 +893,7 @@ def test_tristimulus_weighting_factors_integration(self) -> None: twf = tristimulus_weighting_factors_integration( cmfs, D65, SpectralShape(360, 830, 20), k=1 ) - np.testing.assert_allclose( + xp_assert_close( twf, TWF_INTEGRATION_D65_CIE_1931_2_20_K1, atol=TOLERANCE_ABSOLUTE_TESTS ) @@ -906,15 +904,18 @@ class TestAdjustTristimulusWeightingFactorsASTME308: adjust_tristimulus_weighting_factors_ASTME308` definition unit tests methods. """ - def test_adjust_tristimulus_weighting_factors_ASTME308(self) -> None: + def test_adjust_tristimulus_weighting_factors_ASTME308( + self, + xp: ModuleType, + ) -> None: """ Test :func:`colour.colorimetry.tristimulus_values.\ adjust_tristimulus_weighting_factors_ASTME308` definition. """ - np.testing.assert_allclose( + xp_assert_close( adjust_tristimulus_weighting_factors_ASTME308( - TWF_D65_CIE_1931_2_20, + xp_as_array(TWF_D65_CIE_1931_2_20, xp=xp), SpectralShape(360, 830, 20), SpectralShape(400, 700, 20), ), @@ -929,61 +930,61 @@ class TestSd_to_XYZ_integration: definition unit tests methods. """ - def test_sd_to_XYZ_integration(self) -> None: + def test_sd_to_XYZ_integration(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.tristimulus_values.\ sd_to_XYZ_integration` definition. """ cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["A"]), - np.array([14.46341147, 10.85819624, 2.04695585]), + [14.46341147, 10.85819624, 2.04695585], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( - SD_SAMPLE.values, + xp_as_array(SD_SAMPLE.values, xp=xp), cmfs, SDS_ILLUMINANTS["A"], shape=SD_SAMPLE.shape, ), - np.array([14.46365947, 10.85828084, 2.04663993]), + [14.46365947, 10.85828084, 2.04663993], atol=TOLERANCE_ABSOLUTE_TESTS, ) cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["C"]), - np.array([10.77002699, 9.44876636, 6.62415290]), + [10.77002699, 9.44876636, 6.62415290], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"]), - np.array([11.57540576, 9.98608874, 3.95242590]), + [11.57540576, 9.98608874, 3.95242590], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"], k=683), - np.array([1223.7509261493, 1055.7284645912, 417.8501342332]), + [1223.7509261493, 1055.7284645912, 417.8501342332], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"], shape=SpectralShape(400, 700, 20), ), - np.array([11.98232967, 10.13543929, 3.66442524]), + [11.98232967, 10.13543929, 3.66442524], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_sd_to_XYZ_integration(self) -> None: + def test_domain_range_scale_sd_to_XYZ_integration(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.tristimulus_values.\ sd_to_XYZ_integration` definition domain and range scale support. @@ -995,7 +996,7 @@ def test_domain_range_scale_sd_to_XYZ_integration(self) -> None: d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["A"]), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1009,7 +1010,10 @@ class TestSd_to_XYZ_tristimulus_weighting_factors_ASTME308: definition unit tests methods. """ - def test_sd_to_XYZ_tristimulus_weighting_factors_ASTME308(self) -> None: + def test_sd_to_XYZ_tristimulus_weighting_factors_ASTME308( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.colorimetry.tristimulus_values.\ sd_to_XYZ_tristimulus_weighting_factors_ASTME308` @@ -1017,73 +1021,73 @@ def test_sd_to_XYZ_tristimulus_weighting_factors_ASTME308(self) -> None: """ cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["A"] ), - np.array([14.46341867, 10.85820227, 2.04697034]), + [14.46341867, 10.85820227, 2.04697034], atol=TOLERANCE_ABSOLUTE_TESTS, ) cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["C"] ), - np.array([10.77005571, 9.44877491, 6.62428210]), + [10.77005571, 9.44877491, 6.62428210], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"] ), - np.array([11.57542759, 9.98605604, 3.95273304]), + [11.57542759, 9.98605604, 3.95273304], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 5), "Trim"), cmfs, SDS_ILLUMINANTS["A"], ), - np.array([14.38153638, 10.74503131, 2.01613844]), + [14.38153638, 10.74503131, 2.01613844], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 10), "Interpolate"), cmfs, SDS_ILLUMINANTS["A"], ), - np.array([14.38257202, 10.74568178, 2.01588427]), + [14.38257202, 10.74568178, 2.01588427], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 20), "Interpolate"), cmfs, SDS_ILLUMINANTS["A"], ), - np.array([14.38329645, 10.74603515, 2.01561113]), + [14.38329645, 10.74603515, 2.01561113], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 20), "Interpolate"), cmfs, SDS_ILLUMINANTS["A"], k=1, ), - np.array([1636.74881983, 1222.84626486, 229.36669308]), + [1636.74881983, 1222.84626486, 229.36669308], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_sd_to_XYZ_twf_ASTME308(self) -> None: + def test_domain_range_scale_sd_to_XYZ_twf_ASTME308(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.tristimulus_values.\ sd_to_XYZ_tristimulus_weighting_factors_ASTME308` definition domain and @@ -1098,7 +1102,7 @@ def test_domain_range_scale_sd_to_XYZ_twf_ASTME308(self) -> None: d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["A"] ), @@ -1126,54 +1130,54 @@ def test_sd_to_XYZ_ASTME308_mi_1nm(self) -> None: definition for 1 nm measurement intervals. """ - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, self._cmfs.shape), self._cmfs, self._A ), - np.array([14.46372680, 10.85832950, 2.04663200]), + [14.46372680, 10.85832950, 2.04663200], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, self._cmfs.shape), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.46366018, 10.85827949, 2.04662258]), + [14.46366018, 10.85827949, 2.04662258], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 1)), self._cmfs, self._A, ), - np.array([14.54173397, 10.88628632, 2.04965822]), + [14.54173397, 10.88628632, 2.04965822], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 1)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.54203076, 10.88636754, 2.04964877]), + [14.54203076, 10.88636754, 2.04964877], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 1)), self._cmfs, self._A, k=1, ), - np.array([15.6898152997, 11.7457671769, 2.2114803420]), + [15.6898152997, 11.7457671769, 2.2114803420], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1183,71 +1187,71 @@ def test_sd_to_XYZ_ASTME308_mi_5nm(self) -> None: definition for 5 nm measurement intervals. """ - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, ), - np.array([14.46372173, 10.85832502, 2.04664734]), + [14.46372173, 10.85832502, 2.04664734], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.46366388, 10.85828159, 2.04663915]), + [14.46366388, 10.85828159, 2.04663915], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, mi_5nm_omission_method=False, ), - np.array([14.46373399, 10.85833553, 2.0466465]), + [14.46373399, 10.85833553, 2.0466465], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, ), - np.array([14.54025742, 10.88576251, 2.04950226]), + [14.54025742, 10.88576251, 2.04950226], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.54051517, 10.88583304, 2.04949406]), + [14.54051517, 10.88583304, 2.04949406], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, mi_5nm_omission_method=False, ), - np.array([14.54022093, 10.88575468, 2.04951057]), + [14.54022093, 10.88575468, 2.04951057], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, @@ -1255,11 +1259,11 @@ def test_sd_to_XYZ_ASTME308_mi_5nm(self) -> None: use_practice_range=False, mi_5nm_omission_method=False, ), - np.array([14.46366737, 10.85828552, 2.04663707]), + [14.46366737, 10.85828552, 2.04663707], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, @@ -1267,18 +1271,18 @@ def test_sd_to_XYZ_ASTME308_mi_5nm(self) -> None: use_practice_range=False, mi_5nm_omission_method=False, ), - np.array([14.54051772, 10.88583590, 2.04950113]), + [14.54051772, 10.88583590, 2.04950113], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, k=1, ), - np.array([15.6882479013, 11.7452212708, 2.2113156963]), + [15.6882479013, 11.7452212708, 2.2113156963], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1288,67 +1292,67 @@ def test_sd_to_XYZ_ASTME308_mi_10nm(self) -> None: definition for 10 nm measurement intervals. """ - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 10)), self._cmfs, self._A, ), - np.array([14.47779980, 10.86358645, 2.04751388]), + [14.47779980, 10.86358645, 2.04751388], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 10)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.47773312, 10.86353641, 2.04750445]), + [14.47773312, 10.86353641, 2.04750445], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 10)), self._cmfs, self._A, ), - np.array([14.54137532, 10.88641727, 2.04931318]), + [14.54137532, 10.88641727, 2.04931318], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 10)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.54167211, 10.88649849, 2.04930374]), + [14.54167211, 10.88649849, 2.04930374], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 10)), self._cmfs, self._A, k=1, ), - np.array([15.6894283333, 11.7459084705, 2.2111080639]), + [15.6894283333, 11.7459084705, 2.2111080639], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(401, 701, 10)), self._cmfs, self._A, k=1, ), - np.array([15.6713226093, 11.7392254489, 2.2117708792]), + [15.6713226093, 11.7392254489, 2.2117708792], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1358,71 +1362,71 @@ def test_sd_to_XYZ_ASTME308_mi_20nm(self) -> None: definition for 20 nm measurement intervals. """ - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, ), - np.array([14.50187464, 10.87217124, 2.04918305]), + [14.50187464, 10.87217124, 2.04918305], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.50180785, 10.87212116, 2.04917361]), + [14.50180785, 10.87212116, 2.04917361], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, mi_20nm_interpolation_method=False, ), - np.array([14.50216194, 10.87236873, 2.04977256]), + [14.50216194, 10.87236873, 2.04977256], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, ), - np.array([14.54114025, 10.88634755, 2.04916445]), + [14.54114025, 10.88634755, 2.04916445], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, use_practice_range=False, ), - np.array([14.54143704, 10.88642877, 2.04915501]), + [14.54143704, 10.88642877, 2.04915501], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, mi_20nm_interpolation_method=False, ), - np.array([14.54242562, 10.88694088, 2.04919645]), + [14.54242562, 10.88694088, 2.04919645], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, @@ -1430,11 +1434,11 @@ def test_sd_to_XYZ_ASTME308_mi_20nm(self) -> None: use_practice_range=False, mi_20nm_interpolation_method=False, ), - np.array([14.50209515, 10.87231865, 2.04976312]), + [14.50209515, 10.87231865, 2.04976312], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, @@ -1442,28 +1446,28 @@ def test_sd_to_XYZ_ASTME308_mi_20nm(self) -> None: use_practice_range=False, mi_20nm_interpolation_method=False, ), - np.array([14.54272240, 10.88702210, 2.04918701]), + [14.54272240, 10.88702210, 2.04918701], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, k=1, ), - np.array([15.6891747040, 11.7458332427, 2.2109475945]), + [15.6891747040, 11.7458332427, 2.2109475945], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(401, 701, 20)), self._cmfs, self._A, ), - np.array([14.5220164311, 10.8790959535, 2.0490905325]), + [14.5220164311, 10.8790959535, 2.0490905325], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1473,11 +1477,8 @@ def test_raise_exception_sd_to_XYZ_ASTME308(self) -> None: definition raised exception. """ - pytest.raises( - ValueError, - sd_to_XYZ_ASTME308, - reshape_sd(self._sd, SpectralShape(360, 820, 2)), - ) + with pytest.raises(ValueError): + sd_to_XYZ_ASTME308(reshape_sd(self._sd, SpectralShape(360, 820, 2))) class TestSd_to_XYZ: @@ -1502,21 +1503,21 @@ def test_sd_to_XYZ(self) -> None: # Testing that the cache returns a copy of the data. XYZ = sd_to_XYZ(self._sd, self._cmfs, self._A) - np.testing.assert_allclose( + xp_assert_close( XYZ, - np.array([14.46372680, 10.85832950, 2.04663200]), + [14.46372680, 10.85832950, 2.04663200], atol=TOLERANCE_ABSOLUTE_TESTS, ) XYZ *= 10 - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ(self._sd, self._cmfs, self._A), - np.array([14.46372680, 10.85832950, 2.04663200]), + [14.46372680, 10.85832950, 2.04663200], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ( self._sd, self._cmfs, @@ -1524,42 +1525,40 @@ def test_sd_to_XYZ(self) -> None: method="Integration", shape=SpectralShape(400, 700, 20), ), - np.array([14.52005467, 10.88000966, 2.03888717]), + [14.52005467, 10.88000966, 2.03888717], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ( sds_and_msds_to_msds(SDS_COLOURCHECKERS["babel_average"].values()), ), - np.array( - [ - [12.06344619, 10.33615020, 6.25100082], - [40.27284790, 35.29615976, 23.01540616], - [18.09306423, 18.50797244, 31.67770084], - [11.16523793, 13.25122619, 6.36666038], - [25.83420330, 23.32560917, 40.31232440], - [31.64244829, 41.64149301, 40.76605171], - [40.89999141, 31.22508083, 5.82002195], - [13.60742370, 11.51655221, 35.39885724], - [30.69032237, 19.87942885, 12.46562439], - [8.93362430, 6.46162368, 13.07228804], - [35.66114785, 44.08411919, 10.28724123], - [49.24169657, 43.46327044, 7.13506647], - [7.81888273, 5.89050934, 25.75773837], - [15.18817149, 22.93975934, 8.94663877], - [22.23187260, 12.73987267, 4.62599881], - [60.68157753, 60.55780947, 8.43465082], - [32.27071879, 20.22049576, 28.85354643], - [14.49902213, 19.10377565, 35.60283004], - [90.78293221, 91.26928233, 87.29499532], - [58.53195263, 58.84257471, 58.34877604], - [35.77113141, 35.94371650, 35.85712527], - [18.98962045, 19.11651714, 19.15115933], - [8.87518188, 8.93947283, 9.06486638], - [3.21099614, 3.20073667, 3.25495104], - ] - ), + [ + [12.06344619, 10.33615020, 6.25100082], + [40.27284790, 35.29615976, 23.01540616], + [18.09306423, 18.50797244, 31.67770084], + [11.16523793, 13.25122619, 6.36666038], + [25.83420330, 23.32560917, 40.31232440], + [31.64244829, 41.64149301, 40.76605171], + [40.89999141, 31.22508083, 5.82002195], + [13.60742370, 11.51655221, 35.39885724], + [30.69032237, 19.87942885, 12.46562439], + [8.93362430, 6.46162368, 13.07228804], + [35.66114785, 44.08411919, 10.28724123], + [49.24169657, 43.46327044, 7.13506647], + [7.81888273, 5.89050934, 25.75773837], + [15.18817149, 22.93975934, 8.94663877], + [22.23187260, 12.73987267, 4.62599881], + [60.68157753, 60.55780947, 8.43465082], + [32.27071879, 20.22049576, 28.85354643], + [14.49902213, 19.10377565, 35.60283004], + [90.78293221, 91.26928233, 87.29499532], + [58.53195263, 58.84257471, 58.34877604], + [35.77113141, 35.94371650, 35.85712527], + [18.98962045, 19.11651714, 19.15115933], + [8.87518188, 8.93947283, 9.06486638], + [3.21099614, 3.20073667, 3.25495104], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1577,13 +1576,13 @@ def test_msds_to_XYZ_integration(self) -> None: """ cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_integration(MSDS_TWO, cmfs, SDS_ILLUMINANTS["D65"]), TVS_D65_INTEGRATION_MSDS, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_integration( DATA_TWO, cmfs, @@ -1594,7 +1593,7 @@ def test_msds_to_XYZ_integration(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_integration( DATA_TWO, cmfs, @@ -1606,7 +1605,7 @@ def test_msds_to_XYZ_integration(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_msds_to_XYZ_integration(self) -> None: + def test_domain_range_scale_msds_to_XYZ_integration(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.tristimulus_values.\ msds_to_XYZ_integration` definition domain and range scale support. @@ -1616,7 +1615,7 @@ def test_domain_range_scale_msds_to_XYZ_integration(self) -> None: d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_integration( DATA_TWO, cmfs, @@ -1628,6 +1627,96 @@ def test_domain_range_scale_msds_to_XYZ_integration(self) -> None: ) +class TestMsds_to_XYZ_tristimulus_weighting_factors_ASTME308: + """ + Define :func:`colour.colorimetry.tristimulus_values.\ +msds_to_XYZ_tristimulus_weighting_factors_ASTME308` + definition unit tests methods. + """ + + def test_msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: + """ + Test :func:`colour.colorimetry.tristimulus_values.\ +msds_to_XYZ_tristimulus_weighting_factors_ASTME308` + definition. + """ + + cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] + msds = reshape_msds(MSDS_TWO, SpectralShape(400, 700, 20)) + xp_assert_close( + msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + msds, cmfs, SDS_ILLUMINANTS["A"] + ), + np.array( + [ + sd_to_XYZ_tristimulus_weighting_factors_ASTME308( + sd, cmfs, SDS_ILLUMINANTS["A"] + ) + for sd in msds.to_sds() + ] + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] + xp_assert_close( + msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + msds, cmfs, SDS_ILLUMINANTS["FL2"] + ), + np.array( + [ + sd_to_XYZ_tristimulus_weighting_factors_ASTME308( + sd, cmfs, SDS_ILLUMINANTS["FL2"] + ) + for sd in msds.to_sds() + ] + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + xp_assert_close( + msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + msds, cmfs, SDS_ILLUMINANTS["A"], k=1 + ), + np.array( + [ + sd_to_XYZ_tristimulus_weighting_factors_ASTME308( + sd, cmfs, SDS_ILLUMINANTS["A"], k=1 + ) + for sd in msds.to_sds() + ] + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + def test_domain_range_scale_msds_to_XYZ_twf_ASTME308(self, xp: ModuleType) -> None: # noqa: ARG002 + """ + Test :func:`colour.colorimetry.tristimulus_values.\ +msds_to_XYZ_tristimulus_weighting_factors_ASTME308` definition domain and + range scale support. + """ + + cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] + msds = reshape_msds(MSDS_TWO, SpectralShape(400, 700, 20)) + XYZ = msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + msds, cmfs, SDS_ILLUMINANTS["A"] + ) + + d_r = (("reference", 1), ("1", 0.01), ("100", 1)) + for scale, factor in d_r: + with domain_range_scale(scale): + xp_assert_close( + msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + msds, cmfs, SDS_ILLUMINANTS["A"] + ), + XYZ * factor, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + class TestMsds_to_XYZ_ASTME308: """ Define :func:`colour.colorimetry.tristimulus_values.msds_to_XYZ_ASTME308` @@ -1642,19 +1731,19 @@ def test_msds_to_XYZ_ASTME308(self) -> None: cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] msds = reshape_msds(MSDS_TWO, SpectralShape(400, 700, 20)) - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_ASTME308(msds, cmfs, SDS_ILLUMINANTS["D65"]), TVS_D65_ASTME308_MSDS, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_ASTME308(msds, cmfs, SDS_ILLUMINANTS["D65"], k=1), TVS_D65_ASTME308_K1_MSDS, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_msds_to_XYZ_ASTME308(self) -> None: + def test_domain_range_scale_msds_to_XYZ_ASTME308(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.colorimetry.tristimulus_values.\ msds_to_XYZ_ASTME308` definition domain and range scale support. @@ -1664,7 +1753,7 @@ def test_domain_range_scale_msds_to_XYZ_ASTME308(self) -> None: d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( msds_to_XYZ_ASTME308( reshape_msds(MSDS_TWO, SpectralShape(400, 700, 20)), cmfs, @@ -1680,7 +1769,8 @@ def test_raise_exception_msds_to_XYZ_ASTME308(self) -> None: msds_to_XYZ_ASTME308` definition raise exception. """ - pytest.raises(TypeError, msds_to_XYZ_ASTME308, DATA_TWO) + with pytest.raises(TypeError): + msds_to_XYZ_ASTME308(DATA_TWO) # pyright: ignore class TestAbsoluteIntegrationToXYZ: @@ -1713,7 +1803,7 @@ def test_absolute_integration_to_TVS_1nm(self) -> None: XYZ = method(sd, k=k) XYZ = np.reshape(XYZ, 3) if len(XYZ.shape) > 1 else XYZ ( - np.testing.assert_allclose(XYZ[1], k, atol=5e-5), + xp_assert_close(XYZ[1], k, atol=TOLERANCE_ABSOLUTE_TESTS * 500), ( "1 watt @ 555nm should be approximately 683 candela." f" Failed method: {method}" @@ -1727,7 +1817,7 @@ def test_absolute_integration_to_TVS_1nm(self) -> None: if len(XYZ.shape) > 1: XYZ = np.reshape(XYZ, 3) ( - np.testing.assert_allclose(XYZ[1], k, atol=5e-5), + xp_assert_close(XYZ[1], k, atol=TOLERANCE_ABSOLUTE_TESTS * 500), ( "1 watt @ 555nm should be approximately 683 candela." f" Failed method: {method}" @@ -1762,7 +1852,7 @@ def test_absolute_integration_to_TVS_5nm(self) -> None: XYZ: np.ndarray = method(sd, k=k) XYZ = np.reshape(XYZ, 3) if len(XYZ.shape) > 1 else XYZ ( - np.testing.assert_allclose(XYZ[1], k, atol=5e-2), + xp_assert_close(XYZ[1], k, atol=TOLERANCE_ABSOLUTE_TESTS * 500000), ( "1 watt @ 555nm should be approximately 683 candela. " f"Failed method: {method}" @@ -1776,7 +1866,7 @@ def test_absolute_integration_to_TVS_5nm(self) -> None: if len(XYZ.shape) > 1: XYZ = np.reshape(XYZ, 3) ( - np.testing.assert_allclose(XYZ[1], k, atol=5e-2), + xp_assert_close(XYZ[1], k, atol=TOLERANCE_ABSOLUTE_TESTS * 500000), ( "1 watt @ 555nm should be approximately 683 candela." f"Failed method: {method}" @@ -1790,27 +1880,36 @@ class TestWavelength_to_XYZ: definition unit tests methods. """ - def test_wavelength_to_XYZ(self) -> None: + def test_wavelength_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.tristimulus_values.wavelength_to_XYZ` definition. """ - np.testing.assert_allclose( - wavelength_to_XYZ(480, MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]), - np.array([0.09564, 0.13902, 0.81295]), + xp_assert_close( + wavelength_to_XYZ( + xp_as_array(480.0, xp=xp), + MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], + ), + [0.09564, 0.13902, 0.81295], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - wavelength_to_XYZ(480, MSDS_CMFS["CIE 2015 2 Degree Standard Observer"]), - np.array([0.08182895, 0.17880480, 0.75523790]), + xp_assert_close( + wavelength_to_XYZ( + xp_as_array(480.0, xp=xp), + MSDS_CMFS["CIE 2015 2 Degree Standard Observer"], + ), + [0.08182895, 0.17880480, 0.75523790], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - wavelength_to_XYZ(641.5, MSDS_CMFS["CIE 2015 2 Degree Standard Observer"]), - np.array([0.44575583, 0.18184213, 0.00000000]), + xp_assert_close( + wavelength_to_XYZ( + xp_as_array(641.5, xp=xp), + MSDS_CMFS["CIE 2015 2 Degree Standard Observer"], + ), + [0.44575583, 0.18184213, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1820,11 +1919,13 @@ def test_raise_exception_wavelength_to_XYZ(self) -> None: definition raised exception. """ - pytest.raises(ValueError, wavelength_to_XYZ, 1) + with pytest.raises(ValueError): + wavelength_to_XYZ(1) - pytest.raises(ValueError, wavelength_to_XYZ, 1000) + with pytest.raises(ValueError): + wavelength_to_XYZ(1000) - def test_n_dimensional_wavelength_to_XYZ(self) -> None: + def test_n_dimensional_wavelength_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.tristimulus_values.wavelength_to_XYZ` definition n-dimensional arrays support. @@ -1834,20 +1935,14 @@ def test_n_dimensional_wavelength_to_XYZ(self) -> None: wl = 480 XYZ = wavelength_to_XYZ(wl, cmfs) - wl = np.tile(wl, 6) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - wavelength_to_XYZ(wl, cmfs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(wavelength_to_XYZ(wl, cmfs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - wavelength_to_XYZ(wl, cmfs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(wavelength_to_XYZ(wl, cmfs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - XYZ = np.reshape(XYZ, (2, 3, 1, 3)) - np.testing.assert_allclose( - wavelength_to_XYZ(wl, cmfs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1, 3), xp=xp) + xp_assert_close(wavelength_to_XYZ(wl, cmfs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) diff --git a/colour/colorimetry/tests/test_uniformity.py b/colour/colorimetry/tests/test_uniformity.py index c3addbe107..a378c34aa1 100644 --- a/colour/colorimetry/tests/test_uniformity.py +++ b/colour/colorimetry/tests/test_uniformity.py @@ -8,6 +8,7 @@ from colour.colorimetry import spectral_uniformity from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import xp_assert_close if typing.TYPE_CHECKING: from colour.hints import NDArrayFloat @@ -239,13 +240,13 @@ def test_spectral_uniformity(self) -> None: from colour.quality.datasets import SDS_TCS # noqa: PLC0415 - np.testing.assert_allclose( + xp_assert_close( spectral_uniformity(SDS_TCS["CIE 1995"].values()), DATA_UNIFORMITY_FIRST_ORDER_DERIVATIVES, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spectral_uniformity( SDS_TCS["CIE 1995"].values(), use_second_order_derivatives=True ), diff --git a/colour/colorimetry/tests/test_whiteness.py b/colour/colorimetry/tests/test_whiteness.py index 815b76d2ec..243f5d5fa6 100644 --- a/colour/colorimetry/tests/test_whiteness.py +++ b/colour/colorimetry/tests/test_whiteness.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -16,7 +21,14 @@ ) from colour.colorimetry.whiteness import whiteness from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -42,77 +54,81 @@ class TestWhitenessBerger1959: definition unit tests methods. """ - def test_whiteness_Berger1959(self) -> None: + def test_whiteness_Berger1959(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Berger1959` definition. """ - np.testing.assert_allclose( + xp_assert_close( whiteness_Berger1959( - np.array([95.00000000, 100.00000000, 105.00000000]), - np.array([94.80966767, 100.00000000, 107.30513595]), + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp), + xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp), ), 30.36380179, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( whiteness_Berger1959( - np.array([105.00000000, 100.00000000, 95.00000000]), - np.array([94.80966767, 100.00000000, 107.30513595]), + xp_as_array([105.00000000, 100.00000000, 95.00000000], xp=xp), + xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp), ), 5.530469280673941, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( whiteness_Berger1959( - np.array([100.00000000, 100.00000000, 100.00000000]), - np.array([100.00000000, 100.00000000, 100.00000000]), + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp), + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp), ), 33.300000000000011, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_whiteness_Berger1959(self) -> None: + def test_n_dimensional_whiteness_Berger1959(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Berger1959` definition n_dimensional arrays support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - XYZ_0 = np.array([94.80966767, 100.00000000, 107.30513595]) - W = whiteness_Berger1959(XYZ, XYZ_0) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + XYZ_0 = xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp) + W = as_ndarray(whiteness_Berger1959(XYZ, XYZ_0)) - XYZ = np.tile(XYZ, (6, 1)) - XYZ_0 = np.tile(XYZ_0, (6, 1)) - W = np.tile(W, 6) - np.testing.assert_allclose( - whiteness_Berger1959(XYZ, XYZ_0), W, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + XYZ_0 = xp.tile(xp_as_array(XYZ_0, xp=xp), (6, 1)) + W = xp.tile(xp_as_array(W, xp=xp), (6,)) + xp_assert_close( + whiteness_Berger1959(XYZ, XYZ_0), + W, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_0 = np.reshape(XYZ_0, (2, 3, 3)) - W = np.reshape(W, (2, 3)) - np.testing.assert_allclose( - whiteness_Berger1959(XYZ, XYZ_0), W, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_0 = xp_reshape(xp_as_array(XYZ_0, xp=xp), (2, 3, 3), xp=xp) + W = xp_reshape(xp_as_array(W, xp=xp), (2, 3), xp=xp) + xp_assert_close( + whiteness_Berger1959(XYZ, XYZ_0), + W, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness_Berger1959(self) -> None: + def test_domain_range_scale_whiteness_Berger1959(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Berger1959` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - XYZ_0 = np.array([94.80966767, 100.00000000, 107.30513595]) - W = whiteness_Berger1959(XYZ, XYZ_0) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + XYZ_0 = xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp) + W = as_ndarray(whiteness_Berger1959(XYZ, XYZ_0)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness_Berger1959(XYZ * factor, XYZ_0 * factor), W * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -136,77 +152,81 @@ class TestWhitenessTaube1960: definition unit tests methods. """ - def test_whiteness_Taube1960(self) -> None: + def test_whiteness_Taube1960(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Taube1960` definition. """ - np.testing.assert_allclose( + xp_assert_close( whiteness_Taube1960( - np.array([95.00000000, 100.00000000, 105.00000000]), - np.array([94.80966767, 100.00000000, 107.30513595]), + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp), + xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp), ), 91.407173833416152, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( whiteness_Taube1960( - np.array([105.00000000, 100.00000000, 95.00000000]), - np.array([94.80966767, 100.00000000, 107.30513595]), + xp_as_array([105.00000000, 100.00000000, 95.00000000], xp=xp), + xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp), ), 54.130300134995593, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( whiteness_Taube1960( - np.array([100.00000000, 100.00000000, 100.00000000]), - np.array([100.00000000, 100.00000000, 100.00000000]), + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp), + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp), ), 100.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_whiteness_Taube1960(self) -> None: + def test_n_dimensional_whiteness_Taube1960(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Taube1960` definition n_dimensional arrays support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - XYZ_0 = np.array([94.80966767, 100.00000000, 107.30513595]) - WI = whiteness_Taube1960(XYZ, XYZ_0) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + XYZ_0 = xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp) + WI = as_ndarray(whiteness_Taube1960(XYZ, XYZ_0)) - XYZ = np.tile(XYZ, (6, 1)) - XYZ_0 = np.tile(XYZ_0, (6, 1)) - WI = np.tile(WI, 6) - np.testing.assert_allclose( - whiteness_Taube1960(XYZ, XYZ_0), WI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + XYZ_0 = xp.tile(xp_as_array(XYZ_0, xp=xp), (6, 1)) + WI = xp.tile(xp_as_array(WI, xp=xp), (6,)) + xp_assert_close( + whiteness_Taube1960(XYZ, XYZ_0), + WI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_0 = np.reshape(XYZ_0, (2, 3, 3)) - WI = np.reshape(WI, (2, 3)) - np.testing.assert_allclose( - whiteness_Taube1960(XYZ, XYZ_0), WI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_0 = xp_reshape(xp_as_array(XYZ_0, xp=xp), (2, 3, 3), xp=xp) + WI = xp_reshape(xp_as_array(WI, xp=xp), (2, 3), xp=xp) + xp_assert_close( + whiteness_Taube1960(XYZ, XYZ_0), + WI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness_Taube1960(self) -> None: + def test_domain_range_scale_whiteness_Taube1960(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Taube1960` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - XYZ_0 = np.array([94.80966767, 100.00000000, 107.30513595]) - WI = whiteness_Taube1960(XYZ, XYZ_0) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + XYZ_0 = xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp) + WI = as_ndarray(whiteness_Taube1960(XYZ, XYZ_0)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness_Taube1960(XYZ * factor, XYZ_0 * factor), WI * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -230,64 +250,72 @@ class TestWhitenessStensby1968: definition unit tests methods. """ - def test_whiteness_Stensby1968(self) -> None: + def test_whiteness_Stensby1968(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Stensby1968` definition. """ - np.testing.assert_allclose( - whiteness_Stensby1968(np.array([100.00000000, -2.46875131, -16.72486654])), + xp_assert_close( + whiteness_Stensby1968( + xp_as_array([100.00000000, -2.46875131, -16.72486654], xp=xp) + ), 142.76834569, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - whiteness_Stensby1968(np.array([100.00000000, 14.40943727, -9.61394885])), + xp_assert_close( + whiteness_Stensby1968( + xp_as_array([100.00000000, 14.40943727, -9.61394885], xp=xp) + ), 172.07015836, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - whiteness_Stensby1968(np.array([1, 1, 1])), + xp_assert_close( + whiteness_Stensby1968(xp_as_array([1, 1, 1], xp=xp)), 1.00000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_whiteness_Stensby1968(self) -> None: + def test_n_dimensional_whiteness_Stensby1968(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Stensby1968` definition n_dimensional arrays support. """ - Lab = np.array([100.00000000, -2.46875131, -16.72486654]) - WI = whiteness_Stensby1968(Lab) + Lab = xp_as_array([100.00000000, -2.46875131, -16.72486654], xp=xp) + WI = as_ndarray(whiteness_Stensby1968(Lab)) - Lab = np.tile(Lab, (6, 1)) - WI = np.tile(WI, 6) - np.testing.assert_allclose( - whiteness_Stensby1968(Lab), WI, atol=TOLERANCE_ABSOLUTE_TESTS + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + WI = xp.tile(xp_as_array(WI, xp=xp), (6,)) + xp_assert_close( + whiteness_Stensby1968(Lab), + WI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab = np.reshape(Lab, (2, 3, 3)) - WI = np.reshape(WI, (2, 3)) - np.testing.assert_allclose( - whiteness_Stensby1968(Lab), WI, atol=TOLERANCE_ABSOLUTE_TESTS + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + WI = xp_reshape(xp_as_array(WI, xp=xp), (2, 3), xp=xp) + xp_assert_close( + whiteness_Stensby1968(Lab), + WI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness_Stensby1968(self) -> None: + def test_domain_range_scale_whiteness_Stensby1968(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Stensby1968` definition domain and range scale support. """ - Lab = np.array([100.00000000, -2.46875131, -16.72486654]) - WI = whiteness_Stensby1968(Lab) + Lab = xp_as_array([100.00000000, -2.46875131, -16.72486654], xp=xp) + WI = as_ndarray(whiteness_Stensby1968(Lab)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness_Stensby1968(Lab * factor), WI * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -311,64 +339,74 @@ class TestWhitenessASTM313: definition unit tests methods. """ - def test_whiteness_ASTME313(self) -> None: + def test_whiteness_ASTME313(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_ASTME313` definition. """ - np.testing.assert_allclose( - whiteness_ASTME313(np.array([95.00000000, 100.00000000, 105.00000000])), + xp_assert_close( + whiteness_ASTME313( + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + ), 55.740000000000009, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - whiteness_ASTME313(np.array([105.00000000, 100.00000000, 95.00000000])), + xp_assert_close( + whiteness_ASTME313( + xp_as_array([105.00000000, 100.00000000, 95.00000000], xp=xp) + ), 21.860000000000014, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - whiteness_ASTME313(np.array([100.00000000, 100.00000000, 100.00000000])), + xp_assert_close( + whiteness_ASTME313( + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp) + ), 38.800000000000011, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_whiteness_ASTME313(self) -> None: + def test_n_dimensional_whiteness_ASTME313(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_ASTME313` definition n_dimensional arrays support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - WI = whiteness_ASTME313(XYZ) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + WI = as_ndarray(whiteness_ASTME313(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - WI = np.tile(WI, 6) - np.testing.assert_allclose( - whiteness_ASTME313(XYZ), WI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + WI = xp.tile(xp_as_array(WI, xp=xp), (6,)) + xp_assert_close( + whiteness_ASTME313(XYZ), + WI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - WI = np.reshape(WI, (2, 3)) - np.testing.assert_allclose( - whiteness_ASTME313(XYZ), WI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + WI = xp_reshape(xp_as_array(WI, xp=xp), (2, 3), xp=xp) + xp_assert_close( + whiteness_ASTME313(XYZ), + WI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness_ASTME313(self) -> None: + def test_domain_range_scale_whiteness_ASTME313(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_ASTME313` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - WI = whiteness_ASTME313(XYZ) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + WI = as_ndarray(whiteness_ASTME313(XYZ)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness_ASTME313(XYZ * factor), WI * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -392,72 +430,78 @@ class TestWhitenessGanz1979: definition unit tests methods. """ - def test_whiteness_Ganz1979(self) -> None: + def test_whiteness_Ganz1979(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Ganz1979` definition. """ - np.testing.assert_allclose( - whiteness_Ganz1979(np.array([0.3139, 0.3311]), 100), - np.array([99.33176520, 1.76108290]), + xp_assert_close( + whiteness_Ganz1979(xp_as_array([0.3139, 0.3311], xp=xp), 100), + [99.33176520, 1.76108290], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - whiteness_Ganz1979(np.array([0.3500, 0.3334]), 100), - np.array([23.38525400, -32.66182560]), + xp_assert_close( + whiteness_Ganz1979(xp_as_array([0.3500, 0.3334], xp=xp), 100), + [23.38525400, -32.66182560], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - whiteness_Ganz1979(np.array([0.3334, 0.3334]), 100), - np.array([54.39939920, -16.04152380]), + xp_assert_close( + whiteness_Ganz1979(xp_as_array([0.3334, 0.3334], xp=xp), 100), + [54.39939920, -16.04152380], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_whiteness_Ganz1979(self) -> None: + def test_n_dimensional_whiteness_Ganz1979(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Ganz1979` definition n_dimensional arrays support. """ - xy = np.array([0.3167, 0.3334]) + xy = xp_as_array([0.3167, 0.3334], xp=xp) Y = 100 - WT = whiteness_Ganz1979(xy, Y) + WT = as_ndarray(whiteness_Ganz1979(xy, Y)) - xy = np.tile(xy, (6, 1)) - WT = np.tile(WT, (6, 1)) - np.testing.assert_allclose( - whiteness_Ganz1979(xy, Y), WT, atol=TOLERANCE_ABSOLUTE_TESTS + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + WT = xp.tile(xp_as_array(WT, xp=xp), (6, 1)) + xp_assert_close( + whiteness_Ganz1979(xy, Y), + WT, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - whiteness_Ganz1979(xy, Y), WT, atol=TOLERANCE_ABSOLUTE_TESTS + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + whiteness_Ganz1979(xy, Y), + WT, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.reshape(xy, (2, 3, 2)) - Y = np.reshape(Y, (2, 3)) - WT = np.reshape(WT, (2, 3, 2)) - np.testing.assert_allclose( - whiteness_Ganz1979(xy, Y), WT, atol=TOLERANCE_ABSOLUTE_TESTS + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + WT = xp_reshape(xp_as_array(WT, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close( + whiteness_Ganz1979(xy, Y), + WT, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness_Ganz1979(self) -> None: + def test_domain_range_scale_whiteness_Ganz1979(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_Ganz1979` definition domain and range scale support. """ - xy = np.array([0.3167, 0.3334]) + xy = xp_as_array([0.3167, 0.3334], xp=xp) Y = 100 - WT = whiteness_Ganz1979(xy, Y) + WT = as_ndarray(whiteness_Ganz1979(xy, Y)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness_Ganz1979(xy, Y * factor), WT * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -481,82 +525,94 @@ class TestWhitenessCIE2004: definition unit tests methods. """ - def test_whiteness_CIE2004(self) -> None: + def test_whiteness_CIE2004(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_CIE2004` definition. """ - np.testing.assert_allclose( + xp_assert_close( whiteness_CIE2004( - np.array([0.3139, 0.3311]), 100, np.array([0.3139, 0.3311]) + xp_as_array([0.3139, 0.3311], xp=xp), + 100, + xp_as_array([0.3139, 0.3311], xp=xp), ), - np.array([100.00000000, 0.00000000]), + [100.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( whiteness_CIE2004( - np.array([0.3500, 0.3334]), 100, np.array([0.3139, 0.3311]) + xp_as_array([0.3500, 0.3334], xp=xp), + 100, + xp_as_array([0.3139, 0.3311], xp=xp), ), - np.array([67.21000000, -34.60500000]), + [67.21000000, -34.60500000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( whiteness_CIE2004( - np.array([0.3334, 0.3334]), 100, np.array([0.3139, 0.3311]) + xp_as_array([0.3334, 0.3334], xp=xp), + 100, + xp_as_array([0.3139, 0.3311], xp=xp), ), - np.array([80.49000000, -18.00500000]), + [80.49000000, -18.00500000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_whiteness_CIE2004(self) -> None: + def test_n_dimensional_whiteness_CIE2004(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_CIE2004` definition n_dimensional arrays support. """ - xy = np.array([0.3167, 0.3334]) + xy = xp_as_array([0.3167, 0.3334], xp=xp) Y = 100 - xy_n = np.array([0.3139, 0.3311]) - WT = whiteness_CIE2004(xy, Y, xy_n) - - xy = np.tile(xy, (6, 1)) - WT = np.tile(WT, (6, 1)) - np.testing.assert_allclose( - whiteness_CIE2004(xy, Y, xy_n), WT, atol=TOLERANCE_ABSOLUTE_TESTS + xy_n = xp_as_array([0.3139, 0.3311], xp=xp) + WT = as_ndarray(whiteness_CIE2004(xy, Y, xy_n)) + + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + WT = xp.tile(xp_as_array(WT, xp=xp), (6, 1)) + xp_assert_close( + whiteness_CIE2004(xy, Y, xy_n), + WT, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y = np.tile(Y, 6) - xy_n = np.tile(xy_n, (6, 1)) - np.testing.assert_allclose( - whiteness_CIE2004(xy, Y, xy_n), WT, atol=TOLERANCE_ABSOLUTE_TESTS + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xy_n = xp.tile(xp_as_array(xy_n, xp=xp), (6, 1)) + xp_assert_close( + whiteness_CIE2004(xy, Y, xy_n), + WT, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - xy = np.reshape(xy, (2, 3, 2)) - Y = np.reshape(Y, (2, 3)) - xy_n = np.reshape(xy_n, (2, 3, 2)) - WT = np.reshape(WT, (2, 3, 2)) - np.testing.assert_allclose( - whiteness_CIE2004(xy, Y, xy_n), WT, atol=TOLERANCE_ABSOLUTE_TESTS + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xy_n = xp_reshape(xp_as_array(xy_n, xp=xp), (2, 3, 2), xp=xp) + WT = xp_reshape(xp_as_array(WT, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close( + whiteness_CIE2004(xy, Y, xy_n), + WT, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness_CIE2004(self) -> None: + def test_domain_range_scale_whiteness_CIE2004(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness_CIE2004` definition domain and range scale support. """ - xy = np.array([0.3167, 0.3334]) + xy = xp_as_array([0.3167, 0.3334], xp=xp) Y = 100 - xy_n = np.array([0.3139, 0.3311]) - WT = whiteness_CIE2004(xy, Y, xy_n) + xy_n = xp_as_array([0.3139, 0.3311], xp=xp) + WT = as_ndarray(whiteness_CIE2004(xy, Y, xy_n)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness_CIE2004(xy, Y * factor, xy_n), WT * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -580,14 +636,14 @@ class TestWhiteness: tests methods. """ - def test_whiteness(self) -> None: + def test_whiteness(self, xp: ModuleType) -> None: """Test :func:`colour.colorimetry.whiteness.whiteness` definition.""" # NOTE: Sample ``Y`` is deliberately different from whitepoint ``Y`` to # ensure the dispatcher forwards the sample tristimulus ``Y`` to the # "Ganz 1979" and "CIE 2004" methods rather than the whitepoint one. - XYZ = np.array([95.00000000, 80.00000000, 105.00000000]) - XYZ_0 = np.array([94.80966767, 100.00000000, 107.30513595]) + XYZ = xp_as_array([95.00000000, 80.00000000, 105.00000000], xp=xp) + XYZ_0 = xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp) expected = { "Berger 1959": 23.70380179, @@ -599,20 +655,20 @@ def test_whiteness(self) -> None: } for method, value in expected.items(): - np.testing.assert_allclose( + xp_assert_close( whiteness(XYZ, XYZ_0, method), value, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_whiteness(self) -> None: + def test_domain_range_scale_whiteness(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.whiteness.whiteness` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - XYZ_0 = np.array([94.80966767, 100.00000000, 107.30513595]) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + XYZ_0 = xp_as_array([94.80966767, 100.00000000, 107.30513595], xp=xp) m = ( "Berger 1959", @@ -622,13 +678,13 @@ def test_domain_range_scale_whiteness(self) -> None: "Ganz 1979", "CIE 2004", ) - v = [whiteness(XYZ, XYZ_0, method) for method in m] + v = [as_ndarray(whiteness(XYZ, XYZ_0, method)) for method in m] d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for method, value in zip(m, v, strict=True): for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( whiteness(XYZ * factor, XYZ_0 * factor, method), value * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/tests/test_yellowness.py b/colour/colorimetry/tests/test_yellowness.py index 6cae1e5948..16754f8f7b 100644 --- a/colour/colorimetry/tests/test_yellowness.py +++ b/colour/colorimetry/tests/test_yellowness.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -13,7 +18,14 @@ ) from colour.colorimetry.yellowness import YELLOWNESS_COEFFICIENTS_ASTME313, yellowness from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -36,64 +48,74 @@ class TestYellownessASTMD1925: definition unit tests methods. """ - def test_yellowness_ASTMD1925(self) -> None: + def test_yellowness_ASTMD1925(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness_ASTMD1925` definition. """ - np.testing.assert_allclose( - yellowness_ASTMD1925(np.array([95.00000000, 100.00000000, 105.00000000])), + xp_assert_close( + yellowness_ASTMD1925( + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + ), 10.299999999999997, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - yellowness_ASTMD1925(np.array([105.00000000, 100.00000000, 95.00000000])), + xp_assert_close( + yellowness_ASTMD1925( + xp_as_array([105.00000000, 100.00000000, 95.00000000], xp=xp) + ), 33.700000000000003, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - yellowness_ASTMD1925(np.array([100.00000000, 100.00000000, 100.00000000])), + xp_assert_close( + yellowness_ASTMD1925( + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp) + ), 22.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_yellowness_ASTMD1925(self) -> None: + def test_n_dimensional_yellowness_ASTMD1925(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness_ASTMD1925` definition n_dimensional arrays support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - YI = yellowness_ASTMD1925(XYZ) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + YI = as_ndarray(yellowness_ASTMD1925(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - YI = np.tile(YI, 6) - np.testing.assert_allclose( - yellowness_ASTMD1925(XYZ), YI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + YI = xp.tile(xp_as_array(YI, xp=xp), (6,)) + xp_assert_close( + yellowness_ASTMD1925(XYZ), + YI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - YI = np.reshape(YI, (2, 3)) - np.testing.assert_allclose( - yellowness_ASTMD1925(XYZ), YI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + YI = xp_reshape(xp_as_array(YI, xp=xp), (2, 3), xp=xp) + xp_assert_close( + yellowness_ASTMD1925(XYZ), + YI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_yellowness_ASTMD1925(self) -> None: + def test_domain_range_scale_yellowness_ASTMD1925(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness_ASTMD1925` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - YI = 10.299999999999997 + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + YI = as_ndarray(yellowness_ASTMD1925(XYZ)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTMD1925(XYZ * factor), YI * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -117,74 +139,78 @@ class TestYellownessASTM313Alternative: yellowness_ASTME313_alternative` definition unit tests methods. """ - def test_yellowness_ASTME313_alternative(self) -> None: + def test_yellowness_ASTME313_alternative(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.\ yellowness_ASTME313_alternative` definition. """ - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTME313_alternative( - np.array([95.00000000, 100.00000000, 105.00000000]) + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) ), 11.065000000000003, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTME313_alternative( - np.array([105.00000000, 100.00000000, 95.00000000]) + xp_as_array([105.00000000, 100.00000000, 95.00000000], xp=xp) ), 19.534999999999989, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTME313_alternative( - np.array([100.00000000, 100.00000000, 100.00000000]) + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp) ), 15.300000000000002, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_yellowness_ASTME313_alternative(self) -> None: + def test_n_dimensional_yellowness_ASTME313_alternative( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.yellowness.\ yellowness_ASTME313_alternative` definition n_dimensional arrays support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - YI = yellowness_ASTME313_alternative(XYZ) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + YI = as_ndarray(yellowness_ASTME313_alternative(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - YI = np.tile(YI, 6) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + YI = xp.tile(xp_as_array(YI, xp=xp), (6,)) + xp_assert_close( yellowness_ASTME313_alternative(XYZ), YI, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - YI = np.reshape(YI, (2, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + YI = xp_reshape(xp_as_array(YI, xp=xp), (2, 3), xp=xp) + xp_assert_close( yellowness_ASTME313_alternative(XYZ), YI, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_yellowness_ASTME313_alternative(self) -> None: + def test_domain_range_scale_yellowness_ASTME313_alternative( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.colorimetry.yellowness.\ yellowness_ASTME313_alternative` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - YI = 11.065000000000003 + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + YI = as_ndarray(yellowness_ASTME313_alternative(XYZ)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTME313_alternative(XYZ * factor), YI * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -208,33 +234,39 @@ class TestYellownessASTM313: definition unit tests methods. """ - def test_yellowness_ASTME313(self) -> None: + def test_yellowness_ASTME313(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness_ASTME313` definition. """ - np.testing.assert_allclose( - yellowness_ASTME313(np.array([95.00000000, 100.00000000, 105.00000000])), + xp_assert_close( + yellowness_ASTME313( + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + ), 4.340000000000003, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - yellowness_ASTME313(np.array([105.00000000, 100.00000000, 95.00000000])), + xp_assert_close( + yellowness_ASTME313( + xp_as_array([105.00000000, 100.00000000, 95.00000000], xp=xp) + ), 28.660000000000011, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - yellowness_ASTME313(np.array([100.00000000, 100.00000000, 100.00000000])), + xp_assert_close( + yellowness_ASTME313( + xp_as_array([100.00000000, 100.00000000, 100.00000000], xp=xp) + ), 16.500000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTME313( - np.array([95.00000000, 100.00000000, 105.00000000]), + xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp), YELLOWNESS_COEFFICIENTS_ASTME313["CIE 1931 2 Degree Standard Observer"][ "C" ], @@ -243,40 +275,44 @@ def test_yellowness_ASTME313(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_yellowness_ASTME313(self) -> None: + def test_n_dimensional_yellowness_ASTME313(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness_ASTME313` definition n_dimensional arrays support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - YI = yellowness_ASTME313(XYZ) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + YI = as_ndarray(yellowness_ASTME313(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - YI = np.tile(YI, 6) - np.testing.assert_allclose( - yellowness_ASTME313(XYZ), YI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + YI = xp.tile(xp_as_array(YI, xp=xp), (6,)) + xp_assert_close( + yellowness_ASTME313(XYZ), + YI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - YI = np.reshape(YI, (2, 3)) - np.testing.assert_allclose( - yellowness_ASTME313(XYZ), YI, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + YI = xp_reshape(xp_as_array(YI, xp=xp), (2, 3), xp=xp) + xp_assert_close( + yellowness_ASTME313(XYZ), + YI, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_yellowness_ASTME313(self) -> None: + def test_domain_range_scale_yellowness_ASTME313(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness_ASTME313` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) - YI = 4.340000000000003 + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) + YI = as_ndarray(yellowness_ASTME313(XYZ)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( yellowness_ASTME313(XYZ * factor), YI * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -300,22 +336,22 @@ class TestYellowness: tests methods. """ - def test_domain_range_scale_yellowness(self) -> None: + def test_domain_range_scale_yellowness(self, xp: ModuleType) -> None: """ Test :func:`colour.colorimetry.yellowness.yellowness` definition domain and range scale support. """ - XYZ = np.array([95.00000000, 100.00000000, 105.00000000]) + XYZ = xp_as_array([95.00000000, 100.00000000, 105.00000000], xp=xp) m = ("ASTM D1925", "ASTM E313") - v = [yellowness(XYZ, method) for method in m] + v = [as_ndarray(yellowness(XYZ, method)) for method in m] d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for method, value in zip(m, v, strict=True): for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( yellowness(XYZ * factor, method), value * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/colorimetry/transformations.py b/colour/colorimetry/transformations.py index 6d7ce340a9..07f68dd640 100644 --- a/colour/colorimetry/transformations.py +++ b/colour/colorimetry/transformations.py @@ -34,8 +34,6 @@ import typing -import numpy as np - from colour.algebra import vecmul from colour.colorimetry import ( MSDS_CMFS_LMS, @@ -47,7 +45,7 @@ if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import tstack +from colour.utilities import array_namespace, as_float_array, tstack, xp_as_float_array __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -104,22 +102,30 @@ def RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs( rgb_bar = cmfs[wavelength] - rgb = rgb_bar / np.sum(rgb_bar) + rgb_bar = as_float_array(rgb_bar) + + xp = array_namespace(rgb_bar) - M1 = np.array( + rgb = rgb_bar / xp.sum(rgb_bar) + + M1 = xp_as_float_array( [ [0.49000, 0.31000, 0.20000], [0.17697, 0.81240, 0.01063], [0.00000, 0.01000, 0.99000], - ] + ], + xp=xp, + like=rgb_bar, ) - M2 = np.array( + M2 = xp_as_float_array( [ [0.66697, 1.13240, 1.20063], [0.66697, 1.13240, 1.20063], [0.66697, 1.13240, 1.20063], - ] + ], + xp=xp, + like=rgb_bar, ) xyz = vecmul(M1, rgb) / vecmul(M2, rgb) @@ -178,14 +184,18 @@ def RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs( cmfs = MSDS_CMFS_RGB["Stiles & Burch 1959 10 Degree RGB CMFs"] - rgb_bar = cmfs[wavelength] + rgb_bar = as_float_array(cmfs[wavelength]) - M = np.array( + xp = array_namespace(rgb_bar) + + M = xp_as_float_array( [ [0.341080, 0.189145, 0.387529], [0.139058, 0.837460, 0.073316], [0.000000, 0.039553, 2.026200], - ] + ], + xp=xp, + like=rgb_bar, ) return vecmul(M, rgb_bar) @@ -230,14 +240,18 @@ def RGB_10_degree_cmfs_to_LMS_10_degree_cmfs( cmfs = MSDS_CMFS_RGB["Stiles & Burch 1959 10 Degree RGB CMFs"] - rgb_bar = cmfs[wavelength] + rgb_bar = as_float_array(cmfs[wavelength]) + + xp = array_namespace(rgb_bar) - M = np.array( + M = xp_as_float_array( [ [0.1923252690, 0.749548882, 0.0675726702], [0.0192290085, 0.940908496, 0.113830196], [0.0000000000, 0.0105107859, 0.991427669], - ] + ], + xp=xp, + like=rgb_bar, ) lms_bar = vecmul(M, rgb_bar) @@ -246,7 +260,7 @@ def RGB_10_degree_cmfs_to_LMS_10_degree_cmfs( [ lms_bar[..., 0], lms_bar[..., 1], - np.where(np.asarray(wavelength) > 505, 0, lms_bar[..., -1]), + xp.where(xp_as_float_array(wavelength, xp=xp) > 505, 0, lms_bar[..., -1]), ] ) @@ -288,14 +302,18 @@ def LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs( cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"] - lms_bar = cmfs[wavelength] + lms_bar = as_float_array(cmfs[wavelength]) + + xp = array_namespace(lms_bar) - M = np.array( + M = xp_as_float_array( [ [1.94735469, -1.41445123, 0.36476327], [0.68990272, 0.34832189, 0.00000000], [0.00000000, 0.00000000, 1.93485343], - ] + ], + xp=xp, + like=lms_bar, ) return vecmul(M, lms_bar) @@ -338,14 +356,18 @@ def LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs( cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 10 Degree Cone Fundamentals"] - lms_bar = cmfs[wavelength] + lms_bar = as_float_array(cmfs[wavelength]) + + xp = array_namespace(lms_bar) - M = np.array( + M = xp_as_float_array( [ [1.93986443, -1.34664359, 0.43044935], [0.69283932, 0.34967567, 0.00000000], [0.00000000, 0.00000000, 2.14687945], - ] + ], + xp=xp, + like=lms_bar, ) return vecmul(M, lms_bar) diff --git a/colour/colorimetry/tristimulus_values.py b/colour/colorimetry/tristimulus_values.py index badce7fa41..2260a07405 100644 --- a/colour/colorimetry/tristimulus_values.py +++ b/colour/colorimetry/tristimulus_values.py @@ -19,6 +19,8 @@ - :attr:`colour.SD_TO_XYZ_METHODS` - :func:`colour.sd_to_XYZ` - :func:`colour.colorimetry.msds_to_XYZ_integration` +- :func:`colour.colorimetry.\ +msds_to_XYZ_tristimulus_weighting_factors_ASTME308` - :func:`colour.colorimetry.msds_to_XYZ_ASTME308` - :attr:`colour.MSDS_TO_XYZ_METHODS` - :func:`colour.msds_to_XYZ` @@ -72,8 +74,10 @@ from colour.utilities import ( CACHE_REGISTRY, CanonicalMapping, + array_namespace, as_float_array, as_int_scalar, + as_ndarray, attest, filter_kwargs, from_range_100, @@ -83,6 +87,10 @@ optional, runtime_warning, validate_method, + xp_as_array, + xp_as_float_array, + xp_matrix_transpose, + xp_reshape, ) __author__ = "Colour Developers" @@ -105,6 +113,7 @@ "SD_TO_XYZ_METHODS", "sd_to_XYZ", "msds_to_XYZ_integration", + "msds_to_XYZ_tristimulus_weighting_factors_ASTME308", "msds_to_XYZ_ASTME308", "MSDS_TO_XYZ_METHODS", "msds_to_XYZ", @@ -274,7 +283,8 @@ def lagrange_coefficients_ASTME2022( hash_key = hash((interval, interval_type)) if is_caching_enabled() and hash_key in _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS: - return np.copy(_CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key]) + # Defensive copy so caller mutations don't poison the cache. + return _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key].copy() r_n = np.linspace(1 / interval, 1 - (1 / interval), interval - 1) d = 3 @@ -284,7 +294,7 @@ def lagrange_coefficients_ASTME2022( lica = as_float_array([lagrange_coefficients(r, d) for r in r_n]) - _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key] = np.copy(lica) + _CACHE_LAGRANGE_INTERPOLATING_COEFFICIENTS[hash_key] = lica.copy() return lica @@ -407,10 +417,21 @@ def tristimulus_weighting_factors_ASTME2022( global _CACHE_TRISTIMULUS_WEIGHTING_FACTORS # noqa: PLW0602 - hash_key = hash((cmfs, illuminant, shape, k, get_domain_range_scale())) + hash_key = hash( + ( + cmfs, + illuminant, + shape, + k, + get_domain_range_scale(), + type(illuminant.values).__module__, + ) + ) if is_caching_enabled() and hash_key in _CACHE_TRISTIMULUS_WEIGHTING_FACTORS: - return np.copy(_CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key]) + # Defensive copy so caller mutations don't poison the cache. + cached = _CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key] + return cached.copy() if hasattr(cached, "copy") else np.array(cached) Y = cmfs.values S = illuminant.values @@ -418,6 +439,8 @@ def tristimulus_weighting_factors_ASTME2022( interval_i = int(shape.interval) W = S[::interval_i, None] * Y[::interval_i, :] + xp = array_namespace(W) + # First and last measurement intervals *Lagrange Coefficients*. c_c = lagrange_coefficients_ASTME2022(interval_i, "boundary") # Intermediate measurement intervals *Lagrange Coefficients*. @@ -437,49 +460,49 @@ def tristimulus_weighting_factors_ASTME2022( # Only apply Lagrange interpolation when interval > 1 if r_c > 0: # First interval: W[:3, :] += sum over h of c_c[h, g] * S[h+1] * Y[h+1, :] - first_interval = np.sum( + first_interval = xp.sum( c_c[:, :, None] * (S[1 : r_c + 1, None, None] * Y[1 : r_c + 1, None, :]), axis=0, ) - W = np.concatenate([W[:3, :] + first_interval, W[3:, :]], axis=0) + W = xp.concat([W[:3, :] + first_interval, W[3:, :]], axis=0) # Last interval: W[i_cm-2:i_cm+1, :] += contributions with reversed c_c - last_interval = np.sum( + last_interval = xp.sum( c_c[::-1, :, None] * (S[w_lif : w_lif + r_c, None, None] * Y[w_lif : w_lif + r_c, None, :]), axis=0, ) - W = np.concatenate( + W = xp.concat( [W[: i_cm - 2, :], W[i_cm - 2 : i_cm + 1, :] + last_interval[::-1, :]], axis=0, ) # Intermediate intervals: accumulate c_b contributions for h in range(i_c - 3): - w_indices = (r_c + 1) * (h + 1) + 1 + np.arange(r_c) - contrib = np.sum( + w_indices = (r_c + 1) * (h + 1) + 1 + xp.arange(r_c) + contrib = xp.sum( c_b[:, :, None] * (S[w_indices, None, None] * Y[w_indices, None, :]), axis=0, ) - W = np.concatenate( - [W[:h, :], W[h : h + 4, :] + contrib, W[h + 4 :, :]], axis=0 - ) + W = xp.concat([W[:h, :], W[h : h + 4, :] + contrib, W[h + 4 :, :]], axis=0) # Extrapolation of potential incomplete interval extrap_start = as_int_scalar(w_c - ((w_c - 1) % interval_i)) if extrap_start < w_c: - extrap_contrib = np.sum( + extrap_contrib = xp.sum( S[extrap_start:w_c, None] * Y[extrap_start:w_c, :], axis=0 ) - W = np.concatenate( + W = xp.concat( [W[:i_cm, :], W[i_cm : i_cm + 1, :] + extrap_contrib, W[i_cm + 1 :, :]], axis=0, ) with sdiv_mode(): - W = W * optional(k, sdiv(100, np.sum(W, axis=0)[1])) + W = W * optional(k, sdiv(100, xp.sum(W, axis=0)[1])) - _CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key] = np.copy(W) + _CACHE_TRISTIMULUS_WEIGHTING_FACTORS[hash_key] = ( + W.copy() if hasattr(W, "copy") else np.asarray(W).copy() + ) return W @@ -553,12 +576,14 @@ def adjust_tristimulus_weighting_factors_ASTME308( W = as_float_array(W) + xp = array_namespace(W) + start_index = int((shape_t.start - shape_r.start) / shape_r.interval) end_index = int((shape_r.end - shape_t.end) / shape_r.interval) # Compute sums of trimmed portions - first_summation = np.sum(W[:start_index], axis=0) if start_index > 0 else 0 - last_summation = np.sum(W[-end_index:], axis=0) if end_index > 0 else 0 + first_summation = xp.sum(W[:start_index], axis=0) if start_index > 0 else 0 + last_summation = xp.sum(W[-end_index:], axis=0) if end_index > 0 else 0 # Get the result slice end_slice = -end_index if end_index > 0 else None @@ -566,7 +591,7 @@ def adjust_tristimulus_weighting_factors_ASTME308( # Build adjustment array using row index broadcasting n = W_slice.shape[0] - row_indices = np.arange(n) + row_indices = xp.arange(n) adjustment = (row_indices == 0)[:, None] * first_summation + (row_indices == n - 1)[ :, None ] * last_summation @@ -657,15 +682,20 @@ def tristimulus_weighting_factors_integration( XYZ_b = cmfs.values S = illuminant.values + xp = array_namespace(XYZ_b, S) + + XYZ_b = xp_as_float_array(XYZ_b, xp=xp) + S = xp_as_float_array(S, xp=xp) + d_w = cmfs.shape.interval # normalisation constant k from Y = 100 of perfect diffuser with sdiv_mode(): - k = cast("Real", optional(k, sdiv(100, (np.sum(XYZ_b[..., 1] * S) * d_w)))) + k = cast("Real", optional(k, sdiv(100, (xp.sum(XYZ_b[..., 1] * S) * d_w)))) # --- weights matrix A (DIN EN ISO 18314-4, eq. 7-8) --- # A[i, :] = k * S(λ_i) * [x̄(λ_i), ȳ(λ_i), z̄(λ_i)] * Δλ - return k * S[..., np.newaxis] * XYZ_b * d_w # shape: (n, 3) + return k * S[..., None] * XYZ_b * d_w # shape: (n, 3) def sd_to_XYZ_integration( @@ -746,6 +776,7 @@ def sd_to_XYZ_integration( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -818,9 +849,7 @@ def sd_to_XYZ_integration( else reshape_msds(sd, shape, copy=False) ) - R = np.transpose(sd.values) - shape_R = R.shape - wl_c_r = R.shape[-1] + R = as_float_array(sd.values) else: attest( shape is not None, @@ -830,9 +859,8 @@ def sd_to_XYZ_integration( shape = cast("SpectralShape", shape) R = as_float_array(sd) - shape_R = R.shape - wl_c_r = R.shape[-1] wl_c = len(shape.wavelengths) + wl_c_r = R.shape[-1] attest( wl_c_r == wl_c, @@ -848,13 +876,17 @@ def sd_to_XYZ_integration( runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape, copy=False) - R = np.reshape(R, (-1, wl_c_r)) - A = tristimulus_weighting_factors_integration(cmfs, illuminant, shape, k) - XYZ = np.dot(R, A) + xp = array_namespace(R, A) + + if isinstance(sd, MultiSpectralDistributions): + R = xp_matrix_transpose(R, xp=xp) - XYZ = from_range_100(np.reshape(XYZ, [*list(shape_R[:-1]), 3])) + R = xp_as_float_array(R, xp=xp) + A = xp_as_float_array(A, xp=xp, like=R) + + XYZ = from_range_100(xp.matmul(R, A)) if as_percentage: XYZ /= 100 @@ -920,6 +952,7 @@ def sd_to_XYZ_tristimulus_weighting_factors_ASTME308( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -1001,9 +1034,11 @@ def sd_to_XYZ_tristimulus_weighting_factors_ASTME308( W, SpectralShape(start_w, end_w, sd.shape.interval), sd.shape ) - R = sd.values + R = as_float_array(sd.values) + + xp = array_namespace(R) - XYZ = np.sum(W * R[..., None], axis=0) + XYZ = xp.sum(W * R[..., None], axis=0) return from_range_100(XYZ) @@ -1084,6 +1119,7 @@ def sd_to_XYZ_ASTME308( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -1182,25 +1218,33 @@ def sd_to_XYZ_ASTME308( SpectralShape(sd.shape.start - 20, sd.shape.end + 20, 10), copy=False, ) + + # ASTM E308-15 prescribes Lagrange interpolants for the four padded + # endpoints and every odd-indexed wavelength; the interpolants are + # applied via in-place index assignment, which requires a mutable + # array. The spectral values are materialised to *NumPy* and + # converted back to the original namespace afterwards, supporting + # immutable backends (e.g. *JAX*) as in :class:`colour.continuous.Signal`. + R = as_float_array(sd.values) + xp = array_namespace(R) + + values = np.array(as_ndarray(R)) for i in range(2): - sd[sd.wavelengths[i]] = ( - 3 * sd.values[i + 2] - 3 * sd.values[i + 4] + sd.values[i + 6] - ) - i_e = len(sd.domain) - 1 - i - sd[sd.wavelengths[i_e]] = ( - sd.values[i_e - 6] - 3 * sd.values[i_e - 4] + 3 * sd.values[i_e - 2] - ) + values[i] = 3 * values[i + 2] - 3 * values[i + 4] + values[i + 6] + i_e = values.shape[0] - 1 - i + values[i_e] = values[i_e - 6] - 3 * values[i_e - 4] + 3 * values[i_e - 2] # Interpolating every odd numbered values. - # TODO: Investigate code vectorisation. - for i in range(3, len(sd.domain) - 3, 2): - sd[sd.wavelengths[i]] = ( - -0.0625 * sd.values[i - 3] - + 0.5625 * sd.values[i - 1] - + 0.5625 * sd.values[i + 1] - - 0.0625 * sd.values[i + 3] + for i in range(3, values.shape[0] - 3, 2): + values[i] = ( + -0.0625 * values[i - 3] + + 0.5625 * values[i - 1] + + 0.5625 * values[i + 1] + - 0.0625 * values[i + 3] ) + sd.values = xp_as_array(values, xp=xp, like=R) + # Discarding the additional 20nm padding intervals. sd = reshape_sd( sd, @@ -1337,6 +1381,7 @@ def sd_to_XYZ( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -1396,7 +1441,7 @@ def sd_to_XYZ( ( sd if isinstance(sd, (SpectralDistribution, MultiSpectralDistributions)) - else int_digest(np.asarray(sd).tobytes()) + else int_digest(as_ndarray(sd).tobytes()) ), cmfs, illuminant, @@ -1404,11 +1449,18 @@ def sd_to_XYZ( method, tuple(kwargs.items()), get_domain_range_scale(), + type( + sd.values if hasattr(sd, "values") else sd # pyright: ignore + ).__module__, ) ) if is_caching_enabled() and hash_key in _CACHE_SD_TO_XYZ: - return np.copy(_CACHE_SD_TO_XYZ[hash_key]) + XYZ = _CACHE_SD_TO_XYZ[hash_key] + + xp = array_namespace(XYZ) + + return xp.asarray(XYZ, copy=True) if isinstance(sd, MultiSpectralDistributions): runtime_warning( @@ -1420,7 +1472,10 @@ def sd_to_XYZ( XYZ = function(sd, cmfs, illuminant, k=k, **filter_kwargs(function, **kwargs)) - _CACHE_SD_TO_XYZ[hash_key] = np.copy(XYZ) + if is_caching_enabled(): + xp = array_namespace(XYZ) + + _CACHE_SD_TO_XYZ[hash_key] = xp_as_array(XYZ, xp=xp, copy=True) return XYZ @@ -1517,6 +1572,7 @@ class on the last / tail axis as follows: Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -1663,6 +1719,150 @@ class on the last / tail axis as follows: return sd_to_XYZ_integration(msds, cmfs, illuminant, k, shape) +def msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + msds: MultiSpectralDistributions, + cmfs: MultiSpectralDistributions | None = None, + illuminant: SpectralDistribution | None = None, + k: Real | None = None, +) -> Range100: + """ + Convert the specified multi-spectral distributions to *CIE XYZ* tristimulus + values using the specified colour matching functions and illuminant + with a table of tristimulus weighting factors according to the + *ASTM E308-15* method. + + Parameters + ---------- + msds + Multi-spectral distributions. + cmfs + Standard observer colour matching functions, default to the + *CIE 1931 2 Degree Standard Observer*. + illuminant + Illuminant spectral distribution, default to *CIE Illuminant E*. + k + Normalisation constant :math:`k`. For reflecting or transmitting + object colours, :math:`k` is chosen so that :math:`Y = 100` for + objects for which the spectral reflectance factor + :math:`R(\\lambda)` of the object colour or the spectral + transmittance factor :math:`\\tau(\\lambda)` of the object is equal + to unity for all wavelengths. For self-luminous objects and + illuminants, the constants :math:`k` is usually chosen on the + grounds of convenience. If, however, in the CIE 1931 standard + colorimetric system, the :math:`Y` value is required to be + numerically equal to the absolute value of a photometric quantity, + the constant, :math:`k`, must be put equal to the numerical value + of :math:`K_m`, the maximum spectral luminous efficacy (which is + equal to 683 :math:`lm\\cdot W^{-1}`) and + :math:`\\Phi_\\lambda(\\lambda)` must be the spectral concentration + of the radiometric quantity corresponding to the photometric + quantity required. + + Returns + ------- + :class:`numpy.ndarray` + *CIE XYZ* tristimulus values. + + Notes + ----- + +-----------+-----------------------+---------------+ + | **Range** | **Scale - Reference** | **Scale - 1** | + +===========+=======================+===============+ + | ``XYZ`` | 100 | 1 | + +-----------+-----------------------+---------------+ + + References + ---------- + :cite:`ASTMInternational2015b` + + Examples + -------- + >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS + >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] + >>> illuminant = SDS_ILLUMINANTS["D65"] + >>> shape = SpectralShape(400, 700, 20) + >>> values = np.array( + ... [ + ... 0.0641, + ... 0.0645, + ... 0.0562, + ... 0.0537, + ... 0.0559, + ... 0.0651, + ... 0.0705, + ... 0.0772, + ... 0.0870, + ... 0.1128, + ... 0.1360, + ... 0.1511, + ... 0.1688, + ... 0.1996, + ... 0.2397, + ... 0.2852, + ... ] + ... ) + >>> data = np.transpose([values, values]) + >>> msds = MultiSpectralDistributions(data, shape) + >>> msds_to_XYZ_tristimulus_weighting_factors_ASTME308( + ... msds, cmfs, illuminant + ... ) # doctest: +ELLIPSIS + array([[10.8405832..., 9.6844909..., 6.2155622...], + [10.8405832..., 9.6844909..., 6.2155622...]]) + """ + + cmfs, illuminant = handle_spectral_arguments( + cmfs, + illuminant, + "CIE 1931 2 Degree Standard Observer", + "E", + SPECTRAL_SHAPE_ASTME308, + ) + + if cmfs.shape.interval != 1: + runtime_warning(f'Interpolating "{cmfs.name}" cmfs to 1nm interval.') + cmfs = reshape_msds( + cmfs, + SpectralShape(cmfs.shape.start, cmfs.shape.end, 1), + "Interpolate", + copy=False, + ) + + if illuminant.shape != cmfs.shape: + runtime_warning( + f'Aligning "{illuminant.name}" illuminant shape to "{cmfs.name}" ' + f"colour matching functions shape." + ) + illuminant = reshape_sd(illuminant, cmfs.shape, copy=False) + + if msds.shape.boundaries != cmfs.shape.boundaries: + runtime_warning( + f'Trimming "{msds.name}" multi-spectral distributions boundaries ' + f'using "{cmfs.name}" colour matching functions shape.' + ) + msds = reshape_msds(msds, cmfs.shape, "Trim", copy=False) + + W = tristimulus_weighting_factors_ASTME2022( + cmfs, + illuminant, + SpectralShape(cmfs.shape.start, cmfs.shape.end, msds.shape.interval), + k, + ) + start_w = cmfs.shape.start + end_w = cmfs.shape.start + msds.shape.interval * (W.shape[0] - 1) + W = adjust_tristimulus_weighting_factors_ASTME308( + W, SpectralShape(start_w, end_w, msds.shape.interval), msds.shape + ) + + R = as_float_array(msds.values) + + xp = array_namespace(R) + W = xp_as_float_array(W, xp=xp, like=R) + + XYZ = xp.matmul(xp_matrix_transpose(R, xp=xp), W) + + return from_range_100(XYZ) + + def msds_to_XYZ_ASTME308( msds: MultiSpectralDistributions, cmfs: MultiSpectralDistributions | None = None, @@ -1739,6 +1939,7 @@ def msds_to_XYZ_ASTME308( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -1867,6 +2068,16 @@ def msds_to_XYZ_ASTME308( [24.6240657..., 26.0805317..., 27.6706915...]]) """ + if not isinstance(msds, MultiSpectralDistributions): + error = ( + '"ASTM E308-15" method does not support "ArrayLike" ' + "multi-spectral distributions!" + ) + + raise TypeError(error) + + as_percentage = k is not None + cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, @@ -1875,28 +2086,95 @@ def msds_to_XYZ_ASTME308( SPECTRAL_SHAPE_ASTME308, ) - if isinstance(msds, MultiSpectralDistributions): - return as_float_array( - [ - sd_to_XYZ_ASTME308( - sd, - cmfs, - illuminant, - use_practice_range, - mi_5nm_omission_method, - mi_20nm_interpolation_method, - k, - ) - for sd in msds.to_sds() - ] + if msds.shape.interval not in (1, 5, 10, 20): + error = ( + "Tristimulus values conversion from spectral data according to " + 'practise "ASTM E308-15" should be performed on spectral data ' + "with measurement interval of 1, 5, 10 or 20nm!" ) - error = ( - '"ASTM E308-15" method does not support "ArrayLike" ' - "multi-spectral distributions!" - ) + raise ValueError(error) - raise TypeError(error) + if msds.shape.interval in (10, 20) and ( + msds.shape.start % 10 != 0 or msds.shape.end % 10 != 0 + ): + runtime_warning( + f'"{msds.name}" multi-spectral distributions shape does not start ' + f'at a tenth and will be aligned to "{cmfs.name}" colour matching ' + 'functions shape! Note that practise "ASTM E308-15" does not ' + "define a behaviour in this case." + ) + + msds = reshape_msds(msds, cmfs.shape, copy=False) + + if use_practice_range: + cmfs = reshape_msds(cmfs, SPECTRAL_SHAPE_ASTME308, "Trim", copy=False) + + method = msds_to_XYZ_tristimulus_weighting_factors_ASTME308 + if msds.shape.interval == 1: + method = msds_to_XYZ_integration + elif msds.shape.interval == 5 and mi_5nm_omission_method: + if cmfs.shape.interval != 5: + cmfs = reshape_msds( + cmfs, + SpectralShape(cmfs.shape.start, cmfs.shape.end, 5), + "Interpolate", + copy=False, + ) + method = msds_to_XYZ_integration + elif msds.shape.interval == 20 and mi_20nm_interpolation_method: + msds = msds.copy() + if msds.shape.boundaries != cmfs.shape.boundaries: + runtime_warning( + f'Trimming "{msds.name}" multi-spectral distributions shape to ' + f'"{cmfs.name}" colour matching functions shape.' + ) + msds = reshape_msds(msds, cmfs.shape, "Trim", copy=False) + + # Extrapolation of additional 20nm padding intervals. + msds = reshape_msds( + msds, + SpectralShape(msds.shape.start - 20, msds.shape.end + 20, 10), + copy=False, + ) + + # ASTM E308-15 prescribes Lagrange interpolants for the four padded + # endpoints and every odd-indexed wavelength; the operations are + # applied to the spectral values directly so the same coefficients + # broadcast across all signals along the wavelength axis. The + # recurrence is inherently sequential, so it runs on a *NumPy* copy + # and the result is written back through the MSDS setter. + R = as_ndarray(msds.values).copy() + for i in range(2): + R[i] = 3 * R[i + 2] - 3 * R[i + 4] + R[i + 6] + i_e = R.shape[0] - 1 - i + R[i_e] = R[i_e - 6] - 3 * R[i_e - 4] + 3 * R[i_e - 2] + + # Interpolating every odd numbered values. + for i in range(3, R.shape[0] - 3, 2): + R[i] = ( + -0.0625 * R[i - 3] + + 0.5625 * R[i - 1] + + 0.5625 * R[i + 1] + - 0.0625 * R[i + 3] + ) + + msds.values = R + + # Discarding the additional 20nm padding intervals. + msds = reshape_msds( + msds, + SpectralShape(msds.shape.start + 20, msds.shape.end - 20, 10), + "Trim", + copy=False, + ) + + XYZ = method(msds, cmfs, illuminant, k=k) + + if as_percentage and method is not msds_to_XYZ_integration: + XYZ /= 100 + + return XYZ MSDS_TO_XYZ_METHODS = CanonicalMapping( @@ -2033,6 +2311,7 @@ def msds_to_XYZ( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] >>> illuminant = SDS_ILLUMINANTS["D65"] @@ -2236,10 +2515,13 @@ def wavelength_to_XYZ( """ wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) + cmfs, _illuminant = handle_spectral_arguments(cmfs) shape = cmfs.shape - if np.min(wavelength) < shape.start or np.max(wavelength) > shape.end: + if xp.min(wavelength) < shape.start or xp.max(wavelength) > shape.end: error = ( f'"{wavelength}nm" wavelength is not in ' f'"[{shape.start}, {shape.end}]" domain!' @@ -2247,4 +2529,8 @@ def wavelength_to_XYZ( raise ValueError(error) - return np.reshape(cmfs[np.ravel(wavelength)], (*wavelength.shape, 3)) + return xp_reshape( + cmfs[xp_reshape(wavelength, (-1,), xp=xp)], + (*wavelength.shape, 3), + xp=xp, + ) diff --git a/colour/colorimetry/uniformity.py b/colour/colorimetry/uniformity.py index 14a8e368d1..48ea998494 100644 --- a/colour/colorimetry/uniformity.py +++ b/colour/colorimetry/uniformity.py @@ -20,13 +20,12 @@ if typing.TYPE_CHECKING: from collections.abc import ValuesView -import numpy as np - from colour.colorimetry import ( MultiSpectralDistributions, SpectralDistribution, sds_and_msds_to_msds, ) +from colour.utilities import array_namespace, xp_gradient, xp_matrix_transpose if typing.TYPE_CHECKING: from colour.hints import NDArrayFloat, Sequence @@ -121,9 +120,11 @@ def spectral_uniformity( interval = msds.shape.interval - r_i = np.gradient(np.transpose(msds.values), axis=1) / interval + xp = array_namespace(msds.values) + + r_i = xp_gradient(xp_matrix_transpose(msds.values, xp=xp), axis=1, xp=xp) / interval if use_second_order_derivatives: - r_i = np.gradient(r_i, axis=1) / interval + r_i = xp_gradient(r_i, axis=1, xp=xp) / interval - return np.mean(r_i**2, axis=0) + return xp.mean(r_i**2, axis=0) diff --git a/colour/colorimetry/whiteness.py b/colour/colorimetry/whiteness.py index 4c34f04e88..4d6d569600 100644 --- a/colour/colorimetry/whiteness.py +++ b/colour/colorimetry/whiteness.py @@ -58,6 +58,7 @@ ) from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, as_float_array, filter_kwargs, @@ -67,6 +68,7 @@ tsplit, tstack, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -357,9 +359,13 @@ def whiteness_Ganz1979(xy: ArrayLike, Y: Domain100) -> Range100: array([85.6003766..., 0.6789003...]) """ - x, y = tsplit(xy) Y = to_domain_100(Y) + xp = array_namespace(xy, Y) + + Y = xp_as_float_array(Y, xp=xp, like=xy) + x, y = tsplit(xy) + W = Y - 1868.322 * x - 3695.690 * y + 1809.441 T = -1001.223 * x + 748.366 * y + 68.261 @@ -439,9 +445,13 @@ def whiteness_CIE2004( array([93.85..., -1.305...]) """ - x, y = tsplit(xy) Y = to_domain_100(Y) - x_n, y_n = tsplit(xy_n) + + xp = array_namespace(xy, Y) + + Y = xp_as_float_array(Y, xp=xp, like=xy) + x, y = tsplit(xy) + x_n, y_n = tsplit(xp_as_float_array(xy_n, xp=xp, like=xy)) W = Y + 800 * (x_n - x) + 1700 * (y_n - y) T = (1000 if "1931" in observer else 900) * (x_n - x) - 650 * (y_n - y) diff --git a/colour/conftest.py b/colour/conftest.py new file mode 100644 index 0000000000..209f3773d8 --- /dev/null +++ b/colour/conftest.py @@ -0,0 +1,138 @@ +""" +Pytest Configuration +==================== + +Configure *pytest* with array backend fixtures for *Array API* testing. +""" + +from __future__ import annotations + +import sys +import typing + +import numpy as np +import pytest + +if typing.TYPE_CHECKING: + from colour.hints import Generator, ModuleType + +from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import ( + array_api_enable, + set_default_complex_dtype, + set_default_float_dtype, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "xp", +] + +try: + import jax + + jax.config.update("jax_enable_x64", True) + import jax.numpy as jnp +except ImportError: + jnp = None + +try: + import torch + + torch.set_default_dtype(torch.float64) +except ImportError: + torch = None + + +def _make_backend_parameters() -> list: + """Build the parametrised backend list.""" + + params = [pytest.param((np, "numpy"), id="numpy")] + + if jnp is not None: + params.append(pytest.param((jnp, "jax"), id="jax")) + + if torch is not None: + params.append(pytest.param((torch, "torch"), id="torch")) + if torch.backends.mps.is_available(): + params.append(pytest.param((torch, "torch-mps"), id="torch-mps")) + + return params + + +@pytest.fixture(params=_make_backend_parameters()) +def xp(request: pytest.FixtureRequest) -> Generator[ModuleType, None, None]: + """ + Parametrised array namespace fixture. + + Yields :mod:`numpy` and, when available, :mod:`jax.numpy` and + :mod:`torch`. Non-NumPy backends automatically enable Array API dispatch + for the duration of the test. The ``torch-mps`` variant additionally sets + the default device to ``mps`` and the default dtype to ``float32``. + """ + + backend, variant = request.param + + if variant == "numpy": + yield backend + elif variant == "torch-mps": + with array_api_enable(True): + default_dtype = torch.get_default_dtype() # pyright: ignore + torch.set_default_dtype(torch.float32) # pyright: ignore + torch.set_default_device("mps") # pyright: ignore + set_default_float_dtype(np.float32) + set_default_complex_dtype(np.complex64) + + # Relax test tolerance for float32 precision. A per-test + # ``@pytest.mark.mps_tolerance_absolute(value)`` marker overrides + # the ``5e-4`` default for tests whose float32 deltas need more + # headroom. Tests that thread + # :attr:`colour.constants.TOLERANCE_ABSOLUTE_TESTS` honour it, as + # do ``xp_assert_close`` calls relying on the default tolerances + # (resolved at call time); hard-coded tolerance literals do not. + marker = request.node.get_closest_marker("mps_tolerance_absolute") + tolerance = marker.args[0] if marker else 5e-4 + for module in sys.modules.values(): + if hasattr(module, "TOLERANCE_ABSOLUTE_TESTS"): + module.TOLERANCE_ABSOLUTE_TESTS = tolerance # pyright: ignore + + # Tests that cannot pass at any sane tolerance under float32 + # (large-magnitude radiometry, divergent solvers, hard-coded + # tolerance literals) opt in to a strict expected failure via + # ``@pytest.mark.mps_xfail("reason")``. ``strict=True`` makes + # an unexpected pass a CI failure, so the marker stays honest + # as *MPS* support improves. + xfail_marker = request.node.get_closest_marker("mps_xfail") + if xfail_marker is not None: + request.node.add_marker( + pytest.mark.xfail( + reason=xfail_marker.args[0] + if xfail_marker.args + else "MPS float32", + raises=(AssertionError, RuntimeError, TypeError), + strict=True, + ) + ) + + try: + yield backend + finally: + torch.set_default_device("cpu") # pyright: ignore + torch.set_default_dtype(default_dtype) # pyright: ignore + set_default_float_dtype(np.float64) + set_default_complex_dtype(np.complex128) + + for module in sys.modules.values(): + if hasattr(module, "TOLERANCE_ABSOLUTE_TESTS"): + module.TOLERANCE_ABSOLUTE_TESTS = ( # pyright: ignore + TOLERANCE_ABSOLUTE_TESTS + ) + else: + with array_api_enable(True): + yield backend diff --git a/colour/continuous/abstract.py b/colour/continuous/abstract.py index 6febda46d9..ca90e0e858 100644 --- a/colour/continuous/abstract.py +++ b/colour/continuous/abstract.py @@ -14,10 +14,9 @@ from abc import ABC, abstractmethod from copy import deepcopy -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import ( + Any, ArrayLike, Callable, DTypeFloat, @@ -33,11 +32,14 @@ from colour.utilities import ( MixinCallback, + array_namespace, as_float, + as_float_array, attest, closest, is_uniform, optional, + xp_as_array, ) __author__ = "Colour Developers" @@ -515,7 +517,12 @@ def __iter__(self) -> Generator: Abstract continuous function generator. """ - yield from np.column_stack([self.domain, self.range]) + domain = self.domain[:, None] if self.domain.ndim == 1 else self.domain + range_ = self.range[:, None] if self.range.ndim == 1 else self.range + + xp = array_namespace(domain, range_) + + yield from xp.concat([domain, range_], axis=1) def __len__(self) -> int: """ @@ -837,7 +844,11 @@ def domain_distance(self, a: ArrayLike) -> NDArrayFloat: n = closest(self.domain, a) - return as_float(np.abs(a - n)) + xp = array_namespace(n) + + a = xp_as_array(as_float_array(a), xp=xp, like=n) + + return as_float(xp.abs(a - n)) def is_uniform(self) -> bool: """ @@ -851,14 +862,28 @@ def is_uniform(self) -> bool: return is_uniform(self.domain) - def copy(self) -> Self: + def copy(self, xp: Any = None) -> Self: """ - Return a copy of the sub-class instance. + Return a copy of the sub-class instance, optionally converting + the range values to the specified array namespace. + + Parameters + ---------- + xp + Array namespace module to convert the range values to + (e.g., :mod:`torch`, :mod:`jax.numpy`). If *None*, the copy + retains the original backend. Returns ------- :class:`colour.continuous.AbstractContinuousFunction` Copy of the abstract continuous function. + """ - return deepcopy(self) + copy = deepcopy(self) + + if xp is not None: + copy.range = xp_as_array(as_float_array(copy.range), xp=xp) + + return copy diff --git a/colour/continuous/multi_signals.py b/colour/continuous/multi_signals.py index 6a233bb83f..2285c16f84 100644 --- a/colour/continuous/multi_signals.py +++ b/colour/continuous/multi_signals.py @@ -15,17 +15,20 @@ import typing from collections.abc import Iterator, KeysView, Mapping, ValuesView +from operator import pow # noqa: A004 +from operator import add, iadd, imul, ipow, isub, itruediv, mul, sub, truediv import numpy as np +from colour.algebra import Extrapolator, KernelInterpolator from colour.constants import DTYPE_FLOAT_DEFAULT from colour.continuous import AbstractContinuousFunction, Signal if typing.TYPE_CHECKING: + from types import ModuleType + from colour.hints import ( Any, - Dict, - DTypeFloat, List, Literal, NDArrayFloat, @@ -37,20 +40,36 @@ Type, ) -from colour.hints import ArrayLike, Callable, Sequence, cast +from colour.hints import ArrayLike, Callable, DTypeFloat, Sequence, cast from colour.utilities import ( + array_namespace, as_float_array, + as_ndarray, attest, - first_item, + fill_nan, + full, int_digest, is_iterable, + is_non_ndarray, is_pandas_installed, multiline_repr, + ndarray_copy, + ndarray_copy_enable, optional, required, - tsplit, + runtime_warning, tstack, validate_method, + xp_as_array, + xp_as_float_array, + xp_astype, + xp_atleast_1d, + xp_broadcast_to, + xp_insert, + xp_isin, + xp_reshape, + xp_resize, + xp_setxor1d, ) from colour.utilities.documentation import is_documentation_building @@ -322,11 +341,53 @@ def __init__( super().__init__(kwargs.get("name")) self._signal_type: Type[Signal] = kwargs.get("signal_type", Signal) + self._dtype: Type[DTypeFloat] = kwargs.get("dtype", DTYPE_FLOAT_DEFAULT) + + # Canonical storage owned at the parent level. + self._domain: NDArrayFloat = as_float_array([], self._dtype) + self._range: NDArrayFloat = np.zeros((0, 0), dtype=self._dtype) + self._labels: List[str] = [] + + # Interpolator / extrapolator parameters mirror :class:`Signal`'s + # defaults; sub-classes (e.g. :class:`MultiSpectralDistributions`) + # override via ``kwargs``. + self._interpolator: Type[ProtocolInterpolator] = kwargs.get( + "interpolator", KernelInterpolator + ) + self._interpolator_kwargs: dict = kwargs.get("interpolator_kwargs", {}) + self._extrapolator: Type[ProtocolExtrapolator] = kwargs.get( + "extrapolator", Extrapolator + ) + self._extrapolator_kwargs: dict = kwargs.get( + "extrapolator_kwargs", + {"method": "Constant", "left": float("nan"), "right": float("nan")}, + ) - self._signals: Dict[str, Signal] = self.multi_signals_unpack_data( - data, domain, labels, **kwargs + # ``multi_signals_unpack_data`` normalises every supported input + # format to the canonical ``(domain, range, labels)`` triple; one + # ``isfinite`` check on the full 2-D ``range`` replaces the prior + # per-child fan-out validation. + self._domain, self._range, self._labels = self.multi_signals_unpack_data( + data, domain, labels, dtype=self._dtype, **kwargs ) + if len(self._range) > 0 and self._range.shape[-1] > 0: + xp = array_namespace(self._range) + # Promote ``_domain`` onto ``_range``'s backend so they live on + # the same device for downstream evaluation; mirrors the per- + # signal promotion in :class:`Signal.function`. + self._domain = xp_as_float_array(self._domain, xp=xp, like=self._range) + if not bool(xp.all(xp.isfinite(self._range))): + runtime_warning( + f'"{self.name}" new "range" variable is not finite: ' + f"{self._range}, unpredictable results may occur!" + ) + + # Per-column :class:`Signal` views are materialised on demand by + # the :attr:`signals` property; the canonical state lives at the + # parent level. + self._function: Callable | None = None + @property def dtype(self) -> Type[DTypeFloat]: """ @@ -343,14 +404,24 @@ def dtype(self) -> Type[DTypeFloat]: Multi-signals dtype. """ - return first_item(self._signals.values()).dtype + return self._dtype @dtype.setter def dtype(self, value: Type[DTypeFloat]) -> None: """Setter for the **self.dtype** property.""" - for signal in self._signals.values(): - signal.dtype = value + attest( + value in DTypeFloat.__args__, + f'"dtype" must be one of the following types: {DTypeFloat.__args__}', + ) + + self._dtype = value + + # The following self-assignments are written as intended and + # triggers the rebuild of the underlying function. + if self.domain.dtype != value or self.range.dtype != value: + self.domain = self.domain + self.range = self.range @property def domain(self) -> NDArrayFloat: @@ -371,14 +442,36 @@ def domain(self) -> NDArrayFloat: :math:`x`. """ - return first_item(self._signals.values()).domain + return ndarray_copy(self._domain) @domain.setter def domain(self, value: ArrayLike) -> None: """Setter for the **self.domain** property.""" - for signal in self._signals.values(): - signal.domain = as_float_array(value, self.dtype) + value = as_float_array(value, self.dtype) + + xp = array_namespace(value) + + if not xp.all(xp.isfinite(value)): + runtime_warning( + f'"{self.name}" new "domain" variable is not finite: {value}, ' + f"unpredictable results may occur!" + ) + else: + attest( + xp.all(value[:-1] <= value[1:]), + "The new domain value is not monotonic! ", + ) + + if len(value) != self._range.shape[0]: + xp = array_namespace(self._range) + + self._range = xp_resize( + self._range, (len(value), self._range.shape[1]), xp=xp + ) + + self._domain = value + self._function = None # Invalidate the underlying continuous function. @property def range(self) -> NDArrayFloat: @@ -398,26 +491,38 @@ def range(self) -> NDArrayFloat: Multi-signals' range variable :math:`y`. """ - return tstack([signal.range for signal in self._signals.values()]) + return ndarray_copy(self._range) @range.setter def range(self, value: ArrayLike) -> None: """Setter for the **self.range** property.""" - value = as_float_array(value) + value = as_float_array(value, self.dtype) + + xp = array_namespace(value) + + if not xp.all(xp.isfinite(value)): + runtime_warning( + f'"{self.name}" new "range" variable is not finite: {value}, ' + f"unpredictable results may occur!" + ) if value.ndim in (0, 1): - for signal in self._signals.values(): - signal.range = value + value = xp_broadcast_to( + value[..., None] if value.ndim == 1 else value, + self._range.shape, + xp=xp, + ) + value = as_float_array(value, self.dtype) else: attest( - value.shape[-1] == len(self._signals), + value.shape[-1] == self._range.shape[1], 'Corresponding "y" variable columns must have ' 'same count than underlying "Signal" components!', ) - for signal, y in zip(self._signals.values(), tsplit(value), strict=True): - signal.range = y + self._range = value + self._function = None # Invalidate the underlying continuous function. @property def interpolator(self) -> Type[ProtocolInterpolator]: @@ -437,15 +542,15 @@ def interpolator(self) -> Type[ProtocolInterpolator]: Multi-signals interpolator type. """ - return first_item(self._signals.values()).interpolator + return self._interpolator @interpolator.setter def interpolator(self, value: Type[ProtocolInterpolator]) -> None: """Setter for the **self.interpolator** property.""" - if value is not None: - for signal in self._signals.values(): - signal.interpolator = value + if value is not None and value is not self._interpolator: + self._interpolator = value + self._function = None # Invalidate the underlying continuous function. @property def interpolator_kwargs(self) -> dict: @@ -465,14 +570,14 @@ def interpolator_kwargs(self) -> dict: arguments. """ - return first_item(self._signals.values()).interpolator_kwargs + return self._interpolator_kwargs @interpolator_kwargs.setter def interpolator_kwargs(self, value: dict) -> None: """Setter for the **self.interpolator_kwargs** property.""" - for signal in self._signals.values(): - signal.interpolator_kwargs = value + self._interpolator_kwargs = value + self._function = None # Invalidate the underlying continuous function. @property def extrapolator(self) -> Type[ProtocolExtrapolator]: @@ -492,14 +597,15 @@ def extrapolator(self) -> Type[ProtocolExtrapolator]: Multi-signals extrapolator type. """ - return first_item(self._signals.values()).extrapolator + return self._extrapolator @extrapolator.setter def extrapolator(self, value: Type[ProtocolExtrapolator]) -> None: """Setter for the **self.extrapolator** property.""" - for signal in self._signals.values(): - signal.extrapolator = value + if value is not None and value is not self._extrapolator: + self._extrapolator = value + self._function = None # Invalidate the underlying continuous function. @property def extrapolator_kwargs(self) -> dict: @@ -520,16 +626,17 @@ def extrapolator_kwargs(self) -> dict: arguments. """ - return first_item(self._signals.values()).extrapolator_kwargs + return self._extrapolator_kwargs @extrapolator_kwargs.setter def extrapolator_kwargs(self, value: dict) -> None: """Setter for the **self.extrapolator_kwargs** property.""" - for signal in self._signals.values(): - signal.extrapolator_kwargs = value + self._extrapolator_kwargs = value + self._function = None # Invalidate the underlying continuous function. @property + @ndarray_copy_enable(False) def function(self) -> Callable: """ Getter for the multi-signals callable. @@ -540,14 +647,70 @@ def function(self) -> Callable: Multi-signals callable. """ - return first_item(self._signals.values()).function + if self._function is None: + # Create the underlying continuous function. Each interpolator + # owns its own input conversion, so backend tensors flow + # straight through array-aware ones (Sprague / Linear / Kernel + # / Null) and only get coerced to *NumPy* by the scipy-bound + # ones (CubicSpline / Pchip). ``self.domain`` is promoted to + # ``self.range``'s backend so the interpolator sees both + # variables on the same device, and downstream ``like=`` + # references can canonically use the stored ``x`` axis. + if len(self.domain) != 0 and len(self.range) != 0: + xp = array_namespace(self.domain, self.range) + domain = xp_as_float_array(self.domain, xp=xp, like=self.range) + self._function = self.extrapolator( + self.interpolator( + domain, + self.range, + **self.interpolator_kwargs, + ), + **self.extrapolator_kwargs, + ) + else: + + def _undefined_function( + *args: Any, # noqa: ARG001 + **kwargs: Any, # noqa: ARG001 + ) -> None: + """ + Raise a :class:`ValueError` exception. + + Other Parameters + ---------------- + args + Arguments. + kwargs + Keywords arguments. + + Raises + ------ + ValueError + """ + + error = ( + "Underlying multi-signals interpolator function " + 'does not exists, please ensure that both "domain" ' + 'and "range" variables are defined!' + ) + + raise ValueError(error) + + self._function = cast("Callable", _undefined_function) + + return cast("Callable", self._function) @property - def signals(self) -> Dict[str, Signal]: + def signals(self) -> Mapping[str, Signal]: """ Getter and setter for the dictionary of :class:`colour.continuous.Signal` sub-class instances. + Per-column :class:`Signal` views are materialised lazily on + first access; the canonical state lives at the parent level so + the materialisation is a row-slice of ``self._range`` wrapped in + a :class:`Signal` instance per label. + Parameters ---------- value @@ -561,7 +724,19 @@ def signals(self) -> Dict[str, Signal]: :class:`colour.continuous.Signal` sub-class instances. """ - return self._signals + return { + label: self._signal_type( + self._range[:, i], + self._domain, + name=label, + dtype=self._dtype, + interpolator=self._interpolator, + interpolator_kwargs=self._interpolator_kwargs, + extrapolator=self._extrapolator, + extrapolator_kwargs=self._extrapolator_kwargs, + ) + for i, label in enumerate(self._labels) + } @signals.setter def signals( @@ -570,9 +745,10 @@ def signals( ) -> None: """Setter for the **self.signals** property.""" - self._signals = self.multi_signals_unpack_data( - value, signal_type=self._signal_type + self._domain, self._range, self._labels = self.multi_signals_unpack_data( + value, dtype=self._dtype ) + self._function = None # Invalidate the underlying continuous function. @property def labels(self) -> List[str]: @@ -592,7 +768,7 @@ def labels(self) -> List[str]: :class:`colour.continuous.Signal` sub-class instance names. """ - return [str(key) for key in self._signals] + return list(self._labels) @labels.setter def labels(self, value: Sequence) -> None: @@ -609,13 +785,12 @@ def labels(self, value: Sequence) -> None: ) attest( - len(value) == len(self.labels), - f'"labels" property: length must be "{len(self._signals)}"!', + len(value) == len(self._labels), + f'"labels" property: length must be "{len(self._labels)}"!', ) - self._signals = { - str(value[i]): signal for i, signal in enumerate(self._signals.values()) - } + self._labels = [str(label) for label in value] + self._function = None # Invalidate the underlying continuous function. @property def signal_type(self) -> Type[Signal]: @@ -659,7 +834,9 @@ def __str__(self) -> str: [ 9. 100. 110. 120.]] """ - return str(np.hstack([self.domain[:, None], self.range])) + xp = array_namespace(self.domain, self.range) + + return str(xp.concat([self.domain[:, None], self.range], axis=1)) def __repr__(self) -> str: """ @@ -701,7 +878,9 @@ def __repr__(self) -> str: [ { "formatter": lambda x: repr( # noqa: ARG005 - np.hstack([self.domain[:, None], self.range]) + array_namespace(self.domain, self.range).concat( + [self.domain[:, None], self.range], axis=1 + ) ), }, {"name": "labels"}, @@ -732,17 +911,20 @@ def __hash__(self) -> int: Object hash. """ + # See :meth:`Signal.__hash__` for the host-bytes-plus-namespace rationale. return hash( ( - int_digest(self.domain.tobytes()), - *[hash(signal) for signal in self._signals.values()], - self.interpolator.__name__, - repr(self.interpolator_kwargs), - self.extrapolator.__name__, - repr(self.extrapolator_kwargs), + int_digest(as_ndarray(self._domain).tobytes()), + int_digest(as_ndarray(self._range).tobytes()), + array_namespace(self._domain, self._range).__name__, + self._interpolator.__name__, + repr(self._interpolator_kwargs), + self._extrapolator.__name__, + repr(self._extrapolator_kwargs), ) ) + @ndarray_copy_enable(False) def __getitem__(self, x: ArrayLike | slice) -> NDArrayFloat: """ Return the corresponding range variable :math:`y` for the specified @@ -805,7 +987,11 @@ def __getitem__(self, x: ArrayLike | slice) -> NDArrayFloat: x_r, x_c = (x[0], x[1]) if isinstance(x, tuple) else (x, slice(None)) - values = tstack([signal[x_r] for signal in self._signals.values()]) + # The slice path serves directly from the cached 2-D ``_range`` + # aggregate; the non-slice path routes through ``self.function`` so + # one shared interpolator / extrapolator chain over the 2-D + # aggregate replaces the prior per-child fan-out. + values = self.range[x_r] if isinstance(x_r, slice) else self.function(x_r) return values[..., x_c] # pyright: ignore @@ -914,7 +1100,9 @@ def __setitem__(self, x: ArrayLike | slice, y: ArrayLike) -> None: [ 9. 50. 50. 120. ]] """ - y = as_float_array(y) + xp = array_namespace(self._range) + + y = xp_as_float_array(y, xp=xp, like=self._range) x_r, x_c = (x[0], x[1]) if isinstance(x, tuple) else (x, slice(None)) @@ -924,21 +1112,59 @@ def __setitem__(self, x: ArrayLike | slice, y: ArrayLike) -> None: "or 2-dimensional array!", ) + n_signals = self._range.shape[1] if y.ndim == 0: - y = np.tile(y, len(self._signals)) + y = xp_broadcast_to(y, (1, n_signals), xp=xp) elif y.ndim == 1: y = y[None, :] attest( - y.shape[-1] == len(self._signals), + y.shape[-1] == n_signals, 'Corresponding "y" variable columns must have same count than ' 'underlying "Signal" components!', ) - values = list(zip(self._signals.values(), tsplit(y), strict=True)) + def set_range( + index: ArrayLike | slice, values: ArrayLike, xp: ModuleType + ) -> None: + """ + Set ``self._range`` at ``[index, x_c]`` mutably, round-tripping + through numpy for immutable backends. + """ + + sliced = values[..., x_c] # pyright: ignore + if not is_non_ndarray(self._range): + self._range[index, x_c] = sliced # pyright: ignore + else: + range_ = np.array(as_ndarray(self._range)) + range_[ # pyright: ignore + index if isinstance(index, slice) else as_ndarray(index), x_c + ] = as_ndarray(sliced) + self._range = xp_as_array(range_, xp=xp, like=self._range) + + if isinstance(x_r, slice): + set_range(x_r, y, xp) + else: + x_r = xp_astype( + xp_atleast_1d(xp_as_float_array(x_r, xp=xp, like=self._range), xp=xp), + self.dtype, + xp=xp, + ) + y = xp_resize(y, (x_r.shape[0], n_signals), xp=xp) + domain = xp_as_array(self._domain, xp=xp, like=self._range) + + mask = xp_isin(x_r, domain, xp=xp) + x_m = x_r[mask] + if len(x_m) != 0: + set_range(xp.searchsorted(domain, x_m), y[mask], xp) - for signal, y in values[x_c]: # pyright: ignore - signal[x_r] = y + x_nm = x_r[~mask] + if len(x_nm) != 0: + indexes = xp.searchsorted(domain, x_nm) + self._domain = as_ndarray(xp_insert(domain, indexes, x_nm, xp=xp)) + self._range = xp_insert(self._range, indexes, y[~mask], axis=0, xp=xp) + + self._function = None # Invalidate the underlying continuous function. def __contains__(self, x: ArrayLike | slice) -> bool: """ @@ -967,7 +1193,20 @@ def __contains__(self, x: ArrayLike | slice) -> bool: False """ - return x in first_item(self._signals.values()) + xp = array_namespace(self._domain) + + return bool( + xp.all( + xp.where( + xp.logical_and( + x >= xp.min(self._domain), + x <= xp.max(self._domain), + ), + True, + False, + ) + ) + ) def __eq__(self, other: object) -> bool: """ @@ -1003,18 +1242,24 @@ def __eq__(self, other: object) -> bool: False """ - # NOTE: Comparing "interpolator_kwargs" and "extrapolator_kwargs" using - # their string representation because of presence of NaNs. + # ``interpolator_kwargs`` / ``extrapolator_kwargs`` compared as repr to + # handle NaNs. Different-backend operands are treated as unequal. if isinstance(other, MultiSignals): + xp = array_namespace(self._domain, self._range) + if xp is not array_namespace(other.domain, other.range): + return False + return all( [ - np.array_equal(self.domain, other.domain), - np.array_equal(self.range, other.range), - self.interpolator is other.interpolator, - str(self.interpolator_kwargs) == str(other.interpolator_kwargs), - self.extrapolator is other.extrapolator, - str(self.extrapolator_kwargs) == str(other.extrapolator_kwargs), - self.labels == other.labels, + self._domain.shape == other.domain.shape + and bool(xp.all(self._domain == other.domain)), + self._range.shape == other.range.shape + and bool(xp.all(self._range == other.range)), + self._interpolator is other.interpolator, + repr(self._interpolator_kwargs) == repr(other.interpolator_kwargs), + self._extrapolator is other.extrapolator, + repr(self._extrapolator_kwargs) == repr(other.extrapolator_kwargs), + self._labels == other.labels, ] ) @@ -1169,46 +1414,67 @@ def arithmetical_operation( [ 9. 347. 378. 409.]] """ - multi_signals = self if in_place else self.copy() + operator, ioperator = { + "+": (add, iadd), + "-": (sub, isub), + "*": (mul, imul), + "/": (truediv, itruediv), + "**": (pow, ipow), + }[operation] - if isinstance(a, MultiSignals): - attest( - len(self.signals) == len(a.signals), - '"MultiSignals" operands must have same count than ' - 'underlying "Signal" components!', - ) + n_signals = self._range.shape[1] - for signal_a, signal_b in zip( - multi_signals.signals.values(), a.signals.values(), strict=True - ): - signal_a.arithmetical_operation(signal_b, operation, True) - else: - a = as_float_array(cast("ArrayLike", a)) + if in_place: + if isinstance(a, MultiSignals): + attest( + n_signals == a.range.shape[1], + '"MultiSignals" operands must have same count than ' + 'underlying "Signal" components!', + ) - attest( - a.ndim in range(3), - 'Operand "a" variable must be a numeric or a 1-dimensional or ' - "2-dimensional array!", - ) + xp = array_namespace(self._domain) - if a.ndim in (0, 1): - for signal in multi_signals.signals.values(): - signal.arithmetical_operation(a, operation, True) + self[self._domain] = operator(self._range, a[self._domain]) + exclusive_or = xp_setxor1d(self._domain, a.domain, xp=xp) + self[exclusive_or] = full( + (exclusive_or.shape[0], n_signals), float("nan") + ) else: + operand = as_float_array(cast("ArrayLike", a)) + attest( - a.shape[-1] == len(multi_signals.signals), - 'Operand "a" variable columns must have same count than ' - 'underlying "Signal" components!', + operand.ndim in range(3), + 'Operand "a" variable must be a numeric or a 1-dimensional ' + "or 2-dimensional array!", ) - for signal, y in zip( - multi_signals.signals.values(), tsplit(a), strict=True + if operand.ndim == 0 or ( + operand.ndim == 1 and operand.shape[0] == self._range.shape[0] ): - signal.arithmetical_operation(y, operation, True) + # Scalar or 1-D operand of length ``n_wavelengths`` broadcasts + # over the signal axis. + operand = operand[..., None] if operand.ndim == 1 else operand + else: + attest( + operand.shape[-1] == n_signals, + 'Operand "a" variable columns must have same count than ' + 'underlying "Signal" components!', + ) - return multi_signals + if not isinstance(self._range, np.ndarray) and isinstance( + operand, np.ndarray + ): + xp = array_namespace(self._range) + + operand = xp_as_array(operand, xp=xp, like=self._range) + self.range = ioperator(self._range, operand) + + return self + + return ioperator(self.copy(), a) @staticmethod + @ndarray_copy_enable(True) def multi_signals_unpack_data( data: ( ArrayLike @@ -1224,9 +1490,8 @@ def multi_signals_unpack_data( domain: ArrayLike | KeysView | None = None, labels: Sequence | None = None, dtype: Type[DTypeFloat] | None = None, - signal_type: Type[Signal] = Signal, - **kwargs: Any, - ) -> Dict[str, Signal]: + **kwargs: Any, # noqa: ARG004 + ) -> tuple[NDArrayFloat, NDArrayFloat, List[str]]: """ Unpack specified data for multi-signals instantiation. @@ -1267,19 +1532,24 @@ def multi_signals_unpack_data( Returns ------- - :class:`dict` - Mapping of labeled :class:`colour.continuous.Signal` sub-class - instances. + :class:`tuple` + Tuple of ``(domain, range, labels)`` where ``domain`` is the + unpacked independent variable as an ``(N,)`` array, ``range`` is + the unpacked dependent variable as an ``(N, M)`` array with one + column per signal, and ``labels`` is the list of signal labels + of length ``M``. Examples -------- Unpacking using implicit *domain* and data for a single signal: >>> range_ = np.linspace(10, 100, 10) - >>> signals = MultiSignals.multi_signals_unpack_data(range_) - >>> list(signals.keys()) + >>> domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... range_ + ... ) + >>> labels ['0'] - >>> print(signals["0"]) + >>> print(np.column_stack([domain, range_unpacked])) [[ 0. 10.] [ 1. 20.] [ 2. 30.] @@ -1293,11 +1563,13 @@ def multi_signals_unpack_data( Unpacking using explicit *domain* and data for a single signal: - >>> domain = np.arange(100, 1100, 100) - >>> signals = MultiSignals.multi_signals_unpack_data(range_, domain) - >>> list(signals.keys()) + >>> domain_ = np.arange(100, 1100, 100) + >>> domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... range_, domain_ + ... ) + >>> labels ['0'] - >>> print(signals["0"]) + >>> print(np.column_stack([domain, range_unpacked])) [[ 100. 10.] [ 200. 20.] [ 300. 30.] @@ -1313,10 +1585,12 @@ def multi_signals_unpack_data( >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) >>> range_ += np.array([0, 10, 20]) - >>> signals = MultiSignals.multi_signals_unpack_data(range_, domain) - >>> list(signals.keys()) + >>> domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... range_, domain_ + ... ) + >>> labels ['0', '1', '2'] - >>> print(signals["2"]) + >>> print(np.column_stack([domain, range_unpacked[:, 2]])) [[ 100. 30.] [ 200. 40.] [ 300. 50.] @@ -1330,10 +1604,12 @@ def multi_signals_unpack_data( Unpacking using a *dict*: - >>> signals = MultiSignals.multi_signals_unpack_data(dict(zip(domain, range_))) - >>> list(signals.keys()) + >>> domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... dict(zip(domain_, range_)) + ... ) + >>> labels ['0', '1', '2'] - >>> print(signals["2"]) + >>> print(np.column_stack([domain, range_unpacked[:, 2]])) [[ 100. 30.] [ 200. 40.] [ 300. 50.] @@ -1345,16 +1621,16 @@ def multi_signals_unpack_data( [ 900. 110.] [1000. 120.]] - Unpacking using a sequence of *Signal* instances, note how the keys - are :class:`str` instances because the *Signal* names are used: + Unpacking using a sequence of *Signal* instances: - >>> signals = MultiSignals.multi_signals_unpack_data( - ... dict(zip(domain, range_)) - ... ).values() - >>> signals = MultiSignals.multi_signals_unpack_data(signals) - >>> list(signals.keys()) + >>> from colour.continuous import Signal + >>> signals_seq = [Signal(range_[:, i], domain_, name=str(i)) for i in range(3)] + >>> domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... signals_seq + ... ) + >>> labels ['0', '1', '2'] - >>> print(signals["2"]) + >>> print(np.column_stack([domain, range_unpacked[:, 2]])) [[ 100. 30.] [ 200. 40.] [ 300. 50.] @@ -1366,13 +1642,15 @@ def multi_signals_unpack_data( [ 900. 110.] [1000. 120.]] - Unpacking using *MultiSignals.multi_signals_unpack_data* method output: + Unpacking from an existing :class:`MultiSignals` instance: - >>> signals = MultiSignals.multi_signals_unpack_data(dict(zip(domain, range_))) - >>> signals = MultiSignals.multi_signals_unpack_data(signals) - >>> list(signals.keys()) + >>> multi_signals = MultiSignals(range_, domain_) + >>> domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... multi_signals + ... ) + >>> labels ['0', '1', '2'] - >>> print(signals["2"]) + >>> print(np.column_stack([domain, range_unpacked[:, 2]])) [[ 100. 30.] [ 200. 40.] [ 300. 50.] @@ -1389,10 +1667,10 @@ def multi_signals_unpack_data( >>> if is_pandas_installed(): ... from pandas import Series ... - ... signals = MultiSignals.multi_signals_unpack_data( - ... Series(dict(zip(domain, np.linspace(10, 100, 10)))) + ... domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... Series(dict(zip(domain_, np.linspace(10, 100, 10)))) ... ) - ... print(signals[0]) # doctest: +SKIP + ... print(np.column_stack([domain, range_unpacked])) # doctest: +SKIP [[ 100. 10.] [ 200. 20.] [ 300. 30.] @@ -1410,10 +1688,10 @@ def multi_signals_unpack_data( ... from pandas import DataFrame ... ... data = dict(zip(["a", "b", "c"], tsplit(range_))) - ... signals = MultiSignals.multi_signals_unpack_data( - ... DataFrame(data, domain) + ... domain, range_unpacked, labels = MultiSignals.multi_signals_unpack_data( + ... DataFrame(data, domain_) ... ) - ... print(signals["c"]) # doctest: +SKIP + ... print(np.column_stack([domain, range_unpacked[:, 2]])) # doctest: +SKIP [[ 100. 30.] [ 200. 40.] [ 300. 50.] @@ -1428,78 +1706,97 @@ def multi_signals_unpack_data( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - settings = {} - settings.update(kwargs) - settings.update({"dtype": dtype}) + domain_unpacked: NDArrayFloat = as_float_array([], dtype) + range_unpacked: NDArrayFloat = np.zeros((0, 0), dtype=dtype) + labels_unpacked: List[str] = [] - # domain_unpacked, range_unpacked, signals = ( - # np.array([]), np.array([]), {}) + if data is None: + pass + elif isinstance(data, Signal): + domain_unpacked = as_float_array(data.domain, dtype) + range_unpacked = xp_reshape(as_float_array(data.range, dtype), (-1, 1)) + labels_unpacked = [str(data.name)] + elif isinstance(data, MultiSignals): + domain_unpacked = as_float_array(data.domain, dtype) + range_unpacked = as_float_array(data.range, dtype) + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = list(data.labels) + elif is_non_ndarray(data): + range_unpacked = as_float_array(data, dtype) # pyright: ignore - signals = {} + attest( + range_unpacked.ndim in (1, 2), + 'User "data" must be 1-dimensional or 2-dimensional!', + ) - if isinstance(data, Signal): - signals[data.name] = data - elif isinstance(data, MultiSignals): - signals = data.signals + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = [str(i) for i in range(range_unpacked.shape[1])] elif issubclass(type(data), Sequence) or isinstance( data, (tuple, list, np.ndarray, Iterator, ValuesView) ): data_sequence = list(cast("Sequence", data)) - is_signal = True - for i in data_sequence: - if not isinstance(i, Signal): - is_signal = False - break + is_signal = bool(data_sequence) and all( + isinstance(i, Signal) for i in data_sequence + ) if is_signal: - for signal in data_sequence: - signals[signal.name] = signal_type( - signal.range, signal.domain, **settings - ) + domain_unpacked = as_float_array(data_sequence[0].domain, dtype) + range_unpacked = tstack( + [as_float_array(signal.range, dtype) for signal in data_sequence] + ) + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = [str(signal.name) for signal in data_sequence] else: - data_array = tsplit(data_sequence) + range_unpacked = as_float_array(np.asarray(data_sequence), dtype) attest( - data_array.ndim in (1, 2), + range_unpacked.ndim in (1, 2), 'User "data" must be 1-dimensional or 2-dimensional!', ) - if data_array.ndim == 1: - data_array = data_array[None, :] - - for i, range_unpacked in enumerate(data_array): - signals[str(i)] = signal_type(range_unpacked, domain, **settings) + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = [str(i) for i in range(range_unpacked.shape[1])] elif issubclass(type(data), Mapping) or isinstance(data, dict): data_mapping = dict(cast("Mapping", data)) - is_signal = all(isinstance(i, Signal) for i in data_mapping.values()) + is_signal = bool(data_mapping) and all( + isinstance(i, Signal) for i in data_mapping.values() + ) if is_signal: - for label, signal in data_mapping.items(): - signals[label] = signal_type( - signal.range, signal.domain, **settings - ) - else: - domain_unpacked, range_unpacked = zip( - *sorted(data_mapping.items()), strict=True + first_signal = next(iter(data_mapping.values())) + domain_unpacked = as_float_array(first_signal.domain, dtype) + range_unpacked = tstack( + [ + as_float_array(signal.range, dtype) + for signal in data_mapping.values() + ] ) - for i, values_unpacked in enumerate(tsplit(range_unpacked)): - signals[str(i)] = signal_type( - values_unpacked, domain_unpacked, **settings - ) + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = [str(label) for label in data_mapping] + else: + keys, values = zip(*sorted(data_mapping.items()), strict=True) + domain_unpacked = as_float_array(keys, dtype) + range_unpacked = as_float_array(np.asarray(values), dtype) + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = [str(i) for i in range(range_unpacked.shape[1])] elif is_pandas_installed(): if isinstance(data, Series): - signals["0"] = signal_type(data, **settings) + domain_unpacked = as_float_array(data.index.values, dtype) # pyright: ignore + range_unpacked = xp_reshape(as_float_array(data.values, dtype), (-1, 1)) + labels_unpacked = ["0"] elif isinstance(data, DataFrame): domain_unpacked = as_float_array(data.index.values, dtype) # pyright: ignore - signals = { - label: signal_type( - data[label], - domain_unpacked, - **settings, - ) - for label in data - } + range_unpacked = as_float_array(data.values, dtype) + if range_unpacked.ndim == 1: + range_unpacked = xp_reshape(range_unpacked, (-1, 1)) + labels_unpacked = [str(label) for label in data.columns] if domain is not None: if isinstance(domain, KeysView): @@ -1507,36 +1804,38 @@ def multi_signals_unpack_data( domain_array = as_float_array(domain, dtype) - for signal in signals.values(): + if len(range_unpacked) > 0: attest( - len(domain_array) == len(signal.domain), - 'User "domain" length is not compatible with unpacked "signals"!', + len(domain_array) == len(range_unpacked), + 'User "domain" length is not compatible with unpacked "range"!', ) - signal.domain = domain_array + domain_unpacked = domain_array - signals = {str(label): signal for label, signal in signals.items()} + if len(domain_unpacked) == 0 and len(range_unpacked) > 0: + domain_unpacked = np.arange(range_unpacked.shape[0], dtype=dtype) if labels is not None: attest( - len(labels) == len(signals), - 'User "labels" length is not compatible with unpacked "signals"!', + len(labels) == len(labels_unpacked), + 'User "labels" length is not compatible with unpacked "labels"!', ) if len(labels) != len(set(labels)): labels = [f"{label} - {i}" for i, label in enumerate(labels)] - signals = { - str(labels[i]): signal for i, signal in enumerate(signals.values()) - } + labels_unpacked = [str(label) for label in labels] - for label in signals: - signals[label].name = label + if not labels_unpacked: + labels_unpacked = ["Undefined"] + if range_unpacked.size == 0: + range_unpacked = np.zeros((0, 1), dtype=dtype) - if not signals: - signals = {"Undefined": Signal(name="Undefined")} - - return signals + return ( + ndarray_copy(domain_unpacked), + ndarray_copy(range_unpacked), + labels_unpacked, + ) def fill_nan( self, @@ -1605,8 +1904,16 @@ def fill_nan( method = validate_method(method, ("Interpolation", "Constant")) - for signal in self._signals.values(): - signal.fill_nan(method, default) + if self._labels: + self.domain = fill_nan(self._domain, method, default) + # ``fill_nan`` is 1-D; iterate over the trailing signal axis so + # each column's NaN pattern resolves independently. + self.range = tstack( + [ + fill_nan(self._range[..., i], method, default) + for i in range(self._range.shape[-1]) + ] + ) return self diff --git a/colour/continuous/signal.py b/colour/continuous/signal.py index 95f1813820..650b640d2d 100644 --- a/colour/continuous/signal.py +++ b/colour/continuous/signal.py @@ -25,6 +25,8 @@ from colour.continuous import AbstractContinuousFunction if typing.TYPE_CHECKING: + from types import ModuleType + from colour.hints import ( Any, ArrayLike, @@ -39,10 +41,13 @@ from colour.hints import Callable, DTypeFloat, cast from colour.utilities import ( + array_namespace, as_float_array, + as_ndarray, attest, fill_nan, full, + is_non_ndarray, is_pandas_installed, multiline_repr, ndarray_copy, @@ -53,6 +58,15 @@ tsplit, tstack, validate_method, + xp_as_array, + xp_as_float_array, + xp_astype, + xp_atleast_1d, + xp_insert, + xp_isin, + xp_linspace, + xp_resize, + xp_setxor1d, ) from colour.utilities.common import int_digest from colour.utilities.documentation import is_documentation_building @@ -244,15 +258,15 @@ def __init__( super().__init__(kwargs.get("name")) self._dtype: Type[DTypeFloat] = DTYPE_FLOAT_DEFAULT - self._domain: NDArrayFloat = np.array([]) - self._range: NDArrayFloat = np.array([]) + self._domain: NDArrayFloat = as_float_array([]) + self._range: NDArrayFloat = as_float_array([]) self._interpolator: Type[ProtocolInterpolator] = KernelInterpolator self._interpolator_kwargs: dict = {} self._extrapolator: Type[ProtocolExtrapolator] = Extrapolator self._extrapolator_kwargs: dict = { "method": "Constant", - "left": np.nan, - "right": np.nan, + "left": float("nan"), + "right": float("nan"), } self.range, self.domain = self.signal_unpack_data(data, domain)[::-1] @@ -332,19 +346,23 @@ def domain(self, value: ArrayLike) -> None: value = as_float_array(value, self.dtype) - if not np.all(np.isfinite(value)): + xp = array_namespace(value) + + if not xp.all(xp.isfinite(value)): runtime_warning( f'"{self.name}" new "domain" variable is not finite: {value}, ' f"unpredictable results may occur!" ) else: attest( - np.all(value[:-1] <= value[1:]), + xp.all(value[:-1] <= value[1:]), "The new domain value is not monotonic! ", ) - if value.size != self._range.size: - self._range = np.resize(self._range, value.shape) + if len(value) != len(self._range): + xp = array_namespace(self._range) + + self._range = xp_resize(self._range, value.shape, xp=xp) self._domain = value self._function = None # Invalidate the underlying continuous function. @@ -375,7 +393,9 @@ def range(self, value: ArrayLike) -> None: value = as_float_array(value, self.dtype) - if not np.all(np.isfinite(value)): + xp = array_namespace(value) + + if not xp.all(xp.isfinite(value)): runtime_warning( f'"{self.name}" new "range" variable is not finite: {value}, ' f"unpredictable results may occur!" @@ -522,12 +542,22 @@ def function(self) -> Callable: """ if self._function is None: - # Create the underlying continuous function. - - if self._domain.size != 0 and self._range.size != 0: + # Create the underlying continuous function. Each interpolator + # owns its own input conversion, so backend tensors flow + # straight through array-aware ones (Sprague / Linear / Kernel + # / Null) and only get coerced to *NumPy* by the scipy-bound + # ones (CubicSpline / Pchip). ``self._domain`` is promoted to + # ``self._range``'s backend so the interpolator sees both + # variables on the same device, and downstream ``like=`` + # references can canonically use the stored ``x`` axis. + if len(self._domain) != 0 and len(self._range) != 0: + xp = array_namespace(self._domain, self._range) + domain = xp_as_float_array(self._domain, xp=xp, like=self._range) self._function = self._extrapolator( self._interpolator( - self._domain, self._range, **self._interpolator_kwargs + domain, + self._range, + **self._interpolator_kwargs, ), **self._extrapolator_kwargs, ) @@ -657,10 +687,13 @@ def __hash__(self) -> int: Object hash. """ + # Host-byte digest mixed with the array namespace so cross-backend + # operands hash distinctly, matching ``__eq__``. return hash( ( - int_digest(self._domain.tobytes()), - int_digest(self._range.tobytes()), + int_digest(as_ndarray(self._domain).tobytes()), + int_digest(as_ndarray(self._range).tobytes()), + array_namespace(self._domain, self._range).__name__, self.interpolator.__name__, repr(self.interpolator_kwargs), self.extrapolator.__name__, @@ -782,25 +815,48 @@ def __setitem__(self, x: ArrayLike | slice, y: ArrayLike) -> None: [ 9. 100. ]] """ + def set_range( + index: ArrayLike | slice, values: ArrayLike, xp: ModuleType + ) -> None: + """ + Set ``self._range`` mutably, round-tripping through numpy for + immutable backends. + """ + + if not is_non_ndarray(self._range): + self._range[index] = values # pyright: ignore + else: + range_ = np.array(as_ndarray(self._range)) + range_[index if isinstance(index, slice) else as_ndarray(index)] = ( + as_ndarray(values) + ) + self._range = xp_as_array(range_, xp=xp, like=self._range) + + xp = array_namespace(self._range) + + y = xp_as_float_array(y, xp=xp, like=self._range) + if isinstance(x, slice): - self._range[x] = y + set_range(x, y, xp) else: - x = np.atleast_1d(x).astype(self.dtype) - y = np.resize(y, x.shape) + x = xp_astype( + xp_atleast_1d(xp_as_float_array(x, xp=xp, like=self._range), xp=xp), + self.dtype, + xp=xp, + ) + y = xp_resize(y, x.shape, xp=xp) + domain = xp_as_array(self._domain, xp=xp, like=self._range) - # Matching domain, updating existing `self._range` values. - mask = np.isin(x, self._domain) + mask = xp_isin(x, domain, xp=xp) x_m = x[mask] - indexes = np.searchsorted(self._domain, x_m) - self._range[indexes] = y[mask] + if len(x_m) != 0: + set_range(xp.searchsorted(domain, x_m), y[mask], xp) - # Non matching domain, inserting into existing `self.domain` - # and `self.range`. x_nm = x[~mask] - indexes = np.searchsorted(self._domain, x_nm) - if indexes.size != 0: - self._domain = np.insert(self._domain, indexes, x_nm) - self._range = np.insert(self._range, indexes, y[~mask]) + if len(x_nm) != 0: + indexes = xp.searchsorted(domain, x_nm) + self._domain = as_ndarray(xp_insert(domain, indexes, x_nm, xp=xp)) + self._range = xp_insert(self._range, indexes, y[~mask], xp=xp) self._function = None # Invalidate the underlying continuous function. @@ -831,12 +887,14 @@ def __contains__(self, x: ArrayLike | slice) -> bool: False """ + xp = array_namespace(self._domain) + return bool( - np.all( - np.where( - np.logical_and( - x >= np.min(self._domain), # pyright: ignore - x <= np.max(self._domain), # pyright: ignore + xp.all( + xp.where( + xp.logical_and( + x >= xp.min(self._domain), + x <= xp.max(self._domain), ), True, False, @@ -878,13 +936,19 @@ def __eq__(self, other: object) -> bool: False """ - # NOTE: Comparing "interpolator_kwargs" and "extrapolator_kwargs" using - # their string representation because of presence of NaNs. + # ``interpolator_kwargs`` / ``extrapolator_kwargs`` compared as repr to + # handle NaNs. Different-backend operands are treated as unequal. if isinstance(other, Signal): + xp = array_namespace(self._domain, self._range) + if xp is not array_namespace(other.domain, other.range): + return False + return all( [ - np.array_equal(self._domain, other.domain), - np.array_equal(self._range, other.range), + self._domain.shape == other.domain.shape + and bool(xp.all(self._domain == other.domain)), + self._range.shape == other.range.shape + and bool(xp.all(self._range == other.range)), self._interpolator is other.interpolator, repr(self._interpolator_kwargs) == repr(other.interpolator_kwargs), self._extrapolator is other.extrapolator, @@ -1086,10 +1150,18 @@ def arithmetical_operation( if in_place: if isinstance(a, Signal): + xp = array_namespace(self._domain) + self[self._domain] = operator(self._range, a[self._domain]) - exclusive_or = np.setxor1d(self._domain, a.domain) - self[exclusive_or] = full(exclusive_or.shape, np.nan) + exclusive_or = xp_setxor1d(self._domain, a.domain, xp=xp) + self[exclusive_or] = full(exclusive_or.shape, float("nan")) else: + if not isinstance(self._range, np.ndarray) and isinstance( + a, np.ndarray + ): + xp = array_namespace(self._range) + + a = xp_as_array(a, xp=xp, like=self._range) self.range = ioperator(self._range, a) return self @@ -1177,12 +1249,23 @@ def signal_unpack_data( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - domain_unpacked: NDArrayFloat = np.array([]) - range_unpacked: NDArrayFloat = np.array([]) + domain_unpacked: NDArrayFloat = as_float_array([]) + range_unpacked: NDArrayFloat = as_float_array([]) if isinstance(data, Signal): domain_unpacked = data.domain range_unpacked = data.range + elif is_non_ndarray(data): + data_array = as_float_array(data) # pyright: ignore + + attest(data_array.ndim == 1, 'User "data" must be 1-dimensional!') + + xp = array_namespace(data_array) + + domain_unpacked = xp_linspace( # pyright: ignore + 0, data_array.shape[0] - 1, num=data_array.shape[0], xp=xp + ) + range_unpacked = data_array elif issubclass(type(data), Sequence) or isinstance( data, (tuple, list, np.ndarray, Iterator, ValuesView) ): diff --git a/colour/continuous/tests/test_multi_signal.py b/colour/continuous/tests/test_multi_signal.py index 91fcc5137b..4568c3c4b4 100644 --- a/colour/continuous/tests/test_multi_signal.py +++ b/colour/continuous/tests/test_multi_signal.py @@ -4,6 +4,10 @@ import pickle import textwrap +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType import numpy as np import pytest @@ -13,11 +17,16 @@ from colour.continuous import MultiSignals, Signal from colour.utilities import ( ColourRuntimeWarning, + as_ndarray, attest, is_pandas_installed, is_scipy_installed, tsplit, tstack, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, ) __author__ = "Colour Developers" @@ -102,39 +111,40 @@ def test_pickling(self) -> None: data = pickle.loads(data) # noqa: S301 assert self._multi_signals == data - def test_dtype(self) -> None: + def test_dtype(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.dtype` property. """ - assert self._multi_signals.dtype == DTYPE_FLOAT_DEFAULT + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert multi_signals.dtype == DTYPE_FLOAT_DEFAULT - multi_signals = self._multi_signals.copy() + multi_signals = multi_signals.copy() multi_signals.dtype = np.float32 assert multi_signals.dtype == np.float32 - def test_domain(self) -> None: + def test_domain(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.domain` property. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - np.testing.assert_allclose( - multi_signals[np.array([0, 1, 2])], - np.array([[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]]), + xp_assert_close( + multi_signals[[0, 1, 2]], + [[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals.domain = self._domain_1 * 10 + multi_signals.domain = xp_as_array(self._domain_1 * 10, xp=xp) - np.testing.assert_array_equal(multi_signals.domain, self._domain_1 * 10) + xp_assert_equal(multi_signals.domain, self._domain_1 * 10) - np.testing.assert_allclose( - multi_signals[np.array([0, 1, 2]) * 10], - np.array([[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]]), + xp_assert_close( + multi_signals[xp_as_array([0, 1, 2], xp=xp) * 10], + [[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -146,45 +156,50 @@ def assert_warns() -> None: multi_signals.domain = domain - pytest.warns(ColourRuntimeWarning, assert_warns) + with pytest.warns(ColourRuntimeWarning): + assert_warns() - def test_range(self) -> None: + def test_range(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.range` property. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - np.testing.assert_allclose( - multi_signals[np.array([0, 1, 2])], - np.array([[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]]), + xp_assert_close( + multi_signals[[0, 1, 2]], + [[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals.range = self._range_1 * 10 + multi_signals.range = xp_as_array(self._range_1 * 10, xp=xp) - np.testing.assert_array_equal( - multi_signals.range, tstack([self._range_1] * 3) * 10 - ) + xp_assert_equal(multi_signals.range, tstack([self._range_1] * 3) * 10) - np.testing.assert_allclose( - multi_signals[np.array([0, 1, 2])], - np.array([[10.0, 10.0, 10.0], [20.0, 20.0, 20.0], [30.0, 30.0, 30.0]]) * 10, + xp_assert_close( + multi_signals[[0, 1, 2]], + xp_as_array( + [[10.0, 10.0, 10.0], [20.0, 20.0, 20.0], [30.0, 30.0, 30.0]], xp=xp + ) + * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals.range = self._range_2 * 10 + multi_signals.range = xp_as_array(self._range_2 * 10, xp=xp) - np.testing.assert_array_equal(multi_signals.range, self._range_2 * 10) + xp_assert_equal(multi_signals.range, self._range_2 * 10) - np.testing.assert_allclose( - multi_signals[np.array([0, 1, 2])], - np.array([[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]]) * 10, + xp_assert_close( + multi_signals[[0, 1, 2]], + xp_as_array( + [[10.0, 20.0, 30.0], [20.0, 30.0, 40.0], [30.0, 40.0, 50.0]], xp=xp + ) + * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_interpolator(self) -> None: + def test_interpolator(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.interpolator` property. @@ -193,57 +208,51 @@ def test_interpolator(self) -> None: if not is_scipy_installed(): # pragma: no cover return - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( multi_signals[np.linspace(0, 5, 5)], - np.array( - [ - [10.00000000, 20.00000000, 30.00000000], - [22.83489024, 32.80460562, 42.77432100], - [34.80044921, 44.74343470, 54.68642018], - [47.55353925, 57.52325463, 67.49297001], - [60.00000000, 70.00000000, 80.00000000], - ] - ), + [ + [10.00000000, 20.00000000, 30.00000000], + [22.83489024, 32.80460562, 42.77432100], + [34.80044921, 44.74343470, 54.68642018], + [47.55353925, 57.52325463, 67.49297001], + [60.00000000, 70.00000000, 80.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals.interpolator = CubicSplineInterpolator - np.testing.assert_allclose( + xp_assert_close( multi_signals[np.linspace(0, 5, 5)], - np.array( - [ - [10.00000000, 20.00000000, 30.00000000], - [22.50000000, 32.50000000, 42.50000000], - [35.00000000, 45.00000000, 55.00000000], - [47.50000000, 57.50000000, 67.50000000], - [60.00000000, 70.00000000, 80.00000000], - ] - ), + [ + [10.00000000, 20.00000000, 30.00000000], + [22.50000000, 32.50000000, 42.50000000], + [35.00000000, 45.00000000, 55.00000000], + [47.50000000, 57.50000000, 67.50000000], + [60.00000000, 70.00000000, 80.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_interpolator_kwargs(self) -> None: + def test_interpolator_kwargs(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.\ interpolator_kwargs` property. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( multi_signals[np.linspace(0, 5, 5)], - np.array( - [ - [10.00000000, 20.00000000, 30.00000000], - [22.83489024, 32.80460562, 42.77432100], - [34.80044921, 44.74343470, 54.68642018], - [47.55353925, 57.52325463, 67.49297001], - [60.00000000, 70.00000000, 80.00000000], - ] - ), + [ + [10.00000000, 20.00000000, 30.00000000], + [22.83489024, 32.80460562, 42.77432100], + [34.80044921, 44.74343470, 54.68642018], + [47.55353925, 57.52325463, 67.49297001], + [60.00000000, 70.00000000, 80.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -252,55 +261,55 @@ def test_interpolator_kwargs(self) -> None: "kernel_kwargs": {"a": 1}, } - np.testing.assert_allclose( + xp_assert_close( multi_signals[np.linspace(0, 5, 5)], - np.array( - [ - [10.00000000, 20.00000000, 30.00000000], - [18.91328761, 27.91961505, 36.92594248], - [28.36993142, 36.47562611, 44.58132080], - [44.13100443, 53.13733187, 62.14365930], - [60.00000000, 70.00000000, 80.00000000], - ] - ), + [ + [10.00000000, 20.00000000, 30.00000000], + [18.91328761, 27.91961505, 36.92594248], + [28.36993142, 36.47562611, 44.58132080], + [44.13100443, 53.13733187, 62.14365930], + [60.00000000, 70.00000000, 80.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_extrapolator(self) -> None: + def test_extrapolator(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.extrapolator` property. """ - assert isinstance(self._multi_signals.extrapolator(), Extrapolator) + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert isinstance(multi_signals.extrapolator(), Extrapolator) - def test_extrapolator_kwargs(self) -> None: + def test_extrapolator_kwargs(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.\ extrapolator_kwargs` property. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - attest(np.all(np.isnan(multi_signals[np.array([-1000, 1000])]))) + attest(np.all(np.isnan(as_ndarray(multi_signals[np.array([-1000, 1000])])))) multi_signals.extrapolator_kwargs = { "method": "Linear", } - np.testing.assert_allclose( - multi_signals[np.array([-1000, 1000])], - np.array([[-9990.0, -9980.0, -9970.0], [10010.0, 10020.0, 10030.0]]), + xp_assert_close( + multi_signals[[-1000, 1000]], + [[-9990.0, -9980.0, -9970.0], [10010.0, 10020.0, 10030.0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_function(self) -> None: + def test_function(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.function` property. """ - attest(callable(self._multi_signals.function)) + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + attest(callable(multi_signals.function)) def test_raise_exception_function(self) -> None: """ @@ -308,29 +317,31 @@ def test_raise_exception_function(self) -> None: function` property raised exception. """ - pytest.raises((ValueError, TypeError), MultiSignals().function, 0) + with pytest.raises((ValueError, TypeError)): + MultiSignals().function(0) - def test_signals(self) -> None: + def test_signals(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.signals` property. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - multi_signals.signals = self._range_1 - np.testing.assert_array_equal(multi_signals.domain, self._domain_1) - np.testing.assert_array_equal(multi_signals.range, self._range_1[:, None]) + multi_signals.signals = xp_as_array(self._range_1, xp=xp) + xp_assert_equal(multi_signals.domain, self._domain_1) + xp_assert_equal(multi_signals.range, self._range_1[:, None]) - def test_labels(self) -> None: + def test_labels(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.labels` property. """ - assert self._multi_signals.labels == ["0", "1", "2"] + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert multi_signals.labels == ["0", "1", "2"] - multi_signals = self._multi_signals.copy() + multi_signals = multi_signals.copy() multi_signals.labels = ["a", "b", "c"] @@ -346,41 +357,47 @@ def test_signal_type(self) -> None: assert multi_signals.signal_type == Signal - def test__init__(self) -> None: + def test__init__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__init__` method. """ - multi_signals = MultiSignals(self._range_1) - np.testing.assert_array_equal(multi_signals.domain, self._domain_1) - np.testing.assert_array_equal(multi_signals.range, self._range_1[:, None]) + multi_signals = MultiSignals(xp_as_array(self._range_1, xp=xp)) + xp_assert_equal(multi_signals.domain, self._domain_1) + xp_assert_equal(multi_signals.range, self._range_1[:, None]) - multi_signals = MultiSignals(self._range_1, self._domain_2) - np.testing.assert_array_equal(multi_signals.domain, self._domain_2) - np.testing.assert_array_equal(multi_signals.range, self._range_1[:, None]) + multi_signals = MultiSignals( + xp_as_array(self._range_1, xp=xp), xp_as_array(self._domain_2, xp=xp) + ) + xp_assert_equal(multi_signals.domain, self._domain_2) + xp_assert_equal(multi_signals.range, self._range_1[:, None]) - multi_signals = MultiSignals(self._range_2, self._domain_2) - np.testing.assert_array_equal(multi_signals.domain, self._domain_2) - np.testing.assert_array_equal(multi_signals.range, self._range_2) + multi_signals = MultiSignals( + xp_as_array(self._range_2, xp=xp), xp_as_array(self._domain_2, xp=xp) + ) + xp_assert_equal(multi_signals.domain, self._domain_2) + xp_assert_equal(multi_signals.range, self._range_2) multi_signals = MultiSignals( dict(zip(self._domain_2, self._range_2, strict=True)) ) - np.testing.assert_array_equal(multi_signals.domain, self._domain_2) - np.testing.assert_array_equal(multi_signals.range, self._range_2) + xp_assert_equal(multi_signals.domain, self._domain_2) + xp_assert_equal(multi_signals.range, self._range_2) multi_signals = MultiSignals(multi_signals) - np.testing.assert_array_equal(multi_signals.domain, self._domain_2) - np.testing.assert_array_equal(multi_signals.range, self._range_2) + xp_assert_equal(multi_signals.domain, self._domain_2) + xp_assert_equal(multi_signals.range, self._range_2) class NotSignal(Signal): """Not :class:`Signal` class.""" - multi_signals = MultiSignals(self._range_1, signal_type=NotSignal) + multi_signals = MultiSignals( + xp_as_array(self._range_1, xp=xp), signal_type=NotSignal + ) assert isinstance(multi_signals.signals["0"], NotSignal) - np.testing.assert_array_equal(multi_signals.domain, self._domain_1) - np.testing.assert_array_equal(multi_signals.range, self._range_1[:, None]) + xp_assert_equal(multi_signals.domain, self._domain_1) + xp_assert_equal(multi_signals.range, self._range_1[:, None]) if is_pandas_installed(): from pandas import DataFrame, Series # noqa: PLC0415 @@ -388,21 +405,22 @@ class NotSignal(Signal): multi_signals = MultiSignals( Series(dict(zip(self._domain_2, self._range_1, strict=True))) ) - np.testing.assert_array_equal(multi_signals.domain, self._domain_2) - np.testing.assert_array_equal(multi_signals.range, self._range_1[:, None]) + xp_assert_equal(multi_signals.domain, self._domain_2) + xp_assert_equal(multi_signals.range, self._range_1[:, None]) data = dict(zip(["a", "b", "c"], tsplit(self._range_2), strict=True)) multi_signals = MultiSignals(DataFrame(data, self._domain_2)) - np.testing.assert_array_equal(multi_signals.domain, self._domain_2) - np.testing.assert_array_equal(multi_signals.range, self._range_2) + xp_assert_equal(multi_signals.domain, self._domain_2) + xp_assert_equal(multi_signals.range, self._range_2) - def test__hash__(self) -> None: + def test__hash__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__hash__` method. """ - assert isinstance(hash(self._multi_signals), int) + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert isinstance(hash(multi_signals), int) def test__str__(self) -> None: """ @@ -461,101 +479,93 @@ def test__repr__(self) -> None: assert isinstance(repr(MultiSignals()), str) - def test__getitem__(self) -> None: + def test__getitem__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__getitem__` method. """ - np.testing.assert_allclose( - self._multi_signals[0], - np.array([10.0, 20.0, 30.0]), + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + + xp_assert_close( + multi_signals[0], + [10.0, 20.0, 30.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals[np.array([0, 1, 2])], - np.array( - [ - [10.0, 20.0, 30.0], - [20.0, 30.0, 40.0], - [30.0, 40.0, 50.0], - ] - ), + xp_assert_close( + multi_signals[[0, 1, 2]], + [ + [10.0, 20.0, 30.0], + [20.0, 30.0, 40.0], + [30.0, 40.0, 50.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals[np.linspace(0, 5, 5)], - np.array( - [ - [10.00000000, 20.00000000, 30.00000000], - [22.83489024, 32.80460562, 42.77432100], - [34.80044921, 44.74343470, 54.68642018], - [47.55353925, 57.52325463, 67.49297001], - [60.00000000, 70.00000000, 80.00000000], - ] - ), + xp_assert_close( + multi_signals[np.linspace(0, 5, 5)], + [ + [10.00000000, 20.00000000, 30.00000000], + [22.83489024, 32.80460562, 42.77432100], + [34.80044921, 44.74343470, 54.68642018], + [47.55353925, 57.52325463, 67.49297001], + [60.00000000, 70.00000000, 80.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - attest(np.all(np.isnan(self._multi_signals[np.array([-1000, 1000])]))) + attest(np.all(np.isnan(as_ndarray(multi_signals[np.array([-1000, 1000])])))) - np.testing.assert_allclose( - self._multi_signals[:], - self._multi_signals.range, + xp_assert_close( + multi_signals[:], + multi_signals.range, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals[:, :], # pyright: ignore - self._multi_signals.range, + xp_assert_close( + multi_signals[:, :], + multi_signals.range, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals[0:3], - np.array( - [ - [10.0, 20.0, 30.0], - [20.0, 30.0, 40.0], - [30.0, 40.0, 50.0], - ] - ), + xp_assert_close( + multi_signals[0:3], + [ + [10.0, 20.0, 30.0], + [20.0, 30.0, 40.0], + [30.0, 40.0, 50.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals[:, 0:2], # pyright: ignore - np.array( - [ - [10.0, 20.0], - [20.0, 30.0], - [30.0, 40.0], - [40.0, 50.0], - [50.0, 60.0], - [60.0, 70.0], - [70.0, 80.0], - [80.0, 90.0], - [90.0, 100.0], - [100.0, 110.0], - ] - ), + xp_assert_close( + multi_signals[:, 0:2], + [ + [10.0, 20.0], + [20.0, 30.0], + [30.0, 40.0], + [40.0, 50.0], + [50.0, 60.0], + [60.0, 70.0], + [70.0, 80.0], + [80.0, 90.0], + [90.0, 100.0], + [100.0, 110.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals = self._multi_signals.copy() + multi_signals = multi_signals.copy() multi_signals.extrapolator_kwargs = { "method": "Linear", } - np.testing.assert_array_equal( - multi_signals[np.array([-1000, 1000])], - np.array( - [ - [-9990.0, -9980.0, -9970.0], - [10010.0, 10020.0, 10030.0], - ] - ), + xp_assert_equal( + multi_signals[[-1000, 1000]], + [ + [-9990.0, -9980.0, -9970.0], + [10010.0, 10020.0, 10030.0], + ], ) multi_signals.extrapolator_kwargs = { @@ -563,220 +573,209 @@ def test__getitem__(self) -> None: "left": 0, "right": 1, } - np.testing.assert_array_equal( - multi_signals[np.array([-1000, 1000])], - np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]), + xp_assert_equal( + multi_signals[[-1000, 1000]], + [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], ) - def test__setitem__(self) -> None: + def test__setitem__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__setitem__` method. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) multi_signals[0] = 20 - np.testing.assert_allclose( + xp_assert_close( multi_signals[0], - np.array([20.0, 20.0, 20.0]), + [20.0, 20.0, 20.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals[np.array([0, 1, 2])] = 30 - np.testing.assert_allclose( - multi_signals[np.array([0, 1, 2])], - np.array( - [ - [30.0, 30.0, 30.0], - [30.0, 30.0, 30.0], - [30.0, 30.0, 30.0], - ] - ), + xp_assert_close( + multi_signals[[0, 1, 2]], + [ + [30.0, 30.0, 30.0], + [30.0, 30.0, 30.0], + [30.0, 30.0, 30.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals[np.linspace(0, 5, 5)] = 50 - np.testing.assert_allclose( + xp_assert_close( multi_signals.domain, - np.array( - [ - 0.00, - 1.00, - 1.25, - 2.00, - 2.50, - 3.00, - 3.75, - 4.00, - 5.00, - 6.00, - 7.00, - 8.00, - 9.00, - ] - ), + [ + 0.00, + 1.00, + 1.25, + 2.00, + 2.50, + 3.00, + 3.75, + 4.00, + 5.00, + 6.00, + 7.00, + 8.00, + 9.00, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( multi_signals.range, - np.array( - [ - [50.0, 50.0, 50.0], - [30.0, 30.0, 30.0], - [50.0, 50.0, 50.0], - [30.0, 30.0, 30.0], - [50.0, 50.0, 50.0], - [40.0, 50.0, 60.0], - [50.0, 50.0, 50.0], - [50.0, 60.0, 70.0], - [50.0, 50.0, 50.0], - [70.0, 80.0, 90.0], - [80.0, 90.0, 100.0], - [90.0, 100.0, 110.0], - [100.0, 110.0, 120.0], - ] - ), + [ + [50.0, 50.0, 50.0], + [30.0, 30.0, 30.0], + [50.0, 50.0, 50.0], + [30.0, 30.0, 30.0], + [50.0, 50.0, 50.0], + [40.0, 50.0, 60.0], + [50.0, 50.0, 50.0], + [50.0, 60.0, 70.0], + [50.0, 50.0, 50.0], + [70.0, 80.0, 90.0], + [80.0, 90.0, 100.0], + [90.0, 100.0, 110.0], + [100.0, 110.0, 120.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals[np.array([0, 1, 2])] = np.array([10, 20, 30]) - np.testing.assert_allclose( + xp_assert_close( multi_signals.range, - np.array( - [ - [10.0, 20.0, 30.0], - [10.0, 20.0, 30.0], - [50.0, 50.0, 50.0], - [10.0, 20.0, 30.0], - [50.0, 50.0, 50.0], - [40.0, 50.0, 60.0], - [50.0, 50.0, 50.0], - [50.0, 60.0, 70.0], - [50.0, 50.0, 50.0], - [70.0, 80.0, 90.0], - [80.0, 90.0, 100.0], - [90.0, 100.0, 110.0], - [100.0, 110.0, 120.0], - ] - ), + [ + [10.0, 20.0, 30.0], + [10.0, 20.0, 30.0], + [50.0, 50.0, 50.0], + [10.0, 20.0, 30.0], + [50.0, 50.0, 50.0], + [40.0, 50.0, 60.0], + [50.0, 50.0, 50.0], + [50.0, 60.0, 70.0], + [50.0, 50.0, 50.0], + [70.0, 80.0, 90.0], + [80.0, 90.0, 100.0], + [90.0, 100.0, 110.0], + [100.0, 110.0, 120.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals[np.array([0, 1, 2])] = np.reshape(np.arange(1, 10, 1), (3, 3)) - np.testing.assert_allclose( + multi_signals[np.array([0, 1, 2])] = xp_reshape( + xp.arange(1, 10, 1), + (3, 3), + xp=xp, + ) + xp_assert_close( multi_signals.range, - np.array( - [ - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - [50.0, 50.0, 50.0], - [7.0, 8.0, 9.0], - [50.0, 50.0, 50.0], - [40.0, 50.0, 60.0], - [50.0, 50.0, 50.0], - [50.0, 60.0, 70.0], - [50.0, 50.0, 50.0], - [70.0, 80.0, 90.0], - [80.0, 90.0, 100.0], - [90.0, 100.0, 110.0], - [100.0, 110.0, 120.0], - ] - ), + [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [50.0, 50.0, 50.0], + [7.0, 8.0, 9.0], + [50.0, 50.0, 50.0], + [40.0, 50.0, 60.0], + [50.0, 50.0, 50.0], + [50.0, 60.0, 70.0], + [50.0, 50.0, 50.0], + [70.0, 80.0, 90.0], + [80.0, 90.0, 100.0], + [90.0, 100.0, 110.0], + [100.0, 110.0, 120.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals[:] = 40 - np.testing.assert_allclose( - multi_signals.range, 40, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(multi_signals.range, 40, atol=TOLERANCE_ABSOLUTE_TESTS) multi_signals[:, :] = 50 # pyright: ignore - np.testing.assert_allclose( - multi_signals.range, 50, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(multi_signals.range, 50, atol=TOLERANCE_ABSOLUTE_TESTS) - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) multi_signals[0:3] = 40 - np.testing.assert_allclose( + xp_assert_close( multi_signals[0:3], - np.array( - [ - [40.0, 40.0, 40.0], - [40.0, 40.0, 40.0], - [40.0, 40.0, 40.0], - ] - ), + [ + [40.0, 40.0, 40.0], + [40.0, 40.0, 40.0], + [40.0, 40.0, 40.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals[:, 0:2] = 50 # pyright: ignore - np.testing.assert_allclose( + xp_assert_close( multi_signals.range, - np.array( - [ - [50.0, 50.0, 40.0], - [50.0, 50.0, 40.0], - [50.0, 50.0, 40.0], - [50.0, 50.0, 60.0], - [50.0, 50.0, 70.0], - [50.0, 50.0, 80.0], - [50.0, 50.0, 90.0], - [50.0, 50.0, 100.0], - [50.0, 50.0, 110.0], - [50.0, 50.0, 120.0], - ] - ), + [ + [50.0, 50.0, 40.0], + [50.0, 50.0, 40.0], + [50.0, 50.0, 40.0], + [50.0, 50.0, 60.0], + [50.0, 50.0, 70.0], + [50.0, 50.0, 80.0], + [50.0, 50.0, 90.0], + [50.0, 50.0, 100.0], + [50.0, 50.0, 110.0], + [50.0, 50.0, 120.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test__contains__(self) -> None: + def test__contains__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__contains__` method. """ - assert 0 in self._multi_signals - assert 0.5 in self._multi_signals - assert 1000 not in self._multi_signals + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert 0 in multi_signals + assert 0.5 in multi_signals + assert 1000 not in multi_signals - def test__iter__(self) -> None: + def test__iter__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__iter__` method.""" + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) domain = np.arange(0, 10) - for i, domain_range_value in enumerate(self._multi_signals): - np.testing.assert_array_equal(domain_range_value[0], domain[i]) - np.testing.assert_array_equal(domain_range_value[1:], self._range_2[i]) + for i, domain_range_value in enumerate(multi_signals): + xp_assert_equal(domain_range_value[0], domain[i]) + xp_assert_equal(domain_range_value[1:], self._range_2[i]) - def test__len__(self) -> None: + def test__len__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__len__` method. """ - assert len(self._multi_signals) == 10 + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert len(multi_signals) == 10 - def test__eq__(self) -> None: + def test__eq__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__eq__` method. """ - signal_1 = self._multi_signals.copy() - signal_2 = self._multi_signals.copy() + signal_1 = MultiSignals(xp_as_array(self._range_2, xp=xp)) + signal_2 = MultiSignals(xp_as_array(self._range_2, xp=xp)) assert signal_1 == signal_2 assert signal_1 != () - def test__ne__(self) -> None: + def test__ne__(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.__ne__` method. """ - multi_signals_1 = self._multi_signals.copy() - multi_signals_2 = self._multi_signals.copy() + multi_signals_1 = MultiSignals(xp_as_array(self._range_2, xp=xp)) + multi_signals_2 = MultiSignals(xp_as_array(self._range_2, xp=xp)) multi_signals_2[0] = 20 assert multi_signals_1 != multi_signals_2 @@ -815,290 +814,268 @@ class NotExtrapolator(Extrapolator): } assert multi_signals_1 == multi_signals_2 - def test_arithmetical_operation(self) -> None: + def test_arithmetical_operation(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.\ arithmetical_operation` method. """ - np.testing.assert_allclose( - self._multi_signals.arithmetical_operation(10, "+", False).range, + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + + xp_assert_close( + multi_signals.arithmetical_operation(10, "+", False).range, self._range_2 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals.arithmetical_operation(10, "-", False).range, + xp_assert_close( + multi_signals.arithmetical_operation(10, "-", False).range, self._range_2 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals.arithmetical_operation(10, "*", False).range, + xp_assert_close( + multi_signals.arithmetical_operation(10, "*", False).range, self._range_2 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals.arithmetical_operation(10, "/", False).range, + xp_assert_close( + multi_signals.arithmetical_operation(10, "/", False).range, self._range_2 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals.arithmetical_operation(10, "**", False).range, + xp_assert_close( + multi_signals.arithmetical_operation(10, "**", False).range, self._range_2**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._multi_signals + 10).range, + xp_assert_close( + (multi_signals + 10).range, self._range_2 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._multi_signals - 10).range, + xp_assert_close( + (multi_signals - 10).range, self._range_2 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._multi_signals * 10).range, + xp_assert_close( + (multi_signals * 10).range, self._range_2 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._multi_signals / 10).range, + xp_assert_close( + (multi_signals / 10).range, self._range_2 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._multi_signals**10).range, + xp_assert_close( + (multi_signals**10).range, self._range_2**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( multi_signals.arithmetical_operation(10, "+", True).range, self._range_2 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( multi_signals.arithmetical_operation(10, "-", True).range, self._range_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( multi_signals.arithmetical_operation(10, "*", True).range, self._range_2 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( multi_signals.arithmetical_operation(10, "/", True).range, self._range_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( multi_signals.arithmetical_operation(10, "**", True).range, self._range_2**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - multi_signals = self._multi_signals.copy() - np.testing.assert_allclose( + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + xp_assert_close( multi_signals.arithmetical_operation(self._range_2, "+", False).range, self._range_2 + self._range_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( multi_signals.arithmetical_operation(multi_signals, "+", False).range, self._range_2 + self._range_2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_is_uniform(self) -> None: + def test_is_uniform(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.is_uniform` method. """ - assert self._multi_signals.is_uniform() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert multi_signals.is_uniform() - multi_signals = self._multi_signals.copy() + multi_signals = multi_signals.copy() multi_signals[0.5] = 1.0 assert not multi_signals.is_uniform() - def test_copy(self) -> None: + def test_copy(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.multi_signals.MultiSignals.copy` method.""" - assert self._multi_signals is not self._multi_signals.copy() - assert self._multi_signals == self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + assert multi_signals is not multi_signals.copy() + assert multi_signals == multi_signals.copy() - def test_multi_signals_unpack_data(self) -> None: + def test_multi_signals_unpack_data(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.\ multi_signals_unpack_data` method. """ - signals = MultiSignals.multi_signals_unpack_data(self._range_1) - assert list(signals.keys()) == ["0"] - np.testing.assert_array_equal(signals["0"].domain, self._domain_1) - np.testing.assert_array_equal(signals["0"].range, self._range_1) + domain, range_, labels = MultiSignals.multi_signals_unpack_data( + xp_as_array(self._range_1, xp=xp) + ) + assert labels == ["0"] + xp_assert_equal(domain, self._domain_1) + xp_assert_equal(range_, self._range_1.reshape(-1, 1)) - signals = MultiSignals.multi_signals_unpack_data(self._range_1, self._domain_2) - assert list(signals.keys()) == ["0"] - np.testing.assert_array_equal(signals["0"].domain, self._domain_2) - np.testing.assert_array_equal(signals["0"].range, self._range_1) + domain, range_, labels = MultiSignals.multi_signals_unpack_data( + xp_as_array(self._range_1, xp=xp), xp_as_array(self._domain_2, xp=xp) + ) + assert labels == ["0"] + xp_assert_equal(domain, self._domain_2) + xp_assert_equal(range_, self._range_1.reshape(-1, 1)) - signals = MultiSignals.multi_signals_unpack_data( + domain, range_, labels = MultiSignals.multi_signals_unpack_data( self._range_1, dict(zip(self._domain_2, self._range_1, strict=True)).keys() ) - np.testing.assert_array_equal(signals["0"].domain, self._domain_2) - - signals = MultiSignals.multi_signals_unpack_data(self._range_2, self._domain_2) - assert list(signals.keys()) == ["0", "1", "2"] - np.testing.assert_array_equal(signals["0"].range, self._range_1) - np.testing.assert_array_equal(signals["1"].range, self._range_1 + 10) - np.testing.assert_array_equal(signals["2"].range, self._range_1 + 20) - - signals = MultiSignals.multi_signals_unpack_data( - next( - iter( - MultiSignals.multi_signals_unpack_data( - dict(zip(self._domain_2, self._range_2, strict=True)) - ).values() - ) - ) - ) - np.testing.assert_array_equal(signals["0"].range, self._range_1) + xp_assert_equal(domain, self._domain_2) - signals = MultiSignals.multi_signals_unpack_data( - MultiSignals.multi_signals_unpack_data( - dict(zip(self._domain_2, self._range_2, strict=True)) - ).values() + domain, range_, labels = MultiSignals.multi_signals_unpack_data( + xp_as_array(self._range_2, xp=xp), xp_as_array(self._domain_2, xp=xp) ) - np.testing.assert_array_equal(signals["0"].range, self._range_1) - np.testing.assert_array_equal(signals["1"].range, self._range_1 + 10) - np.testing.assert_array_equal(signals["2"].range, self._range_1 + 20) + assert labels == ["0", "1", "2"] + xp_assert_equal(range_[:, 0], self._range_1) + xp_assert_equal(range_[:, 1], self._range_1 + 10) + xp_assert_equal(range_[:, 2], self._range_1 + 20) - signals = MultiSignals.multi_signals_unpack_data( + domain, range_, labels = MultiSignals.multi_signals_unpack_data( dict(zip(self._domain_2, self._range_2, strict=True)) ) - assert list(signals.keys()) == ["0", "1", "2"] - np.testing.assert_array_equal(signals["0"].range, self._range_1) - np.testing.assert_array_equal(signals["1"].range, self._range_1 + 10) - np.testing.assert_array_equal(signals["2"].range, self._range_1 + 20) - - signals = MultiSignals.multi_signals_unpack_data( - MultiSignals.multi_signals_unpack_data( - dict(zip(self._domain_2, self._range_2, strict=True)) - ) - ) - assert list(signals.keys()) == ["0", "1", "2"] - np.testing.assert_array_equal(signals["0"].range, self._range_1) - np.testing.assert_array_equal(signals["1"].range, self._range_1 + 10) - np.testing.assert_array_equal(signals["2"].range, self._range_1 + 20) + assert labels == ["0", "1", "2"] + xp_assert_equal(range_[:, 0], self._range_1) + xp_assert_equal(range_[:, 1], self._range_1 + 10) + xp_assert_equal(range_[:, 2], self._range_1 + 20) - signals = MultiSignals.multi_signals_unpack_data( + domain, range_, labels = MultiSignals.multi_signals_unpack_data( dict(zip(self._domain_2, self._range_2, strict=True)), labels=["0", "0", "0"], ) - assert list(signals.keys()) == ["0 - 0", "0 - 1", "0 - 2"] + assert labels == ["0 - 0", "0 - 1", "0 - 2"] if is_pandas_installed(): from pandas import DataFrame, Series # noqa: PLC0415 - signals = MultiSignals.multi_signals_unpack_data( + domain, range_, labels = MultiSignals.multi_signals_unpack_data( Series(dict(zip(self._domain_1, self._range_1, strict=True))) ) - assert list(signals.keys()) == ["0"] - np.testing.assert_array_equal(signals["0"].domain, self._domain_1) - np.testing.assert_array_equal(signals["0"].range, self._range_1) + assert labels == ["0"] + xp_assert_equal(domain, self._domain_1) + xp_assert_equal(range_, self._range_1.reshape(-1, 1)) data = dict(zip(["a", "b", "c"], tsplit(self._range_2), strict=True)) - signals = MultiSignals.multi_signals_unpack_data( + domain, range_, labels = MultiSignals.multi_signals_unpack_data( DataFrame(data, self._domain_1) ) - assert list(signals.keys()) == ["a", "b", "c"] - np.testing.assert_array_equal(signals["a"].range, self._range_1) - np.testing.assert_array_equal(signals["b"].range, self._range_1 + 10) - np.testing.assert_array_equal(signals["c"].range, self._range_1 + 20) + assert labels == ["a", "b", "c"] + xp_assert_equal(range_[:, 0], self._range_1) + xp_assert_equal(range_[:, 1], self._range_1 + 10) + xp_assert_equal(range_[:, 2], self._range_1 + 20) - def test_fill_nan(self) -> None: + def test_fill_nan(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.multi_signals.MultiSignals.fill_nan` method. """ - multi_signals = self._multi_signals.copy() + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) multi_signals[3:7] = np.nan - np.testing.assert_allclose( + xp_assert_close( multi_signals.fill_nan().range, - np.array( - [ - [10.0, 20.0, 30.0], - [20.0, 30.0, 40.0], - [30.0, 40.0, 50.0], - [40.0, 50.0, 60.0], - [50.0, 60.0, 70.0], - [60.0, 70.0, 80.0], - [70.0, 80.0, 90.0], - [80.0, 90.0, 100.0], - [90.0, 100.0, 110.0], - [100.0, 110.0, 120.0], - ] - ), + [ + [10.0, 20.0, 30.0], + [20.0, 30.0, 40.0], + [30.0, 40.0, 50.0], + [40.0, 50.0, 60.0], + [50.0, 60.0, 70.0], + [60.0, 70.0, 80.0], + [70.0, 80.0, 90.0], + [80.0, 90.0, 100.0], + [90.0, 100.0, 110.0], + [100.0, 110.0, 120.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) multi_signals[3:7] = np.nan - np.testing.assert_allclose( + xp_assert_close( multi_signals.fill_nan(method="Constant").range, - np.array( - [ - [10.0, 20.0, 30.0], - [20.0, 30.0, 40.0], - [30.0, 40.0, 50.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [80.0, 90.0, 100.0], - [90.0, 100.0, 110.0], - [100.0, 110.0, 120.0], - ] - ), + [ + [10.0, 20.0, 30.0], + [20.0, 30.0, 40.0], + [30.0, 40.0, 50.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [80.0, 90.0, 100.0], + [90.0, 100.0, 110.0], + [100.0, 110.0, 120.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_distance(self) -> None: + def test_domain_distance(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.multi_signals.MultiSignals.\ domain_distance` method. """ - np.testing.assert_allclose( - self._multi_signals.domain_distance(0.5), + multi_signals = MultiSignals(xp_as_array(self._range_2, xp=xp)) + + xp_assert_close( + multi_signals.domain_distance(0.5), 0.5, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._multi_signals.domain_distance(np.linspace(0, 9, 10) + 0.5), - np.array([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]), + xp_assert_close( + multi_signals.domain_distance(np.linspace(0, 9, 10) + 0.5), + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/continuous/tests/test_signal.py b/colour/continuous/tests/test_signal.py index 85d6fff8b8..3e33f72ccf 100644 --- a/colour/continuous/tests/test_signal.py +++ b/colour/continuous/tests/test_signal.py @@ -4,6 +4,10 @@ import pickle import textwrap +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType import numpy as np import pytest @@ -13,9 +17,13 @@ from colour.continuous import Signal from colour.utilities import ( ColourRuntimeWarning, + as_ndarray, attest, is_pandas_installed, is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_assert_equal, ) __author__ = "Colour Developers" @@ -92,33 +100,34 @@ def test_pickling(self) -> None: data = pickle.loads(data) # noqa: S301 assert self._signal == data - def test_dtype(self) -> None: + def test_dtype(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.dtype` property.""" - assert self._signal.dtype == DTYPE_FLOAT_DEFAULT + signal = Signal(xp_as_array(self._range, xp=xp)) + assert signal.dtype == DTYPE_FLOAT_DEFAULT - signal = self._signal.copy() + signal = signal.copy() signal.dtype = np.float32 assert signal.dtype == np.float32 - def test_domain(self) -> None: + def test_domain(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.domain` property.""" - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( - signal[np.array([0, 1, 2])], - np.array([10.0, 20.0, 30.0]), + xp_assert_close( + signal[[0, 1, 2]], + [10.0, 20.0, 30.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - signal.domain = np.arange(0, 10, 1) * 10 + signal.domain = xp.arange(0, 10, 1) * 10 - np.testing.assert_array_equal(signal.domain, np.arange(0, 10, 1) * 10) + xp_assert_equal(signal.domain, np.arange(0, 10, 1) * 10) - np.testing.assert_allclose( - signal[np.array([0, 1, 2]) * 10], - np.array([10.0, 20.0, 30.0]), + xp_assert_close( + signal[xp_as_array([0, 1, 2], xp=xp) * 10], + [10.0, 20.0, 30.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -130,26 +139,27 @@ def assert_warns() -> None: signal.domain = domain - pytest.warns(ColourRuntimeWarning, assert_warns) + with pytest.warns(ColourRuntimeWarning): + assert_warns() - def test_range(self) -> None: + def test_range(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.range` property.""" - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( - signal[np.array([0, 1, 2])], - np.array([10.0, 20.0, 30.0]), + xp_assert_close( + signal[[0, 1, 2]], + [10.0, 20.0, 30.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - signal.range = self._range * 10 + signal.range = xp_as_array(self._range * 10, xp=xp) - np.testing.assert_array_equal(signal.range, self._range * 10) + xp_assert_equal(signal.range, self._range * 10) - np.testing.assert_allclose( - signal[np.array([0, 1, 2])], - np.array([10.0, 20.0, 30.0]) * 10, + xp_assert_close( + signal[[0, 1, 2]], + xp_as_array([10.0, 20.0, 30.0], xp=xp) * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -158,105 +168,102 @@ def assert_warns() -> None: signal.range = self._range * np.inf - pytest.warns(ColourRuntimeWarning, assert_warns) + with pytest.warns(ColourRuntimeWarning): + assert_warns() - def test_interpolator(self) -> None: + def test_interpolator(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.interpolator` property.""" if not is_scipy_installed(): # pragma: no cover return - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( signal[np.linspace(0, 5, 5)], - np.array( - [ - 10.00000000, - 22.83489024, - 34.80044921, - 47.55353925, - 60.00000000, - ] - ), + [ + 10.00000000, + 22.83489024, + 34.80044921, + 47.55353925, + 60.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) signal.interpolator = CubicSplineInterpolator - np.testing.assert_allclose( + xp_assert_close( signal[np.linspace(0, 5, 5)], - np.array([10.0, 22.5, 35.0, 47.5, 60.0]), + [10.0, 22.5, 35.0, 47.5, 60.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_interpolator_kwargs(self) -> None: + def test_interpolator_kwargs(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.signal.Signal.interpolator_kwargs` property. """ - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( signal[np.linspace(0, 5, 5)], - np.array( - [ - 10.00000000, - 22.83489024, - 34.80044921, - 47.55353925, - 60.00000000, - ] - ), + [ + 10.00000000, + 22.83489024, + 34.80044921, + 47.55353925, + 60.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) signal.interpolator_kwargs = {"window": 1, "kernel_kwargs": {"a": 1}} - np.testing.assert_allclose( + xp_assert_close( signal[np.linspace(0, 5, 5)], - np.array( - [ - 10.00000000, - 18.91328761, - 28.36993142, - 44.13100443, - 60.00000000, - ] - ), + [ + 10.00000000, + 18.91328761, + 28.36993142, + 44.13100443, + 60.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_extrapolator(self) -> None: + def test_extrapolator(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.extrapolator` property.""" - assert isinstance(self._signal.extrapolator(), Extrapolator) + signal = Signal(xp_as_array(self._range, xp=xp)) + assert isinstance(signal.extrapolator(), Extrapolator) - def test_extrapolator_kwargs(self) -> None: + def test_extrapolator_kwargs(self, xp: ModuleType) -> None: """ Test :func:`colour.continuous.signal.Signal.extrapolator_kwargs` property. """ - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - attest(np.all(np.isnan(signal[np.array([-1000, 1000])]))) + attest(np.all(np.isnan(as_ndarray(signal[np.array([-1000, 1000])])))) signal.extrapolator_kwargs = { "method": "Linear", } - np.testing.assert_allclose( - signal[np.array([-1000, 1000])], - np.array([-9990.0, 10010.0]), + xp_assert_close( + signal[[-1000, 1000]], + [-9990.0, 10010.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_function(self) -> None: + def test_function(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.function` property.""" - attest(callable(self._signal.function)) + signal = Signal(xp_as_array(self._range, xp=xp)) + attest(callable(signal.function)) def test_raise_exception_function(self) -> None: """ @@ -264,38 +271,42 @@ def test_raise_exception_function(self) -> None: exception. """ - pytest.raises(ValueError, Signal().function, 0) + with pytest.raises(ValueError): + Signal().function(0) - def test__init__(self) -> None: + def test__init__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__init__` method.""" - signal = Signal(self._range) - np.testing.assert_array_equal(signal.domain, np.arange(0, 10, 1)) - np.testing.assert_array_equal(signal.range, self._range) + signal = Signal(xp_as_array(self._range, xp=xp)) + xp_assert_equal(signal.domain, np.arange(0, 10, 1)) + xp_assert_equal(signal.range, self._range) - signal = Signal(self._range, self._domain) - np.testing.assert_array_equal(signal.domain, self._domain) - np.testing.assert_array_equal(signal.range, self._range) + signal = Signal( + xp_as_array(self._range, xp=xp), xp_as_array(self._domain, xp=xp) + ) + xp_assert_equal(signal.domain, self._domain) + xp_assert_equal(signal.range, self._range) signal = Signal(dict(zip(self._domain, self._range, strict=True))) - np.testing.assert_array_equal(signal.domain, self._domain) - np.testing.assert_array_equal(signal.range, self._range) + xp_assert_equal(signal.domain, self._domain) + xp_assert_equal(signal.range, self._range) signal = Signal(signal) - np.testing.assert_array_equal(signal.domain, self._domain) - np.testing.assert_array_equal(signal.range, self._range) + xp_assert_equal(signal.domain, self._domain) + xp_assert_equal(signal.range, self._range) if is_pandas_installed(): from pandas import Series # noqa: PLC0415 signal = Signal(Series(dict(zip(self._domain, self._range, strict=True)))) - np.testing.assert_array_equal(signal.domain, self._domain) - np.testing.assert_array_equal(signal.range, self._range) + xp_assert_equal(signal.domain, self._domain) + xp_assert_equal(signal.range, self._range) - def test__hash__(self) -> None: + def test__hash__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__hash__` method.""" - assert isinstance(hash(self._signal), int) + signal = Signal(xp_as_array(self._range, xp=xp)) + assert isinstance(hash(signal), int) def test__str__(self) -> None: """Test :func:`colour.continuous.signal.Signal.__str__` method.""" @@ -347,175 +358,169 @@ def test__repr__(self) -> None: assert isinstance(repr(Signal()), str) - def test__getitem__(self) -> None: + def test__getitem__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__getitem__` method.""" - assert self._signal[0] == 10.0 + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( - self._signal[np.array([0, 1, 2])], - np.array([10.0, 20.0, 30.0]), + xp_assert_close(float(signal[0]), 10.0, atol=TOLERANCE_ABSOLUTE_TESTS) + + xp_assert_close( + signal[[0, 1, 2]], + [10.0, 20.0, 30.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._signal[np.linspace(0, 5, 5)], - np.array( - [ - 10.00000000, - 22.83489024, - 34.80044921, - 47.55353925, - 60.00000000, - ] - ), + xp_assert_close( + signal[np.linspace(0, 5, 5)], + [ + 10.00000000, + 22.83489024, + 34.80044921, + 47.55353925, + 60.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - attest(np.all(np.isnan(self._signal[np.array([-1000, 1000])]))) + attest(np.all(np.isnan(as_ndarray(signal[np.array([-1000, 1000])])))) - signal = self._signal.copy() + signal = signal.copy() signal.extrapolator_kwargs = { "method": "Linear", } - np.testing.assert_array_equal( - signal[np.array([-1000, 1000])], np.array([-9990.0, 10010.0]) - ) + xp_assert_equal(signal[[-1000, 1000]], [-9990.0, 10010.0]) signal.extrapolator_kwargs = { "method": "Constant", "left": 0, "right": 1, } - np.testing.assert_array_equal( - signal[np.array([-1000, 1000])], np.array([0.0, 1.0]) - ) + xp_assert_equal(signal[[-1000, 1000]], [0.0, 1.0]) - def test__setitem__(self) -> None: + def test__setitem__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__setitem__` method.""" - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) signal[0] = 20 - np.testing.assert_allclose( + xp_assert_close( signal.range, - np.array([20.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]), + [20.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0], + atol=TOLERANCE_ABSOLUTE_TESTS, ) signal[np.array([0, 1, 2])] = 30 - np.testing.assert_allclose( + xp_assert_close( signal.range, - np.array([30.0, 30.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]), + [30.0, 30.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) signal[0:3] = 40 - np.testing.assert_allclose( + xp_assert_close( signal.range, - np.array([40.0, 40.0, 40.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]), + [40.0, 40.0, 40.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) signal[np.linspace(0, 5, 5)] = 50 - np.testing.assert_allclose( + xp_assert_close( signal.domain, - np.array( - [ - 0.00, - 1.00, - 1.25, - 2.00, - 2.50, - 3.00, - 3.75, - 4.00, - 5.00, - 6.00, - 7.00, - 8.00, - 9.00, - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - np.testing.assert_allclose( + [ + 0.00, + 1.00, + 1.25, + 2.00, + 2.50, + 3.00, + 3.75, + 4.00, + 5.00, + 6.00, + 7.00, + 8.00, + 9.00, + ], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + xp_assert_close( signal.range, - np.array( - [ - 50.0, - 40.0, - 50.0, - 40.0, - 50.0, - 40.0, - 50.0, - 50.0, - 50.0, - 70.0, - 80.0, - 90.0, - 100.0, - ] - ), + [ + 50.0, + 40.0, + 50.0, + 40.0, + 50.0, + 40.0, + 50.0, + 50.0, + 50.0, + 70.0, + 80.0, + 90.0, + 100.0, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) signal[np.array([0, 1, 2])] = np.array([10, 20, 30]) - np.testing.assert_allclose( + xp_assert_close( signal.range, - np.array( - [ - 10.0, - 20.0, - 50.0, - 30.0, - 50.0, - 40.0, - 50.0, - 50.0, - 50.0, - 70.0, - 80.0, - 90.0, - 100.0, - ] - ), - atol=TOLERANCE_ABSOLUTE_TESTS, - ) - - def test__contains__(self) -> None: + [ + 10.0, + 20.0, + 50.0, + 30.0, + 50.0, + 40.0, + 50.0, + 50.0, + 50.0, + 70.0, + 80.0, + 90.0, + 100.0, + ], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + def test__contains__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__contains__` method.""" - assert 0 in self._signal - assert 0.5 in self._signal - assert 1000 not in self._signal + signal = Signal(xp_as_array(self._range, xp=xp)) + assert 0 in signal + assert 0.5 in signal + assert 1000 not in signal - def test__iter__(self) -> None: + def test__iter__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__iter__` method.""" + signal = Signal(xp_as_array(self._range, xp=xp)) domain = np.arange(0, 10) - for i, (domain_value, range_value) in enumerate(self._signal): - np.testing.assert_array_equal(domain_value, domain[i]) - np.testing.assert_array_equal(range_value, self._range[i]) + for i, (domain_value, range_value) in enumerate(signal): + xp_assert_equal(domain_value, domain[i]) + xp_assert_equal(range_value, self._range[i]) - def test__len__(self) -> None: + def test__len__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__len__` method.""" - assert len(self._signal) == 10 + signal = Signal(xp_as_array(self._range, xp=xp)) + assert len(signal) == 10 - def test__eq__(self) -> None: + def test__eq__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__eq__` method.""" - signal_1 = self._signal.copy() - signal_2 = self._signal.copy() + signal_1 = Signal(xp_as_array(self._range, xp=xp)) + signal_2 = Signal(xp_as_array(self._range, xp=xp)) assert signal_1 == signal_2 - def test__ne__(self) -> None: + def test__ne__(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.__ne__` method.""" - signal_1 = self._signal.copy() - signal_2 = self._signal.copy() + signal_1 = Signal(xp_as_array(self._range, xp=xp)) + signal_2 = Signal(xp_as_array(self._range, xp=xp)) signal_2[0] = 20 assert signal_1 != signal_2 @@ -554,161 +559,169 @@ class NotExtrapolator(Extrapolator): } assert signal_1 == signal_2 - def test_arithmetical_operation(self) -> None: + def test_arithmetical_operation(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.signal.Signal.arithmetical_operation` method. """ - np.testing.assert_allclose( - self._signal.arithmetical_operation(10, "+", False).range, + signal = Signal(xp_as_array(self._range, xp=xp)) + + xp_assert_close( + signal.arithmetical_operation(10, "+", False).range, self._range + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._signal.arithmetical_operation(10, "-", False).range, + xp_assert_close( + signal.arithmetical_operation(10, "-", False).range, self._range - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._signal.arithmetical_operation(10, "*", False).range, + xp_assert_close( + signal.arithmetical_operation(10, "*", False).range, self._range * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._signal.arithmetical_operation(10, "/", False).range, + xp_assert_close( + signal.arithmetical_operation(10, "/", False).range, self._range / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._signal.arithmetical_operation(10, "**", False).range, + xp_assert_close( + signal.arithmetical_operation(10, "**", False).range, self._range**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._signal + 10).range, + xp_assert_close( + (signal + 10).range, self._range + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._signal - 10).range, + xp_assert_close( + (signal - 10).range, self._range - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._signal * 10).range, + xp_assert_close( + (signal * 10).range, self._range * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._signal / 10).range, + xp_assert_close( + (signal / 10).range, self._range / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - (self._signal**10).range, + xp_assert_close( + (signal**10).range, self._range**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(10, "+", True).range, self._range + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(10, "-", True).range, self._range, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(10, "*", True).range, self._range * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(10, "/", True).range, self._range, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(10, "**", True).range, self._range**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(self._range, "+", False).range, - signal.range + self._range, + as_ndarray(signal.range) + self._range, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( signal.arithmetical_operation(signal, "+", False).range, - signal.range + signal.range, + as_ndarray(signal.range) + as_ndarray(signal.range), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_is_uniform(self) -> None: + def test_is_uniform(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.is_uniform` method.""" - assert self._signal.is_uniform() + signal = Signal(xp_as_array(self._range, xp=xp)) + assert signal.is_uniform() - signal = self._signal.copy() + signal = signal.copy() signal[0.5] = 1.0 assert not signal.is_uniform() - def test_copy(self) -> None: + def test_copy(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.copy` method.""" - assert self._signal is not self._signal.copy() - assert self._signal == self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) + assert signal is not signal.copy() + assert signal == signal.copy() - def test_signal_unpack_data(self) -> None: + def test_signal_unpack_data(self, xp: ModuleType) -> None: """ Test :meth:`colour.continuous.signal.Signal.signal_unpack_data` method. """ - domain, range_ = Signal.signal_unpack_data(self._range) - np.testing.assert_array_equal(range_, self._range) - np.testing.assert_array_equal(domain, np.arange(0, 10, 1)) + domain, range_ = Signal.signal_unpack_data(xp_as_array(self._range, xp=xp)) + xp_assert_equal(range_, self._range) + xp_assert_equal(domain, np.arange(0, 10, 1)) - domain, range_ = Signal.signal_unpack_data(self._range, self._domain) - np.testing.assert_array_equal(range_, self._range) - np.testing.assert_array_equal(domain, self._domain) + domain, range_ = Signal.signal_unpack_data( + xp_as_array(self._range, xp=xp), xp_as_array(self._domain, xp=xp) + ) + xp_assert_equal(range_, self._range) + xp_assert_equal(domain, self._domain) domain, range_ = Signal.signal_unpack_data( self._range, dict(zip(self._domain, self._range, strict=True)).keys() ) - np.testing.assert_array_equal(domain, self._domain) + xp_assert_equal(domain, self._domain) domain, range_ = Signal.signal_unpack_data( dict(zip(self._domain, self._range, strict=True)) ) - np.testing.assert_array_equal(range_, self._range) - np.testing.assert_array_equal(domain, self._domain) + xp_assert_equal(range_, self._range) + xp_assert_equal(domain, self._domain) - domain, range_ = Signal.signal_unpack_data(Signal(self._range, self._domain)) - np.testing.assert_array_equal(range_, self._range) - np.testing.assert_array_equal(domain, self._domain) + domain, range_ = Signal.signal_unpack_data( + Signal(xp_as_array(self._range, xp=xp), xp_as_array(self._domain, xp=xp)) + ) + xp_assert_equal(range_, self._range) + xp_assert_equal(domain, self._domain) if is_pandas_installed(): from pandas import Series # noqa: PLC0415 @@ -716,42 +729,44 @@ def test_signal_unpack_data(self) -> None: domain, range_ = Signal.signal_unpack_data( Series(dict(zip(self._domain, self._range, strict=True))) ) - np.testing.assert_array_equal(range_, self._range) - np.testing.assert_array_equal(domain, self._domain) + xp_assert_equal(range_, self._range) + xp_assert_equal(domain, self._domain) - def test_fill_nan(self) -> None: + def test_fill_nan(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.fill_nan` method.""" - signal = self._signal.copy() + signal = Signal(xp_as_array(self._range, xp=xp)) signal[3:7] = np.nan - np.testing.assert_allclose( + xp_assert_close( signal.fill_nan().range, - np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]), + [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) signal[3:7] = np.nan - np.testing.assert_allclose( + xp_assert_close( signal.fill_nan(method="Constant").range, - np.array([10.0, 20.0, 30.0, 0.0, 0.0, 0.0, 0.0, 80.0, 90.0, 100.0]), + [10.0, 20.0, 30.0, 0.0, 0.0, 0.0, 0.0, 80.0, 90.0, 100.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_distance(self) -> None: + def test_domain_distance(self, xp: ModuleType) -> None: """Test :func:`colour.continuous.signal.Signal.domain_distance` method.""" - np.testing.assert_allclose( - self._signal.domain_distance(0.5), + signal = Signal(xp_as_array(self._range, xp=xp)) + + xp_assert_close( + signal.domain_distance(0.5), 0.5, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - self._signal.domain_distance(np.linspace(0, 9, 10) + 0.5), - np.array([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]), + xp_assert_close( + signal.domain_distance(np.linspace(0, 9, 10) + 0.5), + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/contrast/barten1999.py b/colour/contrast/barten1999.py index ab1ff0b819..2fba5b02b5 100644 --- a/colour/contrast/barten1999.py +++ b/colour/contrast/barten1999.py @@ -36,7 +36,12 @@ if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import as_float, as_float_array +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -90,7 +95,11 @@ def optical_MTF_Barten1999(u: ArrayLike, sigma: ArrayLike = 0.01) -> NDArrayFloa u = as_float_array(u) sigma = as_float_array(sigma) - return as_float(np.exp(-2 * np.pi**2 * sigma**2 * u**2)) + xp = array_namespace(u, sigma) + + sigma = xp_as_float_array(sigma, xp=xp, like=u) + + return as_float(xp.exp(-2 * xp.pi**2 * sigma**2 * u**2)) def pupil_diameter_Barten1999( @@ -135,7 +144,12 @@ def pupil_diameter_Barten1999( X_0 = as_float_array(X_0) Y_0 = X_0 if Y_0 is None else as_float_array(Y_0) - return as_float(5 - 3 * np.tanh(0.4 * np.log10(L * X_0 * Y_0 / 40**2))) + xp = array_namespace(L, X_0, Y_0) + + X_0 = xp_as_float_array(X_0, xp=xp, like=L) + Y_0 = xp_as_float_array(Y_0, xp=xp, like=L) + + return as_float(5 - 3 * xp.tanh(0.4 * xp.log10(L * X_0 * Y_0 / 40**2))) def sigma_Barten1999( @@ -194,7 +208,12 @@ def sigma_Barten1999( C_ab = as_float_array(C_ab) d = as_float_array(d) - return as_float(np.hypot(sigma_0, C_ab * d)) + xp = array_namespace(sigma_0, C_ab, d) + + C_ab = xp_as_float_array(C_ab, xp=xp, like=sigma_0) + d = xp_as_float_array(d, xp=xp, like=sigma_0) + + return as_float(xp.hypot(sigma_0, C_ab * d)) def retinal_illuminance_Barten1999( @@ -493,18 +512,21 @@ def contrast_sensitivity_function_Barten1999( """ u = as_float_array(u) - k = as_float_array(k) - T = as_float_array(T) - X_0 = as_float_array(X_0) - Y_0 = X_0 if Y_0 is None else as_float_array(Y_0) - X_max = as_float_array(X_max) - Y_max = X_max if Y_max is None else as_float_array(Y_max) - N_max = as_float_array(N_max) - n = as_float_array(n) - p = as_float_array(p) - E = as_float_array(E) - phi_0 = as_float_array(phi_0) - u_0 = as_float_array(u_0) + + xp = array_namespace(u) + + k = xp_as_float_array(k, xp=xp, like=u) + T = xp_as_float_array(T, xp=xp, like=u) + X_0 = xp_as_float_array(X_0, xp=xp, like=u) + Y_0 = X_0 if Y_0 is None else xp_as_float_array(Y_0, xp=xp, like=u) + X_max = xp_as_float_array(X_max, xp=xp, like=u) + Y_max = X_max if Y_max is None else xp_as_float_array(Y_max, xp=xp, like=u) + N_max = xp_as_float_array(N_max, xp=xp, like=u) + n = xp_as_float_array(n, xp=xp, like=u) + p = xp_as_float_array(p, xp=xp, like=u) + E = xp_as_float_array(E, xp=xp, like=u) + phi_0 = xp_as_float_array(phi_0, xp=xp, like=u) + u_0 = xp_as_float_array(u_0, xp=xp, like=u) M_opt = optical_MTF_Barten1999(u, sigma) @@ -513,8 +535,8 @@ def contrast_sensitivity_function_Barten1999( * maximum_angular_size_Barten1999(u, Y_0, Y_max, N_max) ) - S = (M_opt / k) / np.sqrt( - 2 / T * M_as * (1 / (n * p * E) + phi_0 / (1 - np.exp(-((u / u_0) ** 2)))) + S = (M_opt / k) / xp.sqrt( + 2 / T * M_as * (1 / (n * p * E) + phi_0 / (1 - xp.exp(-((u / u_0) ** 2)))) ) return as_float(S) diff --git a/colour/contrast/tests/test__init__.py b/colour/contrast/tests/test__init__.py index 6fc199a3d9..0b1da5dad6 100644 --- a/colour/contrast/tests/test__init__.py +++ b/colour/contrast/tests/test__init__.py @@ -2,10 +2,15 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.contrast import contrast_sensitivity_function +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -25,54 +30,56 @@ class TestContrastSensitivityFunction: unit tests methods. """ - def test_contrast_sensitivity_function(self) -> None: + def test_contrast_sensitivity_function(self, xp: ModuleType) -> None: """Test :func:`colour.contrast.contrast_sensitivity_function` definition.""" + _a = lambda v: xp_as_array([v], xp=xp) # noqa: E731 + # Test default method (Barten 1999) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function( - u=4, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 352.761342126727020, + [352.761342126727020], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test explicit Barten 1999 method with different parameters - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function( "Barten 1999", - u=8, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(8), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 177.706338840717340, + [177.706338840717340], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test with another set of parameters - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function( - u=20, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(20), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 37.455090830648620, + [37.455090830648620], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/contrast/tests/test_barten1999.py b/colour/contrast/tests/test_barten1999.py index cbb46fa823..f9e88e1b6b 100644 --- a/colour/contrast/tests/test_barten1999.py +++ b/colour/contrast/tests/test_barten1999.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -15,7 +20,13 @@ retinal_illuminance_Barten1999, sigma_Barten1999, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -40,53 +51,53 @@ class TestOpticalMTFBarten1999: definition unit tests methods. """ - def test_optical_MTF_Barten1999(self) -> None: + def test_optical_MTF_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.optical_MTF_Barten1999` definition. """ - np.testing.assert_allclose( - optical_MTF_Barten1999(4, 0.01), - 0.968910791191297, + xp_assert_close( + optical_MTF_Barten1999(xp_as_array([4], xp=xp), xp_as_array([0.01], xp=xp)), + [0.968910791191297], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - optical_MTF_Barten1999(8, 0.01), - 0.881323136669471, + xp_assert_close( + optical_MTF_Barten1999(xp_as_array([8], xp=xp), xp_as_array([0.01], xp=xp)), + [0.881323136669471], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - optical_MTF_Barten1999(4, 0.05), - 0.454040738727245, + xp_assert_close( + optical_MTF_Barten1999(xp_as_array([4], xp=xp), xp_as_array([0.05], xp=xp)), + [0.454040738727245], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_optical_MTF_Barten1999(self) -> None: + def test_n_dimensional_optical_MTF_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.optical_MTF_Barten1999` definition n-dimensional support. """ - u = np.array([4, 8, 12]) - sigma = np.array([0.01, 0.05, 0.1]) - M_opt = optical_MTF_Barten1999(u, sigma) + u = xp_as_array([4, 8, 12], xp=xp) + sigma = xp_as_array([0.01, 0.05, 0.1], xp=xp) + M_opt = as_ndarray(optical_MTF_Barten1999(u, sigma)) - u = np.tile(u, (6, 1)) - sigma = np.tile(sigma, (6, 1)) - M_opt = np.tile(M_opt, (6, 1)) - np.testing.assert_allclose( + u = xp.tile(xp_as_array(u, xp=xp), (6, 1)) + sigma = xp.tile(xp_as_array(sigma, xp=xp), (6, 1)) + M_opt = xp.tile(xp_as_array(M_opt, xp=xp), (6, 1)) + xp_assert_close( optical_MTF_Barten1999(u, sigma), M_opt, atol=TOLERANCE_ABSOLUTE_TESTS, ) - u = np.reshape(u, (2, 3, 3)) - sigma = np.reshape(sigma, (2, 3, 3)) - M_opt = np.reshape(M_opt, (2, 3, 3)) - np.testing.assert_allclose( + u = xp_reshape(xp_as_array(u, xp=xp), (2, 3, 3), xp=xp) + sigma = xp_reshape(xp_as_array(sigma, xp=xp), (2, 3, 3), xp=xp) + M_opt = xp_reshape(xp_as_array(M_opt, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( optical_MTF_Barten1999(u, sigma), M_opt, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -110,54 +121,62 @@ class TestPupilDiameterBarten1999: definition unit tests methods. """ - def test_pupil_diameter_Barten1999(self) -> None: + def test_pupil_diameter_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.pupil_diameter_Barten1999` definition. """ - np.testing.assert_allclose( - pupil_diameter_Barten1999(20, 60), - 3.262346170373243, + xp_assert_close( + pupil_diameter_Barten1999( + xp_as_array([20], xp=xp), xp_as_array([60], xp=xp) + ), + [3.262346170373243], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - pupil_diameter_Barten1999(0.2, 600), - 3.262346170373243, + xp_assert_close( + pupil_diameter_Barten1999( + xp_as_array([0.2], xp=xp), xp_as_array([600], xp=xp) + ), + [3.262346170373243], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - pupil_diameter_Barten1999(20, 60, 30), - 3.519054451149336, + xp_assert_close( + pupil_diameter_Barten1999( + xp_as_array([20], xp=xp), + xp_as_array([60], xp=xp), + xp_as_array([30], xp=xp), + ), + [3.519054451149336], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_pupil_diameter_Barten1999(self) -> None: + def test_n_dimensional_pupil_diameter_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.pupil_diameter_Barten1999` definition n-dimensional support. """ - L = np.array([0.2, 20, 100]) - X_0 = np.array([60, 120, 240]) - Y_0 = np.array([60, 30, 15]) - d = pupil_diameter_Barten1999(L, X_0, Y_0) + L = xp_as_array([0.2, 20, 100], xp=xp) + X_0 = xp_as_array([60, 120, 240], xp=xp) + Y_0 = xp_as_array([60, 30, 15], xp=xp) + d = as_ndarray(pupil_diameter_Barten1999(L, X_0, Y_0)) - L = np.tile(L, (6, 1)) - X_0 = np.tile(X_0, (6, 1)) - d = np.tile(d, (6, 1)) - np.testing.assert_allclose( + L = xp.tile(xp_as_array(L, xp=xp), (6, 1)) + X_0 = xp.tile(xp_as_array(X_0, xp=xp), (6, 1)) + d = xp.tile(xp_as_array(d, xp=xp), (6, 1)) + xp_assert_close( pupil_diameter_Barten1999(L, X_0, Y_0), d, atol=TOLERANCE_ABSOLUTE_TESTS, ) - L = np.reshape(L, (2, 3, 3)) - X_0 = np.reshape(X_0, (2, 3, 3)) - d = np.reshape(d, (2, 3, 3)) - np.testing.assert_allclose( + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 3), xp=xp) + X_0 = xp_reshape(xp_as_array(X_0, xp=xp), (2, 3, 3), xp=xp) + d = xp_reshape(xp_as_array(d, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( pupil_diameter_Barten1999(L, X_0, Y_0), d, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -181,57 +200,73 @@ class TestSigmaBarten1999: tests methods. """ - def test_sigma_Barten1999(self) -> None: + def test_sigma_Barten1999(self, xp: ModuleType) -> None: """Test :func:`colour.contrast.barten1999.sigma_Barten1999` definition.""" - np.testing.assert_allclose( - sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1), - 0.008791157173231, + xp_assert_close( + sigma_Barten1999( + xp_as_array([0.5 / 60], xp=xp), + xp_as_array([0.08 / 60], xp=xp), + xp_as_array([2.1], xp=xp), + ), + [0.008791157173231], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sigma_Barten1999(0.75 / 60, 0.08 / 60, 2.1), - 0.012809761902549, + xp_assert_close( + sigma_Barten1999( + xp_as_array([0.75 / 60], xp=xp), + xp_as_array([0.08 / 60], xp=xp), + xp_as_array([2.1], xp=xp), + ), + [0.012809761902549], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sigma_Barten1999(0.5 / 60, 0.16 / 60, 2.1), - 0.010040141654601, + xp_assert_close( + sigma_Barten1999( + xp_as_array([0.5 / 60], xp=xp), + xp_as_array([0.16 / 60], xp=xp), + xp_as_array([2.1], xp=xp), + ), + [0.010040141654601], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.5), - 0.008975274678558, + xp_assert_close( + sigma_Barten1999( + xp_as_array([0.5 / 60], xp=xp), + xp_as_array([0.08 / 60], xp=xp), + xp_as_array([2.5], xp=xp), + ), + [0.008975274678558], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sigma_Barten1999(self) -> None: + def test_n_dimensional_sigma_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.sigma_Barten1999` definition n-dimensional support. """ - sigma_0 = np.array([0.25 / 60, 0.5 / 60, 0.75 / 60]) - C_ab = np.array([0.04 / 60, 0.08 / 60, 0.16 / 60]) - d = np.array([2.1, 2.5, 5.0]) - sigma = sigma_Barten1999(sigma_0, C_ab, d) + sigma_0 = xp_as_array([0.25 / 60, 0.5 / 60, 0.75 / 60], xp=xp) + C_ab = xp_as_array([0.04 / 60, 0.08 / 60, 0.16 / 60], xp=xp) + d = xp_as_array([2.1, 2.5, 5.0], xp=xp) + sigma = as_ndarray(sigma_Barten1999(sigma_0, C_ab, d)) - sigma_0 = np.tile(sigma_0, (6, 1)) - C_ab = np.tile(C_ab, (6, 1)) - sigma = np.tile(sigma, (6, 1)) - np.testing.assert_allclose( + sigma_0 = xp.tile(xp_as_array(sigma_0, xp=xp), (6, 1)) + C_ab = xp.tile(xp_as_array(C_ab, xp=xp), (6, 1)) + sigma = xp.tile(xp_as_array(sigma, xp=xp), (6, 1)) + xp_assert_close( sigma_Barten1999(sigma_0, C_ab, d), sigma, atol=TOLERANCE_ABSOLUTE_TESTS, ) - sigma_0 = np.reshape(sigma_0, (2, 3, 3)) - C_ab = np.reshape(C_ab, (2, 3, 3)) - sigma = np.reshape(sigma, (2, 3, 3)) - np.testing.assert_allclose( + sigma_0 = xp_reshape(xp_as_array(sigma_0, xp=xp), (2, 3, 3), xp=xp) + C_ab = xp_reshape(xp_as_array(C_ab, xp=xp), (2, 3, 3), xp=xp) + sigma = xp_reshape(xp_as_array(sigma, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( sigma_Barten1999(sigma_0, C_ab, d), sigma, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -255,53 +290,59 @@ class TestRetinalIlluminanceBarten1999: definition unit tests methods. """ - def test_retinal_illuminance_Barten1999(self) -> None: + def test_retinal_illuminance_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.retinal_illuminance_Barten1999` definition. """ - np.testing.assert_allclose( - retinal_illuminance_Barten1999(20, 2.1, True), - 66.082316060529919, + xp_assert_close( + retinal_illuminance_Barten1999( + xp_as_array([20], xp=xp), xp_as_array([2.1], xp=xp), True + ), + [66.082316060529919], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - retinal_illuminance_Barten1999(20, 2.5, True), - 91.815644777503664, + xp_assert_close( + retinal_illuminance_Barten1999( + xp_as_array([20], xp=xp), xp_as_array([2.5], xp=xp), True + ), + [91.815644777503664], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - retinal_illuminance_Barten1999(20, 2.1, False), - 69.272118011654939, + xp_assert_close( + retinal_illuminance_Barten1999( + xp_as_array([20], xp=xp), xp_as_array([2.1], xp=xp), False + ), + [69.272118011654939], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_retinal_illuminance_Barten1999(self) -> None: + def test_n_dimensional_retinal_illuminance_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.retinal_illuminance_Barten1999` definition n-dimensional support. """ - L = np.array([0.2, 20, 100]) - d = np.array([2.1, 2.5, 5.0]) - E = retinal_illuminance_Barten1999(L, d) + L = xp_as_array([0.2, 20, 100], xp=xp) + d = xp_as_array([2.1, 2.5, 5.0], xp=xp) + E = as_ndarray(retinal_illuminance_Barten1999(L, d)) - L = np.tile(L, (6, 1)) - d = np.tile(d, (6, 1)) - E = np.tile(E, (6, 1)) - np.testing.assert_allclose( + L = xp.tile(xp_as_array(L, xp=xp), (6, 1)) + d = xp.tile(xp_as_array(d, xp=xp), (6, 1)) + E = xp.tile(xp_as_array(E, xp=xp), (6, 1)) + xp_assert_close( retinal_illuminance_Barten1999(L, d), E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - L = np.reshape(L, (2, 3, 3)) - d = np.reshape(d, (2, 3, 3)) - E = np.reshape(E, (2, 3, 3)) - np.testing.assert_allclose( + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 3), xp=xp) + d = xp_reshape(xp_as_array(d, xp=xp), (2, 3, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( retinal_illuminance_Barten1999(L, d), E, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -325,67 +366,94 @@ class TestMaximumAngularSizeBarten1999: definition unit tests methods. """ - def test_maximum_angular_size_Barten1999(self) -> None: + def test_maximum_angular_size_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.\ maximum_angular_size_Barten1999` definition. """ - np.testing.assert_allclose( - maximum_angular_size_Barten1999(4, 60, 12, 15), - 3.572948005052482, + xp_assert_close( + maximum_angular_size_Barten1999( + xp_as_array([4], xp=xp), + xp_as_array([60], xp=xp), + xp_as_array([12], xp=xp), + xp_as_array([15], xp=xp), + ), + [3.572948005052482], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - maximum_angular_size_Barten1999(8, 60, 12, 15), - 1.851640199545103, + xp_assert_close( + maximum_angular_size_Barten1999( + xp_as_array([8], xp=xp), + xp_as_array([60], xp=xp), + xp_as_array([12], xp=xp), + xp_as_array([15], xp=xp), + ), + [1.851640199545103], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - maximum_angular_size_Barten1999(4, 120, 12, 15), - 3.577708763999663, + xp_assert_close( + maximum_angular_size_Barten1999( + xp_as_array([4], xp=xp), + xp_as_array([120], xp=xp), + xp_as_array([12], xp=xp), + xp_as_array([15], xp=xp), + ), + [3.577708763999663], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - maximum_angular_size_Barten1999(4, 60, 24, 15), - 3.698001308168194, + xp_assert_close( + maximum_angular_size_Barten1999( + xp_as_array([4], xp=xp), + xp_as_array([60], xp=xp), + xp_as_array([24], xp=xp), + xp_as_array([15], xp=xp), + ), + [3.698001308168194], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - maximum_angular_size_Barten1999(4, 60, 12, 30), - 6.324555320336758, + xp_assert_close( + maximum_angular_size_Barten1999( + xp_as_array([4], xp=xp), + xp_as_array([60], xp=xp), + xp_as_array([12], xp=xp), + xp_as_array([30], xp=xp), + ), + [6.324555320336758], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_maximum_angular_size_Barten1999(self) -> None: + def test_n_dimensional_maximum_angular_size_Barten1999( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.contrast.barten1999.\ maximum_angular_size_Barten1999` definition n-dimensional support. """ - u = np.array([4, 8, 12]) - X_0 = np.array([60, 120, 240]) - X_max = np.array([12, 14, 16]) - N_max = np.array([15, 20, 25]) - X = maximum_angular_size_Barten1999(u, X_0, X_max, N_max) + u = xp_as_array([4, 8, 12], xp=xp) + X_0 = xp_as_array([60, 120, 240], xp=xp) + X_max = xp_as_array([12, 14, 16], xp=xp) + N_max = xp_as_array([15, 20, 25], xp=xp) + X = as_ndarray(maximum_angular_size_Barten1999(u, X_0, X_max, N_max)) - u = np.tile(u, (6, 1)) - X_0 = np.tile(X_0, (6, 1)) - X = np.tile(X, (6, 1)) - np.testing.assert_allclose( + u = xp.tile(xp_as_array(u, xp=xp), (6, 1)) + X_0 = xp.tile(xp_as_array(X_0, xp=xp), (6, 1)) + X = xp.tile(xp_as_array(X, xp=xp), (6, 1)) + xp_assert_close( maximum_angular_size_Barten1999(u, X_0, X_max, N_max), X, atol=TOLERANCE_ABSOLUTE_TESTS, ) - u = np.reshape(u, (2, 3, 3)) - X_0 = np.reshape(X_0, (2, 3, 3)) - X = np.reshape(X, (2, 3, 3)) - np.testing.assert_allclose( + u = xp_reshape(xp_as_array(u, xp=xp), (2, 3, 3), xp=xp) + X_0 = xp_reshape(xp_as_array(X_0, xp=xp), (2, 3, 3), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( maximum_angular_size_Barten1999(u, X_0, X_max, N_max), X, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -409,166 +477,174 @@ class TestContrastSensitivityFunctionBarten1999: contrast_sensitivity_function_Barten1999` definition unit tests methods. """ - def test_contrast_sensitivity_function_Barten1999(self) -> None: + def test_contrast_sensitivity_function_Barten1999(self, xp: ModuleType) -> None: """ Test :func:`colour.contrast.barten1999.\ contrast_sensitivity_function_Barten1999` definition. """ - np.testing.assert_allclose( + _a = lambda v: xp_as_array([v], xp=xp) # noqa: E731 + + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 352.761342126727020, + [352.761342126727020], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=8, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(8), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 177.706338840717340, + [177.706338840717340], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.02, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.02), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 320.872401634215750, + [320.872401634215750], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=130, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(130), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 455.171315756946400, + [455.171315756946400], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=65, - X_0=120, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(120), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 352.996281545740660, + [352.996281545740660], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=65, - X_0=60, - X_max=24, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(24), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - 358.881580104493650, + [358.881580104493650], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=65, - X_0=240, - X_max=12, - Y_0=60, - Y_max=12, - p=1.2 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(240), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.2e6), ), - contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=240, - Y_max=12, - p=1.2 * 10**6, + as_ndarray( + contrast_sensitivity_function_Barten1999( + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(240), + Y_max=_a(12), + p=_a(1.2e6), + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( contrast_sensitivity_function_Barten1999( - u=4, - sigma=0.01, - E=65, - X_0=60, - X_max=12, - Y_0=60, - Y_max=12, - p=1.4 * 10**6, + u=_a(4), + sigma=_a(0.01), + E=_a(65), + X_0=_a(60), + X_max=_a(12), + Y_0=_a(60), + Y_max=_a(12), + p=_a(1.4e6), ), - 374.791328640476140, + [374.791328640476140], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_contrast_sensitivity_function_Barten1999(self) -> None: + def test_n_dimensional_contrast_sensitivity_function_Barten1999( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.contrast.barten1999.\ contrast_sensitivity_function_Barten1999` definition n-dimensional support. """ - u = np.array([4, 8, 12]) - sigma = np.array([0.01, 0.02, 0.04]) - E = np.array([0.65, 90, 1500]) - X_0 = np.array([60, 120, 240]) - S = contrast_sensitivity_function_Barten1999(u=u, sigma=sigma, E=E, X_0=X_0) + u = xp_as_array([4, 8, 12], xp=xp) + sigma = xp_as_array([0.01, 0.02, 0.04], xp=xp) + E = xp_as_array([0.65, 90, 1500], xp=xp) + X_0 = xp_as_array([60, 120, 240], xp=xp) + S = as_ndarray( + contrast_sensitivity_function_Barten1999(u=u, sigma=sigma, E=E, X_0=X_0) + ) - u = np.tile(u, (6, 1)) - E = np.tile(E, (6, 1)) - S = np.tile(S, (6, 1)) - np.testing.assert_allclose( + u = xp.tile(xp_as_array(u, xp=xp), (6, 1)) + E = xp.tile(xp_as_array(E, xp=xp), (6, 1)) + S = xp.tile(xp_as_array(S, xp=xp), (6, 1)) + xp_assert_close( contrast_sensitivity_function_Barten1999(u=u, sigma=sigma, E=E, X_0=X_0), S, atol=TOLERANCE_ABSOLUTE_TESTS, ) - u = np.reshape(u, (2, 3, 3)) - E = np.reshape(E, (2, 3, 3)) - S = np.reshape(S, (2, 3, 3)) - np.testing.assert_allclose( + u = xp_reshape(xp_as_array(u, xp=xp), (2, 3, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 3), xp=xp) + S = xp_reshape(xp_as_array(S, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( contrast_sensitivity_function_Barten1999(u=u, sigma=sigma, E=E, X_0=X_0), S, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/corresponding/prediction.py b/colour/corresponding/prediction.py index fe60a02f28..730b4c1dae 100644 --- a/colour/corresponding/prediction.py +++ b/colour/corresponding/prediction.py @@ -42,8 +42,6 @@ import typing from dataclasses import dataclass -import numpy as np - from colour.adaptation import ( chromatic_adaptation_CIE1994, chromatic_adaptation_CMCCAT2000, @@ -77,12 +75,14 @@ from colour.utilities import ( CanonicalMapping, MixinDataclassIterable, + array_namespace, as_float_array, as_float_scalar, attest, domain_range_scale, filter_kwargs, full, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -286,13 +286,18 @@ def convert_experiment_results_Breneman1987( ) B_r = B_t = 0.3 + illuminant_xy = Luv_uv_to_xy(illuminant_chromaticities.values[1:3]) + + xp = array_namespace(illuminant_xy) + XYZ_t, XYZ_r = ( xy_to_XYZ( - np.hstack( + xp.concat( [ - Luv_uv_to_xy(illuminant_chromaticities.values[1:3]), + illuminant_xy, full((2, 1), Y_r), - ] + ], + axis=1, ) ) / Y_r @@ -301,19 +306,21 @@ def convert_experiment_results_Breneman1987( xyY_cr, xyY_ct = [], [] for i, experiment_result in enumerate(experiment_results): xyY_cr.append( - np.hstack( + xp.concat( [ Luv_uv_to_xy(experiment_result.values[2]), - samples_luminance[i] * Y_r, - ] + xp_as_float_array([samples_luminance[i] * Y_r], xp=xp), + ], + axis=0, ) ) xyY_ct.append( - np.hstack( + xp.concat( [ Luv_uv_to_xy(experiment_result.values[1]), - samples_luminance[i] * Y_t, - ] + xp_as_float_array([samples_luminance[i] * Y_t], xp=xp), + ], + axis=0, ) ) diff --git a/colour/corresponding/tests/test_prediction.py b/colour/corresponding/tests/test_prediction.py index 46427dea38..6dec1af35e 100644 --- a/colour/corresponding/tests/test_prediction.py +++ b/colour/corresponding/tests/test_prediction.py @@ -18,9 +18,10 @@ CorrespondingColourDataset, convert_experiment_results_Breneman1987, ) +from colour.utilities import xp_as_array, xp_assert_close if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat + from colour.hints import ModuleType, NDArrayFloat __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -235,37 +236,37 @@ def test_convert_experiment_results_Breneman1987(self) -> None: corresponding_colour_dataset = convert_experiment_results_Breneman1987(1) - np.testing.assert_allclose( + xp_assert_close( corresponding_colour_dataset.XYZ_r, DATASET_CORRESPONDING_COLOUR_1.XYZ_r, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( corresponding_colour_dataset.XYZ_t, DATASET_CORRESPONDING_COLOUR_1.XYZ_t, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( corresponding_colour_dataset.XYZ_cr, DATASET_CORRESPONDING_COLOUR_1.XYZ_cr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( corresponding_colour_dataset.XYZ_ct, DATASET_CORRESPONDING_COLOUR_1.XYZ_ct, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( corresponding_colour_dataset.Y_r, DATASET_CORRESPONDING_COLOUR_1.Y_r, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( corresponding_colour_dataset.Y_t, DATASET_CORRESPONDING_COLOUR_1.Y_t, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -279,18 +280,22 @@ class TestCorrespondingChromaticitiesPredictionFairchild1990: methods. """ - def test_corresponding_chromaticities_prediction_Fairchild1990(self) -> None: + def test_corresponding_chromaticities_prediction_Fairchild1990( + self, + xp: ModuleType, + ) -> None: """ Test :func:`colour.corresponding.prediction.\ corresponding_chromaticities_prediction_Fairchild1990` definition. """ - np.testing.assert_allclose( - np.array( + xp_assert_close( + xp_as_array( [ (p.uv_m, p.uv_p) for p in corresponding_chromaticities_prediction_Fairchild1990() - ] + ], + xp=xp, ), DATA_PREDICTION_FAIRCHILD1990, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -303,18 +308,22 @@ class TestCorrespondingChromaticitiesPredictionCIE1994: corresponding_chromaticities_prediction_CIE1994` definition unit tests methods. """ - def test_corresponding_chromaticities_prediction_CIE1994(self) -> None: + def test_corresponding_chromaticities_prediction_CIE1994( + self, + xp: ModuleType, + ) -> None: """ Test :func:`colour.corresponding.prediction.\ corresponding_chromaticities_prediction_CIE1994` definition. """ - np.testing.assert_allclose( - np.array( + xp_assert_close( + xp_as_array( [ (p.uv_m, p.uv_p) for p in corresponding_chromaticities_prediction_CIE1994() - ] + ], + xp=xp, ), DATA_PREDICTION_CIE1994, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -328,18 +337,22 @@ class TestCorrespondingChromaticitiesPredictionCMCCAT2000: methods. """ - def test_corresponding_chromaticities_prediction_CMCCAT2000(self) -> None: + def test_corresponding_chromaticities_prediction_CMCCAT2000( + self, + xp: ModuleType, + ) -> None: """ Test :func:`colour.corresponding.prediction.\ corresponding_chromaticities_prediction_CMCCAT2000` definition. """ - np.testing.assert_allclose( - np.array( + xp_assert_close( + xp_as_array( [ (p.uv_m, p.uv_p) for p in corresponding_chromaticities_prediction_CMCCAT2000() - ] + ], + xp=xp, ), DATA_PREDICTION_CMCCAT2000, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -353,18 +366,22 @@ class TestCorrespondingChromaticitiesPredictionVonKries: methods. """ - def test_corresponding_chromaticities_prediction_VonKries(self) -> None: + def test_corresponding_chromaticities_prediction_VonKries( + self, + xp: ModuleType, + ) -> None: """ Test :func:`colour.corresponding.prediction.\ corresponding_chromaticities_prediction_VonKries` definition. """ - np.testing.assert_allclose( - np.array( + xp_assert_close( + xp_as_array( [ (p.uv_m, p.uv_p) for p in corresponding_chromaticities_prediction_VonKries() - ] + ], + xp=xp, ), DATA_PREDICTION_VONKRIES, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -378,18 +395,22 @@ class TestCorrespondingChromaticitiesPredictionZhai2018: methods. """ - def test_corresponding_chromaticities_prediction_Zhai2018(self) -> None: + def test_corresponding_chromaticities_prediction_Zhai2018( + self, + xp: ModuleType, + ) -> None: """ Test :func:`colour.corresponding.prediction.\ corresponding_chromaticities_prediction_Zhai2018` definition. """ - np.testing.assert_allclose( - np.array( + xp_assert_close( + xp_as_array( [ (p.uv_m, p.uv_p) for p in corresponding_chromaticities_prediction_Zhai2018() - ] + ], + xp=xp, ), DATA_PREDICTION_ZHAI2018, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/difference/cam02_ucs.py b/colour/difference/cam02_ucs.py index e80bc6dfb4..69ceca5779 100644 --- a/colour/difference/cam02_ucs.py +++ b/colour/difference/cam02_ucs.py @@ -20,8 +20,6 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import NDArrayFloat, Literal @@ -29,7 +27,13 @@ from colour.hints import Domain100 # noqa: TC001 from colour.models.cam02_ucs import COEFFICIENTS_UCS_LUO2006, Coefficients_UCS_Luo2006 -from colour.utilities import MixinDataclassArithmetic, as_float, tsplit +from colour.utilities import ( + MixinDataclassArithmetic, + array_namespace, + as_float, + as_float_array, + tsplit, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -145,6 +149,7 @@ def delta_E_Luo2006( Examples -------- + >>> import numpy as np >>> Jpapbp_1 = np.array([54.90433134, -0.08450395, -0.06854831]) >>> Jpapbp_2 = np.array([54.80352754, -3.96940084, -13.57591013]) >>> delta_E_Luo2006(Jpapbp_1, Jpapbp_2, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) @@ -161,6 +166,8 @@ def delta_E_Luo2006( db=np.float64(13.5073618...)) """ + xp = array_namespace(Jpapbp_1, Jpapbp_2) + J_p_1, a_p_1, b_p_1 = tsplit(Jpapbp_1) J_p_2, a_p_2, b_p_2 = tsplit(Jpapbp_2) K_L, _c_1, _c_2 = coefficients.values @@ -169,7 +176,9 @@ def delta_E_Luo2006( a = a_p_1 - a_p_2 b = b_p_1 - b_p_2 - d_E = as_float(np.sqrt(J**2 + a**2 + b**2)) + d_E_sq = as_float_array(J**2 + a**2 + b**2) + + d_E = as_float(xp.sqrt(d_E_sq)) if not additional_data: return d_E @@ -248,6 +257,7 @@ def delta_E_CAM02LCD( Examples -------- + >>> import numpy as np >>> Jpapbp_1 = np.array([54.90433134, -0.08450395, -0.06854831]) >>> Jpapbp_2 = np.array([54.80352754, -3.96940084, -13.57591013]) >>> delta_E_CAM02LCD(Jpapbp_1, Jpapbp_2) # doctest: +ELLIPSIS @@ -336,6 +346,7 @@ def delta_E_CAM02SCD( Examples -------- + >>> import numpy as np >>> Jpapbp_1 = np.array([54.90433134, -0.08450395, -0.06854831]) >>> Jpapbp_2 = np.array([54.80352754, -3.96940084, -13.57591013]) >>> delta_E_CAM02SCD(Jpapbp_1, Jpapbp_2) # doctest: +ELLIPSIS @@ -424,6 +435,7 @@ def delta_E_CAM02UCS( Examples -------- + >>> import numpy as np >>> Jpapbp_1 = np.array([54.90433134, -0.08450395, -0.06854831]) >>> Jpapbp_2 = np.array([54.80352754, -3.96940084, -13.57591013]) >>> delta_E_CAM02UCS(Jpapbp_1, Jpapbp_2) # doctest: +ELLIPSIS diff --git a/colour/difference/delta_e.py b/colour/difference/delta_e.py index 0056074442..c3c46cf6f9 100644 --- a/colour/difference/delta_e.py +++ b/colour/difference/delta_e.py @@ -47,8 +47,6 @@ import typing from dataclasses import astuple, dataclass, field -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import ( Domain1, @@ -60,10 +58,14 @@ from colour.algebra import euclidean_distance from colour.utilities import ( MixinDataclassArithmetic, + array_namespace, as_float, as_float_array, to_domain_100, tsplit, + xp_degrees, + xp_radians, + xp_select, ) from colour.utilities.documentation import DocstringFloat, is_documentation_building @@ -202,6 +204,7 @@ def delta_E_CIE1976( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) >>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) >>> delta_E_CIE1976(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -334,6 +337,7 @@ def delta_E_CIE1994( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) >>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) >>> delta_E_CIE1994(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -359,8 +363,13 @@ def delta_E_CIE1994( dH=np.float64(0.0015891...)) """ - L_1, a_1, b_1 = tsplit(to_domain_100(Lab_1)) - L_2, a_2, b_2 = tsplit(to_domain_100(Lab_2)) + Lab_1 = to_domain_100(Lab_1) + Lab_2 = to_domain_100(Lab_2) + + xp = array_namespace(Lab_1, Lab_2) + + L_1, a_1, b_1 = tsplit(Lab_1) + L_2, a_2, b_2 = tsplit(Lab_2) k_1 = 0.048 if textiles else 0.045 k_2 = 0.014 if textiles else 0.015 @@ -368,8 +377,8 @@ def delta_E_CIE1994( k_C = 1 k_H = 1 - C_1 = np.hypot(a_1, b_1) - C_2 = np.hypot(a_2, b_2) + C_1 = xp.hypot(a_1, b_1) + C_2 = xp.hypot(a_2, b_2) s_L = 1 s_C = 1 + k_1 * C_1 @@ -381,13 +390,13 @@ def delta_E_CIE1994( delta_B = b_1 - b_2 radical = delta_A**2 + delta_B**2 - delta_C**2 - delta_H = np.where(radical > 0, np.sqrt(np.maximum(radical, 0)), 0) + delta_H = xp.sqrt(xp.where(radical > 0, radical, 0.0)) L = delta_L / (k_L * s_L) C = delta_C / (k_C * s_C) H = delta_H / (k_H * s_H) - d_E = as_float(np.sqrt(L**2 + C**2 + H**2)) + d_E = as_float(xp.sqrt(L**2 + C**2 + H**2)) if not additional_data: return d_E @@ -472,6 +481,7 @@ def intermediate_attributes_CIE2000( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) >>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) >>> intermediate_attributes_CIE2000(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -481,32 +491,37 @@ def intermediate_attributes_CIE2000( delta_H_p=np.float64(0.0105030...), R_T=np.float64(-3...)) """ - L_1, a_1, b_1 = tsplit(to_domain_100(Lab_1)) - L_2, a_2, b_2 = tsplit(to_domain_100(Lab_2)) + Lab_1 = to_domain_100(Lab_1) + Lab_2 = to_domain_100(Lab_2) + + xp = array_namespace(Lab_1, Lab_2) - C_1_ab = np.hypot(a_1, b_1) - C_2_ab = np.hypot(a_2, b_2) + L_1, a_1, b_1 = tsplit(Lab_1) + L_2, a_2, b_2 = tsplit(Lab_2) + + C_1_ab = xp.hypot(a_1, b_1) + C_2_ab = xp.hypot(a_2, b_2) C_bar_ab = (C_1_ab + C_2_ab) / 2 C_bar_ab_7 = C_bar_ab**7 - G = 0.5 * (1 - np.sqrt(C_bar_ab_7 / (C_bar_ab_7 + 25**7))) + G = 0.5 * (1 - xp.sqrt(C_bar_ab_7 / (C_bar_ab_7 + 25**7))) a_p_1 = (1 + G) * a_1 a_p_2 = (1 + G) * a_2 - C_p_1 = np.hypot(a_p_1, b_1) - C_p_2 = np.hypot(a_p_2, b_2) + C_p_1 = xp.hypot(a_p_1, b_1) + C_p_2 = xp.hypot(a_p_2, b_2) - h_p_1 = np.where( - np.logical_and(b_1 == 0, a_p_1 == 0), + h_p_1 = xp.where( + xp.logical_and(b_1 == 0, a_p_1 == 0), 0, - np.degrees(np.arctan2(b_1, a_p_1)) % 360, + xp_degrees(xp.atan2(b_1, a_p_1)) % 360, ) - h_p_2 = np.where( - np.logical_and(b_2 == 0, a_p_2 == 0), + h_p_2 = xp.where( + xp.logical_and(b_2 == 0, a_p_2 == 0), 0, - np.degrees(np.arctan2(b_2, a_p_2)) % 360, + xp_degrees(xp.atan2(b_2, a_p_2)) % 360, ) delta_L_p = L_2 - L_1 @@ -515,10 +530,10 @@ def intermediate_attributes_CIE2000( h_p_2_s_1 = h_p_2 - h_p_1 C_p_1_m_2 = C_p_1 * C_p_2 - delta_h_p = np.select( + delta_h_p = xp_select( [ C_p_1_m_2 == 0, - np.fabs(h_p_2_s_1) <= 180, + xp.abs(h_p_2_s_1) <= 180, h_p_2_s_1 > 180, h_p_2_s_1 < -180, ], @@ -528,22 +543,23 @@ def intermediate_attributes_CIE2000( h_p_2_s_1 - 360, h_p_2_s_1 + 360, ], + xp=xp, ) - delta_H_p = 2 * np.sqrt(C_p_1_m_2) * np.sin(np.deg2rad(delta_h_p / 2)) + delta_H_p = 2 * xp.sqrt(C_p_1_m_2) * xp.sin(xp_radians(delta_h_p / 2)) L_bar_p = (L_1 + L_2) / 2 C_bar_p = (C_p_1 + C_p_2) / 2 - a_h_p_1_s_2 = np.fabs(h_p_1 - h_p_2) + a_h_p_1_s_2 = xp.abs(h_p_1 - h_p_2) h_p_1_a_2 = h_p_1 + h_p_2 - h_bar_p = np.select( + h_bar_p = xp_select( [ C_p_1_m_2 == 0, a_h_p_1_s_2 <= 180, - np.logical_and(a_h_p_1_s_2 > 180, h_p_1_a_2 < 360), - np.logical_and(a_h_p_1_s_2 > 180, h_p_1_a_2 >= 360), + xp.logical_and(a_h_p_1_s_2 > 180, h_p_1_a_2 < 360), + xp.logical_and(a_h_p_1_s_2 > 180, h_p_1_a_2 >= 360), ], [ h_p_1_a_2, @@ -551,29 +567,30 @@ def intermediate_attributes_CIE2000( (h_p_1_a_2 + 360) / 2, (h_p_1_a_2 - 360) / 2, ], + xp=xp, ) T = ( 1 - - 0.17 * np.cos(np.deg2rad(h_bar_p - 30)) - + 0.24 * np.cos(np.deg2rad(2 * h_bar_p)) - + 0.32 * np.cos(np.deg2rad(3 * h_bar_p + 6)) - - 0.20 * np.cos(np.deg2rad(4 * h_bar_p - 63)) + - 0.17 * xp.cos(xp_radians(h_bar_p - 30)) + + 0.24 * xp.cos(xp_radians(2 * h_bar_p)) + + 0.32 * xp.cos(xp_radians(3 * h_bar_p + 6)) + - 0.20 * xp.cos(xp_radians(4 * h_bar_p - 63)) ) - delta_theta = 30 * np.exp(-(((h_bar_p - 275) / 25) ** 2)) + delta_theta = 30 * xp.exp(-(((h_bar_p - 275) / 25) ** 2)) C_bar_p_7 = C_bar_p**7 - R_C = 2 * np.sqrt(C_bar_p_7 / (C_bar_p_7 + 25**7)) + R_C = 2 * xp.sqrt(C_bar_p_7 / (C_bar_p_7 + 25**7)) L_bar_p_2 = (L_bar_p - 50) ** 2 - S_L = 1 + ((0.015 * L_bar_p_2) / np.sqrt(20 + L_bar_p_2)) + S_L = 1 + ((0.015 * L_bar_p_2) / xp.sqrt(20 + L_bar_p_2)) S_C = 1 + 0.045 * C_bar_p S_H = 1 + 0.015 * C_bar_p * T - R_T = -np.sin(np.deg2rad(2 * delta_theta)) * R_C + R_T = -xp.sin(xp_radians(2 * delta_theta)) * R_C return Attributes_Specification_CIE2000( S_L, @@ -695,6 +712,7 @@ def delta_E_CIE2000( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) >>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) >>> delta_E_CIE2000(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -732,7 +750,9 @@ def delta_E_CIE2000( C = delta_C_p / (k_C * S_C) H = delta_H_p / (k_H * S_H) - d_E = as_float(np.sqrt(L**2 + C**2 + H**2 + R_T * C * H)) + xp = array_namespace(L) + + d_E = as_float(xp.sqrt(L**2 + C**2 + H**2 + R_T * C * H)) if not additional_data: return d_E @@ -844,6 +864,7 @@ def delta_E_CMC( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) >>> Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) >>> delta_E_CMC(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -858,23 +879,28 @@ def delta_E_CMC( dH=np.float64(0.0037676...)) """ - L_1, a_1, b_1 = tsplit(to_domain_100(Lab_1)) - L_2, a_2, b_2 = tsplit(to_domain_100(Lab_2)) + Lab_1 = to_domain_100(Lab_1) + Lab_2 = to_domain_100(Lab_2) + + xp = array_namespace(Lab_1, Lab_2) - C_1 = np.hypot(a_1, b_1) - C_2 = np.hypot(a_2, b_2) - s_L = np.where(L_1 < 16, 0.511, (0.040975 * L_1) / (1 + 0.01765 * L_1)) + L_1, a_1, b_1 = tsplit(Lab_1) + L_2, a_2, b_2 = tsplit(Lab_2) + + C_1 = xp.hypot(a_1, b_1) + C_2 = xp.hypot(a_2, b_2) + s_L = xp.where(L_1 < 16, 0.511, (0.040975 * L_1) / (1 + 0.01765 * L_1)) s_C = 0.0638 * C_1 / (1 + 0.0131 * C_1) + 0.638 - h_1 = np.degrees(np.arctan2(b_1, a_1)) % 360 + h_1 = xp_degrees(xp.atan2(b_1, a_1)) % 360 - t = np.where( - np.logical_and(h_1 >= 164, h_1 <= 345), - 0.56 + np.fabs(0.2 * np.cos(np.deg2rad(h_1 + 168))), - 0.36 + np.fabs(0.4 * np.cos(np.deg2rad(h_1 + 35))), + t = xp.where( + xp.logical_and(h_1 >= 164, h_1 <= 345), + 0.56 + xp.abs(0.2 * xp.cos(xp_radians(h_1 + 168))), + 0.36 + xp.abs(0.4 * xp.cos(xp_radians(h_1 + 35))), ) C_4 = C_1 * C_1 * C_1 * C_1 - f = np.sqrt(C_4 / (C_4 + 1900)) + f = xp.sqrt(C_4 / (C_4 + 1900)) s_h = s_C * (f * t + 1 - f) delta_L = L_1 - L_2 @@ -882,13 +908,13 @@ def delta_E_CMC( delta_A = a_1 - a_2 delta_B = b_1 - b_2 radical = delta_A**2 + delta_B**2 - delta_C**2 - delta_H = np.where(radical > 0, np.sqrt(np.maximum(radical, 0)), 0) + delta_H = xp.sqrt(xp.where(radical > 0, radical, 0.0)) L = delta_L / (l * s_L) C = delta_C / (c * s_C) H = delta_H / s_h - d_E = as_float(np.sqrt(L**2 + C**2 + H**2)) + d_E = as_float(xp.sqrt(L**2 + C**2 + H**2)) if not additional_data: return d_E @@ -994,6 +1020,7 @@ def delta_E_ITP( Examples -------- + >>> import numpy as np >>> ICtCp_1 = np.array([0.4885468072, -0.04739350675, 0.07475401302]) >>> ICtCp_2 = np.array([0.4899203231, -0.04567508203, 0.07361341775]) >>> delta_E_ITP(ICtCp_1, ICtCp_2) # doctest: +ELLIPSIS @@ -1008,17 +1035,19 @@ def delta_E_ITP( dP=np.float64(-0.0011405...)) """ + xp = array_namespace(ICtCp_1, ICtCp_2) + I_1, T_1, P_1 = tsplit(ICtCp_1) - T_1 *= 0.5 + T_1 = T_1 * 0.5 I_2, T_2, P_2 = tsplit(ICtCp_2) - T_2 *= 0.5 + T_2 = T_2 * 0.5 I = I_2 - I_1 # noqa: E741 T = T_2 - T_1 P = P_2 - P_1 - d_E_ITP = as_float(720 * np.sqrt(I**2 + T**2 + P**2)) + d_E_ITP = as_float(720 * xp.sqrt(I**2 + T**2 + P**2)) if not additional_data: return d_E_ITP @@ -1118,6 +1147,7 @@ def delta_E_HyAB( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([39.91531343, 51.16658481, 146.12933781]) >>> Lab_2 = np.array([53.12207516, -39.92365056, 249.54831278]) >>> delta_E_HyAB(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -1133,8 +1163,12 @@ def delta_E_HyAB( """ dLab = to_domain_100(Lab_1) - to_domain_100(Lab_2) + + xp = array_namespace(dLab) + dL, da, db = tsplit(dLab) - HyAB = as_float(np.abs(dL) + np.hypot(da, db)) + + HyAB = as_float(xp.abs(dL) + xp.hypot(da, db)) if not additional_data: return HyAB @@ -1246,6 +1280,7 @@ def delta_E_HyCH( Examples -------- + >>> import numpy as np >>> Lab_1 = np.array([39.91531343, 51.16658481, 146.12933781]) >>> Lab_2 = np.array([53.12207516, -39.92365056, 249.54831278]) >>> delta_E_HyCH(Lab_1, Lab_2) # doctest: +ELLIPSIS @@ -1260,7 +1295,7 @@ def delta_E_HyCH( dH=np.float64(34.5522171...)) """ - S_L, S_C, S_H, delta_L_p, delta_C_p, delta_H_p, R_T = astuple( + S_L, S_C, S_H, delta_L_p, delta_C_p, delta_H_p, _R_T = astuple( intermediate_attributes_CIE2000(Lab_1, Lab_2) ) @@ -1272,7 +1307,9 @@ def delta_E_HyCH( C = delta_C_p / (k_C * S_C) H = delta_H_p / (k_H * S_H) - HyCH = as_float(np.abs(L) + np.sqrt(C**2 + H**2)) + xp = array_namespace(L) + + HyCH = as_float(xp.abs(L) + xp.sqrt(C**2 + H**2)) if not additional_data: return HyCH diff --git a/colour/difference/huang2015.py b/colour/difference/huang2015.py index e09598e265..07094330e1 100644 --- a/colour/difference/huang2015.py +++ b/colour/difference/huang2015.py @@ -28,6 +28,7 @@ if typing.TYPE_CHECKING: from colour.hints import ArrayLike, Literal, NDArrayFloat +from colour.algebra import spow from colour.utilities import CanonicalMapping, tsplit, validate_method __author__ = "Colour Developers" @@ -136,4 +137,4 @@ def power_function_Huang2015( a, b = tsplit(COEFFICIENTS_HUANG2015[coefficients]) - return a * d_E**b + return a * spow(d_E, b) diff --git a/colour/difference/metamerism_index.py b/colour/difference/metamerism_index.py index 9028858f19..591becd719 100644 --- a/colour/difference/metamerism_index.py +++ b/colour/difference/metamerism_index.py @@ -19,8 +19,6 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import ( Any, @@ -41,11 +39,14 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import XYZ_to_Lab from colour.utilities import ( + array_namespace, as_array, attest, domain_range_scale, filter_kwargs, validate_method, + xp_isclose, + xp_matrix_transpose, ) __author__ = "Colour Developers" @@ -577,22 +578,30 @@ def sd_to_metamerism_index( A_r = tristimulus_weighting_factors_integration(cmfs, illuminant_r, shape=shape) A_t = tristimulus_weighting_factors_integration(cmfs, illuminant_t, shape=shape) - R = np.dot( - np.dot(A_r, np.linalg.inv(np.dot(np.transpose(A_r), A_r))), np.transpose(A_r) + xp = array_namespace(A_r) + + A_r_T = xp_matrix_transpose(A_r, xp=xp) + R = xp.matmul( + xp.matmul(A_r, xp.linalg.inv(xp.matmul(A_r_T, A_r))), + A_r_T, ) - sd_spl_corr = np.dot(R, sd_std.values) + np.dot( - np.identity(R.shape[0]) - R, sd_spl.values + sd_spl_corr = xp.matmul(R, sd_std.values) + xp.matmul( + xp.eye(R.shape[0]) - R, sd_spl.values ) sd_spl_corr = SpectralDistribution(sd_spl_corr, shape) - XYZ_spl_corr_t = np.dot(sd_spl_corr.values, A_t) / 100 - XYZ_std_t = np.dot(sd_std.values, A_t) / 100 - XYZ_spl_corr = np.dot(sd_spl_corr.values, A_r) / 100 - XYZ_std = np.dot(sd_std.values, A_r) / 100 + XYZ_spl_corr_t = xp.matmul(sd_spl_corr.values, A_t) / 100 + XYZ_std_t = xp.matmul(sd_std.values, A_t) / 100 + XYZ_spl_corr = xp.matmul(sd_spl_corr.values, A_r) / 100 + XYZ_std = xp.matmul(sd_std.values, A_r) / 100 attest( - np.allclose(XYZ_std, XYZ_spl_corr, atol=TOLERANCE_ABSOLUTE_TESTS), + bool( + xp.all( + xp_isclose(XYZ_std, XYZ_spl_corr, atol=TOLERANCE_ABSOLUTE_TESTS, xp=xp) + ) + ), "The corrected sample under reference illuminant must be equal " "to the standard under reference illuminant!", ) diff --git a/colour/difference/stress.py b/colour/difference/stress.py index 5ecb7a529a..2f597baa48 100644 --- a/colour/difference/stress.py +++ b/colour/difference/stress.py @@ -22,14 +22,19 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode if typing.TYPE_CHECKING: from colour.hints import ArrayLike, Literal, NDArrayFloat -from colour.utilities import CanonicalMapping, as_float, as_float_array, validate_method +from colour.utilities import ( + CanonicalMapping, + array_namespace, + as_float, + as_float_array, + validate_method, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -69,6 +74,7 @@ def index_stress_Garcia2007(d_E: ArrayLike, d_V: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> d_E = np.array([2.0425, 2.8615, 3.4412]) >>> d_V = np.array([1.2644, 1.2630, 1.8731]) >>> index_stress_Garcia2007(d_E, d_V) # doctest: +ELLIPSIS @@ -76,12 +82,15 @@ def index_stress_Garcia2007(d_E: ArrayLike, d_V: ArrayLike) -> NDArrayFloat: """ d_E = as_float_array(d_E) - d_V = as_float_array(d_V) + + xp = array_namespace(d_E, d_V) + + d_V = xp_as_float_array(d_V, xp=xp, like=d_E) with sdiv_mode(): - F_1 = sdiv(np.sum(d_E**2), np.sum(d_E * d_V)) + F_1 = sdiv(xp.sum(d_E**2), xp.sum(d_E * d_V)) - stress = np.sqrt(sdiv(np.sum((d_E - F_1 * d_V) ** 2), np.sum(F_1**2 * d_V**2))) + stress = xp.sqrt(sdiv(xp.sum((d_E - F_1 * d_V) ** 2), xp.sum(F_1**2 * d_V**2))) return as_float(stress) @@ -130,6 +139,7 @@ def index_stress( Examples -------- + >>> import numpy as np >>> d_E = np.array([2.0425, 2.8615, 3.4412]) >>> d_V = np.array([1.2644, 1.2630, 1.8731]) >>> index_stress(d_E, d_V) # doctest: +ELLIPSIS diff --git a/colour/difference/tests/test__init__.py b/colour/difference/tests/test__init__.py index 36d9af3854..8784b59af5 100644 --- a/colour/difference/tests/test__init__.py +++ b/colour/difference/tests/test__init__.py @@ -2,11 +2,15 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.difference import delta_E -from colour.utilities import domain_range_scale +from colour.utilities import domain_range_scale, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -23,14 +27,14 @@ class TestDelta_E: """Define :func:`colour.difference.delta_E` definition unit tests methods.""" - def test_domain_range_scale_delta_E(self) -> None: + def test_domain_range_scale_delta_E(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_E` definition domain and range scale support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) m = ("CIE 1976", "CIE 1994", "CIE 2000", "CMC", "DIN99") v = [delta_E(Lab_1, Lab_2, method) for method in m] @@ -39,7 +43,7 @@ def test_domain_range_scale_delta_E(self) -> None: for method, value in zip(m, v, strict=True): for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E(Lab_1 * factor, Lab_2 * factor, method), value, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/difference/tests/test_cam02_ucs.py b/colour/difference/tests/test_cam02_ucs.py index 3cb539e6f4..2ff70ecb10 100644 --- a/colour/difference/tests/test_cam02_ucs.py +++ b/colour/difference/tests/test_cam02_ucs.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -10,7 +15,13 @@ from colour.difference import delta_E_CAM02LCD, delta_E_CAM02SCD, delta_E_CAM02UCS from colour.difference.cam02_ucs import delta_E_Luo2006 from colour.models.cam02_ucs import COEFFICIENTS_UCS_LUO2006 -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -30,86 +41,92 @@ class TestDelta_E_Luo2006: tests methods. """ - def test_delta_E_Luo2006(self) -> None: + def test_delta_E_Luo2006(self, xp: ModuleType) -> None: """Test :func:`colour.difference.cam02_ucs.delta_E_Luo2006` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_Luo2006( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), 14.055546437777583, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_Luo2006( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), - COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], - additional_data=True, - ).values - ), - np.array( - [ - 14.055546437777583, - 0.1309140259740277, - 3.8848968900000003, - 13.50736182, - ] + xp_assert_close( + xp.stack( + list( + delta_E_Luo2006( + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), + COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], + additional_data=True, + ).values + ) ), + [ + 14.055546437777583, + 0.1309140259740277, + 3.8848968900000003, + 13.50736182, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_Luo2006( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - delta_E_CAM02LCD( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + as_ndarray( + delta_E_CAM02LCD( + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_Luo2006( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - delta_E_CAM02SCD( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + as_ndarray( + delta_E_CAM02SCD( + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_Luo2006( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - delta_E_CAM02UCS( - np.array([54.90433134, -0.08450395, -0.06854831]), - np.array([54.80352754, -3.96940084, -13.57591013]), + as_ndarray( + delta_E_CAM02UCS( + xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp), + xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp), + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_Luo2006(self) -> None: + def test_n_dimensional_delta_E_Luo2006(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.cam02_ucs.delta_E_Luo2006` definition n-dimensional arrays support. """ - Jpapbp_1 = np.array([54.90433134, -0.08450395, -0.06854831]) - Jpapbp_2 = np.array([54.80352754, -3.96940084, -13.57591013]) + Jpapbp_1 = xp_as_array([54.90433134, -0.08450395, -0.06854831], xp=xp) + Jpapbp_2 = xp_as_array([54.80352754, -3.96940084, -13.57591013], xp=xp) delta_E_p = delta_E_Luo2006( Jpapbp_1, Jpapbp_2, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] ) @@ -120,45 +137,59 @@ def test_n_dimensional_delta_E_Luo2006(self) -> None: additional_data=True, ) - Jpapbp_1 = np.tile(Jpapbp_1, (6, 1)) - Jpapbp_2 = np.tile(Jpapbp_2, (6, 1)) - delta_E_p = np.tile(delta_E_p, 6) - np.testing.assert_allclose( + Jpapbp_1 = xp.tile(xp_as_array(Jpapbp_1, xp=xp), (6, 1)) + Jpapbp_2 = xp.tile(xp_as_array(Jpapbp_2, xp=xp), (6, 1)) + delta_E_p = xp.tile(xp_as_array(delta_E_p, xp=xp), (6,)) + xp_assert_close( delta_E_Luo2006(Jpapbp_1, Jpapbp_2, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), delta_E_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_Luo2006( - Jpapbp_1, - Jpapbp_2, - COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], - additional_data=True, - ).values + xp_assert_close( + xp.stack( + list( + delta_E_Luo2006( + Jpapbp_1, + Jpapbp_2, + COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], + additional_data=True, + ).values + ) + ), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] ), - np.array([np.tile(val, 6) for val in additional_data.values]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Jpapbp_1 = np.reshape(Jpapbp_1, (2, 3, 3)) - Jpapbp_2 = np.reshape(Jpapbp_2, (2, 3, 3)) - delta_E_p = np.reshape(delta_E_p, (2, 3)) - np.testing.assert_allclose( + Jpapbp_1 = xp_reshape(xp_as_array(Jpapbp_1, xp=xp), (2, 3, 3), xp=xp) + Jpapbp_2 = xp_reshape(xp_as_array(Jpapbp_2, xp=xp), (2, 3, 3), xp=xp) + delta_E_p = xp_reshape(xp_as_array(delta_E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( delta_E_Luo2006(Jpapbp_1, Jpapbp_2, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), delta_E_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_Luo2006( - Jpapbp_1, - Jpapbp_2, - COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], - additional_data=True, - ).values + xp_assert_close( + xp.stack( + list( + delta_E_Luo2006( + Jpapbp_1, + Jpapbp_2, + COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], + additional_data=True, + ).values + ) + ), + xp.stack( + [ + xp.full((2, 3), float(as_ndarray(val))) + for val in additional_data.values + ] ), - np.array([np.full((2, 3), val) for val in additional_data.values]), atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/difference/tests/test_delta_e.py b/colour/difference/tests/test_delta_e.py index 764c910675..64d8cf40ec 100644 --- a/colour/difference/tests/test_delta_e.py +++ b/colour/difference/tests/test_delta_e.py @@ -11,6 +11,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -27,7 +32,14 @@ delta_E_ITP, ) from colour.difference.delta_e import intermediate_attributes_CIE2000 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -59,51 +71,56 @@ class TestDelta_E_CIE1976: definition, thus unit tests are not entirely implemented. """ - def test_delta_E_CIE1976(self) -> None: + def test_delta_E_CIE1976(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_CIE1976` definition.""" - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) additional_data = delta_E_CIE1976(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.reshape(np.tile(Lab_1, (6, 1)), (2, 3, 3)) - Lab_2 = np.reshape(np.tile(Lab_2, (6, 1)), (2, 3, 3)) + Lab_1 = xp_reshape(xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)), (2, 3, 3), xp=xp) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1976(Lab_1, Lab_2), - euclidean_distance(Lab_1, Lab_2), + as_ndarray(euclidean_distance(Lab_1, Lab_2)), atol=TOLERANCE_ABSOLUTE_TESTS, ) tmp = delta_E_CIE1976(Lab_1, Lab_2, additional_data=True) - np.testing.assert_allclose( - np.array([*tmp.values]), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp.stack([*tmp.values]), + xp.stack( + [ + xp.full((2, 3), float(as_ndarray(val))) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_CIE1976(self) -> None: + def test_n_dimensional_delta_E_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE1976` definition n-dimensional arrays support. """ - def test_domain_range_scale_delta_E_CIE1976(self) -> None: + def test_domain_range_scale_delta_E_CIE1976(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE1976` definition domain and range scale support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1976(Lab_1 * factor, Lab_2 * factor), - euclidean_distance(Lab_1, Lab_2), + as_ndarray(euclidean_distance(Lab_1, Lab_2)), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -121,61 +138,61 @@ class TestDelta_E_CIE1994: tests methods. """ - def test_delta_E_CIE1994(self) -> None: + def test_delta_E_CIE1994(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_CIE1994` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), ), 1.671119130541200, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), ), 83.779225500887094, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), ), 10.053931954553839, atol=TOLERANCE_ABSOLUTE_TESTS, ) # testing textiles boolean - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), textiles=True, ), 88.335553057506502, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), textiles=True, ), 10.612657890048272, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 8.32281957, -73.58297716]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 8.32281957, -73.58297716], xp=xp), textiles=True, ), 60.368687261063329, @@ -183,90 +200,102 @@ def test_delta_E_CIE1994(self) -> None: ) # testing additional data boolean - np.testing.assert_allclose( - np.array( - delta_E_CIE1994( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), - additional_data=True, - ).values - ), - np.array( - [ - 1.6711191305411999, - -1.6672370199999946, - -0.11383155366801864, - 0.0014983296827213444, - ] + xp_assert_close( + xp.stack( + list( + delta_E_CIE1994( + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), + additional_data=True, + ).values + ) ), + [ + 1.6711191305411999, + -1.6672370199999946, + -0.11383155366801864, + 0.0014983296827213444, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_CIE1994(self) -> None: + def test_n_dimensional_delta_E_CIE1994(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE1994` definition n-dimensional arrays support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) - delta_E = delta_E_CIE1994(Lab_1, Lab_2) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) + delta_E = as_ndarray(delta_E_CIE1994(Lab_1, Lab_2)) additional_data = delta_E_CIE1994(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.tile(Lab_1, (6, 1)) - Lab_2 = np.tile(Lab_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( + Lab_1 = xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)) + Lab_2 = xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( delta_E_CIE1994(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_CIE1994(Lab_1, Lab_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp.stack(list(delta_E_CIE1994(Lab_1, Lab_2, additional_data=True).values)), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_1 = np.reshape(Lab_1, (2, 3, 3)) - Lab_2 = np.reshape(Lab_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( + Lab_1 = xp_reshape(xp_as_array(Lab_1, xp=xp), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp_as_array(Lab_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( delta_E_CIE1994(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_CIE1994(Lab_1, Lab_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp.stack(list(delta_E_CIE1994(Lab_1, Lab_2, additional_data=True).values)), + xp.stack( + [ + xp.full((2, 3), float(as_ndarray(val))) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_CIE1994(self) -> None: + def test_domain_range_scale_delta_E_CIE1994(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE1994` definition domain and range scale support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) delta_E = delta_E_CIE1994(Lab_1, Lab_2) additional_data = delta_E_CIE1994(Lab_1, Lab_2, additional_data=True) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE1994(Lab_1 * factor, Lab_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_CIE1994( - Lab_1 * factor, Lab_2 * factor, additional_data=True - ).values + xp_assert_close( + xp.stack( + list( + delta_E_CIE1994( + Lab_1 * factor, Lab_2 * factor, additional_data=True + ).values + ) ), - np.array(additional_data.values), + xp.stack(list(additional_data.values)), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -289,18 +318,18 @@ class TestIntermediateAttributes_CIE2000: definition unit tests methods. """ - def test_intermediate_attributes_CIE2000(self) -> None: + def test_intermediate_attributes_CIE2000(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.intermediate_attributes_CIE2000` definition. """ - np.testing.assert_allclose( + xp_assert_close( intermediate_attributes_CIE2000( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), ), - np.array( + xp_as_array( [ 1.00010211, 19.07826821, @@ -309,7 +338,8 @@ def test_intermediate_attributes_CIE2000(self) -> None: 2.16616092, 0.01050306, -0.00000000, - ] + ], + xp=xp, ), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -321,61 +351,61 @@ class TestDelta_E_CIE2000: tests methods. """ - def test_delta_E_CIE2000(self) -> None: + def test_delta_E_CIE2000(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_CIE2000` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), ), 1.670930327213592, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), ), 94.03564903, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), ), 14.87906419, atol=TOLERANCE_ABSOLUTE_TESTS, ) # testing textiles boolean - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([50.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([50.00000000, 426.67945353, 72.39590835], xp=xp), textiles=True, ), 95.79205352, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([50.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([50.00000000, 74.05216981, 276.45318193], xp=xp), textiles=True, ), 23.55420943, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([50.00000000, 8.32281957, -73.58297716]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([50.00000000, 8.32281957, -73.58297716], xp=xp), textiles=True, ), 70.63213819, @@ -383,90 +413,112 @@ def test_delta_E_CIE2000(self) -> None: ) # testing additional data boolean - np.testing.assert_allclose( - np.array( - delta_E_CIE2000( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), - additional_data=True, - ).values - ), - np.array( - [ - 1.6709303272135918, - 1.6670667983081124, - 0.11354075196350125, - 0.0022239659222892624, - ] + xp_assert_close( + xp.stack( + list( + delta_E_CIE2000( + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), + additional_data=True, + ).values + ) ), + [ + 1.6709303272135918, + 1.6670667983081124, + 0.11354075196350125, + 0.0022239659222892624, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_CIE2000(self) -> None: + def test_n_dimensional_delta_E_CIE2000(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE2000` definition n-dimensional arrays support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) delta_E = delta_E_CIE2000(Lab_1, Lab_2) additional_data = delta_E_CIE2000(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.tile(Lab_1, (6, 1)) - Lab_2 = np.tile(Lab_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( + Lab_1 = xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)) + Lab_2 = xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( delta_E_CIE2000(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_CIE2000(Lab_1, Lab_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_CIE2000(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_1 = np.reshape(Lab_1, (2, 3, 3)) - Lab_2 = np.reshape(Lab_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( + Lab_1 = xp_reshape(xp_as_array(Lab_1, xp=xp), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp_as_array(Lab_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( delta_E_CIE2000(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_CIE2000(Lab_1, Lab_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_CIE2000(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp_as_array( + [np.full((2, 3), as_ndarray(val)) for val in additional_data.values], + xp=xp, + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_CIE2000(self) -> None: + def test_domain_range_scale_delta_E_CIE2000(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE2000` definition domain and range scale support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) delta_E = delta_E_CIE2000(Lab_1, Lab_2) additional_data = delta_E_CIE2000(Lab_1, Lab_2, additional_data=True) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_CIE2000(Lab_1 * factor, Lab_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_CIE2000( - Lab_1 * factor, Lab_2 * factor, additional_data=True - ).values + xp_assert_close( + xp.stack( + list( + delta_E_CIE2000( + Lab_1 * factor, Lab_2 * factor, additional_data=True + ).values + ) ), - np.array(additional_data.values), + xp.stack(list(additional_data.values)), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -482,7 +534,7 @@ def test_nan_delta_E_CIE2000(self) -> None: delta_E_CIE2000(cases, cases) delta_E_CIE2000(cases, cases, additional_data=True) - def test_delta_E_CIE2000_Sharma2004(self) -> None: + def test_delta_E_CIE2000_Sharma2004(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CIE2000` definition using Sharma (2004) dataset. @@ -530,7 +582,7 @@ def test_delta_E_CIE2000_Sharma2004(self) -> None: # R_T | -0.000000000000000 | -0.000000000353435 | <-- # d_E | 4.746066453039259 | 4.804524508211768 | <-- - Lab_1 = np.array( + Lab_1 = xp_as_array( [ [50.0000, 2.6772, -79.7751], [50.0000, 3.1571, -77.2803], @@ -566,10 +618,11 @@ def test_delta_E_CIE2000_Sharma2004(self) -> None: [90.9257, -0.5406, -0.9208], [6.7747, -0.2908, -2.4247], [2.0776, 0.0795, -1.1350], - ] + ], + xp=xp, ) - Lab_2 = np.array( + Lab_2 = xp_as_array( [ [50.0000, 0.0000, -82.7485], [50.0000, 0.0000, -82.7485], @@ -605,7 +658,8 @@ def test_delta_E_CIE2000_Sharma2004(self) -> None: [88.6381, -0.8985, -0.7239], [5.8714, -0.0985, -2.2286], [0.9033, -0.0636, -0.5514], - ] + ], + xp=xp, ) d_E = np.array( @@ -647,7 +701,9 @@ def test_delta_E_CIE2000_Sharma2004(self) -> None: ] ) - np.testing.assert_allclose(delta_E_CIE2000(Lab_1, Lab_2), d_E, atol=1e-4) + xp_assert_close( + delta_E_CIE2000(Lab_1, Lab_2), d_E, atol=TOLERANCE_ABSOLUTE_TESTS * 1000 + ) class TestDelta_E_CMC: @@ -656,61 +712,61 @@ class TestDelta_E_CMC: methods. """ - def test_delta_E_CMC(self) -> None: + def test_delta_E_CMC(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_CMC` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), ), 0.899699975683419, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), ), 172.70477129, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), ), 20.59732717, atol=TOLERANCE_ABSOLUTE_TESTS, ) # testing l float - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 426.67945353, 72.39590835]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp), l=1, ), 172.70477129, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 74.05216981, 276.45318193]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp), l=1, ), 20.59732717, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC( - np.array([100.00000000, 21.57210357, 272.22819350]), - np.array([100.00000000, 8.32281957, -73.58297716]), + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp), + xp_as_array([100.00000000, 8.32281957, -73.58297716], xp=xp), l=1, ), 121.71841479, @@ -718,86 +774,112 @@ def test_delta_E_CMC(self) -> None: ) # testing additional data boolean - np.testing.assert_allclose( - np.array( - delta_E_CMC( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), - additional_data=True, - ).values - ), - np.array( - [ - 0.8996999756834185, - -0.7743459246308059, - -0.4580766751407644, - 0.003767617866617381, - ] + xp_assert_close( + xp.stack( + list( + delta_E_CMC( + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), + additional_data=True, + ).values + ) ), + [ + 0.8996999756834185, + -0.7743459246308059, + -0.4580766751407644, + 0.003767617866617381, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_CMC(self) -> None: + def test_n_dimensional_delta_E_CMC(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CMC` definition n-dimensional arrays support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) delta_E = delta_E_CMC(Lab_1, Lab_2) additional_data = delta_E_CMC(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.tile(Lab_1, (6, 1)) - Lab_2 = np.tile(Lab_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( - delta_E_CMC(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS + Lab_1 = xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)) + Lab_2 = xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( + delta_E_CMC(Lab_1, Lab_2), + delta_E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_CMC(Lab_1, Lab_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_CMC(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_1 = np.reshape(Lab_1, (2, 3, 3)) - Lab_2 = np.reshape(Lab_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( - delta_E_CMC(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS + Lab_1 = xp_reshape(xp_as_array(Lab_1, xp=xp), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp_as_array(Lab_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( + delta_E_CMC(Lab_1, Lab_2), + delta_E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_CMC(Lab_1, Lab_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_CMC(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp_as_array( + [np.full((2, 3), as_ndarray(val)) for val in additional_data.values], + xp=xp, + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_CMC(self) -> None: + def test_domain_range_scale_delta_E_CMC(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_CMC` definition domain and range scale support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - Lab_2 = np.array([50.65907324, -0.11671910, 402.82235718]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + Lab_2 = xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp) delta_E = delta_E_CMC(Lab_1, Lab_2) additional_data = delta_E_CMC(Lab_1, Lab_2, additional_data=True) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_CMC(Lab_1 * factor, Lab_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_CMC( - Lab_1 * factor, Lab_2 * factor, additional_data=True - ).values + xp_assert_close( + xp.stack( + list( + delta_E_CMC( + Lab_1 * factor, Lab_2 * factor, additional_data=True + ).values + ) ), - np.array(additional_data.values), + xp.stack(list(additional_data.values)), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -820,180 +902,205 @@ class TestDelta_E_ITP: methods. """ - def test_delta_E_ITP(self) -> None: + def test_delta_E_ITP(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_ITP` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (110, 82, 69), Dark Skin - np.array([0.4885468072, -0.04739350675, 0.07475401302]), - np.array([0.4899203231, -0.04567508203, 0.07361341775]), + xp_as_array([0.4885468072, -0.04739350675, 0.07475401302], xp=xp), + xp_as_array([0.4899203231, -0.04567508203, 0.07361341775], xp=xp), ), 1.426572247, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (110, 82, 69), 100% White - np.array([0.7538438727, 0, -6.25e-16]), - np.array([0.7538912244, 0.001930922514, -0.0003599955951]), + xp_as_array([0.7538438727, 0, -6.25e-16], xp=xp), + xp_as_array([0.7538912244, 0.001930922514, -0.0003599955951], xp=xp), ), 0.7426668055, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (0, 0, 0), 100% Black - np.array([0.1596179061, 0, -1.21e-16]), - np.array([0.1603575152, 0.02881444889, -0.009908665843]), + xp_as_array([0.1596179061, 0, -1.21e-16], xp=xp), + xp_as_array([0.1603575152, 0.02881444889, -0.009908665843], xp=xp), ), 12.60096264, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (255, 0, 0), 100% Red - np.array([0.5965650331, -0.2083210482, 0.3699729716]), - np.array([0.596263079, -0.1629742033, 0.3617767026]), + xp_as_array([0.5965650331, -0.2083210482, 0.3699729716], xp=xp), + xp_as_array([0.596263079, -0.1629742033, 0.3617767026], xp=xp), ), 17.36012552, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (0, 255, 0), 100% Green - np.array([0.7055787513, -0.4063731514, -0.07278767382]), - np.array([0.7046946082, -0.3771037586, -0.07141626753]), + xp_as_array([0.7055787513, -0.4063731514, -0.07278767382], xp=xp), + xp_as_array([0.7046946082, -0.3771037586, -0.07141626753], xp=xp), ), 10.60227327, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (255, 0, 0), 100% Blue - np.array([0.5180652611, 0.2932420978, -0.1873112695]), - np.array([0.5167090868, 0.298191609, -0.1824609953]), + xp_as_array([0.5180652611, 0.2932420978, -0.1873112695], xp=xp), + xp_as_array([0.5167090868, 0.298191609, -0.1824609953], xp=xp), ), 4.040270489, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (0, 255, 255), 100% Cyan - np.array([0.7223275939, -0.01290632441, -0.1139004748]), - np.array([0.7215329274, -0.007863821961, -0.1106683944]), + xp_as_array([0.7223275939, -0.01290632441, -0.1139004748], xp=xp), + xp_as_array([0.7215329274, -0.007863821961, -0.1106683944], xp=xp), ), 3.00633812, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (255, 0, 255), 100% Magenta - np.array([0.6401125212, 0.280225698, 0.1665590804]), - np.array([0.640473651, 0.2819981563, 0.1654050172]), + xp_as_array([0.6401125212, 0.280225698, 0.1665590804], xp=xp), + xp_as_array([0.640473651, 0.2819981563, 0.1654050172], xp=xp), ), 1.07944277, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP( # RGB: (255, 255, 0), 100% Yellow - np.array([0.7413041405, -0.3638807621, 0.04959414794]), - np.array([0.7412815181, -0.3299076141, 0.04545287368]), + xp_as_array([0.7413041405, -0.3638807621, 0.04959414794], xp=xp), + xp_as_array([0.7412815181, -0.3299076141, 0.04545287368], xp=xp), ), 12.5885645, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_ITP( - # RGB: (255, 255, 0), 100% Yellow - np.array([0.7413041405, -0.3638807621, 0.04959414794]), - np.array([0.7412815181, -0.3299076141, 0.04545287368]), - additional_data=True, - ).values - ), - np.array( - [ - 12.58856451296948, - -2.262240000006077e-05, - 0.016986573999999977, - -0.004141274260000001, - ] + xp_assert_close( + xp.stack( + list( + delta_E_ITP( + # RGB: (255, 255, 0), 100% Yellow + xp_as_array( + [0.7413041405, -0.3638807621, 0.04959414794], xp=xp + ), + xp_as_array( + [0.7412815181, -0.3299076141, 0.04545287368], xp=xp + ), + additional_data=True, + ).values + ) ), + [ + 12.58856451296948, + -2.262240000006077e-05, + 0.016986573999999977, + -0.004141274260000001, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_ITP(self) -> None: + def test_n_dimensional_delta_E_ITP(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_ITP` definition n-dimensional arrays support. """ - ICtCp_1 = np.array([0.4885468072, -0.04739350675, 0.07475401302]) - ICtCp_2 = np.array([0.4899203231, -0.04567508203, 0.07361341775]) + ICtCp_1 = xp_as_array([0.4885468072, -0.04739350675, 0.07475401302], xp=xp) + ICtCp_2 = xp_as_array([0.4899203231, -0.04567508203, 0.07361341775], xp=xp) delta_E = delta_E_ITP(ICtCp_1, ICtCp_2) additional_data = delta_E_ITP(ICtCp_1, ICtCp_2, additional_data=True) - ICtCp_1 = np.tile(ICtCp_1, (6, 1)) - ICtCp_2 = np.tile(ICtCp_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( + ICtCp_1 = xp.tile(xp_as_array(ICtCp_1, xp=xp), (6, 1)) + ICtCp_2 = xp.tile(xp_as_array(ICtCp_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( delta_E_ITP(ICtCp_1, ICtCp_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS ) - np.testing.assert_allclose( - np.array(delta_E_ITP(ICtCp_1, ICtCp_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_ITP(ICtCp_1, ICtCp_2, additional_data=True).values + ], + xp=xp, + ), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - ICtCp_1 = np.reshape(ICtCp_1, (2, 3, 3)) - ICtCp_2 = np.reshape(ICtCp_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( + ICtCp_1 = xp_reshape(xp_as_array(ICtCp_1, xp=xp), (2, 3, 3), xp=xp) + ICtCp_2 = xp_reshape(xp_as_array(ICtCp_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( delta_E_ITP(ICtCp_1, ICtCp_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS ) - np.testing.assert_allclose( - np.array(delta_E_ITP(ICtCp_1, ICtCp_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_ITP(ICtCp_1, ICtCp_2, additional_data=True).values + ], + xp=xp, + ), + xp_as_array( + [np.full((2, 3), as_ndarray(val)) for val in additional_data.values], + xp=xp, + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_ITP(self) -> None: + def test_domain_range_scale_delta_E_ITP(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_ITP` definition domain and range scale support. """ - ICtCp_1 = np.array([0.4885468072, -0.04739350675, 0.07475401302]) - ICtCp_2 = np.array([0.4899203231, -0.04567508203, 0.07361341775]) + ICtCp_1 = xp_as_array([0.4885468072, -0.04739350675, 0.07475401302], xp=xp) + ICtCp_2 = xp_as_array([0.4899203231, -0.04567508203, 0.07361341775], xp=xp) delta_E = delta_E_ITP(ICtCp_1, ICtCp_2) additional_data = delta_E_ITP(ICtCp_1, ICtCp_2, additional_data=True) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_ITP(ICtCp_1 * factor, ICtCp_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( + xp_assert_close( + xp_as_array( delta_E_ITP( ICtCp_1 * factor, ICtCp_2 * factor, additional_data=True - ).values + ).values, + xp=xp, ), - np.array(additional_data.values), + xp_as_array(additional_data.values, xp=xp), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1016,52 +1123,52 @@ class TestDelta_E_HyAB: tests methods. """ - def test_delta_E_HyAB(self) -> None: + def test_delta_E_HyAB(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_HyAB` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_HyAB( - np.array([39.91531343, 51.16658481, 146.12933781]), - np.array([53.12207516, -39.92365056, 249.54831278]), + xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp), + xp_as_array([53.12207516, -39.92365056, 249.54831278], xp=xp), ), 151.021548177635900, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_HyAB( - np.array([39.91531343, 51.16658481, 146.12933781]), - np.array([28.52234779, 19.46628874, 472.06042624]), + xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp), + xp_as_array([28.52234779, 19.46628874, 472.06042624], xp=xp), ), 338.862022462305200, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_HyAB( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), ), 3.833423402021121, atol=TOLERANCE_ABSOLUTE_TESTS, ) # testing additional data boolean - np.testing.assert_allclose( - np.array( - delta_E_HyAB( - np.array([39.91531343, 51.16658481, 146.12933781]), - np.array([53.12207516, -39.92365056, 249.54831278]), - additional_data=True, - ).values - ), - np.array( - [151.0215481776359, -13.206761730000004, 91.09023537, -103.41897497] + xp_assert_close( + xp.stack( + list( + delta_E_HyAB( + xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp), + xp_as_array([53.12207516, -39.92365056, 249.54831278], xp=xp), + additional_data=True, + ).values + ) ), + [151.0215481776359, -13.206761730000004, 91.09023537, -103.41897497], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_HyAB(self) -> None: + def test_n_dimensional_delta_E_HyAB(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_HyAB` definition n-dimensional arrays support. @@ -1072,60 +1179,82 @@ def test_n_dimensional_delta_E_HyAB(self) -> None: delta_E = delta_E_HyAB(Lab_1, Lab_2) additional_data = delta_E_HyAB(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.tile(Lab_1, (6, 1)) - Lab_2 = np.tile(Lab_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( + Lab_1 = xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)) + Lab_2 = xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( delta_E_HyAB(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_HyAB(Lab_1, Lab_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_HyAB(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_1 = np.reshape(Lab_1, (2, 3, 3)) - Lab_2 = np.reshape(Lab_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( + Lab_1 = xp_reshape(xp_as_array(Lab_1, xp=xp), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp_as_array(Lab_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( delta_E_HyAB(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_HyAB(Lab_1, Lab_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_HyAB(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp_as_array( + [np.full((2, 3), as_ndarray(val)) for val in additional_data.values], + xp=xp, + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_HyAB(self) -> None: + def test_domain_range_scale_delta_E_HyAB(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_HyAB` definition domain and range scale support. """ - Lab_1 = np.array([39.91531343, 51.16658481, 146.12933781]) - Lab_2 = np.array([53.12207516, -39.92365056, 249.54831278]) + Lab_1 = xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp) + Lab_2 = xp_as_array([53.12207516, -39.92365056, 249.54831278], xp=xp) delta_E = delta_E_HyAB(Lab_1, Lab_2) additional_data = delta_E_HyAB(Lab_1, Lab_2, additional_data=True) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_HyAB(Lab_1 * factor, Lab_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_HyAB( - Lab_1 * factor, Lab_2 * factor, additional_data=True - ).values + xp_assert_close( + xp.stack( + list( + delta_E_HyAB( + Lab_1 * factor, Lab_2 * factor, additional_data=True + ).values + ) ), - np.array(additional_data.values), + xp.stack(list(additional_data.values)), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1148,57 +1277,57 @@ class TestDelta_E_HyCH: tests methods. """ - def test_delta_E_HyCH(self) -> None: + def test_delta_E_HyCH(self, xp: ModuleType) -> None: """Test :func:`colour.difference.delta_e.delta_E_HyCH` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_HyCH( - np.array([39.91531343, 51.16658481, 146.12933781]), - np.array([53.12207516, -39.92365056, 249.54831278]), + xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp), + xp_as_array([53.12207516, -39.92365056, 249.54831278], xp=xp), ), 48.664279419760369, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_HyCH( - np.array([39.91531343, 51.16658481, 146.12933781]), - np.array([28.52234779, 19.46628874, 472.06042624]), + xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp), + xp_as_array([28.52234779, 19.46628874, 472.06042624], xp=xp), ), 39.260928157999118, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_HyCH( - np.array([48.99183622, -0.10561667, 400.65619925]), - np.array([50.65907324, -0.11671910, 402.82235718]), + xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp), + xp_as_array([50.65907324, -0.11671910, 402.82235718], xp=xp), ), 1.7806293290163562, atol=TOLERANCE_ABSOLUTE_TESTS, ) # testing additional data boolean - np.testing.assert_allclose( - np.array( - delta_E_HyCH( - np.array([39.91531343, 51.16658481, 146.12933781]), - np.array([53.12207516, -39.92365056, 249.54831278]), - additional_data=True, - ).values - ), - np.array( - [ - 48.66427941976037, - 12.796297245237563, - 9.62582118396321, - 34.5522171764165, - ] + xp_assert_close( + xp.stack( + list( + delta_E_HyCH( + xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp), + xp_as_array([53.12207516, -39.92365056, 249.54831278], xp=xp), + additional_data=True, + ).values + ) ), + [ + 48.66427941976037, + 12.796297245237563, + 9.62582118396321, + 34.5522171764165, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_HyCH(self) -> None: + def test_n_dimensional_delta_E_HyCH(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_HyCH` definition n-dimensional arrays support. @@ -1209,60 +1338,82 @@ def test_n_dimensional_delta_E_HyCH(self) -> None: delta_E = delta_E_HyCH(Lab_1, Lab_2) additional_data = delta_E_HyCH(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.tile(Lab_1, (6, 1)) - Lab_2 = np.tile(Lab_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( + Lab_1 = xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)) + Lab_2 = xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( delta_E_HyCH(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_HyCH(Lab_1, Lab_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_HyCH(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_1 = np.reshape(Lab_1, (2, 3, 3)) - Lab_2 = np.reshape(Lab_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( + Lab_1 = xp_reshape(xp_as_array(Lab_1, xp=xp), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp_as_array(Lab_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( delta_E_HyCH(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_HyCH(Lab_1, Lab_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp_as_array( + [ + as_ndarray(v) + for v in delta_E_HyCH(Lab_1, Lab_2, additional_data=True).values + ], + xp=xp, + ), + xp_as_array( + [np.full((2, 3), as_ndarray(val)) for val in additional_data.values], + xp=xp, + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_HyCH(self) -> None: + def test_domain_range_scale_delta_E_HyCH(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.delta_e.delta_E_HyCH` definition domain and range scale support. """ - Lab_1 = np.array([39.91531343, 51.16658481, 146.12933781]) - Lab_2 = np.array([53.12207516, -39.92365056, 249.54831278]) + Lab_1 = xp_as_array([39.91531343, 51.16658481, 146.12933781], xp=xp) + Lab_2 = xp_as_array([53.12207516, -39.92365056, 249.54831278], xp=xp) delta_E = delta_E_HyCH(Lab_1, Lab_2) additional_data = delta_E_HyCH(Lab_1, Lab_2, additional_data=True) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_HyCH(Lab_1 * factor, Lab_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_HyCH( - Lab_1 * factor, Lab_2 * factor, additional_data=True - ).values + xp_assert_close( + xp.stack( + list( + delta_E_HyCH( + Lab_1 * factor, Lab_2 * factor, additional_data=True + ).values + ) ), - np.array(additional_data.values), + xp.stack(list(additional_data.values)), atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/difference/tests/test_din99.py b/colour/difference/tests/test_din99.py index 1d5a71ec96..5dd525ca7c 100644 --- a/colour/difference/tests/test_din99.py +++ b/colour/difference/tests/test_din99.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.difference import delta_E_DIN99 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,61 +40,61 @@ class TestDelta_E_DIN99: tests methods. """ - def test_delta_E_DIN99(self) -> None: + def test_delta_E_DIN99(self, xp: ModuleType) -> None: """Test :func:`colour.difference.din99.delta_E_DIN99` definition.""" - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99( - np.array([60.25740000, -34.00990000, 36.26770000]), - np.array([60.46260000, -34.17510000, 39.43870000]), + xp_as_array([60.25740000, -34.00990000, 36.26770000], xp=xp), + xp_as_array([60.46260000, -34.17510000, 39.43870000], xp=xp), ), 1.177216620111552, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99( - np.array([63.01090000, -31.09610000, -5.86630000]), - np.array([62.81870000, -29.79460000, -4.08640000]), + xp_as_array([63.01090000, -31.09610000, -5.86630000], xp=xp), + xp_as_array([62.81870000, -29.79460000, -4.08640000], xp=xp), ), 0.987529977993114, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99( - np.array([35.08310000, -44.11640000, 3.79330000]), - np.array([35.02320000, -40.07160000, 1.59010000]), + xp_as_array([35.08310000, -44.11640000, 3.79330000], xp=xp), + xp_as_array([35.02320000, -40.07160000, 1.59010000], xp=xp), ), 1.535894757971742, atol=TOLERANCE_ABSOLUTE_TESTS, ) # testing textiles boolean - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99( - np.array([60.25740000, -34.00990000, 36.26770000]), - np.array([60.46260000, -34.17510000, 39.43870000]), + xp_as_array([60.25740000, -34.00990000, 36.26770000], xp=xp), + xp_as_array([60.46260000, -34.17510000, 39.43870000], xp=xp), textiles=True, ), 1.215652775586509, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99( - np.array([63.01090000, -31.09610000, -5.86630000]), - np.array([62.81870000, -29.79460000, -4.08640000]), + xp_as_array([63.01090000, -31.09610000, -5.86630000], xp=xp), + xp_as_array([62.81870000, -29.79460000, -4.08640000], xp=xp), textiles=True, ), 1.025997138865984, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99( - np.array([35.08310000, -44.11640000, 3.79330000]), - np.array([35.02320000, -40.07160000, 1.59010000]), + xp_as_array([35.08310000, -44.11640000, 3.79330000], xp=xp), + xp_as_array([35.02320000, -40.07160000, 1.59010000], xp=xp), textiles=True, ), 1.539922810033725, @@ -90,112 +102,130 @@ def test_delta_E_DIN99(self) -> None: ) # testing additional data boolean - np.testing.assert_allclose( - np.array( - delta_E_DIN99( - np.array([60.25740000, -34.00990000, 36.26770000]), - np.array([60.46260000, -34.17510000, 39.43870000]), - additional_data=True, - ).values + xp_assert_close( + xp.stack( + list( + delta_E_DIN99( + xp_as_array([60.25740000, -34.00990000, 36.26770000], xp=xp), + xp_as_array([60.46260000, -34.17510000, 39.43870000], xp=xp), + additional_data=True, + ).values + ) ), - np.array([1.1772166201115533, -0.17509302, -0.58040452, -1.00911446]), + [1.1772166201115533, -0.17509302, -0.58040452, -1.00911446], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_delta_E_DIN99_method(self) -> None: + def test_delta_E_DIN99_method(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.din99.delta_E_DIN99` definition *method* parameter support. """ - Lab_1 = np.array([60.25740000, -34.00990000, 36.26770000]) - Lab_2 = np.array([60.46260000, -34.17510000, 39.43870000]) + Lab_1 = xp_as_array([60.25740000, -34.00990000, 36.26770000], xp=xp) + Lab_2 = xp_as_array([60.46260000, -34.17510000, 39.43870000], xp=xp) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99(Lab_1, Lab_2, method="DIN99"), 1.177216620111552, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99(Lab_1, Lab_2, method="DIN99b"), 1.711312965743716, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99(Lab_1, Lab_2, method="DIN99c"), 1.554667171681764, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99(Lab_1, Lab_2, method="DIN99d"), 1.441930871002728, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_delta_E_DIN99(self) -> None: + def test_n_dimensional_delta_E_DIN99(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.din99.delta_E_DIN99` definition n-dimensional arrays support. """ - Lab_1 = np.array([60.25740000, -34.00990000, 36.26770000]) - Lab_2 = np.array([60.46260000, -34.17510000, 39.43870000]) - delta_E = delta_E_DIN99(Lab_1, Lab_2) + Lab_1 = xp_as_array([60.25740000, -34.00990000, 36.26770000], xp=xp) + Lab_2 = xp_as_array([60.46260000, -34.17510000, 39.43870000], xp=xp) + delta_E = as_ndarray(delta_E_DIN99(Lab_1, Lab_2)) additional_data = delta_E_DIN99(Lab_1, Lab_2, additional_data=True) - Lab_1 = np.tile(Lab_1, (6, 1)) - Lab_2 = np.tile(Lab_2, (6, 1)) - delta_E = np.tile(delta_E, 6) - np.testing.assert_allclose( - delta_E_DIN99(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS + Lab_1 = xp.tile(xp_as_array(Lab_1, xp=xp), (6, 1)) + Lab_2 = xp.tile(xp_as_array(Lab_2, xp=xp), (6, 1)) + delta_E = xp.tile(xp_as_array(delta_E, xp=xp), (6,)) + xp_assert_close( + delta_E_DIN99(Lab_1, Lab_2), + delta_E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_DIN99(Lab_1, Lab_2, additional_data=True).values), - np.array([np.tile(val, 6) for val in additional_data.values]), + xp_assert_close( + xp.stack(list(delta_E_DIN99(Lab_1, Lab_2, additional_data=True).values)), + xp.stack( + [ + xp.tile(xp_as_array(val, xp=xp), (6,)) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_1 = np.reshape(Lab_1, (2, 3, 3)) - Lab_2 = np.reshape(Lab_2, (2, 3, 3)) - delta_E = np.reshape(delta_E, (2, 3)) - np.testing.assert_allclose( - delta_E_DIN99(Lab_1, Lab_2), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS + Lab_1 = xp_reshape(xp_as_array(Lab_1, xp=xp), (2, 3, 3), xp=xp) + Lab_2 = xp_reshape(xp_as_array(Lab_2, xp=xp), (2, 3, 3), xp=xp) + delta_E = xp_reshape(xp_as_array(delta_E, xp=xp), (2, 3), xp=xp) + xp_assert_close( + delta_E_DIN99(Lab_1, Lab_2), + delta_E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array(delta_E_DIN99(Lab_1, Lab_2, additional_data=True).values), - np.array([np.full((2, 3), val) for val in additional_data.values]), + xp_assert_close( + xp.stack(list(delta_E_DIN99(Lab_1, Lab_2, additional_data=True).values)), + xp.stack( + [ + xp.full((2, 3), float(as_ndarray(val))) + for val in additional_data.values + ] + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_delta_E_DIN99(self) -> None: + def test_domain_range_scale_delta_E_DIN99(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.din99.delta_E_DIN99` definition domain and range scale support. """ - Lab_1 = np.array([60.25740000, -34.00990000, 36.26770000]) - Lab_2 = np.array([60.46260000, -34.17510000, 39.43870000]) - delta_E = delta_E_DIN99(Lab_1, Lab_2) + Lab_1 = xp_as_array([60.25740000, -34.00990000, 36.26770000], xp=xp) + Lab_2 = xp_as_array([60.46260000, -34.17510000, 39.43870000], xp=xp) + delta_E = as_ndarray(delta_E_DIN99(Lab_1, Lab_2)) additional_data = delta_E_DIN99(Lab_1, Lab_2, additional_data=True) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( delta_E_DIN99(Lab_1 * factor, Lab_2 * factor), delta_E, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - np.array( - delta_E_DIN99( - Lab_1 * factor, Lab_2 * factor, additional_data=True - ).values + xp_assert_close( + xp.stack( + list( + delta_E_DIN99( + Lab_1 * factor, Lab_2 * factor, additional_data=True + ).values + ) ), - np.array(additional_data.values), + xp.stack(list(additional_data.values)), atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/difference/tests/test_huang2015.py b/colour/difference/tests/test_huang2015.py index 34feed9adf..be09c25a26 100644 --- a/colour/difference/tests/test_huang2015.py +++ b/colour/difference/tests/test_huang2015.py @@ -2,10 +2,15 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.difference import power_function_Huang2015 +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -25,16 +30,16 @@ class TestPowerFunctionHuang2015: definition unit tests methods. """ - def test_power_function_Huang2015(self) -> None: + def test_power_function_Huang2015(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.huang2015.power_function_Huang2015` definition. """ - d_E = np.array([2.0425, 2.8615, 3.4412]) + d_E = xp_as_array([2.0425, 2.8615, 3.4412], xp=xp) - np.testing.assert_allclose( + xp_assert_close( power_function_Huang2015(d_E), - np.array([2.35748796, 2.98505036, 3.39651062]), + [2.35748796, 2.98505036, 3.39651062], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/difference/tests/test_metamerism_index.py b/colour/difference/tests/test_metamerism_index.py index b2cfb7e9ee..e5e6c693b3 100644 --- a/colour/difference/tests/test_metamerism_index.py +++ b/colour/difference/tests/test_metamerism_index.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour import ( @@ -19,7 +24,7 @@ XYZ_to_metamerism_index, sd_to_metamerism_index, ) -from colour.utilities import domain_range_scale +from colour.utilities import domain_range_scale, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -40,14 +45,14 @@ class TestLab_to_Metamerism_Index: definition unit tests methods. """ - def test_domain_range_scale_Lab_to_metamerism_index(self) -> None: + def test_domain_range_scale_Lab_to_metamerism_index(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.metamerism_index.Lab_to_metamerism_index` definition domain and range scale support. """ - Lab_1 = np.array([48.99183622, -0.10561667, 400.65619925]) - offset = np.array([0, 0, 2]) + Lab_1 = xp_as_array([48.99183622, -0.10561667, 400.65619925], xp=xp) + offset = xp_as_array([0, 0, 2], xp=xp) c = ("Additive", "Multiplicative") m = ("CIE 1976", "CIE 1994", "CIE 2000", "CMC", "DIN99") @@ -72,7 +77,7 @@ def test_domain_range_scale_Lab_to_metamerism_index(self) -> None: for correction, method, value in it: for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Lab_to_metamerism_index( (Lab_1 + offset) * factor, Lab_1 * factor, @@ -92,14 +97,14 @@ class TestXYZ_to_Metamerism_Index: definition unit tests methods. """ - def test_domain_range_scale_XYZ_to_metamerism_index(self) -> None: + def test_domain_range_scale_XYZ_to_metamerism_index(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.metamerism_index.XYZ_to_metamerism_index` definition domain and range scale support. """ - XYZ_1 = np.array([0.20654008, 0.12197225, 0.05136952]) - offset = np.array([0, 0, 0.01]) + XYZ_1 = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + offset = xp_as_array([0, 0, 0.01], xp=xp) c = ("Additive", "Multiplicative") m = ("CIE 1976", "CIE 1994", "CIE 2000", "CMC", "DIN99") @@ -124,7 +129,7 @@ def test_domain_range_scale_XYZ_to_metamerism_index(self) -> None: for correction, method, value in it: for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_metamerism_index( (XYZ_1 + offset) * factor, XYZ_1 * factor, @@ -144,7 +149,10 @@ class TestSD_to_Metamerism_Index: definition unit tests methods. """ - def test_domain_range_scale_sd_to_metamerism_index(self) -> None: + def test_domain_range_scale_sd_to_metamerism_index( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.difference.metamerism_index.sd_to_metamerism_index` definition domain and range scale support. @@ -255,7 +263,7 @@ def test_domain_range_scale_sd_to_metamerism_index(self) -> None: for observer, illuminant, method, value in it: for scale, _ in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_metamerism_index( N_spl, N_std, diff --git a/colour/difference/tests/test_stress.py b/colour/difference/tests/test_stress.py index 6576168196..fb724efc73 100644 --- a/colour/difference/tests/test_stress.py +++ b/colour/difference/tests/test_stress.py @@ -2,10 +2,15 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.difference import index_stress +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -25,16 +30,16 @@ class TestIndexStress: unit tests methods. """ - def test_index_stress(self) -> None: + def test_index_stress(self, xp: ModuleType) -> None: """ Test :func:`colour.difference.stress.index_stress_Garcia2007` definition. """ - d_E = np.array([2.0425, 2.8615, 3.4412]) - d_V = np.array([1.2644, 1.2630, 1.8731]) + d_E = xp_as_array([2.0425, 2.8615, 3.4412], xp=xp) + d_V = xp_as_array([1.2644, 1.2630, 1.8731], xp=xp) - np.testing.assert_allclose( + xp_assert_close( index_stress(d_E, d_V), 0.121170939369957, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/geometry/ellipse.py b/colour/geometry/ellipse.py index 6c4cd5e9ac..42b6e4bb72 100644 --- a/colour/geometry/ellipse.py +++ b/colour/geometry/ellipse.py @@ -23,18 +23,23 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, Literal + from colour.hints import ArrayLike, Literal, NDArrayFloat -from colour.hints import NDArrayFloat, cast from colour.utilities import ( CanonicalMapping, + array_namespace, ones, tsplit, tstack, validate_method, + xp_as_float_array, + xp_degrees, + xp_eig, + xp_matrix_transpose, + xp_radians, + xp_reshape, + xp_select, ) __author__ = "Colour Developers" @@ -85,16 +90,19 @@ def ellipse_coefficients_general_form(coefficients: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> coefficients = np.array([0.5, 0.5, 2, 1, 45]) >>> ellipse_coefficients_general_form(coefficients) array([ 2.5, -3. , 2.5, -1. , -1. , -3.5]) """ + xp = array_namespace(coefficients) + x_c, y_c, a_a, a_b, theta = tsplit(coefficients) - theta = np.radians(theta) - cos_theta = np.cos(theta) - sin_theta = np.sin(theta) + theta = xp_radians(theta) + cos_theta = xp.cos(theta) + sin_theta = xp.sin(theta) cos_theta_2 = cos_theta**2 sin_theta_2 = sin_theta**2 a_a_2 = a_a**2 @@ -107,7 +115,7 @@ def ellipse_coefficients_general_form(coefficients: ArrayLike) -> NDArrayFloat: e = -b * x_c - 2 * c * y_c f = a * x_c**2 + b * x_c * y_c + c * y_c**2 - a_a_2 * a_b_2 - return np.array([a, b, c, d, e, f]) + return xp_as_float_array([a, b, c, d, e, f], xp=xp) def ellipse_coefficients_canonical_form( @@ -143,37 +151,40 @@ def ellipse_coefficients_canonical_form( Examples -------- + >>> import numpy as np >>> coefficients = np.array([2.5, -3.0, 2.5, -1.0, -1.0, -3.5]) >>> ellipse_coefficients_canonical_form(coefficients) array([ 0.5, 0.5, 2. , 1. , 45. ]) """ + xp = array_namespace(coefficients) + a, b, c, d, e, f = tsplit(coefficients) d_1 = b**2 - 4 * a * c n_p_1 = 2 * (a * e**2 + c * d**2 - b * d * e + d_1 * f) - n_p_2 = np.sqrt((a - c) ** 2 + b**2) + n_p_2 = xp.sqrt((a - c) ** 2 + b**2) - a_a = (-np.sqrt(n_p_1 * (a + c + n_p_2))) / d_1 - a_b = (-np.sqrt(n_p_1 * (a + c - n_p_2))) / d_1 + a_a = (-xp.sqrt(n_p_1 * (a + c + n_p_2))) / d_1 + a_b = (-xp.sqrt(n_p_1 * (a + c - n_p_2))) / d_1 x_c = (2 * c * d - b * e) / d_1 y_c = (2 * a * e - b * d) / d_1 - - theta = np.select( + theta = xp_select( [ - np.logical_and(b == 0, a < c), - np.logical_and(b == 0, a > c), + xp.logical_and(b == 0, a < c), + xp.logical_and(b == 0, a > c), b != 0, ], [ 0, 90, - np.degrees(np.arctan((c - a - n_p_2) / b)), + xp_degrees(xp.arctan((c - a - n_p_2) / b)), ], + xp=xp, ) - return np.array([x_c, y_c, a_a, a_b, theta]) + return xp_as_float_array([x_c, y_c, a_a, a_b, theta], xp=xp) def point_at_angle_on_ellipse(phi: ArrayLike, coefficients: ArrayLike) -> NDArrayFloat: @@ -199,19 +210,23 @@ def point_at_angle_on_ellipse(phi: ArrayLike, coefficients: ArrayLike) -> NDArra Examples -------- + >>> import numpy as np >>> coefficients = np.array([0.5, 0.5, 2, 1, 45]) >>> point_at_angle_on_ellipse(45, coefficients) # doctest: +ELLIPSIS array([1., 2.]) """ - phi = np.radians(phi) + phi = xp_radians(phi) + + xp = array_namespace(phi) + x_c, y_c, a_a, a_b, theta = tsplit(coefficients) - theta = np.radians(theta) + theta = xp_radians(theta) - cos_phi = np.cos(phi) - sin_phi = np.sin(phi) - cos_theta = np.cos(theta) - sin_theta = np.sin(theta) + cos_phi = xp.cos(phi) + sin_phi = xp.sin(phi) + cos_theta = xp.cos(theta) + sin_theta = xp.sin(theta) x = x_c + a_a * cos_theta * cos_phi - a_b * sin_theta * sin_phi y = y_c + a_a * sin_theta * cos_phi + a_b * cos_theta * sin_phi @@ -250,6 +265,7 @@ def ellipse_fitting_Halir1998(a: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> a = np.array([[2, 0], [0, 1], [-2, 0], [0, -1]]) >>> ellipse_fitting_Halir1998(a) # doctest: +ELLIPSIS array([ 0.2425356..., 0. , 0.9701425..., 0. , 0. , @@ -258,6 +274,8 @@ def ellipse_fitting_Halir1998(a: ArrayLike) -> NDArrayFloat: array([-0., -0., 2., 1., 0.]) """ + xp = array_namespace(a) + x, y = tsplit(a) # Quadratic part of the design matrix. @@ -265,28 +283,29 @@ def ellipse_fitting_Halir1998(a: ArrayLike) -> NDArrayFloat: # Linear part of the design matrix. D2 = tstack([x, y, ones(x.shape)]) - D1_T = np.transpose(D1) - D2_T = np.transpose(D2) + D1_T = xp_matrix_transpose(D1, xp=xp) + D2_T = xp_matrix_transpose(D2, xp=xp) # Quadratic part of the scatter matrix. - S1 = np.dot(D1_T, D1) + S1 = xp.matmul(D1_T, D1) # Combined part of the scatter matrix. - S2 = np.dot(D1_T, D2) + S2 = xp.matmul(D1_T, D2) # Linear part of the scatter matrix. - S3 = np.dot(D2_T, D2) + S3 = xp.matmul(D2_T, D2) - T = -np.dot(np.linalg.inv(S3), np.transpose(S2)) + T = -xp.matmul(xp.linalg.inv(S3), xp_matrix_transpose(S2, xp=xp)) # Reduced scatter matrix. - M = S1 + np.dot(S2, T) - M = np.array([M[2, :] / 2, -M[1, :], M[0, :] / 2]) + M = S1 + xp.matmul(S2, T) + M = xp.stack([M[2, :] / 2, -M[1, :], M[0, :] / 2]) - _w, v = np.linalg.eig(M) + _w, v = xp_eig(M, xp=xp) + v = xp.real(v) - A1 = v[:, np.nonzero(4 * v[0, :] * v[2, :] - v[1, :] ** 2 > 0)[0]] - A2 = np.dot(T, A1) + A1 = v[:, xp.nonzero(4 * v[0, :] * v[2, :] - v[1, :] ** 2 > 0)[0]] + A2 = xp.matmul(T, A1) - return cast("NDArrayFloat", np.ravel([A1, A2])) + return xp_reshape(xp.concat([A1, A2], axis=0), (-1,), xp=xp) ELLIPSE_FITTING_METHODS: CanonicalMapping = CanonicalMapping( @@ -335,6 +354,7 @@ def ellipse_fitting( Examples -------- + >>> import numpy as np >>> a = np.array([[2, 0], [0, 1], [-2, 0], [0, -1]]) >>> ellipse_fitting(a) # doctest: +ELLIPSIS array([ 0.2425356..., 0. , 0.9701425..., 0. , 0. , diff --git a/colour/geometry/intersection.py b/colour/geometry/intersection.py index 2c0079f8ff..5820276349 100644 --- a/colour/geometry/intersection.py +++ b/colour/geometry/intersection.py @@ -25,14 +25,19 @@ import typing from dataclasses import dataclass -import numpy as np - from colour.algebra import euclidean_distance, sdiv, sdiv_mode if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, NDArrayFloat + from colour.hints import ArrayLike, NDArrayBoolean, NDArrayFloat -from colour.utilities import as_float_array, tsplit, tstack +from colour.utilities import ( + array_namespace, + as_float_array, + tsplit, + tstack, + xp_as_float_array, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -80,6 +85,7 @@ def extend_line_segment( Examples -------- + >>> import numpy as np >>> a = np.array([0.95694934, 0.13720932]) >>> b = np.array([0.28382835, 0.60608318]) >>> extend_line_segment(a, b) # doctest: +ELLIPSIS @@ -123,9 +129,9 @@ class LineSegmentsIntersections_Specification: """ xy: NDArrayFloat - intersect: NDArrayFloat - parallel: NDArrayFloat - coincident: NDArrayFloat + intersect: NDArrayBoolean + parallel: NDArrayBoolean + coincident: NDArrayBoolean def intersect_line_segments( @@ -163,6 +169,7 @@ def intersect_line_segments( Examples -------- + >>> import numpy as np >>> l_1 = np.array( ... [ ... [[0.15416284, 0.7400497], [0.26331502, 0.53373939]], @@ -199,17 +206,23 @@ def intersect_line_segments( l_1 = as_float_array(l_1) l_2 = as_float_array(l_2) - l_1 = np.reshape(l_1, (-1, 4)) - l_2 = np.reshape(l_2, (-1, 4)) - - r_1, c_1 = l_1.shape[0], l_1.shape[1] - r_2, c_2 = l_2.shape[0], l_2.shape[1] + xp = array_namespace(l_1, l_2) - x_1, y_1, x_2, y_2 = (np.tile(l_1[:, i, None], (1, r_2)) for i in range(c_1)) + l_2 = xp_as_float_array(l_2, xp=xp, like=l_1) - l_2 = np.transpose(l_2) + l_1 = xp_reshape(l_1, (-1, 4), xp=xp) + l_2 = xp_reshape(l_2, (-1, 4), xp=xp) - x_3, y_3, x_4, y_4 = (np.tile(l_2[i, :], (r_1, 1)) for i in range(c_2)) + # ``l_1`` segments held as ``(r_1, 1)`` columns and ``l_2`` segments as + # ``(1, r_2)`` rows; pairwise arithmetic broadcasts to ``(r_1, r_2)`` + # without materialising tiled copies of each component. + x_1, y_1, x_2, y_2 = l_1[:, 0:1], l_1[:, 1:2], l_1[:, 2:3], l_1[:, 3:4] + x_3, y_3, x_4, y_4 = ( + l_2[None, :, 0], + l_2[None, :, 1], + l_2[None, :, 2], + l_2[None, :, 3], + ) x_4_x_3 = x_4 - x_3 y_1_y_3 = y_1 - y_3 @@ -226,11 +239,11 @@ def intersect_line_segments( u_a = sdiv(numerator_a, denominator) u_b = sdiv(numerator_b, denominator) - intersect = np.logical_and.reduce((u_a >= 0, u_a <= 1, u_b >= 0, u_b <= 1)) + intersect = (u_a >= 0) & (u_a <= 1) & (u_b >= 0) & (u_b <= 1) xy = tstack([x_1 + x_2_x_1 * u_a, y_1 + y_2_y_1 * u_a]) - xy = np.where(intersect[..., None], xy, np.nan) + xy = xp.where(intersect[..., None], xy, float("nan")) parallel = denominator == 0 - coincident = np.logical_and.reduce((numerator_a == 0, numerator_b == 0, parallel)) + coincident = (numerator_a == 0) & (numerator_b == 0) & parallel return LineSegmentsIntersections_Specification(xy, intersect, parallel, coincident) @@ -266,12 +279,15 @@ def intersect_ray_circle_2d( Examples -------- >>> intersect_ray_circle_2d([0, 5], [0, 1], 10) - 5.0 + array(5.) >>> intersect_ray_circle_2d([0, 15], [0, 1], 10) - nan + array(nan) """ origin = as_float_array(ray_origin) + + xp = array_namespace(origin) + direction = as_float_array(ray_direction) direction_x = direction[..., 0] @@ -287,18 +303,16 @@ def intersect_ray_circle_2d( discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c has_intersection = discriminant > 0.0 - safe_discriminant = np.sqrt(np.maximum(discriminant, 0.0)) + safe_discriminant = xp.sqrt(xp.clip(discriminant, min=0.0)) distance_1 = (-quadratic_b + safe_discriminant) / (2.0 * quadratic_a) distance_2 = (-quadratic_b - safe_discriminant) / (2.0 * quadratic_a) both_positive = (distance_1 > 0) & (distance_2 > 0) - result = np.where( + result = xp.where( both_positive, - np.minimum(distance_1, distance_2), - np.maximum(distance_1, distance_2), + xp.minimum(distance_1, distance_2), + xp.maximum(distance_1, distance_2), ) forward = result > 0 - result = np.where(has_intersection & forward, result, np.nan) - - return float(result) if np.ndim(result) == 0 else result # pyright: ignore + return xp.where(has_intersection & forward, result, float("nan")) diff --git a/colour/geometry/primitives.py b/colour/geometry/primitives.py index e8426dfb12..7dcb88bd3b 100644 --- a/colour/geometry/primitives.py +++ b/colour/geometry/primitives.py @@ -22,8 +22,6 @@ import typing -import numpy as np - from colour.constants import DTYPE_FLOAT_DEFAULT, DTYPE_INT_DEFAULT if typing.TYPE_CHECKING: @@ -40,11 +38,14 @@ from colour.hints import NDArrayFloat, cast from colour.utilities import ( CanonicalMapping, + array_namespace, as_int_array, filter_kwargs, ones, optional, validate_method, + xp_interp, + xp_reshape, zeros, ) @@ -164,16 +165,18 @@ def primitive_grid( normals = zeros(x_grid1 * y_grid1 * 3) uvs = zeros(x_grid1 * y_grid1 * 2) - y = np.arange(y_grid1) * height / y_grid - height / 2 - x = np.arange(x_grid1) * width / x_grid - width / 2 + xp = array_namespace(positions) + + y = xp.arange(y_grid1) * height / y_grid - height / 2 + x = xp.arange(x_grid1) * width / x_grid - width / 2 - positions[::3] = np.tile(x, y_grid1) - positions[1::3] = -np.repeat(y, x_grid1) + positions[::3] = xp.tile(x, y_grid1) + positions[1::3] = -xp.repeat(y, x_grid1) normals[2::3] = 1 - uvs[::2] = np.tile(np.arange(x_grid1) / x_grid, y_grid1) - uvs[1::2] = np.repeat(1 - np.arange(y_grid1) / y_grid, x_grid1) + uvs[::2] = xp.tile(xp.arange(x_grid1) / x_grid, y_grid1) + uvs[1::2] = xp.repeat(1 - xp.arange(y_grid1) / y_grid, x_grid1) # Faces and outline. faces_indexes = [] @@ -188,12 +191,12 @@ def primitive_grid( faces_indexes.extend([(a, b, d), (b, c, d)]) outline_indexes.extend([(a, b), (b, c), (c, d), (d, a)]) - faces = np.reshape(as_int_array(faces_indexes, dtype_indexes), (-1, 3)) - outline = np.reshape(as_int_array(outline_indexes, dtype_indexes), (-1, 2)) + faces = xp_reshape(as_int_array(faces_indexes, dtype_indexes), (-1, 3), xp=xp) + outline = xp_reshape(as_int_array(outline_indexes, dtype_indexes), (-1, 2), xp=xp) - positions = np.reshape(positions, (-1, 3)) - uvs = np.reshape(uvs, (-1, 2)) - normals = np.reshape(normals, (-1, 3)) + positions = xp_reshape(positions, (-1, 3), xp=xp) + uvs = xp_reshape(uvs, (-1, 2), xp=xp) + normals = xp_reshape(normals, (-1, 3), xp=xp) if axis in ("-x", "+x"): shift, zero_axis = 1, 0 @@ -204,21 +207,25 @@ def primitive_grid( sign = -1 if "-" in axis else 1 - positions = np.roll(positions, shift, -1) - normals = cast("NDArrayFloat", np.roll(normals, shift, -1)) * sign - vertex_colours = np.ravel(positions) - vertex_colours = np.hstack( + positions = xp.roll(positions, shift, axis=-1) + normals = cast("NDArrayFloat", xp.roll(normals, shift, axis=-1)) * sign + + vertex_colours = xp_reshape(positions, (-1,), xp=xp) + vertex_colours = xp.concat( [ - np.reshape( - np.interp( - cast("NDArrayFloat", vertex_colours), - (np.min(vertex_colours), np.max(vertex_colours)), + xp_reshape( + xp_interp( + vertex_colours, + (xp.min(vertex_colours), xp.max(vertex_colours)), (0, 1), + xp=xp, ), positions.shape, + xp=xp, ), ones((positions.shape[0], 1)), - ] + ], + axis=1, ) vertex_colours[..., zero_axis] = 0 @@ -380,11 +387,15 @@ def primitive_cube( w_s, h_s, d_s = width_segments, height_segments, depth_segments + positions = zeros((0, 3)) + + xp = array_namespace(positions) + planes_p = [] if "-z" in axis: planes_p.append(list(primitive_grid(width, depth, w_s, d_s, "-z"))) planes_p[-1][0]["position"][..., 2] -= height / 2 - planes_p[-1][1] = np.fliplr(planes_p[-1][1]) + planes_p[-1][1] = xp.flip(planes_p[-1][1], axis=1) if "+z" in axis: planes_p.append(list(primitive_grid(width, depth, w_s, d_s, "+z"))) planes_p[-1][0]["position"][..., 2] += height / 2 @@ -392,7 +403,7 @@ def primitive_cube( if "-y" in axis: planes_p.append(list(primitive_grid(height, width, h_s, w_s, "-y"))) planes_p[-1][0]["position"][..., 1] -= depth / 2 - planes_p[-1][1] = np.fliplr(planes_p[-1][1]) + planes_p[-1][1] = xp.flip(planes_p[-1][1], axis=1) if "+y" in axis: planes_p.append(list(primitive_grid(height, width, h_s, w_s, "+y"))) planes_p[-1][0]["position"][..., 1] += depth / 2 @@ -400,12 +411,10 @@ def primitive_cube( if "-x" in axis: planes_p.append(list(primitive_grid(depth, height, d_s, h_s, "-x"))) planes_p[-1][0]["position"][..., 0] -= width / 2 - planes_p[-1][1] = np.fliplr(planes_p[-1][1]) + planes_p[-1][1] = xp.flip(planes_p[-1][1], axis=1) if "+x" in axis: planes_p.append(list(primitive_grid(depth, height, d_s, h_s, "+x"))) planes_p[-1][0]["position"][..., 0] += width / 2 - - positions = zeros((0, 3)) uvs = zeros((0, 2)) normals = zeros((0, 3)) @@ -414,12 +423,12 @@ def primitive_cube( offset = 0 for vertices_p, faces_p, outline_p in planes_p: - positions = np.vstack([positions, vertices_p["position"]]) - uvs = np.vstack([uvs, vertices_p["uv"]]) - normals = np.vstack([normals, vertices_p["normal"]]) + positions = xp.concat([positions, vertices_p["position"]], axis=0) + uvs = xp.concat([uvs, vertices_p["uv"]], axis=0) + normals = xp.concat([normals, vertices_p["normal"]], axis=0) - faces = np.vstack([faces, faces_p + offset]) - outline = np.vstack([outline, outline_p + offset]) + faces = xp.concat([faces, faces_p + offset], axis=0) + outline = xp.concat([outline, outline_p + offset], axis=0) offset += vertices_p["position"].shape[0] vertices = zeros( @@ -432,19 +441,22 @@ def primitive_cube( ], # pyright: ignore ) - vertex_colours = np.ravel(positions) - vertex_colours = np.hstack( + vertex_colours = xp_reshape(positions, (-1,), xp=xp) + vertex_colours = xp.concat( [ - np.reshape( - np.interp( - cast("NDArrayFloat", vertex_colours), - (np.min(vertex_colours), np.max(vertex_colours)), + xp_reshape( + xp_interp( + vertex_colours, + (xp.min(vertex_colours), xp.max(vertex_colours)), (0, 1), + xp=xp, ), positions.shape, + xp=xp, ), ones((positions.shape[0], 1)), - ] + ], + axis=1, ) vertices["position"] = positions diff --git a/colour/geometry/section.py b/colour/geometry/section.py index 1df40d9e17..a91c767ab8 100644 --- a/colour/geometry/section.py +++ b/colour/geometry/section.py @@ -27,7 +27,17 @@ from colour.hints import ArrayLike, Literal, NDArrayFloat from colour.hints import List, cast -from colour.utilities import as_float_array, as_float_scalar, required, validate_method +from colour.utilities import ( + array_namespace, + as_float_array, + as_float_scalar, + required, + validate_method, + xp_as_float_array, + xp_reshape, + xp_round, + xp_unique, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -97,25 +107,33 @@ def edges_to_chord(edges: ArrayLike, index: int = 0) -> NDArrayFloat: [-0. , -0.5, 0. ]]) """ - edge_list = cast("List[List[float]]", as_float_array(edges).tolist()) + edges = as_float_array(edges) + + xp = array_namespace(edges) + + edge_list = cast("List[List[float]]", edges.tolist()) edges_ordered = [edge_list.pop(index)] - segment = np.array(edges_ordered[0][1]) + segment = xp_as_float_array(edges_ordered[0][1], xp=xp) while len(edge_list) > 0: - edges_array = np.array(edge_list) - d_0 = np.linalg.norm(edges_array[:, 0, :] - segment, axis=1) - d_1 = np.linalg.norm(edges_array[:, 1, :] - segment, axis=1) - d_0_argmin, d_1_argmin = d_0.argmin(), d_1.argmin() + edges = xp_as_float_array(edge_list, xp=xp) + d_0 = xp.linalg.vector_norm(edges[:, 0, :] - segment, axis=1) + d_1 = xp.linalg.vector_norm(edges[:, 1, :] - segment, axis=1) + d_0_argmin, d_1_argmin = int(xp.argmin(d_0)), int(xp.argmin(d_1)) if d_0[d_0_argmin] < d_1[d_1_argmin]: edges_ordered.append(edge_list.pop(d_0_argmin)) - segment = np.array(edges_ordered[-1][1]) + segment = xp_as_float_array(edges_ordered[-1][1], xp=xp) else: edges_ordered.append(edge_list.pop(d_1_argmin)) - segment = np.array(edges_ordered[-1][0]) + segment = xp_as_float_array(edges_ordered[-1][0], xp=xp) - return np.reshape(as_float_array(edges_ordered), (-1, segment.shape[-1])) + return xp_reshape( + xp_as_float_array(edges_ordered, xp=xp, like=segment), + (-1, segment.shape[-1]), + xp=xp, + ) def close_chord(vertices: ArrayLike) -> NDArrayFloat: @@ -143,7 +161,9 @@ def close_chord(vertices: ArrayLike) -> NDArrayFloat: vertices = as_float_array(vertices) - return np.vstack([vertices, vertices[0]]) + xp = array_namespace(vertices) + + return xp.concat([vertices, vertices[0:1]], axis=0) def unique_vertices( @@ -181,11 +201,13 @@ def unique_vertices( vertices = as_float_array(vertices) - unique, indexes = np.unique( - vertices.round(decimals=decimals), axis=0, return_index=True + xp = array_namespace(vertices) + + unique, indexes = xp_unique( + xp_round(vertices, decimals=decimals, xp=xp), axis=0, return_index=True, xp=xp ) - return unique[np.argsort(indexes)] + return unique[xp.argsort(indexes)] @required("trimesh") @@ -262,18 +284,20 @@ def hull_section( ) if axis == "+x": - normal, plane = np.array([1, 0, 0]), np.array([origin, 0, 0]) + normal, plane = as_float_array([1, 0, 0]), as_float_array([origin, 0, 0]) elif axis == "+y": - normal, plane = np.array([0, 1, 0]), np.array([0, origin, 0]) + normal, plane = as_float_array([0, 1, 0]), as_float_array([0, origin, 0]) elif axis == "+z": - normal, plane = np.array([0, 0, 1]), np.array([0, 0, origin]) + normal, plane = as_float_array([0, 0, 1]), as_float_array([0, 0, origin]) + + xp = array_namespace(normal) if normalise: vertices = hull.vertices * normal origin = as_float_scalar( - linear_conversion(origin, [0, 1], [np.min(vertices), np.max(vertices)]) + linear_conversion(origin, [0, 1], [xp.min(vertices), xp.max(vertices)]) ) - plane = np.where(plane != 0, origin, plane) + plane = xp.where(plane != 0, origin, plane) section = trimesh.intersections.mesh_plane(hull, normal, plane) if len(section) == 0: diff --git a/colour/geometry/tests/test__init__.py b/colour/geometry/tests/test__init__.py index ae1aa809b8..27c43b2c5a 100644 --- a/colour/geometry/tests/test__init__.py +++ b/colour/geometry/tests/test__init__.py @@ -2,8 +2,6 @@ from __future__ import annotations -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.geometry import ( primitive, @@ -15,6 +13,7 @@ primitive_vertices_quad_mpl, primitive_vertices_sphere, ) +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -43,14 +42,14 @@ def test_primitive(self) -> None: grid_expected = primitive_grid() assert len(grid_result) == len(grid_expected) for i in range(len(grid_result)): - np.testing.assert_array_equal(grid_result[i], grid_expected[i]) + xp_assert_equal(grid_result[i], grid_expected[i]) # Test Cube method cube_result = primitive("Cube") cube_expected = primitive_cube() assert len(cube_result) == len(cube_expected) for i in range(len(cube_result)): - np.testing.assert_array_equal(cube_result[i], cube_expected[i]) + xp_assert_equal(cube_result[i], cube_expected[i]) class TestPrimitiveVertices: @@ -62,25 +61,25 @@ class TestPrimitiveVertices: def test_primitive_vertices(self) -> None: """Test :func:`colour.geometry.primitive_vertices` definition.""" - np.testing.assert_allclose( + xp_assert_close( primitive_vertices("Quad MPL"), primitive_vertices_quad_mpl(), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices("Grid MPL"), primitive_vertices_grid_mpl(), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices("Cube MPL"), primitive_vertices_cube_mpl(), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices("Sphere"), primitive_vertices_sphere(), atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/geometry/tests/test_ellipse.py b/colour/geometry/tests/test_ellipse.py index 1aa5b76868..707b8603ba 100644 --- a/colour/geometry/tests/test_ellipse.py +++ b/colour/geometry/tests/test_ellipse.py @@ -2,7 +2,11 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.geometry import ( @@ -11,6 +15,7 @@ ellipse_fitting_Halir1998, point_at_angle_on_ellipse, ) +from colour.utilities import xp_as_array, xp_assert_close, xp_linspace __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -33,25 +38,25 @@ class TestEllipseCoefficientsCanonicalForm: definition unit tests methods. """ - def test_ellipse_coefficients_canonical_form(self) -> None: + def test_ellipse_coefficients_canonical_form(self, xp: ModuleType) -> None: """ Test :func:`colour.geometry.ellipse.\ ellipse_coefficients_canonical_form` definition. """ - np.testing.assert_allclose( + xp_assert_close( ellipse_coefficients_canonical_form( - np.array([2.5, -3.0, 2.5, -1.0, -1.0, -3.5]) + xp_as_array([2.5, -3.0, 2.5, -1.0, -1.0, -3.5], xp=xp) ), - np.array([0.5, 0.5, 2, 1, 45]), + [0.5, 0.5, 2, 1, 45], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ellipse_coefficients_canonical_form( - np.array([1.0, 0.0, 1.0, 0.0, 0.0, -1.0]) + xp_as_array([1.0, 0.0, 1.0, 0.0, 0.0, -1.0], xp=xp) ), - np.array([0.0, 0.0, 1, 1, 0]), + [0.0, 0.0, 1, 1, 0], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -62,21 +67,21 @@ class TestEllipseCoefficientsGeneralForm: definition unit tests methods. """ - def test_ellipse_coefficients_general_form(self) -> None: + def test_ellipse_coefficients_general_form(self, xp: ModuleType) -> None: """ Test :func:`colour.geometry.ellipse.ellipse_coefficients_general_form` definition. """ - np.testing.assert_allclose( - ellipse_coefficients_general_form(np.array([0.5, 0.5, 2, 1, 45])), - np.array([2.5, -3.0, 2.5, -1.0, -1.0, -3.5]), + xp_assert_close( + ellipse_coefficients_general_form(xp_as_array([0.5, 0.5, 2, 1, 45], xp=xp)), + [2.5, -3.0, 2.5, -1.0, -1.0, -3.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ellipse_coefficients_general_form(np.array([0.0, 0.0, 1, 1, 0])), - np.array([1.0, 0.0, 1.0, 0.0, 0.0, -1.0]), + xp_assert_close( + ellipse_coefficients_general_form(xp_as_array([0.0, 0.0, 1, 1, 0], xp=xp)), + [1.0, 0.0, 1.0, 0.0, 0.0, -1.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -87,38 +92,38 @@ class TestPointAtAngleOnEllipse: definition unit tests methods. """ - def test_point_at_angle_on_ellipse(self) -> None: + def test_point_at_angle_on_ellipse(self, xp: ModuleType) -> None: """ Test :func:`colour.geometry.ellipse.point_at_angle_on_ellipse` definition. """ - np.testing.assert_allclose( + xp_assert_close( point_at_angle_on_ellipse( - np.array([0, 90, 180, 270]), np.array([0.0, 0.0, 2, 1, 0]) + xp_as_array([0, 90, 180, 270], xp=xp), + xp_as_array([0.0, 0.0, 2, 1, 0], xp=xp), ), - np.array([[2, 0], [0, 1], [-2, 0], [0, -1]]), + [[2, 0], [0, 1], [-2, 0], [0, -1]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( point_at_angle_on_ellipse( - np.linspace(0, 360, 10), np.array([0.5, 0.5, 2, 1, 45]) - ), - np.array( - [ - [1.91421356, 1.91421356], - [1.12883096, 2.03786992], - [0.04921137, 1.44193985], - [-0.81947922, 0.40526565], - [-1.07077081, -0.58708129], - [-0.58708129, -1.07077081], - [0.40526565, -0.81947922], - [1.44193985, 0.04921137], - [2.03786992, 1.12883096], - [1.91421356, 1.91421356], - ] + xp_linspace(0, 360, num=10, xp=xp), # pyright: ignore + xp_as_array([0.5, 0.5, 2, 1, 45], xp=xp), ), + [ + [1.91421356, 1.91421356], + [1.12883096, 2.03786992], + [0.04921137, 1.44193985], + [-0.81947922, 0.40526565], + [-1.07077081, -0.58708129], + [-0.58708129, -1.07077081], + [0.40526565, -0.81947922], + [1.44193985, 0.04921137], + [2.03786992, 1.12883096], + [1.91421356, 1.91421356], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -129,23 +134,23 @@ class TestEllipseFittingHalir1998: definition unit tests methods. """ - def test_ellipse_fitting_Halir1998(self) -> None: + def test_ellipse_fitting_Halir1998(self, xp: ModuleType) -> None: """ Test :func:`colour.geometry.ellipse.ellipse_fitting_Halir1998` definition. """ - np.testing.assert_allclose( - ellipse_fitting_Halir1998(np.array([[2, 0], [0, 1], [-2, 0], [0, -1]])), - np.array( - [ - 0.24253563, - 0.00000000, - 0.97014250, - 0.00000000, - 0.00000000, - -0.97014250, - ] + xp_assert_close( + ellipse_fitting_Halir1998( + xp_as_array([[2, 0], [0, 1], [-2, 0], [0, -1]], xp=xp) ), + [ + 0.24253563, + 0.00000000, + 0.97014250, + 0.00000000, + 0.00000000, + -0.97014250, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/geometry/tests/test_intersection.py b/colour/geometry/tests/test_intersection.py index c1ca51ec74..f791928d43 100644 --- a/colour/geometry/tests/test_intersection.py +++ b/colour/geometry/tests/test_intersection.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,6 +15,7 @@ intersect_line_segments, intersect_ray_circle_2d, ) +from colour.utilities import as_ndarray, xp_as_array, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,35 +37,35 @@ class TestExtendLineSegment: tests methods. """ - def test_extend_line_segment(self) -> None: + def test_extend_line_segment(self, xp: ModuleType) -> None: """Test :func:`colour.geometry.intersection.extend_line_segment` definition.""" - np.testing.assert_allclose( + xp_assert_close( extend_line_segment( - np.array([0.95694934, 0.13720932]), - np.array([0.28382835, 0.60608318]), + xp_as_array([0.95694934, 0.13720932], xp=xp), + xp_as_array([0.28382835, 0.60608318], xp=xp), ), - np.array([-0.5367248, 1.17765341]), + [-0.5367248, 1.17765341], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( extend_line_segment( - np.array([0.95694934, 0.13720932]), - np.array([0.28382835, 0.60608318]), + xp_as_array([0.95694934, 0.13720932], xp=xp), + xp_as_array([0.28382835, 0.60608318], xp=xp), 5, ), - np.array([-3.81893739, 3.46393435]), + [-3.81893739, 3.46393435], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( extend_line_segment( - np.array([0.95694934, 0.13720932]), - np.array([0.28382835, 0.60608318]), + xp_as_array([0.95694934, 0.13720932], xp=xp), + xp_as_array([0.28382835, 0.60608318], xp=xp), -1, ), - np.array([1.1043815, 0.03451295]), + [1.1043815, 0.03451295], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -70,63 +76,63 @@ class TestIntersectLineSegments: definition unit tests methods. """ - def test_intersect_line_segments(self) -> None: + def test_intersect_line_segments(self, xp: ModuleType) -> None: """ Test :func:`colour.geometry.intersection.intersect_line_segments` definition. """ - l_1 = np.array( + l_1 = xp_as_array( [ [[0.15416284, 0.7400497], [0.26331502, 0.53373939]], [[0.01457496, 0.91874701], [0.90071485, 0.03342143]], - ] + ], + xp=xp, ) - l_2 = np.array( + l_2 = xp_as_array( [ [[0.95694934, 0.13720932], [0.28382835, 0.60608318]], [[0.94422514, 0.85273554], [0.00225923, 0.52122603]], [[0.55203763, 0.48537741], [0.76813415, 0.16071675]], [[0.01457496, 0.91874701], [0.90071485, 0.03342143]], - ] + ], + xp=xp, ) s = intersect_line_segments(l_1, l_2) - np.testing.assert_allclose( + xp_assert_close( s.xy, - np.array( + [ [ - [ - [np.nan, np.nan], - [0.22791841, 0.60064309], - [np.nan, np.nan], - [np.nan, np.nan], - ], - [ - [0.42814517, 0.50555685], - [0.30560559, 0.62798382], - [0.7578749, 0.17613012], - [np.nan, np.nan], - ], - ] - ), + [np.nan, np.nan], + [0.22791841, 0.60064309], + [np.nan, np.nan], + [np.nan, np.nan], + ], + [ + [0.42814517, 0.50555685], + [0.30560559, 0.62798382], + [0.7578749, 0.17613012], + [np.nan, np.nan], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal( + xp_assert_equal( s.intersect, - np.array([[False, True, False, False], [True, True, True, False]]), + [[False, True, False, False], [True, True, True, False]], ) - np.testing.assert_array_equal( + xp_assert_equal( s.parallel, - np.array([[False, False, False, False], [False, False, False, True]]), + [[False, False, False, False], [False, False, False, True]], ) - np.testing.assert_array_equal( + xp_assert_equal( s.coincident, - np.array([[False, False, False, False], [False, False, False, True]]), + [[False, False, False, False], [False, False, False, True]], ) @@ -136,32 +142,80 @@ class TestIntersectRayCircle2D: definition unit tests methods. """ - def test_intersect_ray_circle_2d(self) -> None: + def test_intersect_ray_circle_2d(self, xp: ModuleType) -> None: """ Test :func:`colour.geometry.intersection.\ intersect_ray_circle_2d` definition. """ # Ray pointing up from inside a circle. - distance = intersect_ray_circle_2d([0, 5], [0, 1], 10.0) - np.testing.assert_allclose(distance, 5.0, atol=1e-10) + distance = float( + as_ndarray( + intersect_ray_circle_2d( + xp_as_array([0.0, 5.0], xp=xp), + xp_as_array([0.0, 1.0], xp=xp), + 10.0, + ) + ) + ) + xp_assert_close(distance, 5.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) # Ray pointing down from inside, should hit far side. - distance = intersect_ray_circle_2d([0, 5], [0, -1], 10.0) - np.testing.assert_allclose(distance, 15.0, atol=1e-10) + distance = float( + as_ndarray( + intersect_ray_circle_2d( + xp_as_array([0.0, 5.0], xp=xp), + xp_as_array([0.0, -1.0], xp=xp), + 10.0, + ) + ) + ) + xp_assert_close(distance, 15.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) # No forward intersection (outside, pointing away). - distance = intersect_ray_circle_2d([0, 15], [0, 1], 10.0) + distance = float( + as_ndarray( + intersect_ray_circle_2d( + xp_as_array([0.0, 15.0], xp=xp), + xp_as_array([0.0, 1.0], xp=xp), + 10.0, + ) + ) + ) assert np.isnan(distance) # Horizontal ray from offset origin (3,0) -> hits circle r=5 at x=5. - distance = intersect_ray_circle_2d([3, 0], [1, 0], 5.0) - np.testing.assert_allclose(distance, 2.0, atol=1e-10) + distance = float( + as_ndarray( + intersect_ray_circle_2d( + xp_as_array([3.0, 0.0], xp=xp), + xp_as_array([1.0, 0.0], xp=xp), + 5.0, + ) + ) + ) + xp_assert_close(distance, 2.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) # Tangent (touch only): no forward intersection. - distance = intersect_ray_circle_2d([0, 10], [1, 0], 10.0) + distance = float( + as_ndarray( + intersect_ray_circle_2d( + xp_as_array([0.0, 10.0], xp=xp), + xp_as_array([1.0, 0.0], xp=xp), + 10.0, + ) + ) + ) assert np.isnan(distance) # Ray from origin pointing outward. - distance = intersect_ray_circle_2d([0, 0], [1, 0], 5.0) - np.testing.assert_allclose(distance, 5.0, atol=1e-10) + distance = float( + as_ndarray( + intersect_ray_circle_2d( + xp_as_array([0.0, 0.0], xp=xp), + xp_as_array([1.0, 0.0], xp=xp), + 5.0, + ) + ) + ) + xp_assert_close(distance, 5.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) diff --git a/colour/geometry/tests/test_primitives.py b/colour/geometry/tests/test_primitives.py index 7498036702..f175a72e23 100644 --- a/colour/geometry/tests/test_primitives.py +++ b/colour/geometry/tests/test_primitives.py @@ -10,6 +10,7 @@ primitive_cube, primitive_grid, ) +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -37,47 +38,43 @@ def test_primitive_grid(self) -> None: """ vertices, faces, outline = primitive_grid() - np.testing.assert_allclose( + xp_assert_close( vertices["position"], - np.array( - [ - [-0.5, 0.5, 0.0], - [0.5, 0.5, 0.0], - [-0.5, -0.5, 0.0], - [0.5, -0.5, 0.0], - ] - ), + [ + [-0.5, 0.5, 0.0], + [0.5, 0.5, 0.0], + [-0.5, -0.5, 0.0], + [0.5, -0.5, 0.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["uv"], - np.array([[0, 1], [1, 1], [0, 0], [1, 0]]), + [[0, 1], [1, 1], [0, 0], [1, 0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["normal"], - np.array([[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]]), + [[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["colour"], - np.array( - [ - [0, 1, 0, 1], - [1, 1, 0, 1], - [0, 0, 0, 1], - [1, 0, 0, 1], - ] - ), + [ + [0, 1, 0, 1], + [1, 1, 0, 1], + [0, 0, 0, 1], + [1, 0, 0, 1], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_equal(faces, np.array([[0, 2, 1], [2, 3, 1]])) + xp_assert_equal(faces, np.array([[0, 2, 1], [2, 3, 1]])) - np.testing.assert_equal(outline, np.array([[0, 2], [2, 3], [3, 1], [1, 0]])) + xp_assert_equal(outline, np.array([[0, 2], [2, 3], [3, 1], [1, 0]])) vertices, faces, outline = primitive_grid( width=0.2, @@ -87,67 +84,59 @@ def test_primitive_grid(self) -> None: axis="+z", ) - np.testing.assert_allclose( + xp_assert_close( vertices["position"], - np.array( - [ - [-0.10000000, 0.20000000, 0.00000000], - [0.10000000, 0.20000000, 0.00000000], - [-0.10000000, -0.00000000, 0.00000000], - [0.10000000, -0.00000000, 0.00000000], - [-0.10000000, -0.20000000, 0.00000000], - [0.10000000, -0.20000000, 0.00000000], - ] - ), + [ + [-0.10000000, 0.20000000, 0.00000000], + [0.10000000, 0.20000000, 0.00000000], + [-0.10000000, -0.00000000, 0.00000000], + [0.10000000, -0.00000000, 0.00000000], + [-0.10000000, -0.20000000, 0.00000000], + [0.10000000, -0.20000000, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["uv"], - np.array( - [ - [0.00000000, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.50000000], - [1.00000000, 0.50000000], - [0.00000000, 0.00000000], - [1.00000000, 0.00000000], - ] - ), + [ + [0.00000000, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.50000000], + [1.00000000, 0.50000000], + [0.00000000, 0.00000000], + [1.00000000, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["normal"], - np.array( - [ - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - ] - ), + [ + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["colour"], - np.array( - [ - [0.25000000, 1.00000000, 0.00000000, 1.00000000], - [0.75000000, 1.00000000, 0.00000000, 1.00000000], - [0.25000000, 0.50000000, 0.00000000, 1.00000000], - [0.75000000, 0.50000000, 0.00000000, 1.00000000], - [0.25000000, 0.00000000, 0.00000000, 1.00000000], - [0.75000000, 0.00000000, 0.00000000, 1.00000000], - ] - ), + [ + [0.25000000, 1.00000000, 0.00000000, 1.00000000], + [0.75000000, 1.00000000, 0.00000000, 1.00000000], + [0.25000000, 0.50000000, 0.00000000, 1.00000000], + [0.75000000, 0.50000000, 0.00000000, 1.00000000], + [0.25000000, 0.00000000, 0.00000000, 1.00000000], + [0.75000000, 0.00000000, 0.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_equal( + xp_assert_equal( faces, np.array( [ @@ -159,7 +148,7 @@ def test_primitive_grid(self) -> None: ), ) - np.testing.assert_equal( + xp_assert_equal( outline, np.array( [ @@ -176,7 +165,7 @@ def test_primitive_grid(self) -> None: ) for plane in MAPPING_PLANE_TO_AXIS: - np.testing.assert_allclose( + xp_assert_close( primitive_grid(axis=plane)[0]["position"], primitive_grid(axis=MAPPING_PLANE_TO_AXIS[plane])[0]["position"], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -196,139 +185,131 @@ def test_primitive_cube(self) -> None: """ vertices, faces, outline = primitive_cube() - np.testing.assert_allclose( + xp_assert_close( vertices["position"], - np.array( - [ - [-0.5, 0.5, -0.5], - [0.5, 0.5, -0.5], - [-0.5, -0.5, -0.5], - [0.5, -0.5, -0.5], - [-0.5, 0.5, 0.5], - [0.5, 0.5, 0.5], - [-0.5, -0.5, 0.5], - [0.5, -0.5, 0.5], - [0.5, -0.5, -0.5], - [0.5, -0.5, 0.5], - [-0.5, -0.5, -0.5], - [-0.5, -0.5, 0.5], - [0.5, 0.5, -0.5], - [0.5, 0.5, 0.5], - [-0.5, 0.5, -0.5], - [-0.5, 0.5, 0.5], - [-0.5, -0.5, 0.5], - [-0.5, 0.5, 0.5], - [-0.5, -0.5, -0.5], - [-0.5, 0.5, -0.5], - [0.5, -0.5, 0.5], - [0.5, 0.5, 0.5], - [0.5, -0.5, -0.5], - [0.5, 0.5, -0.5], - ] - ), + [ + [-0.5, 0.5, -0.5], + [0.5, 0.5, -0.5], + [-0.5, -0.5, -0.5], + [0.5, -0.5, -0.5], + [-0.5, 0.5, 0.5], + [0.5, 0.5, 0.5], + [-0.5, -0.5, 0.5], + [0.5, -0.5, 0.5], + [0.5, -0.5, -0.5], + [0.5, -0.5, 0.5], + [-0.5, -0.5, -0.5], + [-0.5, -0.5, 0.5], + [0.5, 0.5, -0.5], + [0.5, 0.5, 0.5], + [-0.5, 0.5, -0.5], + [-0.5, 0.5, 0.5], + [-0.5, -0.5, 0.5], + [-0.5, 0.5, 0.5], + [-0.5, -0.5, -0.5], + [-0.5, 0.5, -0.5], + [0.5, -0.5, 0.5], + [0.5, 0.5, 0.5], + [0.5, -0.5, -0.5], + [0.5, 0.5, -0.5], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["uv"], - np.array( - [ - [0, 1], - [1, 1], - [0, 0], - [1, 0], - [0, 1], - [1, 1], - [0, 0], - [1, 0], - [0, 1], - [1, 1], - [0, 0], - [1, 0], - [0, 1], - [1, 1], - [0, 0], - [1, 0], - [0, 1], - [1, 1], - [0, 0], - [1, 0], - [0, 1], - [1, 1], - [0, 0], - [1, 0], - ] - ), + [ + [0, 1], + [1, 1], + [0, 0], + [1, 0], + [0, 1], + [1, 1], + [0, 0], + [1, 0], + [0, 1], + [1, 1], + [0, 0], + [1, 0], + [0, 1], + [1, 1], + [0, 0], + [1, 0], + [0, 1], + [1, 1], + [0, 0], + [1, 0], + [0, 1], + [1, 1], + [0, 0], + [1, 0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["normal"], - np.array( - [ - [0, 0, -1.0], - [0, 0, -1], - [0, 0, -1], - [0, 0, -1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, -1, 0], - [0, -1, 0], - [0, -1, 0], - [0, -1, 0], - [0, 1, 0], - [0, 1, 0], - [0, 1, 0], - [0, 1, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - ] - ), + [ + [0, 0, -1.0], + [0, 0, -1], + [0, 0, -1], + [0, 0, -1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, -1, 0], + [0, -1, 0], + [0, -1, 0], + [0, -1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["colour"], - np.array( - [ - [0, 1, 0, 1], - [1, 1, 0, 1], - [0, 0, 0, 1], - [1, 0, 0, 1], - [0, 1, 1, 1], - [1, 1, 1, 1], - [0, 0, 1, 1], - [1, 0, 1, 1], - [1, 0, 0, 1], - [1, 0, 1, 1], - [0, 0, 0, 1], - [0, 0, 1, 1], - [1, 1, 0, 1], - [1, 1, 1, 1], - [0, 1, 0, 1], - [0, 1, 1, 1], - [0, 0, 1, 1], - [0, 1, 1, 1], - [0, 0, 0, 1], - [0, 1, 0, 1], - [1, 0, 1, 1], - [1, 1, 1, 1], - [1, 0, 0, 1], - [1, 1, 0, 1], - ] - ), + [ + [0, 1, 0, 1], + [1, 1, 0, 1], + [0, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 1, 1], + [1, 0, 1, 1], + [1, 0, 0, 1], + [1, 0, 1, 1], + [0, 0, 0, 1], + [0, 0, 1, 1], + [1, 1, 0, 1], + [1, 1, 1, 1], + [0, 1, 0, 1], + [0, 1, 1, 1], + [0, 0, 1, 1], + [0, 1, 1, 1], + [0, 0, 0, 1], + [0, 1, 0, 1], + [1, 0, 1, 1], + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 1, 0, 1], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_equal( + xp_assert_equal( faces, np.array( [ @@ -348,7 +329,7 @@ def test_primitive_cube(self) -> None: ), ) - np.testing.assert_equal( + xp_assert_equal( outline, np.array( [ @@ -389,251 +370,243 @@ def test_primitive_cube(self) -> None: depth_segments=3, ) - np.testing.assert_allclose( + xp_assert_close( vertices["position"], - np.array( - [ - [-0.10000000, 0.30000001, -0.20000000], - [0.10000000, 0.30000001, -0.20000000], - [-0.10000000, 0.10000000, -0.20000000], - [0.10000000, 0.10000000, -0.20000000], - [-0.10000000, -0.10000000, -0.20000000], - [0.10000000, -0.10000000, -0.20000000], - [-0.10000000, -0.30000001, -0.20000000], - [0.10000000, -0.30000001, -0.20000000], - [-0.10000000, 0.30000001, 0.20000000], - [0.10000000, 0.30000001, 0.20000000], - [-0.10000000, 0.10000000, 0.20000000], - [0.10000000, 0.10000000, 0.20000000], - [-0.10000000, -0.10000000, 0.20000000], - [0.10000000, -0.10000000, 0.20000000], - [-0.10000000, -0.30000001, 0.20000000], - [0.10000000, -0.30000001, 0.20000000], - [0.10000000, -0.30000001, -0.20000000], - [0.10000000, -0.30000001, 0.00000000], - [0.10000000, -0.30000001, 0.20000000], - [-0.10000000, -0.30000001, -0.20000000], - [-0.10000000, -0.30000001, 0.00000000], - [-0.10000000, -0.30000001, 0.20000000], - [0.10000000, 0.30000001, -0.20000000], - [0.10000000, 0.30000001, 0.00000000], - [0.10000000, 0.30000001, 0.20000000], - [-0.10000000, 0.30000001, -0.20000000], - [-0.10000000, 0.30000001, 0.00000000], - [-0.10000000, 0.30000001, 0.20000000], - [-0.10000000, -0.30000001, 0.20000000], - [-0.10000000, -0.10000000, 0.20000000], - [-0.10000000, 0.10000000, 0.20000000], - [-0.10000000, 0.30000001, 0.20000000], - [-0.10000000, -0.30000001, -0.00000000], - [-0.10000000, -0.10000000, -0.00000000], - [-0.10000000, 0.10000000, -0.00000000], - [-0.10000000, 0.30000001, -0.00000000], - [-0.10000000, -0.30000001, -0.20000000], - [-0.10000000, -0.10000000, -0.20000000], - [-0.10000000, 0.10000000, -0.20000000], - [-0.10000000, 0.30000001, -0.20000000], - [0.10000000, -0.30000001, 0.20000000], - [0.10000000, -0.10000000, 0.20000000], - [0.10000000, 0.10000000, 0.20000000], - [0.10000000, 0.30000001, 0.20000000], - [0.10000000, -0.30000001, -0.00000000], - [0.10000000, -0.10000000, -0.00000000], - [0.10000000, 0.10000000, -0.00000000], - [0.10000000, 0.30000001, -0.00000000], - [0.10000000, -0.30000001, -0.20000000], - [0.10000000, -0.10000000, -0.20000000], - [0.10000000, 0.10000000, -0.20000000], - [0.10000000, 0.30000001, -0.20000000], - ] - ), + [ + [-0.10000000, 0.30000001, -0.20000000], + [0.10000000, 0.30000001, -0.20000000], + [-0.10000000, 0.10000000, -0.20000000], + [0.10000000, 0.10000000, -0.20000000], + [-0.10000000, -0.10000000, -0.20000000], + [0.10000000, -0.10000000, -0.20000000], + [-0.10000000, -0.30000001, -0.20000000], + [0.10000000, -0.30000001, -0.20000000], + [-0.10000000, 0.30000001, 0.20000000], + [0.10000000, 0.30000001, 0.20000000], + [-0.10000000, 0.10000000, 0.20000000], + [0.10000000, 0.10000000, 0.20000000], + [-0.10000000, -0.10000000, 0.20000000], + [0.10000000, -0.10000000, 0.20000000], + [-0.10000000, -0.30000001, 0.20000000], + [0.10000000, -0.30000001, 0.20000000], + [0.10000000, -0.30000001, -0.20000000], + [0.10000000, -0.30000001, 0.00000000], + [0.10000000, -0.30000001, 0.20000000], + [-0.10000000, -0.30000001, -0.20000000], + [-0.10000000, -0.30000001, 0.00000000], + [-0.10000000, -0.30000001, 0.20000000], + [0.10000000, 0.30000001, -0.20000000], + [0.10000000, 0.30000001, 0.00000000], + [0.10000000, 0.30000001, 0.20000000], + [-0.10000000, 0.30000001, -0.20000000], + [-0.10000000, 0.30000001, 0.00000000], + [-0.10000000, 0.30000001, 0.20000000], + [-0.10000000, -0.30000001, 0.20000000], + [-0.10000000, -0.10000000, 0.20000000], + [-0.10000000, 0.10000000, 0.20000000], + [-0.10000000, 0.30000001, 0.20000000], + [-0.10000000, -0.30000001, -0.00000000], + [-0.10000000, -0.10000000, -0.00000000], + [-0.10000000, 0.10000000, -0.00000000], + [-0.10000000, 0.30000001, -0.00000000], + [-0.10000000, -0.30000001, -0.20000000], + [-0.10000000, -0.10000000, -0.20000000], + [-0.10000000, 0.10000000, -0.20000000], + [-0.10000000, 0.30000001, -0.20000000], + [0.10000000, -0.30000001, 0.20000000], + [0.10000000, -0.10000000, 0.20000000], + [0.10000000, 0.10000000, 0.20000000], + [0.10000000, 0.30000001, 0.20000000], + [0.10000000, -0.30000001, -0.00000000], + [0.10000000, -0.10000000, -0.00000000], + [0.10000000, 0.10000000, -0.00000000], + [0.10000000, 0.30000001, -0.00000000], + [0.10000000, -0.30000001, -0.20000000], + [0.10000000, -0.10000000, -0.20000000], + [0.10000000, 0.10000000, -0.20000000], + [0.10000000, 0.30000001, -0.20000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["uv"], - np.array( - [ - [0.00000000, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.66666669], - [1.00000000, 0.66666669], - [0.00000000, 0.33333334], - [1.00000000, 0.33333334], - [0.00000000, 0.00000000], - [1.00000000, 0.00000000], - [0.00000000, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.66666669], - [1.00000000, 0.66666669], - [0.00000000, 0.33333334], - [1.00000000, 0.33333334], - [0.00000000, 0.00000000], - [1.00000000, 0.00000000], - [0.00000000, 1.00000000], - [0.50000000, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.00000000], - [0.50000000, 0.00000000], - [1.00000000, 0.00000000], - [0.00000000, 1.00000000], - [0.50000000, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.00000000], - [0.50000000, 0.00000000], - [1.00000000, 0.00000000], - [0.00000000, 1.00000000], - [0.33333334, 1.00000000], - [0.66666669, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.50000000], - [0.33333334, 0.50000000], - [0.66666669, 0.50000000], - [1.00000000, 0.50000000], - [0.00000000, 0.00000000], - [0.33333334, 0.00000000], - [0.66666669, 0.00000000], - [1.00000000, 0.00000000], - [0.00000000, 1.00000000], - [0.33333334, 1.00000000], - [0.66666669, 1.00000000], - [1.00000000, 1.00000000], - [0.00000000, 0.50000000], - [0.33333334, 0.50000000], - [0.66666669, 0.50000000], - [1.00000000, 0.50000000], - [0.00000000, 0.00000000], - [0.33333334, 0.00000000], - [0.66666669, 0.00000000], - [1.00000000, 0.00000000], - ] - ), + [ + [0.00000000, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.66666669], + [1.00000000, 0.66666669], + [0.00000000, 0.33333334], + [1.00000000, 0.33333334], + [0.00000000, 0.00000000], + [1.00000000, 0.00000000], + [0.00000000, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.66666669], + [1.00000000, 0.66666669], + [0.00000000, 0.33333334], + [1.00000000, 0.33333334], + [0.00000000, 0.00000000], + [1.00000000, 0.00000000], + [0.00000000, 1.00000000], + [0.50000000, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.00000000], + [0.50000000, 0.00000000], + [1.00000000, 0.00000000], + [0.00000000, 1.00000000], + [0.50000000, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.00000000], + [0.50000000, 0.00000000], + [1.00000000, 0.00000000], + [0.00000000, 1.00000000], + [0.33333334, 1.00000000], + [0.66666669, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.50000000], + [0.33333334, 0.50000000], + [0.66666669, 0.50000000], + [1.00000000, 0.50000000], + [0.00000000, 0.00000000], + [0.33333334, 0.00000000], + [0.66666669, 0.00000000], + [1.00000000, 0.00000000], + [0.00000000, 1.00000000], + [0.33333334, 1.00000000], + [0.66666669, 1.00000000], + [1.00000000, 1.00000000], + [0.00000000, 0.50000000], + [0.33333334, 0.50000000], + [0.66666669, 0.50000000], + [1.00000000, 0.50000000], + [0.00000000, 0.00000000], + [0.33333334, 0.00000000], + [0.66666669, 0.00000000], + [1.00000000, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["normal"], - np.array( - [ - [-0.0, -0.0, -1.0], - [0, 0, -1], - [0, 0, -1], - [0, 0, -1], - [0, 0, -1], - [0, 0, -1], - [0, 0, -1], - [0, 0, -1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - [0, -1, 0], - [0, -1, 0], - [0, -1, 0], - [0, -1, 0], - [0, -1, 0], - [0, -1, 0], - [0, 1, 0], - [0, 1, 0], - [0, 1, 0], - [0, 1, 0], - [0, 1, 0], - [0, 1, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [-1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - [1, 0, 0], - ] - ), + [ + [-0.0, -0.0, -1.0], + [0, 0, -1], + [0, 0, -1], + [0, 0, -1], + [0, 0, -1], + [0, 0, -1], + [0, 0, -1], + [0, 0, -1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, -1, 0], + [0, -1, 0], + [0, -1, 0], + [0, -1, 0], + [0, -1, 0], + [0, -1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [-1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + [1, 0, 0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( vertices["colour"], - np.array( - [ - [0.33333334, 1.00000000, 0.16666667, 1.00000000], - [0.66666669, 1.00000000, 0.16666667, 1.00000000], - [0.33333334, 0.66666669, 0.16666667, 1.00000000], - [0.66666669, 0.66666669, 0.16666667, 1.00000000], - [0.33333334, 0.33333334, 0.16666667, 1.00000000], - [0.66666669, 0.33333334, 0.16666667, 1.00000000], - [0.33333334, 0.00000000, 0.16666667, 1.00000000], - [0.66666669, 0.00000000, 0.16666667, 1.00000000], - [0.33333334, 1.00000000, 0.83333331, 1.00000000], - [0.66666669, 1.00000000, 0.83333331, 1.00000000], - [0.33333334, 0.66666669, 0.83333331, 1.00000000], - [0.66666669, 0.66666669, 0.83333331, 1.00000000], - [0.33333334, 0.33333334, 0.83333331, 1.00000000], - [0.66666669, 0.33333334, 0.83333331, 1.00000000], - [0.33333334, 0.00000000, 0.83333331, 1.00000000], - [0.66666669, 0.00000000, 0.83333331, 1.00000000], - [0.66666669, 0.00000000, 0.16666667, 1.00000000], - [0.66666669, 0.00000000, 0.50000000, 1.00000000], - [0.66666669, 0.00000000, 0.83333331, 1.00000000], - [0.33333334, 0.00000000, 0.16666667, 1.00000000], - [0.33333334, 0.00000000, 0.50000000, 1.00000000], - [0.33333334, 0.00000000, 0.83333331, 1.00000000], - [0.66666669, 1.00000000, 0.16666667, 1.00000000], - [0.66666669, 1.00000000, 0.50000000, 1.00000000], - [0.66666669, 1.00000000, 0.83333331, 1.00000000], - [0.33333334, 1.00000000, 0.16666667, 1.00000000], - [0.33333334, 1.00000000, 0.50000000, 1.00000000], - [0.33333334, 1.00000000, 0.83333331, 1.00000000], - [0.33333334, 0.00000000, 0.83333331, 1.00000000], - [0.33333334, 0.33333334, 0.83333331, 1.00000000], - [0.33333334, 0.66666669, 0.83333331, 1.00000000], - [0.33333334, 1.00000000, 0.83333331, 1.00000000], - [0.33333334, 0.00000000, 0.50000000, 1.00000000], - [0.33333334, 0.33333334, 0.50000000, 1.00000000], - [0.33333334, 0.66666669, 0.50000000, 1.00000000], - [0.33333334, 1.00000000, 0.50000000, 1.00000000], - [0.33333334, 0.00000000, 0.16666667, 1.00000000], - [0.33333334, 0.33333334, 0.16666667, 1.00000000], - [0.33333334, 0.66666669, 0.16666667, 1.00000000], - [0.33333334, 1.00000000, 0.16666667, 1.00000000], - [0.66666669, 0.00000000, 0.83333331, 1.00000000], - [0.66666669, 0.33333334, 0.83333331, 1.00000000], - [0.66666669, 0.66666669, 0.83333331, 1.00000000], - [0.66666669, 1.00000000, 0.83333331, 1.00000000], - [0.66666669, 0.00000000, 0.50000000, 1.00000000], - [0.66666669, 0.33333334, 0.50000000, 1.00000000], - [0.66666669, 0.66666669, 0.50000000, 1.00000000], - [0.66666669, 1.00000000, 0.50000000, 1.00000000], - [0.66666669, 0.00000000, 0.16666667, 1.00000000], - [0.66666669, 0.33333334, 0.16666667, 1.00000000], - [0.66666669, 0.66666669, 0.16666667, 1.00000000], - [0.66666669, 1.00000000, 0.16666667, 1.00000000], - ] - ), + [ + [0.33333334, 1.00000000, 0.16666667, 1.00000000], + [0.66666669, 1.00000000, 0.16666667, 1.00000000], + [0.33333334, 0.66666669, 0.16666667, 1.00000000], + [0.66666669, 0.66666669, 0.16666667, 1.00000000], + [0.33333334, 0.33333334, 0.16666667, 1.00000000], + [0.66666669, 0.33333334, 0.16666667, 1.00000000], + [0.33333334, 0.00000000, 0.16666667, 1.00000000], + [0.66666669, 0.00000000, 0.16666667, 1.00000000], + [0.33333334, 1.00000000, 0.83333331, 1.00000000], + [0.66666669, 1.00000000, 0.83333331, 1.00000000], + [0.33333334, 0.66666669, 0.83333331, 1.00000000], + [0.66666669, 0.66666669, 0.83333331, 1.00000000], + [0.33333334, 0.33333334, 0.83333331, 1.00000000], + [0.66666669, 0.33333334, 0.83333331, 1.00000000], + [0.33333334, 0.00000000, 0.83333331, 1.00000000], + [0.66666669, 0.00000000, 0.83333331, 1.00000000], + [0.66666669, 0.00000000, 0.16666667, 1.00000000], + [0.66666669, 0.00000000, 0.50000000, 1.00000000], + [0.66666669, 0.00000000, 0.83333331, 1.00000000], + [0.33333334, 0.00000000, 0.16666667, 1.00000000], + [0.33333334, 0.00000000, 0.50000000, 1.00000000], + [0.33333334, 0.00000000, 0.83333331, 1.00000000], + [0.66666669, 1.00000000, 0.16666667, 1.00000000], + [0.66666669, 1.00000000, 0.50000000, 1.00000000], + [0.66666669, 1.00000000, 0.83333331, 1.00000000], + [0.33333334, 1.00000000, 0.16666667, 1.00000000], + [0.33333334, 1.00000000, 0.50000000, 1.00000000], + [0.33333334, 1.00000000, 0.83333331, 1.00000000], + [0.33333334, 0.00000000, 0.83333331, 1.00000000], + [0.33333334, 0.33333334, 0.83333331, 1.00000000], + [0.33333334, 0.66666669, 0.83333331, 1.00000000], + [0.33333334, 1.00000000, 0.83333331, 1.00000000], + [0.33333334, 0.00000000, 0.50000000, 1.00000000], + [0.33333334, 0.33333334, 0.50000000, 1.00000000], + [0.33333334, 0.66666669, 0.50000000, 1.00000000], + [0.33333334, 1.00000000, 0.50000000, 1.00000000], + [0.33333334, 0.00000000, 0.16666667, 1.00000000], + [0.33333334, 0.33333334, 0.16666667, 1.00000000], + [0.33333334, 0.66666669, 0.16666667, 1.00000000], + [0.33333334, 1.00000000, 0.16666667, 1.00000000], + [0.66666669, 0.00000000, 0.83333331, 1.00000000], + [0.66666669, 0.33333334, 0.83333331, 1.00000000], + [0.66666669, 0.66666669, 0.83333331, 1.00000000], + [0.66666669, 1.00000000, 0.83333331, 1.00000000], + [0.66666669, 0.00000000, 0.50000000, 1.00000000], + [0.66666669, 0.33333334, 0.50000000, 1.00000000], + [0.66666669, 0.66666669, 0.50000000, 1.00000000], + [0.66666669, 1.00000000, 0.50000000, 1.00000000], + [0.66666669, 0.00000000, 0.16666667, 1.00000000], + [0.66666669, 0.33333334, 0.16666667, 1.00000000], + [0.66666669, 0.66666669, 0.16666667, 1.00000000], + [0.66666669, 1.00000000, 0.16666667, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_equal( + xp_assert_equal( faces, np.array( [ @@ -685,7 +658,7 @@ def test_primitive_cube(self) -> None: ), ) - np.testing.assert_equal( + xp_assert_equal( outline, np.array( [ @@ -782,7 +755,7 @@ def test_primitive_cube(self) -> None: ) for plane in MAPPING_PLANE_TO_AXIS: - np.testing.assert_allclose( + xp_assert_close( primitive_cube(planes=[plane])[0]["position"], # pyright: ignore primitive_cube(planes=[MAPPING_PLANE_TO_AXIS[plane]])[0]["position"], # pyright: ignore atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/geometry/tests/test_section.py b/colour/geometry/tests/test_section.py index 54cf4e07eb..816486668e 100644 --- a/colour/geometry/tests/test_section.py +++ b/colour/geometry/tests/test_section.py @@ -2,13 +2,17 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.geometry import hull_section, primitive_cube from colour.geometry.section import close_chord, edges_to_chord, unique_vertices -from colour.utilities import is_trimesh_installed +from colour.utilities import is_trimesh_installed, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,10 +35,10 @@ class TestEdgesToChord: tests methods. """ - def test_edges_to_chord(self) -> None: + def test_edges_to_chord(self, xp: ModuleType) -> None: """Test :func:`colour.geometry.section.edges_to_chord` definition.""" - edges = np.array( + edges = xp_as_array( [ [[0.0, -0.5, 0.0], [0.5, -0.5, 0.0]], [[-0.5, -0.5, 0.0], [0.0, -0.5, 0.0]], @@ -44,56 +48,53 @@ def test_edges_to_chord(self) -> None: [[-0.5, 0.5, 0.0], [-0.5, 0.0, 0.0]], [[0.5, -0.5, 0.0], [0.5, 0.0, 0.0]], [[0.5, 0.0, 0.0], [0.5, 0.5, 0.0]], - ] + ], + xp=xp, ) - np.testing.assert_allclose( + xp_assert_close( edges_to_chord(edges), - np.array( - [ - [0.0, -0.5, 0.0], - [0.5, -0.5, 0.0], - [0.5, -0.5, -0.0], - [0.5, 0.0, -0.0], - [0.5, 0.0, -0.0], - [0.5, 0.5, -0.0], - [0.5, 0.5, 0.0], - [0.0, 0.5, 0.0], - [0.0, 0.5, 0.0], - [-0.5, 0.5, 0.0], - [-0.5, 0.5, -0.0], - [-0.5, 0.0, -0.0], - [-0.5, 0.0, -0.0], - [-0.5, -0.5, -0.0], - [-0.5, -0.5, 0.0], - [0.0, -0.5, 0.0], - ] - ), + [ + [0.0, -0.5, 0.0], + [0.5, -0.5, 0.0], + [0.5, -0.5, -0.0], + [0.5, 0.0, -0.0], + [0.5, 0.0, -0.0], + [0.5, 0.5, -0.0], + [0.5, 0.5, 0.0], + [0.0, 0.5, 0.0], + [0.0, 0.5, 0.0], + [-0.5, 0.5, 0.0], + [-0.5, 0.5, -0.0], + [-0.5, 0.0, -0.0], + [-0.5, 0.0, -0.0], + [-0.5, -0.5, -0.0], + [-0.5, -0.5, 0.0], + [0.0, -0.5, 0.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( edges_to_chord(edges, 5), - np.array( - [ - [-0.5, 0.5, 0.0], - [-0.5, 0.0, 0.0], - [-0.5, 0.0, 0.0], - [-0.5, -0.5, 0.0], - [-0.5, -0.5, 0.0], - [0.0, -0.5, 0.0], - [0.0, -0.5, 0.0], - [0.5, -0.5, 0.0], - [0.5, -0.5, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.5, 0.0], - [0.5, 0.5, 0.0], - [0.0, 0.5, 0.0], - [0.0, 0.5, 0.0], - [-0.5, 0.5, 0.0], - ] - ), + [ + [-0.5, 0.5, 0.0], + [-0.5, 0.0, 0.0], + [-0.5, 0.0, 0.0], + [-0.5, -0.5, 0.0], + [-0.5, -0.5, 0.0], + [0.0, -0.5, 0.0], + [0.0, -0.5, 0.0], + [0.5, -0.5, 0.0], + [0.5, -0.5, 0.0], + [0.5, 0.0, 0.0], + [0.5, 0.0, 0.0], + [0.5, 0.5, 0.0], + [0.5, 0.5, 0.0], + [0.0, 0.5, 0.0], + [0.0, 0.5, 0.0], + [-0.5, 0.5, 0.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -104,12 +105,12 @@ class TestCloseChord: methods. """ - def test_close_chord(self) -> None: + def test_close_chord(self, xp: ModuleType) -> None: """Test :func:`colour.geometry.section.close_chord` definition.""" - np.testing.assert_allclose( - close_chord(np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]])), - np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5], [0.0, 0.5, 0.0]]), + xp_assert_close( + close_chord(xp_as_array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]], xp=xp)), + [[0.0, 0.5, 0.0], [0.0, 0.0, 0.5], [0.0, 0.5, 0.0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -120,23 +121,25 @@ class TestUniqueVertices: tests methods. """ - def test_unique_vertices(self) -> None: + def test_unique_vertices(self, xp: ModuleType) -> None: """Test :func:`colour.geometry.section.unique_vertices` definition.""" - np.testing.assert_allclose( + xp_assert_close( unique_vertices( - np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5], [0.0, 0.5, 0.0]]) + xp_as_array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5], [0.0, 0.5, 0.0]], xp=xp) ), - np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]]), + [[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( unique_vertices( - np.array([[0.0, 0.51, 0.0], [0.0, 0.0, 0.51], [0.0, 0.52, 0.0]]), + xp_as_array( + [[0.0, 0.51, 0.0], [0.0, 0.0, 0.51], [0.0, 0.52, 0.0]], xp=xp + ), 1, ), - np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]]), + [[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -158,77 +161,70 @@ def test_hull_section(self) -> None: vertices, faces, _outline = primitive_cube(1, 1, 1, 2, 2, 2) hull = trimesh.Trimesh(vertices["position"], faces, process=False) - np.testing.assert_allclose( + xp_assert_close( hull_section(hull, origin=0), - np.array( - [ - [0.0, -0.5, 0.0], - [0.5, -0.5, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.5, 0.0], - [0.0, 0.5, 0.0], - [-0.5, 0.5, 0.0], - [-0.5, 0.0, 0.0], - [-0.5, -0.5, 0.0], - [0.0, -0.5, 0.0], - ] - ), + [ + [0.0, -0.5, 0.0], + [0.5, -0.5, 0.0], + [0.5, 0.0, 0.0], + [0.5, 0.5, 0.0], + [0.0, 0.5, 0.0], + [-0.5, 0.5, 0.0], + [-0.5, 0.0, 0.0], + [-0.5, -0.5, 0.0], + [0.0, -0.5, 0.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hull_section(hull, axis="+x", origin=0), - np.array( - [ - [0.0, 0.0, -0.5], - [0.0, 0.5, -0.5], - [0.0, 0.5, 0.0], - [0.0, 0.5, 0.5], - [0.0, 0.0, 0.5], - [0.0, -0.5, 0.5], - [0.0, -0.5, 0.0], - [0.0, -0.5, -0.5], - [0.0, 0.0, -0.5], - ] - ), + [ + [0.0, 0.0, -0.5], + [0.0, 0.5, -0.5], + [0.0, 0.5, 0.0], + [0.0, 0.5, 0.5], + [0.0, 0.0, 0.5], + [0.0, -0.5, 0.5], + [0.0, -0.5, 0.0], + [0.0, -0.5, -0.5], + [0.0, 0.0, -0.5], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hull_section(hull, axis="+y", origin=0), - np.array( - [ - [0.0, 0.0, -0.5], - [-0.5, 0.0, -0.5], - [-0.5, 0.0, 0.0], - [-0.5, 0.0, 0.5], - [0.0, 0.0, 0.5], - [0.5, 0.0, 0.5], - [0.5, 0.0, 0.0], - [0.5, 0.0, -0.5], - [0.0, 0.0, -0.5], - ] - ), + [ + [0.0, 0.0, -0.5], + [-0.5, 0.0, -0.5], + [-0.5, 0.0, 0.0], + [-0.5, 0.0, 0.5], + [0.0, 0.0, 0.5], + [0.5, 0.0, 0.5], + [0.5, 0.0, 0.0], + [0.5, 0.0, -0.5], + [0.0, 0.0, -0.5], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) hull.vertices = (hull.vertices + 0.5) * 2 - np.testing.assert_allclose( + xp_assert_close( hull_section(hull, origin=0.5, normalise=True), - np.array( - [ - [1.0, 0.0, 1.0], - [2.0, 0.0, 1.0], - [2.0, 1.0, 1.0], - [2.0, 2.0, 1.0], - [1.0, 2.0, 1.0], - [0.0, 2.0, 1.0], - [0.0, 1.0, 1.0], - [0.0, 0.0, 1.0], - [1.0, 0.0, 1.0], - ] - ), + [ + [1.0, 0.0, 1.0], + [2.0, 0.0, 1.0], + [2.0, 1.0, 1.0], + [2.0, 2.0, 1.0], + [1.0, 2.0, 1.0], + [0.0, 2.0, 1.0], + [0.0, 1.0, 1.0], + [0.0, 0.0, 1.0], + [1.0, 0.0, 1.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - pytest.raises(ValueError, hull_section, hull, origin=-1) + with pytest.raises(ValueError): + hull_section(hull, origin=-1) diff --git a/colour/geometry/tests/test_vertices.py b/colour/geometry/tests/test_vertices.py index 23321020e1..4c1f1b8650 100644 --- a/colour/geometry/tests/test_vertices.py +++ b/colour/geometry/tests/test_vertices.py @@ -2,7 +2,6 @@ from __future__ import annotations -import numpy as np import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -13,6 +12,7 @@ primitive_vertices_quad_mpl, primitive_vertices_sphere, ) +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -41,70 +41,67 @@ def test_primitive_vertices_quad_mpl(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_quad_mpl(), - np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]), + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_quad_mpl(axis="+y"), - np.array([[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]), + [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_quad_mpl(axis="+x"), - np.array([[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]]), + [[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_quad_mpl( width=0.2, height=0.4, depth=0.6, - origin=np.array([0.2, 0.4]), + origin=[0.2, 0.4], axis="+z", ), - np.array( - [ - [0.2, 0.4, 0.6], - [0.4, 0.4, 0.6], - [0.4, 0.8, 0.6], - [0.2, 0.8, 0.6], - ] - ), + [ + [0.2, 0.4, 0.6], + [0.4, 0.4, 0.6], + [0.4, 0.8, 0.6], + [0.2, 0.8, 0.6], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_quad_mpl( width=-0.2, height=-0.4, depth=-0.6, - origin=np.array([-0.2, -0.4]), + origin=[-0.2, -0.4], axis="+z", ), - np.array( - [ - [-0.2, -0.4, -0.6], - [-0.4, -0.4, -0.6], - [-0.4, -0.8, -0.6], - [-0.2, -0.8, -0.6], - ] - ), + [ + [-0.2, -0.4, -0.6], + [-0.4, -0.4, -0.6], + [-0.4, -0.8, -0.6], + [-0.2, -0.8, -0.6], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) for plane in ("xy", "xz", "yz"): - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_quad_mpl(axis=plane), primitive_vertices_quad_mpl(axis=MAPPING_PLANE_TO_AXIS[plane]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - pytest.raises(ValueError, lambda: primitive_vertices_quad_mpl(axis="Undefined")) + with pytest.raises(ValueError): + primitive_vertices_quad_mpl(axis="Undefined") class TestPrimitiveVerticesGridMpl: @@ -119,79 +116,75 @@ def test_primitive_vertices_grid_mpl(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_grid_mpl(), - np.array([[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]]), + [[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_grid_mpl(axis="+y"), - np.array([[[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]]), + [[[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_grid_mpl(axis="+x"), - np.array([[[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]]]), + [[[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_grid_mpl( width=0.2, height=0.4, depth=0.6, width_segments=1, height_segments=2, - origin=np.array([0.2, 0.4]), + origin=[0.2, 0.4], axis="+z", ), - np.array( - [ - [ - [0.20000000, 0.40000000, 0.60000000], - [0.40000000, 0.40000000, 0.60000000], - [0.40000000, 0.60000000, 0.60000000], - [0.20000000, 0.60000000, 0.60000000], - ], - [ - [0.20000000, 0.60000000, 0.60000000], - [0.40000000, 0.60000000, 0.60000000], - [0.40000000, 0.80000000, 0.60000000], - [0.20000000, 0.80000000, 0.60000000], - ], - ] - ), + [ + [ + [0.20000000, 0.40000000, 0.60000000], + [0.40000000, 0.40000000, 0.60000000], + [0.40000000, 0.60000000, 0.60000000], + [0.20000000, 0.60000000, 0.60000000], + ], + [ + [0.20000000, 0.60000000, 0.60000000], + [0.40000000, 0.60000000, 0.60000000], + [0.40000000, 0.80000000, 0.60000000], + [0.20000000, 0.80000000, 0.60000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_grid_mpl( width=-0.2, height=-0.4, depth=-0.6, width_segments=1, height_segments=2, - origin=np.array([-0.2, -0.4]), + origin=[-0.2, -0.4], axis="+z", ), - np.array( - [ - [ - [-0.20000000, -0.40000000, -0.60000000], - [-0.40000000, -0.40000000, -0.60000000], - [-0.40000000, -0.60000000, -0.60000000], - [-0.20000000, -0.60000000, -0.60000000], - ], - [ - [-0.20000000, -0.60000000, -0.60000000], - [-0.40000000, -0.60000000, -0.60000000], - [-0.40000000, -0.80000000, -0.60000000], - [-0.20000000, -0.80000000, -0.60000000], - ], - ] - ), + [ + [ + [-0.20000000, -0.40000000, -0.60000000], + [-0.40000000, -0.40000000, -0.60000000], + [-0.40000000, -0.60000000, -0.60000000], + [-0.20000000, -0.60000000, -0.60000000], + ], + [ + [-0.20000000, -0.60000000, -0.60000000], + [-0.40000000, -0.60000000, -0.60000000], + [-0.40000000, -0.80000000, -0.60000000], + [-0.20000000, -0.80000000, -0.60000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -208,88 +201,86 @@ def test_primitive_vertices_cube_mpl(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(), - np.array( - [ - [ - [0, 0, 0], - [1, 0, 0], - [1, 1, 0], - [0, 1, 0], - ], - [ - [0, 0, 1], - [1, 0, 1], - [1, 1, 1], - [0, 1, 1], - ], - [ - [0, 0, 0], - [1, 0, 0], - [1, 0, 1], - [0, 0, 1], - ], - [ - [0, 1, 0], - [1, 1, 0], - [1, 1, 1], - [0, 1, 1], - ], - [ - [0, 0, 0], - [0, 1, 0], - [0, 1, 1], - [0, 0, 1], - ], - [ - [1, 0, 0], - [1, 1, 0], - [1, 1, 1], - [1, 0, 1], - ], - ] - ), + [ + [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + ], + [ + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + [0, 1, 1], + ], + [ + [0, 0, 0], + [1, 0, 0], + [1, 0, 1], + [0, 0, 1], + ], + [ + [0, 1, 0], + [1, 1, 0], + [1, 1, 1], + [0, 1, 1], + ], + [ + [0, 0, 0], + [0, 1, 0], + [0, 1, 1], + [0, 0, 1], + ], + [ + [1, 0, 0], + [1, 1, 0], + [1, 1, 1], + [1, 0, 1], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=["+x"]), - np.array([[[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]]]), + [[[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=["-x"]), - np.array([[[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]]]), + [[[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=["+y"]), - np.array([[[0, 1, 0], [1, 1, 0], [1, 1, 1], [0, 1, 1]]]), + [[[0, 1, 0], [1, 1, 0], [1, 1, 1], [0, 1, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=["-y"]), - np.array([[[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]]), + [[[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=["+z"]), - np.array([[[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]]]), + [[[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=["-z"]), - np.array([[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]]), + [[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl( width=0.2, height=0.4, @@ -297,148 +288,146 @@ def test_primitive_vertices_cube_mpl(self) -> None: width_segments=1, height_segments=2, depth_segments=3, - origin=np.array([0.2, 0.4, 0.6]), - ), - np.array( - [ - [ - [0.20000000, 0.60000000, 0.40000000], - [0.40000000, 0.60000000, 0.40000000], - [0.40000000, 0.80000000, 0.40000000], - [0.20000000, 0.80000000, 0.40000000], - ], - [ - [0.20000000, 0.80000000, 0.40000000], - [0.40000000, 0.80000000, 0.40000000], - [0.40000000, 1.00000000, 0.40000000], - [0.20000000, 1.00000000, 0.40000000], - ], - [ - [0.20000000, 1.00000000, 0.40000000], - [0.40000000, 1.00000000, 0.40000000], - [0.40000000, 1.20000000, 0.40000000], - [0.20000000, 1.20000000, 0.40000000], - ], - [ - [0.20000000, 0.60000000, 0.80000000], - [0.40000000, 0.60000000, 0.80000000], - [0.40000000, 0.80000000, 0.80000000], - [0.20000000, 0.80000000, 0.80000000], - ], - [ - [0.20000000, 0.80000000, 0.80000000], - [0.40000000, 0.80000000, 0.80000000], - [0.40000000, 1.00000000, 0.80000000], - [0.20000000, 1.00000000, 0.80000000], - ], - [ - [0.20000000, 1.00000000, 0.80000000], - [0.40000000, 1.00000000, 0.80000000], - [0.40000000, 1.20000000, 0.80000000], - [0.20000000, 1.20000000, 0.80000000], - ], - [ - [0.20000000, 0.60000000, 0.40000000], - [0.40000000, 0.60000000, 0.40000000], - [0.40000000, 0.60000000, 0.60000000], - [0.20000000, 0.60000000, 0.60000000], - ], - [ - [0.20000000, 0.60000000, 0.60000000], - [0.40000000, 0.60000000, 0.60000000], - [0.40000000, 0.60000000, 0.80000000], - [0.20000000, 0.60000000, 0.80000000], - ], - [ - [0.20000000, 1.20000000, 0.40000000], - [0.40000000, 1.20000000, 0.40000000], - [0.40000000, 1.20000000, 0.60000000], - [0.20000000, 1.20000000, 0.60000000], - ], - [ - [0.20000000, 1.20000000, 0.60000000], - [0.40000000, 1.20000000, 0.60000000], - [0.40000000, 1.20000000, 0.80000000], - [0.20000000, 1.20000000, 0.80000000], - ], - [ - [0.20000000, 0.60000000, 0.40000000], - [0.20000000, 0.80000000, 0.40000000], - [0.20000000, 0.80000000, 0.60000000], - [0.20000000, 0.60000000, 0.60000000], - ], - [ - [0.20000000, 0.60000000, 0.60000000], - [0.20000000, 0.80000000, 0.60000000], - [0.20000000, 0.80000000, 0.80000000], - [0.20000000, 0.60000000, 0.80000000], - ], - [ - [0.20000000, 0.80000000, 0.40000000], - [0.20000000, 1.00000000, 0.40000000], - [0.20000000, 1.00000000, 0.60000000], - [0.20000000, 0.80000000, 0.60000000], - ], - [ - [0.20000000, 0.80000000, 0.60000000], - [0.20000000, 1.00000000, 0.60000000], - [0.20000000, 1.00000000, 0.80000000], - [0.20000000, 0.80000000, 0.80000000], - ], - [ - [0.20000000, 1.00000000, 0.40000000], - [0.20000000, 1.20000000, 0.40000000], - [0.20000000, 1.20000000, 0.60000000], - [0.20000000, 1.00000000, 0.60000000], - ], - [ - [0.20000000, 1.00000000, 0.60000000], - [0.20000000, 1.20000000, 0.60000000], - [0.20000000, 1.20000000, 0.80000000], - [0.20000000, 1.00000000, 0.80000000], - ], - [ - [0.40000000, 0.60000000, 0.40000000], - [0.40000000, 0.80000000, 0.40000000], - [0.40000000, 0.80000000, 0.60000000], - [0.40000000, 0.60000000, 0.60000000], - ], - [ - [0.40000000, 0.60000000, 0.60000000], - [0.40000000, 0.80000000, 0.60000000], - [0.40000000, 0.80000000, 0.80000000], - [0.40000000, 0.60000000, 0.80000000], - ], - [ - [0.40000000, 0.80000000, 0.40000000], - [0.40000000, 1.00000000, 0.40000000], - [0.40000000, 1.00000000, 0.60000000], - [0.40000000, 0.80000000, 0.60000000], - ], - [ - [0.40000000, 0.80000000, 0.60000000], - [0.40000000, 1.00000000, 0.60000000], - [0.40000000, 1.00000000, 0.80000000], - [0.40000000, 0.80000000, 0.80000000], - ], - [ - [0.40000000, 1.00000000, 0.40000000], - [0.40000000, 1.20000000, 0.40000000], - [0.40000000, 1.20000000, 0.60000000], - [0.40000000, 1.00000000, 0.60000000], - ], - [ - [0.40000000, 1.00000000, 0.60000000], - [0.40000000, 1.20000000, 0.60000000], - [0.40000000, 1.20000000, 0.80000000], - [0.40000000, 1.00000000, 0.80000000], - ], - ] + origin=[0.2, 0.4, 0.6], ), + [ + [ + [0.20000000, 0.60000000, 0.40000000], + [0.40000000, 0.60000000, 0.40000000], + [0.40000000, 0.80000000, 0.40000000], + [0.20000000, 0.80000000, 0.40000000], + ], + [ + [0.20000000, 0.80000000, 0.40000000], + [0.40000000, 0.80000000, 0.40000000], + [0.40000000, 1.00000000, 0.40000000], + [0.20000000, 1.00000000, 0.40000000], + ], + [ + [0.20000000, 1.00000000, 0.40000000], + [0.40000000, 1.00000000, 0.40000000], + [0.40000000, 1.20000000, 0.40000000], + [0.20000000, 1.20000000, 0.40000000], + ], + [ + [0.20000000, 0.60000000, 0.80000000], + [0.40000000, 0.60000000, 0.80000000], + [0.40000000, 0.80000000, 0.80000000], + [0.20000000, 0.80000000, 0.80000000], + ], + [ + [0.20000000, 0.80000000, 0.80000000], + [0.40000000, 0.80000000, 0.80000000], + [0.40000000, 1.00000000, 0.80000000], + [0.20000000, 1.00000000, 0.80000000], + ], + [ + [0.20000000, 1.00000000, 0.80000000], + [0.40000000, 1.00000000, 0.80000000], + [0.40000000, 1.20000000, 0.80000000], + [0.20000000, 1.20000000, 0.80000000], + ], + [ + [0.20000000, 0.60000000, 0.40000000], + [0.40000000, 0.60000000, 0.40000000], + [0.40000000, 0.60000000, 0.60000000], + [0.20000000, 0.60000000, 0.60000000], + ], + [ + [0.20000000, 0.60000000, 0.60000000], + [0.40000000, 0.60000000, 0.60000000], + [0.40000000, 0.60000000, 0.80000000], + [0.20000000, 0.60000000, 0.80000000], + ], + [ + [0.20000000, 1.20000000, 0.40000000], + [0.40000000, 1.20000000, 0.40000000], + [0.40000000, 1.20000000, 0.60000000], + [0.20000000, 1.20000000, 0.60000000], + ], + [ + [0.20000000, 1.20000000, 0.60000000], + [0.40000000, 1.20000000, 0.60000000], + [0.40000000, 1.20000000, 0.80000000], + [0.20000000, 1.20000000, 0.80000000], + ], + [ + [0.20000000, 0.60000000, 0.40000000], + [0.20000000, 0.80000000, 0.40000000], + [0.20000000, 0.80000000, 0.60000000], + [0.20000000, 0.60000000, 0.60000000], + ], + [ + [0.20000000, 0.60000000, 0.60000000], + [0.20000000, 0.80000000, 0.60000000], + [0.20000000, 0.80000000, 0.80000000], + [0.20000000, 0.60000000, 0.80000000], + ], + [ + [0.20000000, 0.80000000, 0.40000000], + [0.20000000, 1.00000000, 0.40000000], + [0.20000000, 1.00000000, 0.60000000], + [0.20000000, 0.80000000, 0.60000000], + ], + [ + [0.20000000, 0.80000000, 0.60000000], + [0.20000000, 1.00000000, 0.60000000], + [0.20000000, 1.00000000, 0.80000000], + [0.20000000, 0.80000000, 0.80000000], + ], + [ + [0.20000000, 1.00000000, 0.40000000], + [0.20000000, 1.20000000, 0.40000000], + [0.20000000, 1.20000000, 0.60000000], + [0.20000000, 1.00000000, 0.60000000], + ], + [ + [0.20000000, 1.00000000, 0.60000000], + [0.20000000, 1.20000000, 0.60000000], + [0.20000000, 1.20000000, 0.80000000], + [0.20000000, 1.00000000, 0.80000000], + ], + [ + [0.40000000, 0.60000000, 0.40000000], + [0.40000000, 0.80000000, 0.40000000], + [0.40000000, 0.80000000, 0.60000000], + [0.40000000, 0.60000000, 0.60000000], + ], + [ + [0.40000000, 0.60000000, 0.60000000], + [0.40000000, 0.80000000, 0.60000000], + [0.40000000, 0.80000000, 0.80000000], + [0.40000000, 0.60000000, 0.80000000], + ], + [ + [0.40000000, 0.80000000, 0.40000000], + [0.40000000, 1.00000000, 0.40000000], + [0.40000000, 1.00000000, 0.60000000], + [0.40000000, 0.80000000, 0.60000000], + ], + [ + [0.40000000, 0.80000000, 0.60000000], + [0.40000000, 1.00000000, 0.60000000], + [0.40000000, 1.00000000, 0.80000000], + [0.40000000, 0.80000000, 0.80000000], + ], + [ + [0.40000000, 1.00000000, 0.40000000], + [0.40000000, 1.20000000, 0.40000000], + [0.40000000, 1.20000000, 0.60000000], + [0.40000000, 1.00000000, 0.60000000], + ], + [ + [0.40000000, 1.00000000, 0.60000000], + [0.40000000, 1.20000000, 0.60000000], + [0.40000000, 1.20000000, 0.80000000], + [0.40000000, 1.00000000, 0.80000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl( width=-0.2, height=-0.4, @@ -446,149 +435,147 @@ def test_primitive_vertices_cube_mpl(self) -> None: width_segments=1, height_segments=2, depth_segments=3, - origin=np.array([-0.2, -0.4, -0.6]), - ), - np.array( - [ - [ - [-0.20000000, -0.60000000, -0.40000000], - [-0.40000000, -0.60000000, -0.40000000], - [-0.40000000, -0.80000000, -0.40000000], - [-0.20000000, -0.80000000, -0.40000000], - ], - [ - [-0.20000000, -0.80000000, -0.40000000], - [-0.40000000, -0.80000000, -0.40000000], - [-0.40000000, -1.00000000, -0.40000000], - [-0.20000000, -1.00000000, -0.40000000], - ], - [ - [-0.20000000, -1.00000000, -0.40000000], - [-0.40000000, -1.00000000, -0.40000000], - [-0.40000000, -1.20000000, -0.40000000], - [-0.20000000, -1.20000000, -0.40000000], - ], - [ - [-0.20000000, -0.60000000, -0.80000000], - [-0.40000000, -0.60000000, -0.80000000], - [-0.40000000, -0.80000000, -0.80000000], - [-0.20000000, -0.80000000, -0.80000000], - ], - [ - [-0.20000000, -0.80000000, -0.80000000], - [-0.40000000, -0.80000000, -0.80000000], - [-0.40000000, -1.00000000, -0.80000000], - [-0.20000000, -1.00000000, -0.80000000], - ], - [ - [-0.20000000, -1.00000000, -0.80000000], - [-0.40000000, -1.00000000, -0.80000000], - [-0.40000000, -1.20000000, -0.80000000], - [-0.20000000, -1.20000000, -0.80000000], - ], - [ - [-0.20000000, -0.60000000, -0.40000000], - [-0.40000000, -0.60000000, -0.40000000], - [-0.40000000, -0.60000000, -0.60000000], - [-0.20000000, -0.60000000, -0.60000000], - ], - [ - [-0.20000000, -0.60000000, -0.60000000], - [-0.40000000, -0.60000000, -0.60000000], - [-0.40000000, -0.60000000, -0.80000000], - [-0.20000000, -0.60000000, -0.80000000], - ], - [ - [-0.20000000, -1.20000000, -0.40000000], - [-0.40000000, -1.20000000, -0.40000000], - [-0.40000000, -1.20000000, -0.60000000], - [-0.20000000, -1.20000000, -0.60000000], - ], - [ - [-0.20000000, -1.20000000, -0.60000000], - [-0.40000000, -1.20000000, -0.60000000], - [-0.40000000, -1.20000000, -0.80000000], - [-0.20000000, -1.20000000, -0.80000000], - ], - [ - [-0.20000000, -0.60000000, -0.40000000], - [-0.20000000, -0.80000000, -0.40000000], - [-0.20000000, -0.80000000, -0.60000000], - [-0.20000000, -0.60000000, -0.60000000], - ], - [ - [-0.20000000, -0.60000000, -0.60000000], - [-0.20000000, -0.80000000, -0.60000000], - [-0.20000000, -0.80000000, -0.80000000], - [-0.20000000, -0.60000000, -0.80000000], - ], - [ - [-0.20000000, -0.80000000, -0.40000000], - [-0.20000000, -1.00000000, -0.40000000], - [-0.20000000, -1.00000000, -0.60000000], - [-0.20000000, -0.80000000, -0.60000000], - ], - [ - [-0.20000000, -0.80000000, -0.60000000], - [-0.20000000, -1.00000000, -0.60000000], - [-0.20000000, -1.00000000, -0.80000000], - [-0.20000000, -0.80000000, -0.80000000], - ], - [ - [-0.20000000, -1.00000000, -0.40000000], - [-0.20000000, -1.20000000, -0.40000000], - [-0.20000000, -1.20000000, -0.60000000], - [-0.20000000, -1.00000000, -0.60000000], - ], - [ - [-0.20000000, -1.00000000, -0.60000000], - [-0.20000000, -1.20000000, -0.60000000], - [-0.20000000, -1.20000000, -0.80000000], - [-0.20000000, -1.00000000, -0.80000000], - ], - [ - [-0.40000000, -0.60000000, -0.40000000], - [-0.40000000, -0.80000000, -0.40000000], - [-0.40000000, -0.80000000, -0.60000000], - [-0.40000000, -0.60000000, -0.60000000], - ], - [ - [-0.40000000, -0.60000000, -0.60000000], - [-0.40000000, -0.80000000, -0.60000000], - [-0.40000000, -0.80000000, -0.80000000], - [-0.40000000, -0.60000000, -0.80000000], - ], - [ - [-0.40000000, -0.80000000, -0.40000000], - [-0.40000000, -1.00000000, -0.40000000], - [-0.40000000, -1.00000000, -0.60000000], - [-0.40000000, -0.80000000, -0.60000000], - ], - [ - [-0.40000000, -0.80000000, -0.60000000], - [-0.40000000, -1.00000000, -0.60000000], - [-0.40000000, -1.00000000, -0.80000000], - [-0.40000000, -0.80000000, -0.80000000], - ], - [ - [-0.40000000, -1.00000000, -0.40000000], - [-0.40000000, -1.20000000, -0.40000000], - [-0.40000000, -1.20000000, -0.60000000], - [-0.40000000, -1.00000000, -0.60000000], - ], - [ - [-0.40000000, -1.00000000, -0.60000000], - [-0.40000000, -1.20000000, -0.60000000], - [-0.40000000, -1.20000000, -0.80000000], - [-0.40000000, -1.00000000, -0.80000000], - ], - ] + origin=[-0.2, -0.4, -0.6], ), + [ + [ + [-0.20000000, -0.60000000, -0.40000000], + [-0.40000000, -0.60000000, -0.40000000], + [-0.40000000, -0.80000000, -0.40000000], + [-0.20000000, -0.80000000, -0.40000000], + ], + [ + [-0.20000000, -0.80000000, -0.40000000], + [-0.40000000, -0.80000000, -0.40000000], + [-0.40000000, -1.00000000, -0.40000000], + [-0.20000000, -1.00000000, -0.40000000], + ], + [ + [-0.20000000, -1.00000000, -0.40000000], + [-0.40000000, -1.00000000, -0.40000000], + [-0.40000000, -1.20000000, -0.40000000], + [-0.20000000, -1.20000000, -0.40000000], + ], + [ + [-0.20000000, -0.60000000, -0.80000000], + [-0.40000000, -0.60000000, -0.80000000], + [-0.40000000, -0.80000000, -0.80000000], + [-0.20000000, -0.80000000, -0.80000000], + ], + [ + [-0.20000000, -0.80000000, -0.80000000], + [-0.40000000, -0.80000000, -0.80000000], + [-0.40000000, -1.00000000, -0.80000000], + [-0.20000000, -1.00000000, -0.80000000], + ], + [ + [-0.20000000, -1.00000000, -0.80000000], + [-0.40000000, -1.00000000, -0.80000000], + [-0.40000000, -1.20000000, -0.80000000], + [-0.20000000, -1.20000000, -0.80000000], + ], + [ + [-0.20000000, -0.60000000, -0.40000000], + [-0.40000000, -0.60000000, -0.40000000], + [-0.40000000, -0.60000000, -0.60000000], + [-0.20000000, -0.60000000, -0.60000000], + ], + [ + [-0.20000000, -0.60000000, -0.60000000], + [-0.40000000, -0.60000000, -0.60000000], + [-0.40000000, -0.60000000, -0.80000000], + [-0.20000000, -0.60000000, -0.80000000], + ], + [ + [-0.20000000, -1.20000000, -0.40000000], + [-0.40000000, -1.20000000, -0.40000000], + [-0.40000000, -1.20000000, -0.60000000], + [-0.20000000, -1.20000000, -0.60000000], + ], + [ + [-0.20000000, -1.20000000, -0.60000000], + [-0.40000000, -1.20000000, -0.60000000], + [-0.40000000, -1.20000000, -0.80000000], + [-0.20000000, -1.20000000, -0.80000000], + ], + [ + [-0.20000000, -0.60000000, -0.40000000], + [-0.20000000, -0.80000000, -0.40000000], + [-0.20000000, -0.80000000, -0.60000000], + [-0.20000000, -0.60000000, -0.60000000], + ], + [ + [-0.20000000, -0.60000000, -0.60000000], + [-0.20000000, -0.80000000, -0.60000000], + [-0.20000000, -0.80000000, -0.80000000], + [-0.20000000, -0.60000000, -0.80000000], + ], + [ + [-0.20000000, -0.80000000, -0.40000000], + [-0.20000000, -1.00000000, -0.40000000], + [-0.20000000, -1.00000000, -0.60000000], + [-0.20000000, -0.80000000, -0.60000000], + ], + [ + [-0.20000000, -0.80000000, -0.60000000], + [-0.20000000, -1.00000000, -0.60000000], + [-0.20000000, -1.00000000, -0.80000000], + [-0.20000000, -0.80000000, -0.80000000], + ], + [ + [-0.20000000, -1.00000000, -0.40000000], + [-0.20000000, -1.20000000, -0.40000000], + [-0.20000000, -1.20000000, -0.60000000], + [-0.20000000, -1.00000000, -0.60000000], + ], + [ + [-0.20000000, -1.00000000, -0.60000000], + [-0.20000000, -1.20000000, -0.60000000], + [-0.20000000, -1.20000000, -0.80000000], + [-0.20000000, -1.00000000, -0.80000000], + ], + [ + [-0.40000000, -0.60000000, -0.40000000], + [-0.40000000, -0.80000000, -0.40000000], + [-0.40000000, -0.80000000, -0.60000000], + [-0.40000000, -0.60000000, -0.60000000], + ], + [ + [-0.40000000, -0.60000000, -0.60000000], + [-0.40000000, -0.80000000, -0.60000000], + [-0.40000000, -0.80000000, -0.80000000], + [-0.40000000, -0.60000000, -0.80000000], + ], + [ + [-0.40000000, -0.80000000, -0.40000000], + [-0.40000000, -1.00000000, -0.40000000], + [-0.40000000, -1.00000000, -0.60000000], + [-0.40000000, -0.80000000, -0.60000000], + ], + [ + [-0.40000000, -0.80000000, -0.60000000], + [-0.40000000, -1.00000000, -0.60000000], + [-0.40000000, -1.00000000, -0.80000000], + [-0.40000000, -0.80000000, -0.80000000], + ], + [ + [-0.40000000, -1.00000000, -0.40000000], + [-0.40000000, -1.20000000, -0.40000000], + [-0.40000000, -1.20000000, -0.60000000], + [-0.40000000, -1.00000000, -0.60000000], + ], + [ + [-0.40000000, -1.00000000, -0.60000000], + [-0.40000000, -1.20000000, -0.60000000], + [-0.40000000, -1.20000000, -0.80000000], + [-0.40000000, -1.00000000, -0.80000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) for plane in MAPPING_PLANE_TO_AXIS: - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_cube_mpl(planes=[plane]), primitive_vertices_cube_mpl(planes=[MAPPING_PLANE_TO_AXIS[plane]]), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -607,391 +594,382 @@ def test_primitive_vertices_sphere(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_sphere(), - np.array( - [ - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.19134172, -0.00000000, 0.46193977], - [-0.35355339, -0.00000000, 0.35355339], - [-0.46193977, -0.00000000, 0.19134172], - [-0.50000000, -0.00000000, 0.00000000], - [-0.46193977, -0.00000000, -0.19134172], - [-0.35355339, -0.00000000, -0.35355339], - [-0.19134172, -0.00000000, -0.46193977], - [-0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.13529903, -0.13529903, 0.46193977], - [-0.25000000, -0.25000000, 0.35355339], - [-0.32664074, -0.32664074, 0.19134172], - [-0.35355339, -0.35355339, 0.00000000], - [-0.32664074, -0.32664074, -0.19134172], - [-0.25000000, -0.25000000, -0.35355339], - [-0.13529903, -0.13529903, -0.46193977], - [-0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.00000000, -0.19134172, 0.46193977], - [0.00000000, -0.35355339, 0.35355339], - [0.00000000, -0.46193977, 0.19134172], - [0.00000000, -0.50000000, 0.00000000], - [0.00000000, -0.46193977, -0.19134172], - [0.00000000, -0.35355339, -0.35355339], - [0.00000000, -0.19134172, -0.46193977], - [0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.13529903, -0.13529903, 0.46193977], - [0.25000000, -0.25000000, 0.35355339], - [0.32664074, -0.32664074, 0.19134172], - [0.35355339, -0.35355339, 0.00000000], - [0.32664074, -0.32664074, -0.19134172], - [0.25000000, -0.25000000, -0.35355339], - [0.13529903, -0.13529903, -0.46193977], - [0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.19134172, 0.00000000, 0.46193977], - [0.35355339, 0.00000000, 0.35355339], - [0.46193977, 0.00000000, 0.19134172], - [0.50000000, 0.00000000, 0.00000000], - [0.46193977, 0.00000000, -0.19134172], - [0.35355339, 0.00000000, -0.35355339], - [0.19134172, 0.00000000, -0.46193977], - [0.00000000, 0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.13529903, 0.13529903, 0.46193977], - [0.25000000, 0.25000000, 0.35355339], - [0.32664074, 0.32664074, 0.19134172], - [0.35355339, 0.35355339, 0.00000000], - [0.32664074, 0.32664074, -0.19134172], - [0.25000000, 0.25000000, -0.35355339], - [0.13529903, 0.13529903, -0.46193977], - [0.00000000, 0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.00000000, 0.19134172, 0.46193977], - [0.00000000, 0.35355339, 0.35355339], - [0.00000000, 0.46193977, 0.19134172], - [0.00000000, 0.50000000, 0.00000000], - [0.00000000, 0.46193977, -0.19134172], - [0.00000000, 0.35355339, -0.35355339], - [0.00000000, 0.19134172, -0.46193977], - [0.00000000, 0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.13529903, 0.13529903, 0.46193977], - [-0.25000000, 0.25000000, 0.35355339], - [-0.32664074, 0.32664074, 0.19134172], - [-0.35355339, 0.35355339, 0.00000000], - [-0.32664074, 0.32664074, -0.19134172], - [-0.25000000, 0.25000000, -0.35355339], - [-0.13529903, 0.13529903, -0.46193977], - [-0.00000000, 0.00000000, -0.50000000], - ], - ] - ), + [ + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.19134172, -0.00000000, 0.46193977], + [-0.35355339, -0.00000000, 0.35355339], + [-0.46193977, -0.00000000, 0.19134172], + [-0.50000000, -0.00000000, 0.00000000], + [-0.46193977, -0.00000000, -0.19134172], + [-0.35355339, -0.00000000, -0.35355339], + [-0.19134172, -0.00000000, -0.46193977], + [-0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.13529903, -0.13529903, 0.46193977], + [-0.25000000, -0.25000000, 0.35355339], + [-0.32664074, -0.32664074, 0.19134172], + [-0.35355339, -0.35355339, 0.00000000], + [-0.32664074, -0.32664074, -0.19134172], + [-0.25000000, -0.25000000, -0.35355339], + [-0.13529903, -0.13529903, -0.46193977], + [-0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.00000000, -0.19134172, 0.46193977], + [0.00000000, -0.35355339, 0.35355339], + [0.00000000, -0.46193977, 0.19134172], + [0.00000000, -0.50000000, 0.00000000], + [0.00000000, -0.46193977, -0.19134172], + [0.00000000, -0.35355339, -0.35355339], + [0.00000000, -0.19134172, -0.46193977], + [0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.13529903, -0.13529903, 0.46193977], + [0.25000000, -0.25000000, 0.35355339], + [0.32664074, -0.32664074, 0.19134172], + [0.35355339, -0.35355339, 0.00000000], + [0.32664074, -0.32664074, -0.19134172], + [0.25000000, -0.25000000, -0.35355339], + [0.13529903, -0.13529903, -0.46193977], + [0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.19134172, 0.00000000, 0.46193977], + [0.35355339, 0.00000000, 0.35355339], + [0.46193977, 0.00000000, 0.19134172], + [0.50000000, 0.00000000, 0.00000000], + [0.46193977, 0.00000000, -0.19134172], + [0.35355339, 0.00000000, -0.35355339], + [0.19134172, 0.00000000, -0.46193977], + [0.00000000, 0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.13529903, 0.13529903, 0.46193977], + [0.25000000, 0.25000000, 0.35355339], + [0.32664074, 0.32664074, 0.19134172], + [0.35355339, 0.35355339, 0.00000000], + [0.32664074, 0.32664074, -0.19134172], + [0.25000000, 0.25000000, -0.35355339], + [0.13529903, 0.13529903, -0.46193977], + [0.00000000, 0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.00000000, 0.19134172, 0.46193977], + [0.00000000, 0.35355339, 0.35355339], + [0.00000000, 0.46193977, 0.19134172], + [0.00000000, 0.50000000, 0.00000000], + [0.00000000, 0.46193977, -0.19134172], + [0.00000000, 0.35355339, -0.35355339], + [0.00000000, 0.19134172, -0.46193977], + [0.00000000, 0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.13529903, 0.13529903, 0.46193977], + [-0.25000000, 0.25000000, 0.35355339], + [-0.32664074, 0.32664074, 0.19134172], + [-0.35355339, 0.35355339, 0.00000000], + [-0.32664074, 0.32664074, -0.19134172], + [-0.25000000, 0.25000000, -0.35355339], + [-0.13529903, 0.13529903, -0.46193977], + [-0.00000000, 0.00000000, -0.50000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_sphere(intermediate=True), - np.array( - [ - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.25663998, -0.10630376, 0.41573481], - [-0.38408888, -0.15909482, 0.27778512], - [-0.45306372, -0.18766514, 0.09754516], - [-0.45306372, -0.18766514, -0.09754516], - [-0.38408888, -0.15909482, -0.27778512], - [-0.25663998, -0.10630376, -0.41573481], - [-0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.10630376, -0.25663998, 0.41573481], - [-0.15909482, -0.38408888, 0.27778512], - [-0.18766514, -0.45306372, 0.09754516], - [-0.18766514, -0.45306372, -0.09754516], - [-0.15909482, -0.38408888, -0.27778512], - [-0.10630376, -0.25663998, -0.41573481], - [-0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.10630376, -0.25663998, 0.41573481], - [0.15909482, -0.38408888, 0.27778512], - [0.18766514, -0.45306372, 0.09754516], - [0.18766514, -0.45306372, -0.09754516], - [0.15909482, -0.38408888, -0.27778512], - [0.10630376, -0.25663998, -0.41573481], - [0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.25663998, -0.10630376, 0.41573481], - [0.38408888, -0.15909482, 0.27778512], - [0.45306372, -0.18766514, 0.09754516], - [0.45306372, -0.18766514, -0.09754516], - [0.38408888, -0.15909482, -0.27778512], - [0.25663998, -0.10630376, -0.41573481], - [0.00000000, -0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.25663998, 0.10630376, 0.41573481], - [0.38408888, 0.15909482, 0.27778512], - [0.45306372, 0.18766514, 0.09754516], - [0.45306372, 0.18766514, -0.09754516], - [0.38408888, 0.15909482, -0.27778512], - [0.25663998, 0.10630376, -0.41573481], - [0.00000000, 0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [0.10630376, 0.25663998, 0.41573481], - [0.15909482, 0.38408888, 0.27778512], - [0.18766514, 0.45306372, 0.09754516], - [0.18766514, 0.45306372, -0.09754516], - [0.15909482, 0.38408888, -0.27778512], - [0.10630376, 0.25663998, -0.41573481], - [0.00000000, 0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.10630376, 0.25663998, 0.41573481], - [-0.15909482, 0.38408888, 0.27778512], - [-0.18766514, 0.45306372, 0.09754516], - [-0.18766514, 0.45306372, -0.09754516], - [-0.15909482, 0.38408888, -0.27778512], - [-0.10630376, 0.25663998, -0.41573481], - [-0.00000000, 0.00000000, -0.50000000], - ], - [ - [0.00000000, 0.00000000, 0.50000000], - [-0.25663998, 0.10630376, 0.41573481], - [-0.38408888, 0.15909482, 0.27778512], - [-0.45306372, 0.18766514, 0.09754516], - [-0.45306372, 0.18766514, -0.09754516], - [-0.38408888, 0.15909482, -0.27778512], - [-0.25663998, 0.10630376, -0.41573481], - [-0.00000000, 0.00000000, -0.50000000], - ], - ] - ), + [ + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.25663998, -0.10630376, 0.41573481], + [-0.38408888, -0.15909482, 0.27778512], + [-0.45306372, -0.18766514, 0.09754516], + [-0.45306372, -0.18766514, -0.09754516], + [-0.38408888, -0.15909482, -0.27778512], + [-0.25663998, -0.10630376, -0.41573481], + [-0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.10630376, -0.25663998, 0.41573481], + [-0.15909482, -0.38408888, 0.27778512], + [-0.18766514, -0.45306372, 0.09754516], + [-0.18766514, -0.45306372, -0.09754516], + [-0.15909482, -0.38408888, -0.27778512], + [-0.10630376, -0.25663998, -0.41573481], + [-0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.10630376, -0.25663998, 0.41573481], + [0.15909482, -0.38408888, 0.27778512], + [0.18766514, -0.45306372, 0.09754516], + [0.18766514, -0.45306372, -0.09754516], + [0.15909482, -0.38408888, -0.27778512], + [0.10630376, -0.25663998, -0.41573481], + [0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.25663998, -0.10630376, 0.41573481], + [0.38408888, -0.15909482, 0.27778512], + [0.45306372, -0.18766514, 0.09754516], + [0.45306372, -0.18766514, -0.09754516], + [0.38408888, -0.15909482, -0.27778512], + [0.25663998, -0.10630376, -0.41573481], + [0.00000000, -0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.25663998, 0.10630376, 0.41573481], + [0.38408888, 0.15909482, 0.27778512], + [0.45306372, 0.18766514, 0.09754516], + [0.45306372, 0.18766514, -0.09754516], + [0.38408888, 0.15909482, -0.27778512], + [0.25663998, 0.10630376, -0.41573481], + [0.00000000, 0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [0.10630376, 0.25663998, 0.41573481], + [0.15909482, 0.38408888, 0.27778512], + [0.18766514, 0.45306372, 0.09754516], + [0.18766514, 0.45306372, -0.09754516], + [0.15909482, 0.38408888, -0.27778512], + [0.10630376, 0.25663998, -0.41573481], + [0.00000000, 0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.10630376, 0.25663998, 0.41573481], + [-0.15909482, 0.38408888, 0.27778512], + [-0.18766514, 0.45306372, 0.09754516], + [-0.18766514, 0.45306372, -0.09754516], + [-0.15909482, 0.38408888, -0.27778512], + [-0.10630376, 0.25663998, -0.41573481], + [-0.00000000, 0.00000000, -0.50000000], + ], + [ + [0.00000000, 0.00000000, 0.50000000], + [-0.25663998, 0.10630376, 0.41573481], + [-0.38408888, 0.15909482, 0.27778512], + [-0.45306372, 0.18766514, 0.09754516], + [-0.45306372, 0.18766514, -0.09754516], + [-0.38408888, 0.15909482, -0.27778512], + [-0.25663998, 0.10630376, -0.41573481], + [-0.00000000, 0.00000000, -0.50000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_sphere(segments=6, axis="+y"), - np.array( - [ - [ - [0.00000000, 0.50000000, 0.00000000], - [-0.00000000, 0.43301270, -0.25000000], - [-0.00000000, 0.25000000, -0.43301270], - [-0.00000000, 0.00000000, -0.50000000], - [-0.00000000, -0.25000000, -0.43301270], - [-0.00000000, -0.43301270, -0.25000000], - [-0.00000000, -0.50000000, -0.00000000], - ], - [ - [0.00000000, 0.50000000, 0.00000000], - [-0.21650635, 0.43301270, -0.12500000], - [-0.37500000, 0.25000000, -0.21650635], - [-0.43301270, 0.00000000, -0.25000000], - [-0.37500000, -0.25000000, -0.21650635], - [-0.21650635, -0.43301270, -0.12500000], - [-0.00000000, -0.50000000, -0.00000000], - ], - [ - [0.00000000, 0.50000000, 0.00000000], - [-0.21650635, 0.43301270, 0.12500000], - [-0.37500000, 0.25000000, 0.21650635], - [-0.43301270, 0.00000000, 0.25000000], - [-0.37500000, -0.25000000, 0.21650635], - [-0.21650635, -0.43301270, 0.12500000], - [-0.00000000, -0.50000000, 0.00000000], - ], - [ - [0.00000000, 0.50000000, 0.00000000], - [0.00000000, 0.43301270, 0.25000000], - [0.00000000, 0.25000000, 0.43301270], - [0.00000000, 0.00000000, 0.50000000], - [0.00000000, -0.25000000, 0.43301270], - [0.00000000, -0.43301270, 0.25000000], - [0.00000000, -0.50000000, 0.00000000], - ], - [ - [0.00000000, 0.50000000, 0.00000000], - [0.21650635, 0.43301270, 0.12500000], - [0.37500000, 0.25000000, 0.21650635], - [0.43301270, 0.00000000, 0.25000000], - [0.37500000, -0.25000000, 0.21650635], - [0.21650635, -0.43301270, 0.12500000], - [0.00000000, -0.50000000, 0.00000000], - ], - [ - [0.00000000, 0.50000000, 0.00000000], - [0.21650635, 0.43301270, -0.12500000], - [0.37500000, 0.25000000, -0.21650635], - [0.43301270, 0.00000000, -0.25000000], - [0.37500000, -0.25000000, -0.21650635], - [0.21650635, -0.43301270, -0.12500000], - [0.00000000, -0.50000000, -0.00000000], - ], - ] - ), + [ + [ + [0.00000000, 0.50000000, 0.00000000], + [-0.00000000, 0.43301270, -0.25000000], + [-0.00000000, 0.25000000, -0.43301270], + [-0.00000000, 0.00000000, -0.50000000], + [-0.00000000, -0.25000000, -0.43301270], + [-0.00000000, -0.43301270, -0.25000000], + [-0.00000000, -0.50000000, -0.00000000], + ], + [ + [0.00000000, 0.50000000, 0.00000000], + [-0.21650635, 0.43301270, -0.12500000], + [-0.37500000, 0.25000000, -0.21650635], + [-0.43301270, 0.00000000, -0.25000000], + [-0.37500000, -0.25000000, -0.21650635], + [-0.21650635, -0.43301270, -0.12500000], + [-0.00000000, -0.50000000, -0.00000000], + ], + [ + [0.00000000, 0.50000000, 0.00000000], + [-0.21650635, 0.43301270, 0.12500000], + [-0.37500000, 0.25000000, 0.21650635], + [-0.43301270, 0.00000000, 0.25000000], + [-0.37500000, -0.25000000, 0.21650635], + [-0.21650635, -0.43301270, 0.12500000], + [-0.00000000, -0.50000000, 0.00000000], + ], + [ + [0.00000000, 0.50000000, 0.00000000], + [0.00000000, 0.43301270, 0.25000000], + [0.00000000, 0.25000000, 0.43301270], + [0.00000000, 0.00000000, 0.50000000], + [0.00000000, -0.25000000, 0.43301270], + [0.00000000, -0.43301270, 0.25000000], + [0.00000000, -0.50000000, 0.00000000], + ], + [ + [0.00000000, 0.50000000, 0.00000000], + [0.21650635, 0.43301270, 0.12500000], + [0.37500000, 0.25000000, 0.21650635], + [0.43301270, 0.00000000, 0.25000000], + [0.37500000, -0.25000000, 0.21650635], + [0.21650635, -0.43301270, 0.12500000], + [0.00000000, -0.50000000, 0.00000000], + ], + [ + [0.00000000, 0.50000000, 0.00000000], + [0.21650635, 0.43301270, -0.12500000], + [0.37500000, 0.25000000, -0.21650635], + [0.43301270, 0.00000000, -0.25000000], + [0.37500000, -0.25000000, -0.21650635], + [0.21650635, -0.43301270, -0.12500000], + [0.00000000, -0.50000000, -0.00000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_sphere(segments=6, axis="+x"), - np.array( - [ - [ - [0.50000000, 0.00000000, 0.00000000], - [0.43301270, -0.25000000, -0.00000000], - [0.25000000, -0.43301270, -0.00000000], - [0.00000000, -0.50000000, -0.00000000], - [-0.25000000, -0.43301270, -0.00000000], - [-0.43301270, -0.25000000, -0.00000000], - [-0.50000000, -0.00000000, -0.00000000], - ], - [ - [0.50000000, 0.00000000, 0.00000000], - [0.43301270, -0.12500000, -0.21650635], - [0.25000000, -0.21650635, -0.37500000], - [0.00000000, -0.25000000, -0.43301270], - [-0.25000000, -0.21650635, -0.37500000], - [-0.43301270, -0.12500000, -0.21650635], - [-0.50000000, -0.00000000, -0.00000000], - ], - [ - [0.50000000, 0.00000000, 0.00000000], - [0.43301270, 0.12500000, -0.21650635], - [0.25000000, 0.21650635, -0.37500000], - [0.00000000, 0.25000000, -0.43301270], - [-0.25000000, 0.21650635, -0.37500000], - [-0.43301270, 0.12500000, -0.21650635], - [-0.50000000, 0.00000000, -0.00000000], - ], - [ - [0.50000000, 0.00000000, 0.00000000], - [0.43301270, 0.25000000, 0.00000000], - [0.25000000, 0.43301270, 0.00000000], - [0.00000000, 0.50000000, 0.00000000], - [-0.25000000, 0.43301270, 0.00000000], - [-0.43301270, 0.25000000, 0.00000000], - [-0.50000000, 0.00000000, 0.00000000], - ], - [ - [0.50000000, 0.00000000, 0.00000000], - [0.43301270, 0.12500000, 0.21650635], - [0.25000000, 0.21650635, 0.37500000], - [0.00000000, 0.25000000, 0.43301270], - [-0.25000000, 0.21650635, 0.37500000], - [-0.43301270, 0.12500000, 0.21650635], - [-0.50000000, 0.00000000, 0.00000000], - ], - [ - [0.50000000, 0.00000000, 0.00000000], - [0.43301270, -0.12500000, 0.21650635], - [0.25000000, -0.21650635, 0.37500000], - [0.00000000, -0.25000000, 0.43301270], - [-0.25000000, -0.21650635, 0.37500000], - [-0.43301270, -0.12500000, 0.21650635], - [-0.50000000, -0.00000000, 0.00000000], - ], - ] - ), + [ + [ + [0.50000000, 0.00000000, 0.00000000], + [0.43301270, -0.25000000, -0.00000000], + [0.25000000, -0.43301270, -0.00000000], + [0.00000000, -0.50000000, -0.00000000], + [-0.25000000, -0.43301270, -0.00000000], + [-0.43301270, -0.25000000, -0.00000000], + [-0.50000000, -0.00000000, -0.00000000], + ], + [ + [0.50000000, 0.00000000, 0.00000000], + [0.43301270, -0.12500000, -0.21650635], + [0.25000000, -0.21650635, -0.37500000], + [0.00000000, -0.25000000, -0.43301270], + [-0.25000000, -0.21650635, -0.37500000], + [-0.43301270, -0.12500000, -0.21650635], + [-0.50000000, -0.00000000, -0.00000000], + ], + [ + [0.50000000, 0.00000000, 0.00000000], + [0.43301270, 0.12500000, -0.21650635], + [0.25000000, 0.21650635, -0.37500000], + [0.00000000, 0.25000000, -0.43301270], + [-0.25000000, 0.21650635, -0.37500000], + [-0.43301270, 0.12500000, -0.21650635], + [-0.50000000, 0.00000000, -0.00000000], + ], + [ + [0.50000000, 0.00000000, 0.00000000], + [0.43301270, 0.25000000, 0.00000000], + [0.25000000, 0.43301270, 0.00000000], + [0.00000000, 0.50000000, 0.00000000], + [-0.25000000, 0.43301270, 0.00000000], + [-0.43301270, 0.25000000, 0.00000000], + [-0.50000000, 0.00000000, 0.00000000], + ], + [ + [0.50000000, 0.00000000, 0.00000000], + [0.43301270, 0.12500000, 0.21650635], + [0.25000000, 0.21650635, 0.37500000], + [0.00000000, 0.25000000, 0.43301270], + [-0.25000000, 0.21650635, 0.37500000], + [-0.43301270, 0.12500000, 0.21650635], + [-0.50000000, 0.00000000, 0.00000000], + ], + [ + [0.50000000, 0.00000000, 0.00000000], + [0.43301270, -0.12500000, 0.21650635], + [0.25000000, -0.21650635, 0.37500000], + [0.00000000, -0.25000000, 0.43301270], + [-0.25000000, -0.21650635, 0.37500000], + [-0.43301270, -0.12500000, 0.21650635], + [-0.50000000, -0.00000000, 0.00000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_sphere( radius=100, segments=6, - origin=np.array([-0.2, -0.4, -0.6]), + origin=[-0.2, -0.4, -0.6], axis="+x", ), - np.array( - [ - [ - [99.80000000, -0.40000000, -0.60000000], - [86.40254038, -50.40000000, -0.60000000], - [49.80000000, -87.00254038, -0.60000000], - [-0.20000000, -100.40000000, -0.60000000], - [-50.20000000, -87.00254038, -0.60000000], - [-86.80254038, -50.40000000, -0.60000000], - [-100.20000000, -0.40000000, -0.60000000], - ], - [ - [99.80000000, -0.40000000, -0.60000000], - [86.40254038, -25.40000000, -43.90127019], - [49.80000000, -43.70127019, -75.60000000], - [-0.20000000, -50.40000000, -87.20254038], - [-50.20000000, -43.70127019, -75.60000000], - [-86.80254038, -25.40000000, -43.90127019], - [-100.20000000, -0.40000000, -0.60000000], - ], - [ - [99.80000000, -0.40000000, -0.60000000], - [86.40254038, 24.60000000, -43.90127019], - [49.80000000, 42.90127019, -75.60000000], - [-0.20000000, 49.60000000, -87.20254038], - [-50.20000000, 42.90127019, -75.60000000], - [-86.80254038, 24.60000000, -43.90127019], - [-100.20000000, -0.40000000, -0.60000000], - ], - [ - [99.80000000, -0.40000000, -0.60000000], - [86.40254038, 49.60000000, -0.60000000], - [49.80000000, 86.20254038, -0.60000000], - [-0.20000000, 99.60000000, -0.60000000], - [-50.20000000, 86.20254038, -0.60000000], - [-86.80254038, 49.60000000, -0.60000000], - [-100.20000000, -0.40000000, -0.60000000], - ], - [ - [99.80000000, -0.40000000, -0.60000000], - [86.40254038, 24.60000000, 42.70127019], - [49.80000000, 42.90127019, 74.40000000], - [-0.20000000, 49.60000000, 86.00254038], - [-50.20000000, 42.90127019, 74.40000000], - [-86.80254038, 24.60000000, 42.70127019], - [-100.20000000, -0.40000000, -0.60000000], - ], - [ - [99.80000000, -0.40000000, -0.60000000], - [86.40254038, -25.40000000, 42.70127019], - [49.80000000, -43.70127019, 74.40000000], - [-0.20000000, -50.40000000, 86.00254038], - [-50.20000000, -43.70127019, 74.40000000], - [-86.80254038, -25.40000000, 42.70127019], - [-100.20000000, -0.40000000, -0.60000000], - ], - ] - ), + [ + [ + [99.80000000, -0.40000000, -0.60000000], + [86.40254038, -50.40000000, -0.60000000], + [49.80000000, -87.00254038, -0.60000000], + [-0.20000000, -100.40000000, -0.60000000], + [-50.20000000, -87.00254038, -0.60000000], + [-86.80254038, -50.40000000, -0.60000000], + [-100.20000000, -0.40000000, -0.60000000], + ], + [ + [99.80000000, -0.40000000, -0.60000000], + [86.40254038, -25.40000000, -43.90127019], + [49.80000000, -43.70127019, -75.60000000], + [-0.20000000, -50.40000000, -87.20254038], + [-50.20000000, -43.70127019, -75.60000000], + [-86.80254038, -25.40000000, -43.90127019], + [-100.20000000, -0.40000000, -0.60000000], + ], + [ + [99.80000000, -0.40000000, -0.60000000], + [86.40254038, 24.60000000, -43.90127019], + [49.80000000, 42.90127019, -75.60000000], + [-0.20000000, 49.60000000, -87.20254038], + [-50.20000000, 42.90127019, -75.60000000], + [-86.80254038, 24.60000000, -43.90127019], + [-100.20000000, -0.40000000, -0.60000000], + ], + [ + [99.80000000, -0.40000000, -0.60000000], + [86.40254038, 49.60000000, -0.60000000], + [49.80000000, 86.20254038, -0.60000000], + [-0.20000000, 99.60000000, -0.60000000], + [-50.20000000, 86.20254038, -0.60000000], + [-86.80254038, 49.60000000, -0.60000000], + [-100.20000000, -0.40000000, -0.60000000], + ], + [ + [99.80000000, -0.40000000, -0.60000000], + [86.40254038, 24.60000000, 42.70127019], + [49.80000000, 42.90127019, 74.40000000], + [-0.20000000, 49.60000000, 86.00254038], + [-50.20000000, 42.90127019, 74.40000000], + [-86.80254038, 24.60000000, 42.70127019], + [-100.20000000, -0.40000000, -0.60000000], + ], + [ + [99.80000000, -0.40000000, -0.60000000], + [86.40254038, -25.40000000, 42.70127019], + [49.80000000, -43.70127019, 74.40000000], + [-0.20000000, -50.40000000, 86.00254038], + [-50.20000000, -43.70127019, 74.40000000], + [-86.80254038, -25.40000000, 42.70127019], + [-100.20000000, -0.40000000, -0.60000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) for plane in ("xy", "xz", "yz"): - np.testing.assert_allclose( + xp_assert_close( primitive_vertices_sphere(axis=plane), primitive_vertices_sphere(axis=MAPPING_PLANE_TO_AXIS[plane]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - pytest.raises(ValueError, lambda: primitive_vertices_quad_mpl(axis="Undefined")) + with pytest.raises(ValueError): + primitive_vertices_quad_mpl(axis="Undefined") diff --git a/colour/geometry/vertices.py b/colour/geometry/vertices.py index 07282f9906..f5482e11b3 100644 --- a/colour/geometry/vertices.py +++ b/colour/geometry/vertices.py @@ -17,8 +17,6 @@ import typing -import numpy as np - from colour.algebra import spherical_to_cartesian from colour.geometry import MAPPING_PLANE_TO_AXIS @@ -27,6 +25,7 @@ from colour.utilities import ( CanonicalMapping, + array_namespace, as_float_array, filter_kwargs, full, @@ -34,6 +33,8 @@ tsplit, tstack, validate_method, + xp_matrix_transpose, + xp_radians, zeros, ) @@ -414,40 +415,46 @@ def primitive_vertices_sphere( origin = as_float_array(origin) + xp = array_namespace(origin) + axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower() axis = validate_method( axis, ("+x", "+y", "+z"), '"{0}" axis invalid, it must be one of {1}!' ) if not intermediate: - theta = np.tile( - np.radians(np.linspace(0, 180, segments + 1)), + theta = xp.tile( + xp_radians(xp.linspace(0, 180, segments + 1)), (int(segments) + 1, 1), ) - phi = np.transpose( - np.tile( - np.radians(np.linspace(-180, 180, segments + 1)), + phi = xp_matrix_transpose( + xp.tile( + xp_radians(xp.linspace(-180, 180, segments + 1)), (int(segments) + 1, 1), - ) + ), + xp=xp, ) else: - theta = np.tile( - np.radians(np.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]), + theta = xp.tile( + xp_radians(xp.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]), (int(segments) + 1, 1), ) - theta = np.hstack( + + theta = xp.concat( [ zeros((segments + 1, 1)), theta, - full((segments + 1, 1), np.pi), - ] + full((segments + 1, 1), xp.pi), + ], + axis=1, ) - phi = np.transpose( - np.tile( - np.radians(np.linspace(-180, 180, segments + 1)) - + np.radians(360 / segments / 2), + phi = xp_matrix_transpose( + xp.tile( + xp_radians(xp.linspace(-180, 180, segments + 1)) + + xp_radians(360 / segments / 2), (int(segments), 1), - ) + ), + xp=xp, ) rho = ones(phi.shape) * radius @@ -461,9 +468,9 @@ def primitive_vertices_sphere( if axis == "+z": pass elif axis == "+y": - vertices = np.roll(vertices, 2, -1) + vertices = xp.roll(vertices, 2, axis=-1) elif axis == "+x": - vertices = np.roll(vertices, 1, -1) + vertices = xp.roll(vertices, 1, axis=-1) return vertices + origin diff --git a/colour/graph/conversion.py b/colour/graph/conversion.py index aa8e6b63c1..e97b333ae6 100644 --- a/colour/graph/conversion.py +++ b/colour/graph/conversion.py @@ -202,15 +202,19 @@ from colour.recovery import XYZ_to_sd from colour.temperature import CCT_to_mired, CCT_to_uv, mired_to_CCT, uv_to_CCT from colour.utilities import ( + array_namespace, as_float_array, domain_range_scale, filter_kwargs, get_domain_range_scale_metadata, + is_array_api_enabled, + is_non_ndarray, message_box, required, tsplit, tstack, validate_method, + xp_as_float_array, zeros, ) @@ -1979,7 +1983,7 @@ def describe_conversion_path( if filtered_kwargs: message += f"\n\n[ Filtered Arguments ]\n\n{pformat(filtered_kwargs)}" - if mode in ("extended",): + if mode == "extended": docstring = textwrap.dedent( str(_lower_order_function(conversion_function).__doc__) ).strip() @@ -2253,8 +2257,35 @@ def convert( # conversion function name. filtered_kwargs.update(kwargs.get(conversion_function_name, {})) + # Promote numpy array kwargs to match the input's backend, + # preventing device mismatches when partial() bakes numpy + # defaults (e.g. XYZ_w) that meet backend tensors at runtime. + promoted_function = conversion_function + if is_array_api_enabled() and is_non_ndarray(a): + xp = array_namespace(a) + + if isinstance(conversion_function, partial): + promoted = { + kwarg: xp_as_float_array(value, xp=xp, like=a) + for kwarg, value in conversion_function.keywords.items() + if isinstance(value, np.ndarray) + } + if promoted: + promoted_function = partial( + conversion_function.func, + **{**conversion_function.keywords, **promoted}, + ) + else: + promoted_function = conversion_function + else: + promoted_function = conversion_function + + for kwarg, value in list(filtered_kwargs.items()): + if isinstance(value, np.ndarray): + filtered_kwargs[kwarg] = xp_as_float_array(value, xp=xp, like=a) + with domain_range_scale("1"): - a = conversion_function(a, **filtered_kwargs) + a = promoted_function(a, **filtered_kwargs) # Scale output from scale-1 to reference on last iteration if i == len(conversion_path_list) - 1 and to_reference_scale: diff --git a/colour/graph/tests/test_common.py b/colour/graph/tests/test_common.py index 93653b606c..7fe66e6401 100644 --- a/colour/graph/tests/test_common.py +++ b/colour/graph/tests/test_common.py @@ -2,13 +2,13 @@ from __future__ import annotations -import unittest - import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.graph import colourspace_model_to_reference from colour.models import COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -22,7 +22,7 @@ ] -class TestColourspaceModelToReference(unittest.TestCase): +class TestColourspaceModelToReference: """ Define :func:`colour.graph.common.colourspace_model_to_reference` definition unit tests methods. @@ -36,62 +36,65 @@ def test_colourspace_model_to_reference(self) -> None: Lab_1 = np.array([0.41527875, 0.52638583, 0.26923179]) Lab_reference = colourspace_model_to_reference(Lab_1, "CIE Lab") - np.testing.assert_allclose( + xp_assert_close( Lab_reference, - np.array([41.52787529, 52.63858304, 26.92317922]), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) Luv_1 = np.array([0.41527875, 0.52638583, 0.26923179]) Luv_reference = colourspace_model_to_reference(Luv_1, "CIE Luv") - np.testing.assert_allclose( + xp_assert_close( Luv_reference, - np.array([41.52787529, 52.63858304, 26.92317922]), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) CAM02LCD_1 = np.array([0.5, 0.5, 0.5]) CAM02LCD_reference = colourspace_model_to_reference(CAM02LCD_1, "CAM02LCD") - np.testing.assert_allclose( + xp_assert_close( CAM02LCD_reference, - np.array([50.0, 50.0, 50.0]), + [50.0, 50.0, 50.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) XYZ_1 = np.array([0.20654008, 0.12197225, 0.05136952]) XYZ_reference = colourspace_model_to_reference(XYZ_1, "CIE XYZ") - np.testing.assert_allclose( + xp_assert_close( XYZ_reference, - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) RGB_1 = np.array([0.5, 0.3, 0.8]) RGB_reference = colourspace_model_to_reference(RGB_1, "RGB") - np.testing.assert_allclose( + xp_assert_close( RGB_reference, - np.array([0.5, 0.3, 0.8]), + [0.5, 0.3, 0.8], atol=TOLERANCE_ABSOLUTE_TESTS, ) value_1 = np.array([0.5, 0.5, 0.5]) value_reference = colourspace_model_to_reference(value_1, "Invalid Model") - np.testing.assert_allclose( + xp_assert_close( value_reference, value_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - for model in COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE: - with self.subTest(model=model): - np.testing.assert_allclose( - colourspace_model_to_reference(value_1, model), - value_1 - * COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model], - atol=TOLERANCE_ABSOLUTE_TESTS, - err_msg=f"Mismatch for model: {model}", - ) - + @pytest.mark.parametrize( + "model", list(COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE) + ) + def test_colourspace_model_to_reference_all_models(self, model: str) -> None: + """ + Test :func:`colour.graph.common.colourspace_model_to_reference` + definition for all models. + """ -if __name__ == "__main__": - unittest.main() + value_1 = np.array([0.5, 0.5, 0.5]) + xp_assert_close( + colourspace_model_to_reference(value_1, model), + value_1 * COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model], + atol=TOLERANCE_ABSOLUTE_TESTS, + err_msg=f"Mismatch for model: {model}", + ) diff --git a/colour/graph/tests/test_conversion.py b/colour/graph/tests/test_conversion.py index e517b58684..a9f0c157a5 100644 --- a/colour/graph/tests/test_conversion.py +++ b/colour/graph/tests/test_conversion.py @@ -21,7 +21,10 @@ RGB_COLOURSPACE_ACES2065_1, XYZ_to_Lab, ) -from colour.utilities import get_domain_range_scale_metadata # noqa: E402 +from colour.utilities import ( # noqa: E402 + get_domain_range_scale_metadata, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -77,16 +80,16 @@ def test_convert(self) -> None: "Spectral Distribution", "sRGB", ) - np.testing.assert_allclose( + xp_assert_close( RGB_a, - np.array([0.49034776, 0.30185875, 0.23587685]), + [0.49034776, 0.30185875, 0.23587685], atol=TOLERANCE_ABSOLUTE_TESTS, ) Jpapbp = convert(RGB_a, "Output-Referred RGB", "CAM16UCS") - np.testing.assert_allclose( + xp_assert_close( Jpapbp, - np.array([0.40738741, 0.12046560, 0.09284385]), + [0.40738741, 0.12046560, 0.09284385], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -94,42 +97,42 @@ def test_convert(self) -> None: # NOTE: The "CIE XYZ" tristimulus values to "sRGB" matrix is given # rounded at 4 decimals as per "IEC 61966-2-1:1999" and thus preventing # exact roundtrip. - np.testing.assert_allclose(RGB_a, RGB_b, atol=1e-4) + xp_assert_close(RGB_a, RGB_b, atol=TOLERANCE_ABSOLUTE_TESTS * 1000) - np.testing.assert_allclose( + xp_assert_close( convert("#808080", "Hexadecimal", "Scene-Referred RGB"), - np.array([0.21586050, 0.21586050, 0.21586050]), + [0.21586050, 0.21586050, 0.21586050], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( convert("#808080", "Hexadecimal", "RGB Luminance"), 0.21586050, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( convert( convert( - np.array([0.5, 0.5, 0.5]), + [0.5, 0.5, 0.5], "Output-Referred RGB", "Scene-Referred RGB", ), "RGB", "YCbCr", ), - np.array([0.49215686, 0.50196078, 0.50196078]), + [0.49215686, 0.50196078, 0.50196078], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( convert( RGB_a, "RGB", "Scene-Referred RGB", RGB_to_RGB={"output_colourspace": RGB_COLOURSPACE_ACES2065_1}, ), - np.array([0.37308227, 0.31241444, 0.24746366]), + [0.37308227, 0.31241444, 0.24746366], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -150,7 +153,7 @@ def test_convert_direct_keyword_argument_passing(self) -> None: a = np.array([0.20654008, 0.12197225, 0.05136952]) illuminant = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D50"] - np.testing.assert_allclose( + xp_assert_close( convert(a, "CIE XYZ", "CIE UVW", XYZ_to_UVW={"illuminant": illuminant}), convert(a, "CIE XYZ", "CIE UVW", illuminant=illuminant), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -159,15 +162,13 @@ def test_convert_direct_keyword_argument_passing(self) -> None: # Illuminant "ndarray" is converted to tuple here so that it can # be hashed by the "sd_to_XYZ" definition, this should never occur # in practical application. - pytest.raises( - AttributeError, - lambda: convert( + with pytest.raises(AttributeError): + convert( SDS_COLOURCHECKERS["ColorChecker N Ohta"]["dark skin"], "Spectral Distribution", "sRGB", illuminant=tuple(illuminant), - ), - ) + ) def test_convert_reference_scale(self) -> None: """ @@ -184,7 +185,7 @@ def test_convert_reference_scale(self) -> None: range_scale = metadata["range"] Lab_manual_scaled = Lab_manual * range_scale - np.testing.assert_allclose( + xp_assert_close( Lab_auto, Lab_manual_scaled, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -202,7 +203,7 @@ def test_convert_reference_scale(self) -> None: Lab_manual_normalized = Lab_native / range_scale XYZ_manual = convert(Lab_manual_normalized, "CIE Lab", "CIE XYZ") - np.testing.assert_allclose( + xp_assert_close( XYZ_auto, XYZ_manual, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -220,7 +221,7 @@ def test_convert_reference_scale(self) -> None: from_reference_scale=True, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_roundtrip, XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -233,7 +234,7 @@ def test_convert_reference_scale(self) -> None: LCHab = convert(XYZ, "CIE XYZ", "CIE LCHab", to_reference_scale=True) # L component should be identical - np.testing.assert_allclose( + xp_assert_close( Lab[0], LCHab[0], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -241,7 +242,7 @@ def test_convert_reference_scale(self) -> None: # C should equal sqrt(a^2 + b^2) expected_C = np.sqrt(Lab[1] ** 2 + Lab[2] ** 2) - np.testing.assert_allclose( + xp_assert_close( LCHab[1], expected_C, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -255,7 +256,7 @@ def test_convert_reference_scale(self) -> None: from_reference_scale=True, to_reference_scale=True, ) - np.testing.assert_allclose( + xp_assert_close( Lab_roundtrip, Lab, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -268,7 +269,7 @@ def test_convert_reference_scale(self) -> None: from_reference_scale=True, to_reference_scale=True, ) - np.testing.assert_allclose( + xp_assert_close( LCHab_roundtrip, LCHab, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -279,7 +280,7 @@ def test_convert_reference_scale(self) -> None: LCHuv = convert(XYZ, "CIE XYZ", "CIE LCHuv", to_reference_scale=True) # L component should be identical - np.testing.assert_allclose( + xp_assert_close( Luv[0], LCHuv[0], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -287,7 +288,7 @@ def test_convert_reference_scale(self) -> None: # C should equal sqrt(u^2 + v^2) expected_C = np.sqrt(Luv[1] ** 2 + Luv[2] ** 2) - np.testing.assert_allclose( + xp_assert_close( LCHuv[1], expected_C, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -301,7 +302,7 @@ def test_convert_reference_scale(self) -> None: from_reference_scale=True, to_reference_scale=True, ) - np.testing.assert_allclose( + xp_assert_close( Luv_roundtrip, Luv, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -314,7 +315,7 @@ def test_convert_reference_scale(self) -> None: from_reference_scale=True, to_reference_scale=True, ) - np.testing.assert_allclose( + xp_assert_close( LCHuv_roundtrip, LCHuv, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/io/luts/lut.py b/colour/io/luts/lut.py index 8fa845768f..465db2eb19 100644 --- a/colour/io/luts/lut.py +++ b/colour/io/luts/lut.py @@ -2130,7 +2130,7 @@ def invert(self, **kwargs: Any) -> LUT3D: weights = weights / np.sum(weights, axis=1, keepdims=True) # Weighted average: sum over neighbors dimension - weighted_table = np.sum(table[indices] * weights[..., np.newaxis], axis=1) + weighted_table = np.sum(table[indices] * weights[..., None], axis=1) LUT_q.table = np.reshape( weighted_table, diff --git a/colour/io/luts/tests/test__init__.py b/colour/io/luts/tests/test__init__.py index e60458bd49..e5676e88d3 100644 --- a/colour/io/luts/tests/test__init__.py +++ b/colour/io/luts/tests/test__init__.py @@ -6,12 +6,12 @@ import shutil import tempfile -import numpy as np import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.hints import cast from colour.io import LUT1D, LUTSequence, read_LUT, write_LUT +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -43,33 +43,31 @@ def test_read_LUT(self) -> None: read_LUT(os.path.join(ROOT_LUTS, "sony_spi1d", "eotf_sRGB_1D.spi1d")), ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.table, - np.array( - [ - -7.73990000e-03, - 5.16000000e-04, - 1.22181000e-02, - 3.96819000e-02, - 8.71438000e-02, - 1.57439400e-01, - 2.52950100e-01, - 3.75757900e-01, - 5.27729400e-01, - 7.10566500e-01, - 9.25840600e-01, - 1.17501630e00, - 1.45946870e00, - 1.78049680e00, - 2.13933380e00, - 2.53715520e00, - ] - ), + [ + -7.73990000e-03, + 5.16000000e-04, + 1.22181000e-02, + 3.96819000e-02, + 8.71438000e-02, + 1.57439400e-01, + 2.52950100e-01, + 3.75757900e-01, + 5.27729400e-01, + 7.10566500e-01, + 9.25840600e-01, + 1.17501630e00, + 1.45946870e00, + 1.78049680e00, + 2.13933380e00, + 2.53715520e00, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "eotf sRGB 1D" assert LUT_1.dimensions == 1 - np.testing.assert_array_equal(LUT_1.domain, np.array([-0.1, 1.5])) + xp_assert_equal(LUT_1.domain, [-0.1, 1.5]) assert LUT_1.size == 16 assert LUT_1.comments == [ 'Generated by "Colour 0.3.11".', @@ -80,28 +78,26 @@ def test_read_LUT(self) -> None: "LUTSequence", read_LUT(os.path.join(ROOT_LUTS, "resolve_cube", "LogC_Video.cube")), ) - np.testing.assert_allclose( + xp_assert_close( LUT_2[0].table, - np.array( - [ - [0.00000000, 0.00000000, 0.00000000], - [0.02708500, 0.02708500, 0.02708500], - [0.06304900, 0.06304900, 0.06304900], - [0.11314900, 0.11314900, 0.11314900], - [0.18304900, 0.18304900, 0.18304900], - [0.28981100, 0.28981100, 0.28981100], - [0.41735300, 0.41735300, 0.41735300], - [0.54523100, 0.54523100, 0.54523100], - [0.67020500, 0.67020500, 0.67020500], - [0.78963000, 0.78963000, 0.78963000], - [0.88646800, 0.88646800, 0.88646800], - [0.94549100, 0.94549100, 0.94549100], - [0.97644900, 0.97644900, 0.97644900], - [0.98924800, 0.98924800, 0.98924800], - [0.99379700, 0.99379700, 0.99379700], - [1.00000000, 1.00000000, 1.00000000], - ] - ), + [ + [0.00000000, 0.00000000, 0.00000000], + [0.02708500, 0.02708500, 0.02708500], + [0.06304900, 0.06304900, 0.06304900], + [0.11314900, 0.11314900, 0.11314900], + [0.18304900, 0.18304900, 0.18304900], + [0.28981100, 0.28981100, 0.28981100], + [0.41735300, 0.41735300, 0.41735300], + [0.54523100, 0.54523100, 0.54523100], + [0.67020500, 0.67020500, 0.67020500], + [0.78963000, 0.78963000, 0.78963000], + [0.88646800, 0.88646800, 0.88646800], + [0.94549100, 0.94549100, 0.94549100], + [0.97644900, 0.97644900, 0.97644900], + [0.98924800, 0.98924800, 0.98924800], + [0.99379700, 0.99379700, 0.99379700], + [1.00000000, 1.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_2[1].size == 4 @@ -119,11 +115,8 @@ def test_raise_exception_read_LUT(self) -> None: exception. """ - pytest.raises( - ValueError, - read_LUT, - os.path.join(ROOT_LUTS, "sony_spi1d", "Exception_Raising.spi1d"), - ) + with pytest.raises(ValueError): + read_LUT(os.path.join(ROOT_LUTS, "sony_spi1d", "Exception_Raising.spi1d")) class TestWriteLUT: diff --git a/colour/io/luts/tests/test_cinespace_csp.py b/colour/io/luts/tests/test_cinespace_csp.py index 459aea930e..fbc0223439 100644 --- a/colour/io/luts/tests/test_cinespace_csp.py +++ b/colour/io/luts/tests/test_cinespace_csp.py @@ -19,7 +19,7 @@ read_LUT_Cinespace, write_LUT_Cinespace, ) -from colour.utilities import tstack +from colour.utilities import tstack, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -54,55 +54,53 @@ def test_read_LUT_Cinespace(self) -> None: read_LUT_Cinespace(os.path.join(ROOT_LUTS, "ACES_Proxy_10_to_ACES.csp")), ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.table, - np.array( - [ - [4.88300000e-04, 4.88300000e-04, 4.88300000e-04], - [7.71400000e-04, 7.71400000e-04, 7.71400000e-04], - [1.21900000e-03, 1.21900000e-03, 1.21900000e-03], - [1.92600000e-03, 1.92600000e-03, 1.92600000e-03], - [3.04400000e-03, 3.04400000e-03, 3.04400000e-03], - [4.80900000e-03, 4.80900000e-03, 4.80900000e-03], - [7.59900000e-03, 7.59900000e-03, 7.59900000e-03], - [1.20100000e-02, 1.20100000e-02, 1.20100000e-02], - [1.89700000e-02, 1.89700000e-02, 1.89700000e-02], - [2.99800000e-02, 2.99800000e-02, 2.99800000e-02], - [4.73700000e-02, 4.73700000e-02, 4.73700000e-02], - [7.48400000e-02, 7.48400000e-02, 7.48400000e-02], - [1.18300000e-01, 1.18300000e-01, 1.18300000e-01], - [1.86900000e-01, 1.86900000e-01, 1.86900000e-01], - [2.95200000e-01, 2.95200000e-01, 2.95200000e-01], - [4.66500000e-01, 4.66500000e-01, 4.66500000e-01], - [7.37100000e-01, 7.37100000e-01, 7.37100000e-01], - [1.16500000e00, 1.16500000e00, 1.16500000e00], - [1.84000000e00, 1.84000000e00, 1.84000000e00], - [2.90800000e00, 2.90800000e00, 2.90800000e00], - [4.59500000e00, 4.59500000e00, 4.59500000e00], - [7.26000000e00, 7.26000000e00, 7.26000000e00], - [1.14700000e01, 1.14700000e01, 1.14700000e01], - [1.81300000e01, 1.81300000e01, 1.81300000e01], - [2.86400000e01, 2.86400000e01, 2.86400000e01], - [4.52500000e01, 4.52500000e01, 4.52500000e01], - [7.15100000e01, 7.15100000e01, 7.15100000e01], - [1.13000000e02, 1.13000000e02, 1.13000000e02], - [1.78500000e02, 1.78500000e02, 1.78500000e02], - [2.82100000e02, 2.82100000e02, 2.82100000e02], - [4.45700000e02, 4.45700000e02, 4.45700000e02], - [7.04300000e02, 7.04300000e02, 7.04300000e02], - ] - ), + [ + [4.88300000e-04, 4.88300000e-04, 4.88300000e-04], + [7.71400000e-04, 7.71400000e-04, 7.71400000e-04], + [1.21900000e-03, 1.21900000e-03, 1.21900000e-03], + [1.92600000e-03, 1.92600000e-03, 1.92600000e-03], + [3.04400000e-03, 3.04400000e-03, 3.04400000e-03], + [4.80900000e-03, 4.80900000e-03, 4.80900000e-03], + [7.59900000e-03, 7.59900000e-03, 7.59900000e-03], + [1.20100000e-02, 1.20100000e-02, 1.20100000e-02], + [1.89700000e-02, 1.89700000e-02, 1.89700000e-02], + [2.99800000e-02, 2.99800000e-02, 2.99800000e-02], + [4.73700000e-02, 4.73700000e-02, 4.73700000e-02], + [7.48400000e-02, 7.48400000e-02, 7.48400000e-02], + [1.18300000e-01, 1.18300000e-01, 1.18300000e-01], + [1.86900000e-01, 1.86900000e-01, 1.86900000e-01], + [2.95200000e-01, 2.95200000e-01, 2.95200000e-01], + [4.66500000e-01, 4.66500000e-01, 4.66500000e-01], + [7.37100000e-01, 7.37100000e-01, 7.37100000e-01], + [1.16500000e00, 1.16500000e00, 1.16500000e00], + [1.84000000e00, 1.84000000e00, 1.84000000e00], + [2.90800000e00, 2.90800000e00, 2.90800000e00], + [4.59500000e00, 4.59500000e00, 4.59500000e00], + [7.26000000e00, 7.26000000e00, 7.26000000e00], + [1.14700000e01, 1.14700000e01, 1.14700000e01], + [1.81300000e01, 1.81300000e01, 1.81300000e01], + [2.86400000e01, 2.86400000e01, 2.86400000e01], + [4.52500000e01, 4.52500000e01, 4.52500000e01], + [7.15100000e01, 7.15100000e01, 7.15100000e01], + [1.13000000e02, 1.13000000e02, 1.13000000e02], + [1.78500000e02, 1.78500000e02, 1.78500000e02], + [2.82100000e02, 2.82100000e02, 2.82100000e02], + [4.45700000e02, 4.45700000e02, 4.45700000e02], + [7.04300000e02, 7.04300000e02, 7.04300000e02], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "ACES Proxy 10 to ACES" assert LUT_1.dimensions == 2 - np.testing.assert_array_equal(LUT_1.domain, np.array([[0, 0, 0], [1, 1, 1]])) + xp_assert_equal(LUT_1.domain, [[0, 0, 0], [1, 1, 1]]) assert LUT_1.size == 32 assert LUT_1.comments == [] LUT_2 = cast("LUT3x1D", read_LUT_Cinespace(os.path.join(ROOT_LUTS, "Demo.csp"))) assert LUT_2.comments == ["Comments are ignored by most parsers"] - np.testing.assert_array_equal(LUT_2.domain, np.array([[0, 0, 0], [1, 2, 3]])) + xp_assert_equal(LUT_2.domain, [[0, 0, 0], [1, 2, 3]]) LUT_3 = cast( "LUT3D", @@ -207,10 +205,10 @@ def test_write_LUT_Cinespace(self) -> None: LUT_4_r = cast( "LUT3x1D", read_LUT_Cinespace(os.path.join(ROOT_LUTS, "Ragged_Domain.csp")) ) - np.testing.assert_allclose( - LUT_4_t.domain, LUT_4_r.domain, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close(LUT_4_t.domain, LUT_4_r.domain, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + LUT_4_t.table, LUT_4_r.table, atol=TOLERANCE_ABSOLUTE_TESTS * 500 ) - np.testing.assert_allclose(LUT_4_t.table, LUT_4_r.table, atol=5e-5) LUT_5_r = cast( "LUTSequence", @@ -300,4 +298,5 @@ def test_raise_exception_write_LUT_Cinespace(self) -> None: definition raised exception. """ - pytest.raises(TypeError, write_LUT_Cinespace, object(), "") + with pytest.raises(TypeError): + write_LUT_Cinespace(object(), "") # pyright: ignore diff --git a/colour/io/luts/tests/test_iridas_cube.py b/colour/io/luts/tests/test_iridas_cube.py index be8df222b8..00ef548492 100644 --- a/colour/io/luts/tests/test_iridas_cube.py +++ b/colour/io/luts/tests/test_iridas_cube.py @@ -6,11 +6,10 @@ import shutil import tempfile -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.hints import cast from colour.io import LUT1D, LUTSequence, read_LUT_IridasCube, write_LUT_IridasCube +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -44,55 +43,53 @@ def test_read_LUT_IridasCube(self) -> None: os.path.join(ROOT_LUTS, "ACES_Proxy_10_to_ACES.cube") ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.table, - np.array( - [ - [4.88300000e-04, 4.88300000e-04, 4.88300000e-04], - [7.71400000e-04, 7.71400000e-04, 7.71400000e-04], - [1.21900000e-03, 1.21900000e-03, 1.21900000e-03], - [1.92600000e-03, 1.92600000e-03, 1.92600000e-03], - [3.04400000e-03, 3.04400000e-03, 3.04400000e-03], - [4.80900000e-03, 4.80900000e-03, 4.80900000e-03], - [7.59900000e-03, 7.59900000e-03, 7.59900000e-03], - [1.20100000e-02, 1.20100000e-02, 1.20100000e-02], - [1.89700000e-02, 1.89700000e-02, 1.89700000e-02], - [2.99800000e-02, 2.99800000e-02, 2.99800000e-02], - [4.73700000e-02, 4.73700000e-02, 4.73700000e-02], - [7.48400000e-02, 7.48400000e-02, 7.48400000e-02], - [1.18300000e-01, 1.18300000e-01, 1.18300000e-01], - [1.86900000e-01, 1.86900000e-01, 1.86900000e-01], - [2.95200000e-01, 2.95200000e-01, 2.95200000e-01], - [4.66500000e-01, 4.66500000e-01, 4.66500000e-01], - [7.37100000e-01, 7.37100000e-01, 7.37100000e-01], - [1.16500000e00, 1.16500000e00, 1.16500000e00], - [1.84000000e00, 1.84000000e00, 1.84000000e00], - [2.90800000e00, 2.90800000e00, 2.90800000e00], - [4.59500000e00, 4.59500000e00, 4.59500000e00], - [7.26000000e00, 7.26000000e00, 7.26000000e00], - [1.14700000e01, 1.14700000e01, 1.14700000e01], - [1.81300000e01, 1.81300000e01, 1.81300000e01], - [2.86400000e01, 2.86400000e01, 2.86400000e01], - [4.52500000e01, 4.52500000e01, 4.52500000e01], - [7.15100000e01, 7.15100000e01, 7.15100000e01], - [1.13000000e02, 1.13000000e02, 1.13000000e02], - [1.78500000e02, 1.78500000e02, 1.78500000e02], - [2.82100000e02, 2.82100000e02, 2.82100000e02], - [4.45700000e02, 4.45700000e02, 4.45700000e02], - [7.04300000e02, 7.04300000e02, 7.04300000e02], - ] - ), + [ + [4.88300000e-04, 4.88300000e-04, 4.88300000e-04], + [7.71400000e-04, 7.71400000e-04, 7.71400000e-04], + [1.21900000e-03, 1.21900000e-03, 1.21900000e-03], + [1.92600000e-03, 1.92600000e-03, 1.92600000e-03], + [3.04400000e-03, 3.04400000e-03, 3.04400000e-03], + [4.80900000e-03, 4.80900000e-03, 4.80900000e-03], + [7.59900000e-03, 7.59900000e-03, 7.59900000e-03], + [1.20100000e-02, 1.20100000e-02, 1.20100000e-02], + [1.89700000e-02, 1.89700000e-02, 1.89700000e-02], + [2.99800000e-02, 2.99800000e-02, 2.99800000e-02], + [4.73700000e-02, 4.73700000e-02, 4.73700000e-02], + [7.48400000e-02, 7.48400000e-02, 7.48400000e-02], + [1.18300000e-01, 1.18300000e-01, 1.18300000e-01], + [1.86900000e-01, 1.86900000e-01, 1.86900000e-01], + [2.95200000e-01, 2.95200000e-01, 2.95200000e-01], + [4.66500000e-01, 4.66500000e-01, 4.66500000e-01], + [7.37100000e-01, 7.37100000e-01, 7.37100000e-01], + [1.16500000e00, 1.16500000e00, 1.16500000e00], + [1.84000000e00, 1.84000000e00, 1.84000000e00], + [2.90800000e00, 2.90800000e00, 2.90800000e00], + [4.59500000e00, 4.59500000e00, 4.59500000e00], + [7.26000000e00, 7.26000000e00, 7.26000000e00], + [1.14700000e01, 1.14700000e01, 1.14700000e01], + [1.81300000e01, 1.81300000e01, 1.81300000e01], + [2.86400000e01, 2.86400000e01, 2.86400000e01], + [4.52500000e01, 4.52500000e01, 4.52500000e01], + [7.15100000e01, 7.15100000e01, 7.15100000e01], + [1.13000000e02, 1.13000000e02, 1.13000000e02], + [1.78500000e02, 1.78500000e02, 1.78500000e02], + [2.82100000e02, 2.82100000e02, 2.82100000e02], + [4.45700000e02, 4.45700000e02, 4.45700000e02], + [7.04300000e02, 7.04300000e02, 7.04300000e02], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "ACES Proxy 10 to ACES" assert LUT_1.dimensions == 2 - np.testing.assert_array_equal(LUT_1.domain, np.array([[0, 0, 0], [1, 1, 1]])) + xp_assert_equal(LUT_1.domain, [[0, 0, 0], [1, 1, 1]]) assert LUT_1.size == 32 assert LUT_1.comments == [] LUT_2 = read_LUT_IridasCube(os.path.join(ROOT_LUTS, "Demo.cube")) assert LUT_2.comments == ["Comments can go anywhere"] - np.testing.assert_array_equal(LUT_2.domain, np.array([[0, 0, 0], [1, 2, 3]])) + xp_assert_equal(LUT_2.domain, [[0, 0, 0], [1, 2, 3]]) LUT_3 = read_LUT_IridasCube( os.path.join(ROOT_LUTS, "Three_Dimensional_Table.cube") diff --git a/colour/io/luts/tests/test_lut.py b/colour/io/luts/tests/test_lut.py index 09072a7de7..545049e965 100644 --- a/colour/io/luts/tests/test_lut.py +++ b/colour/io/luts/tests/test_lut.py @@ -24,7 +24,14 @@ from colour.io.luts import LUT1D, LUT3D, LUT3x1D, LUT_to_LUT from colour.io.luts.lut import AbstractLUT -from colour.utilities import as_float_array, is_scipy_installed, tsplit, tstack +from colour.utilities import ( + as_float_array, + is_scipy_installed, + tsplit, + tstack, + xp_assert_close, + xp_assert_equal, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -251,13 +258,11 @@ def test__init__(self) -> None: LUT = LUT1D(self._table_1) - np.testing.assert_allclose( - LUT.table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(LUT.table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS) assert str(id(LUT)) == LUT.name - np.testing.assert_array_equal(LUT.domain, self._domain_1) + xp_assert_equal(LUT.domain, self._domain_1) assert LUT.dimensions == self._dimensions @@ -270,11 +275,11 @@ def test_table(self) -> None: LUT = LUT1D() - np.testing.assert_array_equal(LUT.table, LUT.linear_table(self._size)) + xp_assert_equal(LUT.table, LUT.linear_table(self._size)) table_1 = self._table_1 * 0.8 + 0.1 LUT.table = table_1 - np.testing.assert_allclose(LUT.table, table_1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(LUT.table, table_1, atol=TOLERANCE_ABSOLUTE_TESTS) def test_name(self) -> None: """ @@ -296,11 +301,11 @@ def test_domain(self) -> None: LUT = LUT1D() - np.testing.assert_array_equal(LUT.domain, self._domain_1) + xp_assert_equal(LUT.domain, self._domain_1) domain = self._domain_1 * 0.8 + 0.1 LUT.domain = domain - np.testing.assert_array_equal(LUT.domain, domain) + xp_assert_equal(LUT.domain, domain) def test_size(self) -> None: """ @@ -400,91 +405,91 @@ def test_arithmetical_operation(self) -> None: LUT_1 = LUT1D() LUT_2 = LUT1D() - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "+", False).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "-", False).table, self._table_1 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "*", False).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "/", False).table, self._table_1 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "**", False).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 + 10).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 - 10).table, self._table_1 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 * 10).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 / 10).table, self._table_1 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1**10).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "+", True).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "-", True).table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "*", True).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "/", True).table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "**", True).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -492,13 +497,13 @@ def test_arithmetical_operation(self) -> None: LUT_2 = LUT1D() - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(self._table_1, "+", False).table, LUT_2.table + self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(LUT_2, "+", False).table, LUT_2.table + LUT_2.table, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -511,13 +516,13 @@ def test_linear_table(self) -> None: LUT_1 = LUT1D() - np.testing.assert_allclose( + xp_assert_close( LUT_1.linear_table(self._size), self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spow(LUT1D.linear_table(**self._table_3_kwargs), 1 / 2.6), self._table_3, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -542,7 +547,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_1, **self._invert_kwargs_1 ) - np.testing.assert_allclose( + xp_assert_close( LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_1, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -552,7 +557,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_2, **self._invert_kwargs_2 ) - np.testing.assert_allclose( + xp_assert_close( LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -565,7 +570,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_2, **self._invert_kwargs_2 ) - np.testing.assert_allclose( # pragma: no cover + xp_assert_close( # pragma: no cover LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -583,7 +588,7 @@ def test_apply(self) -> None: LUT_1 = LUT1D(self._table_2) - np.testing.assert_allclose( + xp_assert_close( LUT_1.apply(RANDOM_TRIPLETS), self._applied_1, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -592,7 +597,7 @@ def test_apply(self) -> None: LUT_2 = LUT1D(domain=self._domain_2) LUT_2.table = spow(LUT_2.table, 1 / 2.2) - np.testing.assert_allclose( + xp_assert_close( LUT_2.apply(RANDOM_TRIPLETS), self._applied_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -600,7 +605,7 @@ def test_apply(self) -> None: LUT_3 = LUT1D(self._table_3, domain=self._domain_3) - np.testing.assert_allclose( + xp_assert_close( LUT_3.apply(RANDOM_TRIPLETS), self._applied_3, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -608,7 +613,7 @@ def test_apply(self) -> None: LUT_4 = LUT1D(self._table_2) - np.testing.assert_allclose( + xp_assert_close( LUT_4.apply( RANDOM_TRIPLETS, direction="Inverse", @@ -803,13 +808,11 @@ def test__init__(self) -> None: LUT = LUT3x1D(self._table_1) - np.testing.assert_allclose( - LUT.table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(LUT.table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS) assert str(id(LUT)) == LUT.name - np.testing.assert_array_equal(LUT.domain, self._domain_1) + xp_assert_equal(LUT.domain, self._domain_1) assert LUT.dimensions == self._dimensions @@ -822,11 +825,11 @@ def test_table(self) -> None: LUT = LUT3x1D() - np.testing.assert_array_equal(LUT.table, LUT.linear_table(self._size)) + xp_assert_equal(LUT.table, LUT.linear_table(self._size)) table_1 = self._table_1 * 0.8 + 0.1 LUT.table = table_1 - np.testing.assert_allclose(LUT.table, table_1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(LUT.table, table_1, atol=TOLERANCE_ABSOLUTE_TESTS) def test_name(self) -> None: """ @@ -848,11 +851,11 @@ def test_domain(self) -> None: LUT = LUT3x1D() - np.testing.assert_array_equal(LUT.domain, self._domain_1) + xp_assert_equal(LUT.domain, self._domain_1) domain = self._domain_1 * 0.8 + 0.1 LUT.domain = domain - np.testing.assert_array_equal(LUT.domain, domain) + xp_assert_equal(LUT.domain, domain) def test_size(self) -> None: """ @@ -952,91 +955,91 @@ def test_arithmetical_operation(self) -> None: LUT_1 = LUT3x1D() LUT_2 = LUT3x1D() - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "+", False).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "-", False).table, self._table_1 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "*", False).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "/", False).table, self._table_1 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "**", False).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 + 10).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 - 10).table, self._table_1 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 * 10).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 / 10).table, self._table_1 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1**10).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "+", True).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "-", True).table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "*", True).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "/", True).table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "**", True).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1044,13 +1047,13 @@ def test_arithmetical_operation(self) -> None: LUT_2 = LUT3x1D() - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(self._table_1, "+", False).table, LUT_2.table + self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(LUT_2, "+", False).table, LUT_2.table + LUT_2.table, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1063,13 +1066,13 @@ def test_linear_table(self) -> None: LUT_1 = LUT3x1D() - np.testing.assert_allclose( + xp_assert_close( LUT_1.linear_table(self._size), self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spow(LUT3x1D.linear_table(**self._table_3_kwargs), 1 / 2.6), self._table_3, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1094,7 +1097,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_1, **self._invert_kwargs_1 ) - np.testing.assert_allclose( + xp_assert_close( LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_1, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1104,7 +1107,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_2, **self._invert_kwargs_2 ) - np.testing.assert_allclose( + xp_assert_close( LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1117,7 +1120,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_2, **self._invert_kwargs_2 ) - np.testing.assert_allclose( # pragma: no cover + xp_assert_close( # pragma: no cover LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1135,7 +1138,7 @@ def test_apply(self) -> None: LUT_1 = LUT3x1D(self._table_2) - np.testing.assert_allclose( + xp_assert_close( LUT_1.apply(RANDOM_TRIPLETS), self._applied_1, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1144,7 +1147,7 @@ def test_apply(self) -> None: LUT_2 = LUT3x1D(domain=self._domain_2) LUT_2.table = spow(LUT_2.table, 1 / 2.2) - np.testing.assert_allclose( + xp_assert_close( LUT_2.apply(RANDOM_TRIPLETS), self._applied_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1152,7 +1155,7 @@ def test_apply(self) -> None: LUT_3 = LUT3x1D(self._table_3, domain=self._domain_3) - np.testing.assert_allclose( + xp_assert_close( LUT_3.apply(RANDOM_TRIPLETS), self._applied_3, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1160,7 +1163,7 @@ def test_apply(self) -> None: LUT_4 = LUT3x1D(self._table_2) - np.testing.assert_allclose( + xp_assert_close( LUT_4.apply( RANDOM_TRIPLETS, direction="Inverse", @@ -1382,13 +1385,11 @@ def test__init__(self) -> None: LUT = LUT3D(self._table_1) - np.testing.assert_allclose( - LUT.table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(LUT.table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS) assert str(id(LUT)) == LUT.name - np.testing.assert_array_equal(LUT.domain, self._domain_1) + xp_assert_equal(LUT.domain, self._domain_1) assert LUT.dimensions == self._dimensions @@ -1401,11 +1402,11 @@ def test_table(self) -> None: LUT = LUT3D() - np.testing.assert_array_equal(LUT.table, LUT.linear_table(self._size)) + xp_assert_equal(LUT.table, LUT.linear_table(self._size)) table_1 = self._table_1 * 0.8 + 0.1 LUT.table = table_1 - np.testing.assert_allclose(LUT.table, table_1, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(LUT.table, table_1, atol=TOLERANCE_ABSOLUTE_TESTS) def test_name(self) -> None: """ @@ -1427,11 +1428,11 @@ def test_domain(self) -> None: LUT = LUT3D() - np.testing.assert_array_equal(LUT.domain, self._domain_1) + xp_assert_equal(LUT.domain, self._domain_1) domain = self._domain_1 * 0.8 + 0.1 LUT.domain = domain - np.testing.assert_array_equal(LUT.domain, domain) + xp_assert_equal(LUT.domain, domain) def test_size(self) -> None: """ @@ -1531,91 +1532,91 @@ def test_arithmetical_operation(self) -> None: LUT_1 = LUT3D() LUT_2 = LUT3D() - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "+", False).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "-", False).table, self._table_1 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "*", False).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "/", False).table, self._table_1 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.arithmetical_operation(10, "**", False).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 + 10).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 - 10).table, self._table_1 - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 * 10).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1 / 10).table, self._table_1 / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( (LUT_1**10).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "+", True).table, self._table_1 + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "-", True).table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "*", True).table, self._table_1 * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "/", True).table, self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(10, "**", True).table, self._table_1**10, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1623,13 +1624,13 @@ def test_arithmetical_operation(self) -> None: LUT_2 = LUT3D() - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(self._table_1, "+", False).table, LUT_2.table + self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.arithmetical_operation(LUT_2, "+", False).table, LUT_2.table + LUT_2.table, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1642,13 +1643,13 @@ def test_linear_table(self) -> None: LUT_1 = LUT3D() - np.testing.assert_allclose( + xp_assert_close( LUT_1.linear_table(self._size), self._table_1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spow(LUT3D.linear_table(**self._table_3_kwargs), 1 / 2.6), self._table_3, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1676,7 +1677,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_1, **self._invert_kwargs_1 ) - np.testing.assert_allclose( + xp_assert_close( LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_1, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1686,7 +1687,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_2, **self._invert_kwargs_2 ) - np.testing.assert_allclose( + xp_assert_close( LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1699,7 +1700,7 @@ def test_invert(self) -> None: interpolator=self._interpolator_2, **self._invert_kwargs_2 ) - np.testing.assert_allclose( # pragma: no cover + xp_assert_close( # pragma: no cover LUT_i.apply(RANDOM_TRIPLETS), self._inverted_apply_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1717,7 +1718,7 @@ def test_apply(self) -> None: LUT_1 = LUT3D(self._table_2) - np.testing.assert_allclose( + xp_assert_close( LUT_1.apply(RANDOM_TRIPLETS), self._applied_1, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1726,7 +1727,7 @@ def test_apply(self) -> None: LUT_2 = LUT3D(domain=self._domain_2) LUT_2.table = spow(LUT_2.table, 1 / 2.2) - np.testing.assert_allclose( + xp_assert_close( LUT_2.apply(RANDOM_TRIPLETS), self._applied_2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1734,7 +1735,7 @@ def test_apply(self) -> None: LUT_3 = LUT3D(self._table_3, domain=self._domain_3) - np.testing.assert_allclose( + xp_assert_close( LUT_3.apply(RANDOM_TRIPLETS), self._applied_3, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1742,7 +1743,7 @@ def test_apply(self) -> None: LUT_4 = LUT3D(self._table_2) - np.testing.assert_allclose( + xp_assert_close( LUT_4.apply( RANDOM_TRIPLETS, direction="Inverse", @@ -1788,206 +1789,206 @@ def test_LUT_to_LUT(self) -> None: assert LUT3x1D(tstack([table, table, table])) == LUT # "LUT" 1D to "LUT" 3D. - pytest.raises(ValueError, lambda: LUT_to_LUT(self._LUT_1, LUT3D)) + with pytest.raises(ValueError): + LUT_to_LUT(self._LUT_1, LUT3D) LUT = LUT_to_LUT(self._LUT_1, LUT3D, force_conversion=True, size=5) - np.testing.assert_allclose( + xp_assert_close( LUT.table, - np.array( + [ [ [ - [ - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.53156948], - [0.00000000, 0.00000000, 0.72933741], - [0.00000000, 0.00000000, 0.87726669], - [0.00000000, 0.00000000, 1.00000000], - ], - [ - [0.00000000, 0.53156948, 0.00000000], - [0.00000000, 0.53156948, 0.53156948], - [0.00000000, 0.53156948, 0.72933741], - [0.00000000, 0.53156948, 0.87726669], - [0.00000000, 0.53156948, 1.00000000], - ], - [ - [0.00000000, 0.72933741, 0.00000000], - [0.00000000, 0.72933741, 0.53156948], - [0.00000000, 0.72933741, 0.72933741], - [0.00000000, 0.72933741, 0.87726669], - [0.00000000, 0.72933741, 1.00000000], - ], - [ - [0.00000000, 0.87726669, 0.00000000], - [0.00000000, 0.87726669, 0.53156948], - [0.00000000, 0.87726669, 0.72933741], - [0.00000000, 0.87726669, 0.87726669], - [0.00000000, 0.87726669, 1.00000000], - ], - [ - [0.00000000, 1.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.53156948], - [0.00000000, 1.00000000, 0.72933741], - [0.00000000, 1.00000000, 0.87726669], - [0.00000000, 1.00000000, 1.00000000], - ], + [0.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.53156948], + [0.00000000, 0.00000000, 0.72933741], + [0.00000000, 0.00000000, 0.87726669], + [0.00000000, 0.00000000, 1.00000000], ], [ - [ - [0.53156948, 0.00000000, 0.00000000], - [0.53156948, 0.00000000, 0.53156948], - [0.53156948, 0.00000000, 0.72933741], - [0.53156948, 0.00000000, 0.87726669], - [0.53156948, 0.00000000, 1.00000000], - ], - [ - [0.53156948, 0.53156948, 0.00000000], - [0.53156948, 0.53156948, 0.53156948], - [0.53156948, 0.53156948, 0.72933741], - [0.53156948, 0.53156948, 0.87726669], - [0.53156948, 0.53156948, 1.00000000], - ], - [ - [0.53156948, 0.72933741, 0.00000000], - [0.53156948, 0.72933741, 0.53156948], - [0.53156948, 0.72933741, 0.72933741], - [0.53156948, 0.72933741, 0.87726669], - [0.53156948, 0.72933741, 1.00000000], - ], - [ - [0.53156948, 0.87726669, 0.00000000], - [0.53156948, 0.87726669, 0.53156948], - [0.53156948, 0.87726669, 0.72933741], - [0.53156948, 0.87726669, 0.87726669], - [0.53156948, 0.87726669, 1.00000000], - ], - [ - [0.53156948, 1.00000000, 0.00000000], - [0.53156948, 1.00000000, 0.53156948], - [0.53156948, 1.00000000, 0.72933741], - [0.53156948, 1.00000000, 0.87726669], - [0.53156948, 1.00000000, 1.00000000], - ], + [0.00000000, 0.53156948, 0.00000000], + [0.00000000, 0.53156948, 0.53156948], + [0.00000000, 0.53156948, 0.72933741], + [0.00000000, 0.53156948, 0.87726669], + [0.00000000, 0.53156948, 1.00000000], ], [ - [ - [0.72933741, 0.00000000, 0.00000000], - [0.72933741, 0.00000000, 0.53156948], - [0.72933741, 0.00000000, 0.72933741], - [0.72933741, 0.00000000, 0.87726669], - [0.72933741, 0.00000000, 1.00000000], - ], - [ - [0.72933741, 0.53156948, 0.00000000], - [0.72933741, 0.53156948, 0.53156948], - [0.72933741, 0.53156948, 0.72933741], - [0.72933741, 0.53156948, 0.87726669], - [0.72933741, 0.53156948, 1.00000000], - ], - [ - [0.72933741, 0.72933741, 0.00000000], - [0.72933741, 0.72933741, 0.53156948], - [0.72933741, 0.72933741, 0.72933741], - [0.72933741, 0.72933741, 0.87726669], - [0.72933741, 0.72933741, 1.00000000], - ], - [ - [0.72933741, 0.87726669, 0.00000000], - [0.72933741, 0.87726669, 0.53156948], - [0.72933741, 0.87726669, 0.72933741], - [0.72933741, 0.87726669, 0.87726669], - [0.72933741, 0.87726669, 1.00000000], - ], - [ - [0.72933741, 1.00000000, 0.00000000], - [0.72933741, 1.00000000, 0.53156948], - [0.72933741, 1.00000000, 0.72933741], - [0.72933741, 1.00000000, 0.87726669], - [0.72933741, 1.00000000, 1.00000000], - ], + [0.00000000, 0.72933741, 0.00000000], + [0.00000000, 0.72933741, 0.53156948], + [0.00000000, 0.72933741, 0.72933741], + [0.00000000, 0.72933741, 0.87726669], + [0.00000000, 0.72933741, 1.00000000], ], [ - [ - [0.87726669, 0.00000000, 0.00000000], - [0.87726669, 0.00000000, 0.53156948], - [0.87726669, 0.00000000, 0.72933741], - [0.87726669, 0.00000000, 0.87726669], - [0.87726669, 0.00000000, 1.00000000], - ], - [ - [0.87726669, 0.53156948, 0.00000000], - [0.87726669, 0.53156948, 0.53156948], - [0.87726669, 0.53156948, 0.72933741], - [0.87726669, 0.53156948, 0.87726669], - [0.87726669, 0.53156948, 1.00000000], - ], - [ - [0.87726669, 0.72933741, 0.00000000], - [0.87726669, 0.72933741, 0.53156948], - [0.87726669, 0.72933741, 0.72933741], - [0.87726669, 0.72933741, 0.87726669], - [0.87726669, 0.72933741, 1.00000000], - ], - [ - [0.87726669, 0.87726669, 0.00000000], - [0.87726669, 0.87726669, 0.53156948], - [0.87726669, 0.87726669, 0.72933741], - [0.87726669, 0.87726669, 0.87726669], - [0.87726669, 0.87726669, 1.00000000], - ], - [ - [0.87726669, 1.00000000, 0.00000000], - [0.87726669, 1.00000000, 0.53156948], - [0.87726669, 1.00000000, 0.72933741], - [0.87726669, 1.00000000, 0.87726669], - [0.87726669, 1.00000000, 1.00000000], - ], + [0.00000000, 0.87726669, 0.00000000], + [0.00000000, 0.87726669, 0.53156948], + [0.00000000, 0.87726669, 0.72933741], + [0.00000000, 0.87726669, 0.87726669], + [0.00000000, 0.87726669, 1.00000000], ], [ - [ - [1.00000000, 0.00000000, 0.00000000], - [1.00000000, 0.00000000, 0.53156948], - [1.00000000, 0.00000000, 0.72933741], - [1.00000000, 0.00000000, 0.87726669], - [1.00000000, 0.00000000, 1.00000000], - ], - [ - [1.00000000, 0.53156948, 0.00000000], - [1.00000000, 0.53156948, 0.53156948], - [1.00000000, 0.53156948, 0.72933741], - [1.00000000, 0.53156948, 0.87726669], - [1.00000000, 0.53156948, 1.00000000], - ], - [ - [1.00000000, 0.72933741, 0.00000000], - [1.00000000, 0.72933741, 0.53156948], - [1.00000000, 0.72933741, 0.72933741], - [1.00000000, 0.72933741, 0.87726669], - [1.00000000, 0.72933741, 1.00000000], - ], - [ - [1.00000000, 0.87726669, 0.00000000], - [1.00000000, 0.87726669, 0.53156948], - [1.00000000, 0.87726669, 0.72933741], - [1.00000000, 0.87726669, 0.87726669], - [1.00000000, 0.87726669, 1.00000000], - ], - [ - [1.00000000, 1.00000000, 0.00000000], - [1.00000000, 1.00000000, 0.53156948], - [1.00000000, 1.00000000, 0.72933741], - [1.00000000, 1.00000000, 0.87726669], - [1.00000000, 1.00000000, 1.00000000], - ], + [0.00000000, 1.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.53156948], + [0.00000000, 1.00000000, 0.72933741], + [0.00000000, 1.00000000, 0.87726669], + [0.00000000, 1.00000000, 1.00000000], ], - ] - ), + ], + [ + [ + [0.53156948, 0.00000000, 0.00000000], + [0.53156948, 0.00000000, 0.53156948], + [0.53156948, 0.00000000, 0.72933741], + [0.53156948, 0.00000000, 0.87726669], + [0.53156948, 0.00000000, 1.00000000], + ], + [ + [0.53156948, 0.53156948, 0.00000000], + [0.53156948, 0.53156948, 0.53156948], + [0.53156948, 0.53156948, 0.72933741], + [0.53156948, 0.53156948, 0.87726669], + [0.53156948, 0.53156948, 1.00000000], + ], + [ + [0.53156948, 0.72933741, 0.00000000], + [0.53156948, 0.72933741, 0.53156948], + [0.53156948, 0.72933741, 0.72933741], + [0.53156948, 0.72933741, 0.87726669], + [0.53156948, 0.72933741, 1.00000000], + ], + [ + [0.53156948, 0.87726669, 0.00000000], + [0.53156948, 0.87726669, 0.53156948], + [0.53156948, 0.87726669, 0.72933741], + [0.53156948, 0.87726669, 0.87726669], + [0.53156948, 0.87726669, 1.00000000], + ], + [ + [0.53156948, 1.00000000, 0.00000000], + [0.53156948, 1.00000000, 0.53156948], + [0.53156948, 1.00000000, 0.72933741], + [0.53156948, 1.00000000, 0.87726669], + [0.53156948, 1.00000000, 1.00000000], + ], + ], + [ + [ + [0.72933741, 0.00000000, 0.00000000], + [0.72933741, 0.00000000, 0.53156948], + [0.72933741, 0.00000000, 0.72933741], + [0.72933741, 0.00000000, 0.87726669], + [0.72933741, 0.00000000, 1.00000000], + ], + [ + [0.72933741, 0.53156948, 0.00000000], + [0.72933741, 0.53156948, 0.53156948], + [0.72933741, 0.53156948, 0.72933741], + [0.72933741, 0.53156948, 0.87726669], + [0.72933741, 0.53156948, 1.00000000], + ], + [ + [0.72933741, 0.72933741, 0.00000000], + [0.72933741, 0.72933741, 0.53156948], + [0.72933741, 0.72933741, 0.72933741], + [0.72933741, 0.72933741, 0.87726669], + [0.72933741, 0.72933741, 1.00000000], + ], + [ + [0.72933741, 0.87726669, 0.00000000], + [0.72933741, 0.87726669, 0.53156948], + [0.72933741, 0.87726669, 0.72933741], + [0.72933741, 0.87726669, 0.87726669], + [0.72933741, 0.87726669, 1.00000000], + ], + [ + [0.72933741, 1.00000000, 0.00000000], + [0.72933741, 1.00000000, 0.53156948], + [0.72933741, 1.00000000, 0.72933741], + [0.72933741, 1.00000000, 0.87726669], + [0.72933741, 1.00000000, 1.00000000], + ], + ], + [ + [ + [0.87726669, 0.00000000, 0.00000000], + [0.87726669, 0.00000000, 0.53156948], + [0.87726669, 0.00000000, 0.72933741], + [0.87726669, 0.00000000, 0.87726669], + [0.87726669, 0.00000000, 1.00000000], + ], + [ + [0.87726669, 0.53156948, 0.00000000], + [0.87726669, 0.53156948, 0.53156948], + [0.87726669, 0.53156948, 0.72933741], + [0.87726669, 0.53156948, 0.87726669], + [0.87726669, 0.53156948, 1.00000000], + ], + [ + [0.87726669, 0.72933741, 0.00000000], + [0.87726669, 0.72933741, 0.53156948], + [0.87726669, 0.72933741, 0.72933741], + [0.87726669, 0.72933741, 0.87726669], + [0.87726669, 0.72933741, 1.00000000], + ], + [ + [0.87726669, 0.87726669, 0.00000000], + [0.87726669, 0.87726669, 0.53156948], + [0.87726669, 0.87726669, 0.72933741], + [0.87726669, 0.87726669, 0.87726669], + [0.87726669, 0.87726669, 1.00000000], + ], + [ + [0.87726669, 1.00000000, 0.00000000], + [0.87726669, 1.00000000, 0.53156948], + [0.87726669, 1.00000000, 0.72933741], + [0.87726669, 1.00000000, 0.87726669], + [0.87726669, 1.00000000, 1.00000000], + ], + ], + [ + [ + [1.00000000, 0.00000000, 0.00000000], + [1.00000000, 0.00000000, 0.53156948], + [1.00000000, 0.00000000, 0.72933741], + [1.00000000, 0.00000000, 0.87726669], + [1.00000000, 0.00000000, 1.00000000], + ], + [ + [1.00000000, 0.53156948, 0.00000000], + [1.00000000, 0.53156948, 0.53156948], + [1.00000000, 0.53156948, 0.72933741], + [1.00000000, 0.53156948, 0.87726669], + [1.00000000, 0.53156948, 1.00000000], + ], + [ + [1.00000000, 0.72933741, 0.00000000], + [1.00000000, 0.72933741, 0.53156948], + [1.00000000, 0.72933741, 0.72933741], + [1.00000000, 0.72933741, 0.87726669], + [1.00000000, 0.72933741, 1.00000000], + ], + [ + [1.00000000, 0.87726669, 0.00000000], + [1.00000000, 0.87726669, 0.53156948], + [1.00000000, 0.87726669, 0.72933741], + [1.00000000, 0.87726669, 0.87726669], + [1.00000000, 0.87726669, 1.00000000], + ], + [ + [1.00000000, 1.00000000, 0.00000000], + [1.00000000, 1.00000000, 0.53156948], + [1.00000000, 1.00000000, 0.72933741], + [1.00000000, 1.00000000, 0.87726669], + [1.00000000, 1.00000000, 1.00000000], + ], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) # "LUT" 3x1D to "LUT" 1D. - pytest.raises(ValueError, lambda: LUT_to_LUT(self._LUT_2, LUT1D)) + with pytest.raises(ValueError): + LUT_to_LUT(self._LUT_2, LUT1D) channel_weights = np.array([1.0, 0.0, 0.0]) LUT = LUT_to_LUT( @@ -2022,206 +2023,206 @@ def test_LUT_to_LUT(self) -> None: assert LUT == self._LUT_2 # "LUT" 3x1D to "LUT" 3D. - pytest.raises(ValueError, lambda: LUT_to_LUT(self._LUT_2, LUT3D)) + with pytest.raises(ValueError): + LUT_to_LUT(self._LUT_2, LUT3D) LUT = LUT_to_LUT(self._LUT_2, LUT3D, force_conversion=True, size=5) - np.testing.assert_allclose( + xp_assert_close( LUT.table, - np.array( + [ [ [ - [ - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.26578474], - [0.00000000, 0.00000000, 0.36466870], - [0.00000000, 0.00000000, 0.43863334], - [0.00000000, 0.00000000, 0.50000000], - ], - [ - [0.00000000, 0.39867711, 0.00000000], - [0.00000000, 0.39867711, 0.26578474], - [0.00000000, 0.39867711, 0.36466870], - [0.00000000, 0.39867711, 0.43863334], - [0.00000000, 0.39867711, 0.50000000], - ], - [ - [0.00000000, 0.54700305, 0.00000000], - [0.00000000, 0.54700305, 0.26578474], - [0.00000000, 0.54700305, 0.36466870], - [0.00000000, 0.54700305, 0.43863334], - [0.00000000, 0.54700305, 0.50000000], - ], - [ - [0.00000000, 0.65795001, 0.00000000], - [0.00000000, 0.65795001, 0.26578474], - [0.00000000, 0.65795001, 0.36466870], - [0.00000000, 0.65795001, 0.43863334], - [0.00000000, 0.65795001, 0.50000000], - ], - [ - [0.00000000, 0.75000000, 0.00000000], - [0.00000000, 0.75000000, 0.26578474], - [0.00000000, 0.75000000, 0.36466870], - [0.00000000, 0.75000000, 0.43863334], - [0.00000000, 0.75000000, 0.50000000], - ], + [0.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.26578474], + [0.00000000, 0.00000000, 0.36466870], + [0.00000000, 0.00000000, 0.43863334], + [0.00000000, 0.00000000, 0.50000000], ], [ - [ - [0.53156948, 0.00000000, 0.00000000], - [0.53156948, 0.00000000, 0.26578474], - [0.53156948, 0.00000000, 0.36466870], - [0.53156948, 0.00000000, 0.43863334], - [0.53156948, 0.00000000, 0.50000000], - ], - [ - [0.53156948, 0.39867711, 0.00000000], - [0.53156948, 0.39867711, 0.26578474], - [0.53156948, 0.39867711, 0.36466870], - [0.53156948, 0.39867711, 0.43863334], - [0.53156948, 0.39867711, 0.50000000], - ], - [ - [0.53156948, 0.54700305, 0.00000000], - [0.53156948, 0.54700305, 0.26578474], - [0.53156948, 0.54700305, 0.36466870], - [0.53156948, 0.54700305, 0.43863334], - [0.53156948, 0.54700305, 0.50000000], - ], - [ - [0.53156948, 0.65795001, 0.00000000], - [0.53156948, 0.65795001, 0.26578474], - [0.53156948, 0.65795001, 0.36466870], - [0.53156948, 0.65795001, 0.43863334], - [0.53156948, 0.65795001, 0.50000000], - ], - [ - [0.53156948, 0.75000000, 0.00000000], - [0.53156948, 0.75000000, 0.26578474], - [0.53156948, 0.75000000, 0.36466870], - [0.53156948, 0.75000000, 0.43863334], - [0.53156948, 0.75000000, 0.50000000], - ], + [0.00000000, 0.39867711, 0.00000000], + [0.00000000, 0.39867711, 0.26578474], + [0.00000000, 0.39867711, 0.36466870], + [0.00000000, 0.39867711, 0.43863334], + [0.00000000, 0.39867711, 0.50000000], ], [ - [ - [0.72933741, 0.00000000, 0.00000000], - [0.72933741, 0.00000000, 0.26578474], - [0.72933741, 0.00000000, 0.36466870], - [0.72933741, 0.00000000, 0.43863334], - [0.72933741, 0.00000000, 0.50000000], - ], - [ - [0.72933741, 0.39867711, 0.00000000], - [0.72933741, 0.39867711, 0.26578474], - [0.72933741, 0.39867711, 0.36466870], - [0.72933741, 0.39867711, 0.43863334], - [0.72933741, 0.39867711, 0.50000000], - ], - [ - [0.72933741, 0.54700305, 0.00000000], - [0.72933741, 0.54700305, 0.26578474], - [0.72933741, 0.54700305, 0.36466870], - [0.72933741, 0.54700305, 0.43863334], - [0.72933741, 0.54700305, 0.50000000], - ], - [ - [0.72933741, 0.65795001, 0.00000000], - [0.72933741, 0.65795001, 0.26578474], - [0.72933741, 0.65795001, 0.36466870], - [0.72933741, 0.65795001, 0.43863334], - [0.72933741, 0.65795001, 0.50000000], - ], - [ - [0.72933741, 0.75000000, 0.00000000], - [0.72933741, 0.75000000, 0.26578474], - [0.72933741, 0.75000000, 0.36466870], - [0.72933741, 0.75000000, 0.43863334], - [0.72933741, 0.75000000, 0.50000000], - ], + [0.00000000, 0.54700305, 0.00000000], + [0.00000000, 0.54700305, 0.26578474], + [0.00000000, 0.54700305, 0.36466870], + [0.00000000, 0.54700305, 0.43863334], + [0.00000000, 0.54700305, 0.50000000], ], [ - [ - [0.87726669, 0.00000000, 0.00000000], - [0.87726669, 0.00000000, 0.26578474], - [0.87726669, 0.00000000, 0.36466870], - [0.87726669, 0.00000000, 0.43863334], - [0.87726669, 0.00000000, 0.50000000], - ], - [ - [0.87726669, 0.39867711, 0.00000000], - [0.87726669, 0.39867711, 0.26578474], - [0.87726669, 0.39867711, 0.36466870], - [0.87726669, 0.39867711, 0.43863334], - [0.87726669, 0.39867711, 0.50000000], - ], - [ - [0.87726669, 0.54700305, 0.00000000], - [0.87726669, 0.54700305, 0.26578474], - [0.87726669, 0.54700305, 0.36466870], - [0.87726669, 0.54700305, 0.43863334], - [0.87726669, 0.54700305, 0.50000000], - ], - [ - [0.87726669, 0.65795001, 0.00000000], - [0.87726669, 0.65795001, 0.26578474], - [0.87726669, 0.65795001, 0.36466870], - [0.87726669, 0.65795001, 0.43863334], - [0.87726669, 0.65795001, 0.50000000], - ], - [ - [0.87726669, 0.75000000, 0.00000000], - [0.87726669, 0.75000000, 0.26578474], - [0.87726669, 0.75000000, 0.36466870], - [0.87726669, 0.75000000, 0.43863334], - [0.87726669, 0.75000000, 0.50000000], - ], + [0.00000000, 0.65795001, 0.00000000], + [0.00000000, 0.65795001, 0.26578474], + [0.00000000, 0.65795001, 0.36466870], + [0.00000000, 0.65795001, 0.43863334], + [0.00000000, 0.65795001, 0.50000000], ], [ - [ - [1.00000000, 0.00000000, 0.00000000], - [1.00000000, 0.00000000, 0.26578474], - [1.00000000, 0.00000000, 0.36466870], - [1.00000000, 0.00000000, 0.43863334], - [1.00000000, 0.00000000, 0.50000000], - ], - [ - [1.00000000, 0.39867711, 0.00000000], - [1.00000000, 0.39867711, 0.26578474], - [1.00000000, 0.39867711, 0.36466870], - [1.00000000, 0.39867711, 0.43863334], - [1.00000000, 0.39867711, 0.50000000], - ], - [ - [1.00000000, 0.54700305, 0.00000000], - [1.00000000, 0.54700305, 0.26578474], - [1.00000000, 0.54700305, 0.36466870], - [1.00000000, 0.54700305, 0.43863334], - [1.00000000, 0.54700305, 0.50000000], - ], - [ - [1.00000000, 0.65795001, 0.00000000], - [1.00000000, 0.65795001, 0.26578474], - [1.00000000, 0.65795001, 0.36466870], - [1.00000000, 0.65795001, 0.43863334], - [1.00000000, 0.65795001, 0.50000000], - ], - [ - [1.00000000, 0.75000000, 0.00000000], - [1.00000000, 0.75000000, 0.26578474], - [1.00000000, 0.75000000, 0.36466870], - [1.00000000, 0.75000000, 0.43863334], - [1.00000000, 0.75000000, 0.50000000], - ], + [0.00000000, 0.75000000, 0.00000000], + [0.00000000, 0.75000000, 0.26578474], + [0.00000000, 0.75000000, 0.36466870], + [0.00000000, 0.75000000, 0.43863334], + [0.00000000, 0.75000000, 0.50000000], ], - ] - ), + ], + [ + [ + [0.53156948, 0.00000000, 0.00000000], + [0.53156948, 0.00000000, 0.26578474], + [0.53156948, 0.00000000, 0.36466870], + [0.53156948, 0.00000000, 0.43863334], + [0.53156948, 0.00000000, 0.50000000], + ], + [ + [0.53156948, 0.39867711, 0.00000000], + [0.53156948, 0.39867711, 0.26578474], + [0.53156948, 0.39867711, 0.36466870], + [0.53156948, 0.39867711, 0.43863334], + [0.53156948, 0.39867711, 0.50000000], + ], + [ + [0.53156948, 0.54700305, 0.00000000], + [0.53156948, 0.54700305, 0.26578474], + [0.53156948, 0.54700305, 0.36466870], + [0.53156948, 0.54700305, 0.43863334], + [0.53156948, 0.54700305, 0.50000000], + ], + [ + [0.53156948, 0.65795001, 0.00000000], + [0.53156948, 0.65795001, 0.26578474], + [0.53156948, 0.65795001, 0.36466870], + [0.53156948, 0.65795001, 0.43863334], + [0.53156948, 0.65795001, 0.50000000], + ], + [ + [0.53156948, 0.75000000, 0.00000000], + [0.53156948, 0.75000000, 0.26578474], + [0.53156948, 0.75000000, 0.36466870], + [0.53156948, 0.75000000, 0.43863334], + [0.53156948, 0.75000000, 0.50000000], + ], + ], + [ + [ + [0.72933741, 0.00000000, 0.00000000], + [0.72933741, 0.00000000, 0.26578474], + [0.72933741, 0.00000000, 0.36466870], + [0.72933741, 0.00000000, 0.43863334], + [0.72933741, 0.00000000, 0.50000000], + ], + [ + [0.72933741, 0.39867711, 0.00000000], + [0.72933741, 0.39867711, 0.26578474], + [0.72933741, 0.39867711, 0.36466870], + [0.72933741, 0.39867711, 0.43863334], + [0.72933741, 0.39867711, 0.50000000], + ], + [ + [0.72933741, 0.54700305, 0.00000000], + [0.72933741, 0.54700305, 0.26578474], + [0.72933741, 0.54700305, 0.36466870], + [0.72933741, 0.54700305, 0.43863334], + [0.72933741, 0.54700305, 0.50000000], + ], + [ + [0.72933741, 0.65795001, 0.00000000], + [0.72933741, 0.65795001, 0.26578474], + [0.72933741, 0.65795001, 0.36466870], + [0.72933741, 0.65795001, 0.43863334], + [0.72933741, 0.65795001, 0.50000000], + ], + [ + [0.72933741, 0.75000000, 0.00000000], + [0.72933741, 0.75000000, 0.26578474], + [0.72933741, 0.75000000, 0.36466870], + [0.72933741, 0.75000000, 0.43863334], + [0.72933741, 0.75000000, 0.50000000], + ], + ], + [ + [ + [0.87726669, 0.00000000, 0.00000000], + [0.87726669, 0.00000000, 0.26578474], + [0.87726669, 0.00000000, 0.36466870], + [0.87726669, 0.00000000, 0.43863334], + [0.87726669, 0.00000000, 0.50000000], + ], + [ + [0.87726669, 0.39867711, 0.00000000], + [0.87726669, 0.39867711, 0.26578474], + [0.87726669, 0.39867711, 0.36466870], + [0.87726669, 0.39867711, 0.43863334], + [0.87726669, 0.39867711, 0.50000000], + ], + [ + [0.87726669, 0.54700305, 0.00000000], + [0.87726669, 0.54700305, 0.26578474], + [0.87726669, 0.54700305, 0.36466870], + [0.87726669, 0.54700305, 0.43863334], + [0.87726669, 0.54700305, 0.50000000], + ], + [ + [0.87726669, 0.65795001, 0.00000000], + [0.87726669, 0.65795001, 0.26578474], + [0.87726669, 0.65795001, 0.36466870], + [0.87726669, 0.65795001, 0.43863334], + [0.87726669, 0.65795001, 0.50000000], + ], + [ + [0.87726669, 0.75000000, 0.00000000], + [0.87726669, 0.75000000, 0.26578474], + [0.87726669, 0.75000000, 0.36466870], + [0.87726669, 0.75000000, 0.43863334], + [0.87726669, 0.75000000, 0.50000000], + ], + ], + [ + [ + [1.00000000, 0.00000000, 0.00000000], + [1.00000000, 0.00000000, 0.26578474], + [1.00000000, 0.00000000, 0.36466870], + [1.00000000, 0.00000000, 0.43863334], + [1.00000000, 0.00000000, 0.50000000], + ], + [ + [1.00000000, 0.39867711, 0.00000000], + [1.00000000, 0.39867711, 0.26578474], + [1.00000000, 0.39867711, 0.36466870], + [1.00000000, 0.39867711, 0.43863334], + [1.00000000, 0.39867711, 0.50000000], + ], + [ + [1.00000000, 0.54700305, 0.00000000], + [1.00000000, 0.54700305, 0.26578474], + [1.00000000, 0.54700305, 0.36466870], + [1.00000000, 0.54700305, 0.43863334], + [1.00000000, 0.54700305, 0.50000000], + ], + [ + [1.00000000, 0.65795001, 0.00000000], + [1.00000000, 0.65795001, 0.26578474], + [1.00000000, 0.65795001, 0.36466870], + [1.00000000, 0.65795001, 0.43863334], + [1.00000000, 0.65795001, 0.50000000], + ], + [ + [1.00000000, 0.75000000, 0.00000000], + [1.00000000, 0.75000000, 0.26578474], + [1.00000000, 0.75000000, 0.36466870], + [1.00000000, 0.75000000, 0.43863334], + [1.00000000, 0.75000000, 0.50000000], + ], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) # "LUT" 3D to "LUT" 1D. - pytest.raises(ValueError, lambda: LUT_to_LUT(self._LUT_3, LUT1D)) + with pytest.raises(ValueError): + LUT_to_LUT(self._LUT_3, LUT1D) channel_weights = np.array([1.0, 0.0, 0.0]) LUT = LUT_to_LUT( @@ -2232,28 +2233,26 @@ def test_LUT_to_LUT(self) -> None: channel_weights=channel_weights, ) - np.testing.assert_allclose( + xp_assert_close( LUT.table, - np.array( - [ - 0.00000000, - 0.29202031, - 0.40017033, - 0.48115651, - 0.54837380, - 0.60691337, - 0.65935329, - 0.70721023, - 0.75146458, - 0.79279273, - 0.83168433, - 0.86850710, - 0.90354543, - 0.93702451, - 0.96912624, - 1.00000000, - ] - ), + [ + 0.00000000, + 0.29202031, + 0.40017033, + 0.48115651, + 0.54837380, + 0.60691337, + 0.65935329, + 0.70721023, + 0.75146458, + 0.79279273, + 0.83168433, + 0.86850710, + 0.90354543, + 0.93702451, + 0.96912624, + 1.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -2266,58 +2265,55 @@ def test_LUT_to_LUT(self) -> None: channel_weights=channel_weights, ) - np.testing.assert_allclose( + xp_assert_close( LUT.table, - np.array( - [ - 0.04562817, - 0.24699999, - 0.40967557, - 0.50401689, - 0.57985117, - 0.64458830, - 0.70250077, - 0.75476586, - 0.80317708, - 0.83944710, - 0.86337188, - 0.88622285, - 0.90786039, - 0.92160338, - 0.92992641, - 0.93781796, - ] - ), + [ + 0.04562817, + 0.24699999, + 0.40967557, + 0.50401689, + 0.57985117, + 0.64458830, + 0.70250077, + 0.75476586, + 0.80317708, + 0.83944710, + 0.86337188, + 0.88622285, + 0.90786039, + 0.92160338, + 0.92992641, + 0.93781796, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) # "LUT" 3D to "LUT" 3x1D. - pytest.raises(ValueError, lambda: LUT_to_LUT(self._LUT_3, LUT3x1D)) + with pytest.raises(ValueError): + LUT_to_LUT(self._LUT_3, LUT3x1D) LUT = LUT_to_LUT(self._LUT_3, LUT3x1D, force_conversion=True, size=16) - np.testing.assert_allclose( + xp_assert_close( LUT.table, - np.array( - [ - [0.00000000, 0.00000000, 0.00000000], - [0.29202031, 0.29202031, 0.29202031], - [0.40017033, 0.40017033, 0.40017033], - [0.48115651, 0.48115651, 0.48115651], - [0.54837380, 0.54837380, 0.54837380], - [0.60691337, 0.60691337, 0.60691337], - [0.65935329, 0.65935329, 0.65935329], - [0.70721023, 0.70721023, 0.70721023], - [0.75146458, 0.75146458, 0.75146458], - [0.79279273, 0.79279273, 0.79279273], - [0.83168433, 0.83168433, 0.83168433], - [0.86850710, 0.86850710, 0.86850710], - [0.90354543, 0.90354543, 0.90354543], - [0.93702451, 0.93702451, 0.93702451], - [0.96912624, 0.96912624, 0.96912624], - [1.00000000, 1.00000000, 1.00000000], - ] - ), + [ + [0.00000000, 0.00000000, 0.00000000], + [0.29202031, 0.29202031, 0.29202031], + [0.40017033, 0.40017033, 0.40017033], + [0.48115651, 0.48115651, 0.48115651], + [0.54837380, 0.54837380, 0.54837380], + [0.60691337, 0.60691337, 0.60691337], + [0.65935329, 0.65935329, 0.65935329], + [0.70721023, 0.70721023, 0.70721023], + [0.75146458, 0.75146458, 0.75146458], + [0.79279273, 0.79279273, 0.79279273], + [0.83168433, 0.83168433, 0.83168433], + [0.86850710, 0.86850710, 0.86850710], + [0.90354543, 0.90354543, 0.90354543], + [0.93702451, 0.93702451, 0.93702451], + [0.96912624, 0.96912624, 0.96912624], + [1.00000000, 1.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/io/luts/tests/test_operator.py b/colour/io/luts/tests/test_operator.py index d5e14cce16..87833de9ea 100644 --- a/colour/io/luts/tests/test_operator.py +++ b/colour/io/luts/tests/test_operator.py @@ -8,7 +8,7 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io.luts import AbstractLUTSequenceOperator, LUTOperatorMatrix -from colour.utilities import tstack, zeros +from colour.utilities import tstack, xp_assert_close, xp_assert_equal, zeros __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -87,7 +87,7 @@ def test_matrix(self) -> None: M = np.identity(3) lut_operator_matrix = LUTOperatorMatrix(M) - np.testing.assert_array_equal(lut_operator_matrix.matrix, np.identity(4)) + xp_assert_equal(lut_operator_matrix.matrix, np.identity(4)) def test_offset(self) -> None: """ @@ -98,7 +98,7 @@ def test_offset(self) -> None: offset = zeros(3) lut_operator_matrix = LUTOperatorMatrix(np.identity(3), offset) - np.testing.assert_array_equal(lut_operator_matrix.offset, zeros(4)) + xp_assert_equal(lut_operator_matrix.offset, zeros(4)) def test__str__(self) -> None: """ @@ -171,64 +171,56 @@ def test_apply(self) -> None: samples = np.linspace(0, 1, 5) RGB = tstack([samples, samples, samples]) - np.testing.assert_array_equal(LUTOperatorMatrix().apply(RGB), RGB) + xp_assert_equal(LUTOperatorMatrix().apply(RGB), RGB) - np.testing.assert_allclose( + xp_assert_close( self._lut_operator_matrix.apply(RGB), - np.array( - [ - [0.25000000, 0.50000000, 0.75000000], - [0.30000000, 0.75000000, 1.20000000], - [0.35000000, 1.00000000, 1.65000000], - [0.40000000, 1.25000000, 2.10000000], - [0.45000000, 1.50000000, 2.55000000], - ] - ), + [ + [0.25000000, 0.50000000, 0.75000000], + [0.30000000, 0.75000000, 1.20000000], + [0.35000000, 1.00000000, 1.65000000], + [0.40000000, 1.25000000, 2.10000000], + [0.45000000, 1.50000000, 2.55000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._lut_operator_matrix.apply(RGB, apply_offset_first=True), - np.array( - [ - [0.13333333, 0.53333333, 0.93333333], - [0.18333333, 0.78333333, 1.38333333], - [0.23333333, 1.03333333, 1.83333333], - [0.28333333, 1.28333333, 2.28333333], - [0.33333333, 1.53333333, 2.73333333], - ] - ), + [ + [0.13333333, 0.53333333, 0.93333333], + [0.18333333, 0.78333333, 1.38333333], + [0.23333333, 1.03333333, 1.83333333], + [0.28333333, 1.28333333, 2.28333333], + [0.33333333, 1.53333333, 2.73333333], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) RGBA = tstack([samples, samples, samples, samples]) - np.testing.assert_array_equal(LUTOperatorMatrix().apply(RGBA), RGBA) + xp_assert_equal(LUTOperatorMatrix().apply(RGBA), RGBA) - np.testing.assert_allclose( + xp_assert_close( self._lut_operator_matrix.apply(RGBA), - np.array( - [ - [0.25000000, 0.50000000, 0.75000000, 1.00000000], - [0.35000000, 0.86666667, 1.38333333, 1.90000000], - [0.45000000, 1.23333333, 2.01666667, 2.80000000], - [0.55000000, 1.60000000, 2.65000000, 3.70000000], - [0.65000000, 1.96666667, 3.28333333, 4.60000000], - ] - ), + [ + [0.25000000, 0.50000000, 0.75000000, 1.00000000], + [0.35000000, 0.86666667, 1.38333333, 1.90000000], + [0.45000000, 1.23333333, 2.01666667, 2.80000000], + [0.55000000, 1.60000000, 2.65000000, 3.70000000], + [0.65000000, 1.96666667, 3.28333333, 4.60000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._lut_operator_matrix.apply(RGBA, apply_offset_first=True), - np.array( - [ - [0.33333333, 1.00000000, 1.66666667, 2.33333333], - [0.43333333, 1.36666667, 2.30000000, 3.23333333], - [0.53333333, 1.73333333, 2.93333333, 4.13333333], - [0.63333333, 2.10000000, 3.56666667, 5.03333333], - [0.73333333, 2.46666667, 4.20000000, 5.93333333], - ], - ), + [ + [0.33333333, 1.00000000, 1.66666667, 2.33333333], + [0.43333333, 1.36666667, 2.30000000, 3.23333333], + [0.53333333, 1.73333333, 2.93333333, 4.13333333], + [0.63333333, 2.10000000, 3.56666667, 5.03333333], + [0.73333333, 2.46666667, 4.20000000, 5.93333333], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/io/luts/tests/test_resolve_cube.py b/colour/io/luts/tests/test_resolve_cube.py index 56ec3e9fab..ec1cd0d054 100644 --- a/colour/io/luts/tests/test_resolve_cube.py +++ b/colour/io/luts/tests/test_resolve_cube.py @@ -6,7 +6,6 @@ import shutil import tempfile -import numpy as np import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -19,6 +18,7 @@ read_LUT_ResolveCube, write_LUT_ResolveCube, ) +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -53,49 +53,47 @@ def test_read_LUT_ResolveCube(self) -> None: read_LUT_ResolveCube(os.path.join(ROOT_LUTS, "ACES_Proxy_10_to_ACES.cube")), ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.table, - np.array( - [ - [4.88300000e-04, 4.88300000e-04, 4.88300000e-04], - [7.71400000e-04, 7.71400000e-04, 7.71400000e-04], - [1.21900000e-03, 1.21900000e-03, 1.21900000e-03], - [1.92600000e-03, 1.92600000e-03, 1.92600000e-03], - [3.04400000e-03, 3.04400000e-03, 3.04400000e-03], - [4.80900000e-03, 4.80900000e-03, 4.80900000e-03], - [7.59900000e-03, 7.59900000e-03, 7.59900000e-03], - [1.20100000e-02, 1.20100000e-02, 1.20100000e-02], - [1.89700000e-02, 1.89700000e-02, 1.89700000e-02], - [2.99800000e-02, 2.99800000e-02, 2.99800000e-02], - [4.73700000e-02, 4.73700000e-02, 4.73700000e-02], - [7.48400000e-02, 7.48400000e-02, 7.48400000e-02], - [1.18300000e-01, 1.18300000e-01, 1.18300000e-01], - [1.86900000e-01, 1.86900000e-01, 1.86900000e-01], - [2.95200000e-01, 2.95200000e-01, 2.95200000e-01], - [4.66500000e-01, 4.66500000e-01, 4.66500000e-01], - [7.37100000e-01, 7.37100000e-01, 7.37100000e-01], - [1.16500000e00, 1.16500000e00, 1.16500000e00], - [1.84000000e00, 1.84000000e00, 1.84000000e00], - [2.90800000e00, 2.90800000e00, 2.90800000e00], - [4.59500000e00, 4.59500000e00, 4.59500000e00], - [7.26000000e00, 7.26000000e00, 7.26000000e00], - [1.14700000e01, 1.14700000e01, 1.14700000e01], - [1.81300000e01, 1.81300000e01, 1.81300000e01], - [2.86400000e01, 2.86400000e01, 2.86400000e01], - [4.52500000e01, 4.52500000e01, 4.52500000e01], - [7.15100000e01, 7.15100000e01, 7.15100000e01], - [1.13000000e02, 1.13000000e02, 1.13000000e02], - [1.78500000e02, 1.78500000e02, 1.78500000e02], - [2.82100000e02, 2.82100000e02, 2.82100000e02], - [4.45700000e02, 4.45700000e02, 4.45700000e02], - [7.04300000e02, 7.04300000e02, 7.04300000e02], - ] - ), + [ + [4.88300000e-04, 4.88300000e-04, 4.88300000e-04], + [7.71400000e-04, 7.71400000e-04, 7.71400000e-04], + [1.21900000e-03, 1.21900000e-03, 1.21900000e-03], + [1.92600000e-03, 1.92600000e-03, 1.92600000e-03], + [3.04400000e-03, 3.04400000e-03, 3.04400000e-03], + [4.80900000e-03, 4.80900000e-03, 4.80900000e-03], + [7.59900000e-03, 7.59900000e-03, 7.59900000e-03], + [1.20100000e-02, 1.20100000e-02, 1.20100000e-02], + [1.89700000e-02, 1.89700000e-02, 1.89700000e-02], + [2.99800000e-02, 2.99800000e-02, 2.99800000e-02], + [4.73700000e-02, 4.73700000e-02, 4.73700000e-02], + [7.48400000e-02, 7.48400000e-02, 7.48400000e-02], + [1.18300000e-01, 1.18300000e-01, 1.18300000e-01], + [1.86900000e-01, 1.86900000e-01, 1.86900000e-01], + [2.95200000e-01, 2.95200000e-01, 2.95200000e-01], + [4.66500000e-01, 4.66500000e-01, 4.66500000e-01], + [7.37100000e-01, 7.37100000e-01, 7.37100000e-01], + [1.16500000e00, 1.16500000e00, 1.16500000e00], + [1.84000000e00, 1.84000000e00, 1.84000000e00], + [2.90800000e00, 2.90800000e00, 2.90800000e00], + [4.59500000e00, 4.59500000e00, 4.59500000e00], + [7.26000000e00, 7.26000000e00, 7.26000000e00], + [1.14700000e01, 1.14700000e01, 1.14700000e01], + [1.81300000e01, 1.81300000e01, 1.81300000e01], + [2.86400000e01, 2.86400000e01, 2.86400000e01], + [4.52500000e01, 4.52500000e01, 4.52500000e01], + [7.15100000e01, 7.15100000e01, 7.15100000e01], + [1.13000000e02, 1.13000000e02, 1.13000000e02], + [1.78500000e02, 1.78500000e02, 1.78500000e02], + [2.82100000e02, 2.82100000e02, 2.82100000e02], + [4.45700000e02, 4.45700000e02, 4.45700000e02], + [7.04300000e02, 7.04300000e02, 7.04300000e02], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "ACES Proxy 10 to ACES" assert LUT_1.dimensions == 2 - np.testing.assert_array_equal(LUT_1.domain, np.array([[0, 0, 0], [1, 1, 1]])) + xp_assert_equal(LUT_1.domain, [[0, 0, 0], [1, 1, 1]]) assert LUT_1.size == 32 assert LUT_1.comments == [] @@ -103,7 +101,7 @@ def test_read_LUT_ResolveCube(self) -> None: "LUT3x1D", read_LUT_ResolveCube(os.path.join(ROOT_LUTS, "Demo.cube")) ) assert LUT_2.comments == ["Comments can't go anywhere"] - np.testing.assert_array_equal(LUT_2.domain, np.array([[0, 0, 0], [3, 3, 3]])) + xp_assert_equal(LUT_2.domain, [[0, 0, 0], [3, 3, 3]]) LUT_3 = cast( "LUT3D", @@ -118,28 +116,26 @@ def test_read_LUT_ResolveCube(self) -> None: "LUTSequence", read_LUT_ResolveCube(os.path.join(ROOT_LUTS, "LogC_Video.cube")), ) - np.testing.assert_allclose( + xp_assert_close( LUT_4[0].table, - np.array( - [ - [0.00000000, 0.00000000, 0.00000000], - [0.02708500, 0.02708500, 0.02708500], - [0.06304900, 0.06304900, 0.06304900], - [0.11314900, 0.11314900, 0.11314900], - [0.18304900, 0.18304900, 0.18304900], - [0.28981100, 0.28981100, 0.28981100], - [0.41735300, 0.41735300, 0.41735300], - [0.54523100, 0.54523100, 0.54523100], - [0.67020500, 0.67020500, 0.67020500], - [0.78963000, 0.78963000, 0.78963000], - [0.88646800, 0.88646800, 0.88646800], - [0.94549100, 0.94549100, 0.94549100], - [0.97644900, 0.97644900, 0.97644900], - [0.98924800, 0.98924800, 0.98924800], - [0.99379700, 0.99379700, 0.99379700], - [1.00000000, 1.00000000, 1.00000000], - ] - ), + [ + [0.00000000, 0.00000000, 0.00000000], + [0.02708500, 0.02708500, 0.02708500], + [0.06304900, 0.06304900, 0.06304900], + [0.11314900, 0.11314900, 0.11314900], + [0.18304900, 0.18304900, 0.18304900], + [0.28981100, 0.28981100, 0.28981100], + [0.41735300, 0.41735300, 0.41735300], + [0.54523100, 0.54523100, 0.54523100], + [0.67020500, 0.67020500, 0.67020500], + [0.78963000, 0.78963000, 0.78963000], + [0.88646800, 0.88646800, 0.88646800], + [0.94549100, 0.94549100, 0.94549100], + [0.97644900, 0.97644900, 0.97644900], + [0.98924800, 0.98924800, 0.98924800], + [0.99379700, 0.99379700, 0.99379700], + [1.00000000, 1.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_4[1].size == 4 @@ -281,4 +277,5 @@ def test_raise_exception_write_LUT_ResolveCube(self) -> None: definition raised exception. """ - pytest.raises(TypeError, write_LUT_ResolveCube, object(), "") + with pytest.raises(TypeError): + write_LUT_ResolveCube(object(), "") # pyright: ignore diff --git a/colour/io/luts/tests/test_sequence.py b/colour/io/luts/tests/test_sequence.py index fa79709e38..10235c5192 100644 --- a/colour/io/luts/tests/test_sequence.py +++ b/colour/io/luts/tests/test_sequence.py @@ -20,7 +20,7 @@ LUTSequence, ) from colour.models import gamma_function -from colour.utilities import as_float_array, tstack +from colour.utilities import as_float_array, tstack, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -446,16 +446,14 @@ def apply( samples = np.linspace(0, 1, 5) RGB = tstack([samples, samples, samples]) - np.testing.assert_allclose( + xp_assert_close( LUT_sequence.apply(RGB, GammaOperator={"direction": "Inverse"}), - np.array( - [ - [0.03386629, 0.03386629, 0.03386629], - [0.27852298, 0.27852298, 0.27852298], - [0.46830881, 0.46830881, 0.46830881], - [0.65615595, 0.65615595, 0.65615595], - [0.75000000, 0.75000000, 0.75000000], - ] - ), + [ + [0.03386629, 0.03386629, 0.03386629], + [0.27852298, 0.27852298, 0.27852298], + [0.46830881, 0.46830881, 0.46830881], + [0.65615595, 0.65615595, 0.65615595], + [0.75000000, 0.75000000, 0.75000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/io/luts/tests/test_sony_spi1d.py b/colour/io/luts/tests/test_sony_spi1d.py index 8c3d07e2e1..5f7a47d9c0 100644 --- a/colour/io/luts/tests/test_sony_spi1d.py +++ b/colour/io/luts/tests/test_sony_spi1d.py @@ -6,10 +6,9 @@ import shutil import tempfile -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import read_LUT_SonySPI1D, write_LUT_SonySPI1D +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -38,33 +37,31 @@ def test_read_LUT_SonySPI1D(self) -> None: LUT_1 = read_LUT_SonySPI1D(os.path.join(ROOT_LUTS, "eotf_sRGB_1D.spi1d")) - np.testing.assert_allclose( + xp_assert_close( LUT_1.table, - np.array( - [ - -7.73990000e-03, - 5.16000000e-04, - 1.22181000e-02, - 3.96819000e-02, - 8.71438000e-02, - 1.57439400e-01, - 2.52950100e-01, - 3.75757900e-01, - 5.27729400e-01, - 7.10566500e-01, - 9.25840600e-01, - 1.17501630e00, - 1.45946870e00, - 1.78049680e00, - 2.13933380e00, - 2.53715520e00, - ] - ), + [ + -7.73990000e-03, + 5.16000000e-04, + 1.22181000e-02, + 3.96819000e-02, + 8.71438000e-02, + 1.57439400e-01, + 2.52950100e-01, + 3.75757900e-01, + 5.27729400e-01, + 7.10566500e-01, + 9.25840600e-01, + 1.17501630e00, + 1.45946870e00, + 1.78049680e00, + 2.13933380e00, + 2.53715520e00, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "eotf sRGB 1D" assert LUT_1.dimensions == 1 - np.testing.assert_array_equal(LUT_1.domain, np.array([-0.1, 1.5])) + xp_assert_equal(LUT_1.domain, [-0.1, 1.5]) assert LUT_1.size == 16 assert LUT_1.comments == [ 'Generated by "Colour 0.3.11".', @@ -76,9 +73,7 @@ def test_read_LUT_SonySPI1D(self) -> None: 'Generated by "Colour 0.3.11".', '"colour.models.eotf_sRGB".', ] - np.testing.assert_array_equal( - LUT_2.domain, np.array([[-0.1, -0.1, -0.1], [1.5, 1.5, 1.5]]) - ) + xp_assert_equal(LUT_2.domain, [[-0.1, -0.1, -0.1], [1.5, 1.5, 1.5]]) class TestWriteLUTSonySPI1D: diff --git a/colour/io/luts/tests/test_sony_spi3d.py b/colour/io/luts/tests/test_sony_spi3d.py index d9dab110af..a29cd60b7a 100644 --- a/colour/io/luts/tests/test_sony_spi3d.py +++ b/colour/io/luts/tests/test_sony_spi3d.py @@ -6,11 +6,9 @@ import shutil import tempfile -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import LUT3D, LUTSequence, read_LUT_SonySPI3D, write_LUT_SonySPI3D -from colour.utilities import as_int_array +from colour.utilities import as_int_array, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -39,121 +37,119 @@ def test_read_LUT_SonySPI3D(self) -> None: LUT_1 = read_LUT_SonySPI3D(os.path.join(ROOT_LUTS, "Colour_Correct.spi3d")) - np.testing.assert_allclose( + xp_assert_close( LUT_1.table, - np.array( + [ [ [ - [ - [0.00000000e00, 0.00000000e00, 0.00000000e00], - [0.00000000e00, 0.00000000e00, 4.16653000e-01], - [0.00000000e00, 0.00000000e00, 8.33306000e-01], - [1.00000000e-06, 1.00000000e-06, 1.24995900e00], - ], - [ - [-2.62310000e-02, 3.77102000e-01, -2.62310000e-02], - [1.96860000e-02, 2.44702000e-01, 2.44702000e-01], - [1.43270000e-02, 3.30993000e-01, 6.47660000e-01], - [9.02200000e-03, 3.72791000e-01, 1.10033100e00], - ], - [ - [-5.24630000e-02, 7.54204000e-01, -5.24630000e-02], - [0.00000000e00, 6.16667000e-01, 3.08333000e-01], - [3.93720000e-02, 4.89403000e-01, 4.89403000e-01], - [3.57730000e-02, 5.78763000e-01, 8.50258000e-01], - ], - [ - [-7.86940000e-02, 1.13130600e00, -7.86940000e-02], - [-3.59270000e-02, 1.02190800e00, 3.16685000e-01], - [3.09040000e-02, 8.31171000e-01, 5.64415000e-01], - [5.90590000e-02, 7.34105000e-01, 7.34105000e-01], - ], + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [0.00000000e00, 0.00000000e00, 4.16653000e-01], + [0.00000000e00, 0.00000000e00, 8.33306000e-01], + [1.00000000e-06, 1.00000000e-06, 1.24995900e00], + ], + [ + [-2.62310000e-02, 3.77102000e-01, -2.62310000e-02], + [1.96860000e-02, 2.44702000e-01, 2.44702000e-01], + [1.43270000e-02, 3.30993000e-01, 6.47660000e-01], + [9.02200000e-03, 3.72791000e-01, 1.10033100e00], + ], + [ + [-5.24630000e-02, 7.54204000e-01, -5.24630000e-02], + [0.00000000e00, 6.16667000e-01, 3.08333000e-01], + [3.93720000e-02, 4.89403000e-01, 4.89403000e-01], + [3.57730000e-02, 5.78763000e-01, 8.50258000e-01], + ], + [ + [-7.86940000e-02, 1.13130600e00, -7.86940000e-02], + [-3.59270000e-02, 1.02190800e00, 3.16685000e-01], + [3.09040000e-02, 8.31171000e-01, 5.64415000e-01], + [5.90590000e-02, 7.34105000e-01, 7.34105000e-01], ], + ], + [ [ - [ - [3.98947000e-01, -1.77060000e-02, -1.77060000e-02], - [3.33333000e-01, 0.00000000e00, 3.33333000e-01], - [3.90623000e-01, 0.00000000e00, 7.81246000e-01], - [4.04320000e-01, 0.00000000e00, 1.21296000e00], - ], - [ - [2.94597000e-01, 2.94597000e-01, 6.95820000e-02], - [4.16655000e-01, 4.16655000e-01, 4.16655000e-01], - [4.16655000e-01, 4.16655000e-01, 8.33308000e-01], - [4.16656000e-01, 4.16656000e-01, 1.24996100e00], - ], - [ - [3.49416000e-01, 6.57749000e-01, 4.10830000e-02], - [3.40435000e-01, 7.43769000e-01, 3.40435000e-01], - [2.69700000e-01, 4.94715000e-01, 4.94715000e-01], - [3.47660000e-01, 6.64327000e-01, 9.80993000e-01], - ], - [ - [3.44991000e-01, 1.05021300e00, -7.62100000e-03], - [3.14204000e-01, 1.12087100e00, 3.14204000e-01], - [3.08333000e-01, 9.25000000e-01, 6.16667000e-01], - [2.89386000e-01, 7.39417000e-01, 7.39417000e-01], - ], + [3.98947000e-01, -1.77060000e-02, -1.77060000e-02], + [3.33333000e-01, 0.00000000e00, 3.33333000e-01], + [3.90623000e-01, 0.00000000e00, 7.81246000e-01], + [4.04320000e-01, 0.00000000e00, 1.21296000e00], ], [ - [ - [7.97894000e-01, -3.54120000e-02, -3.54120000e-02], - [7.52767000e-01, -2.84790000e-02, 3.62144000e-01], - [6.66667000e-01, 0.00000000e00, 6.66667000e-01], - [7.46911000e-01, 0.00000000e00, 1.12036600e00], - ], - [ - [6.33333000e-01, 3.16667000e-01, 0.00000000e00], - [7.32278000e-01, 3.15626000e-01, 3.15626000e-01], - [6.66667000e-01, 3.33333000e-01, 6.66667000e-01], - [7.81246000e-01, 3.90623000e-01, 1.17186900e00], - ], - [ - [5.89195000e-01, 5.89195000e-01, 1.39164000e-01], - [5.94601000e-01, 5.94601000e-01, 3.69586000e-01], - [8.33311000e-01, 8.33311000e-01, 8.33311000e-01], - [8.33311000e-01, 8.33311000e-01, 1.24996300e00], - ], - [ - [6.63432000e-01, 9.30188000e-01, 1.29920000e-01], - [6.82749000e-01, 9.91082000e-01, 3.74416000e-01], - [7.07102000e-01, 1.11043500e00, 7.07102000e-01], - [5.19714000e-01, 7.44729000e-01, 7.44729000e-01], - ], + [2.94597000e-01, 2.94597000e-01, 6.95820000e-02], + [4.16655000e-01, 4.16655000e-01, 4.16655000e-01], + [4.16655000e-01, 4.16655000e-01, 8.33308000e-01], + [4.16656000e-01, 4.16656000e-01, 1.24996100e00], + ], + [ + [3.49416000e-01, 6.57749000e-01, 4.10830000e-02], + [3.40435000e-01, 7.43769000e-01, 3.40435000e-01], + [2.69700000e-01, 4.94715000e-01, 4.94715000e-01], + [3.47660000e-01, 6.64327000e-01, 9.80993000e-01], + ], + [ + [3.44991000e-01, 1.05021300e00, -7.62100000e-03], + [3.14204000e-01, 1.12087100e00, 3.14204000e-01], + [3.08333000e-01, 9.25000000e-01, 6.16667000e-01], + [2.89386000e-01, 7.39417000e-01, 7.39417000e-01], + ], + ], + [ + [ + [7.97894000e-01, -3.54120000e-02, -3.54120000e-02], + [7.52767000e-01, -2.84790000e-02, 3.62144000e-01], + [6.66667000e-01, 0.00000000e00, 6.66667000e-01], + [7.46911000e-01, 0.00000000e00, 1.12036600e00], ], [ - [ - [1.19684100e00, -5.31170000e-02, -5.31170000e-02], - [1.16258800e00, -5.03720000e-02, 3.53948000e-01], - [1.08900300e00, -3.13630000e-02, 7.15547000e-01], - [1.00000000e00, 0.00000000e00, 1.00000000e00], - ], - [ - [1.03843900e00, 3.10899000e-01, -5.28700000e-02], - [1.13122500e00, 2.97920000e-01, 2.97920000e-01], - [1.08610100e00, 3.04855000e-01, 6.95478000e-01], - [1.00000000e00, 3.33333000e-01, 1.00000000e00], - ], - [ - [8.91318000e-01, 6.19823000e-01, 7.68330000e-02], - [9.50000000e-01, 6.33333000e-01, 3.16667000e-01], - [1.06561000e00, 6.48957000e-01, 6.48957000e-01], - [1.00000000e00, 6.66667000e-01, 1.00000000e00], - ], - [ - [8.83792000e-01, 8.83792000e-01, 2.08746000e-01], - [8.89199000e-01, 8.89199000e-01, 4.39168000e-01], - [8.94606000e-01, 8.94606000e-01, 6.69590000e-01], - [1.24996600e00, 1.24996600e00, 1.24996600e00], - ], + [6.33333000e-01, 3.16667000e-01, 0.00000000e00], + [7.32278000e-01, 3.15626000e-01, 3.15626000e-01], + [6.66667000e-01, 3.33333000e-01, 6.66667000e-01], + [7.81246000e-01, 3.90623000e-01, 1.17186900e00], ], - ] - ), + [ + [5.89195000e-01, 5.89195000e-01, 1.39164000e-01], + [5.94601000e-01, 5.94601000e-01, 3.69586000e-01], + [8.33311000e-01, 8.33311000e-01, 8.33311000e-01], + [8.33311000e-01, 8.33311000e-01, 1.24996300e00], + ], + [ + [6.63432000e-01, 9.30188000e-01, 1.29920000e-01], + [6.82749000e-01, 9.91082000e-01, 3.74416000e-01], + [7.07102000e-01, 1.11043500e00, 7.07102000e-01], + [5.19714000e-01, 7.44729000e-01, 7.44729000e-01], + ], + ], + [ + [ + [1.19684100e00, -5.31170000e-02, -5.31170000e-02], + [1.16258800e00, -5.03720000e-02, 3.53948000e-01], + [1.08900300e00, -3.13630000e-02, 7.15547000e-01], + [1.00000000e00, 0.00000000e00, 1.00000000e00], + ], + [ + [1.03843900e00, 3.10899000e-01, -5.28700000e-02], + [1.13122500e00, 2.97920000e-01, 2.97920000e-01], + [1.08610100e00, 3.04855000e-01, 6.95478000e-01], + [1.00000000e00, 3.33333000e-01, 1.00000000e00], + ], + [ + [8.91318000e-01, 6.19823000e-01, 7.68330000e-02], + [9.50000000e-01, 6.33333000e-01, 3.16667000e-01], + [1.06561000e00, 6.48957000e-01, 6.48957000e-01], + [1.00000000e00, 6.66667000e-01, 1.00000000e00], + ], + [ + [8.83792000e-01, 8.83792000e-01, 2.08746000e-01], + [8.89199000e-01, 8.89199000e-01, 4.39168000e-01], + [8.94606000e-01, 8.94606000e-01, 6.69590000e-01], + [1.24996600e00, 1.24996600e00, 1.24996600e00], + ], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "Colour Correct" assert LUT_1.dimensions == 3 - np.testing.assert_array_equal(LUT_1.domain, np.array([[0, 0, 0], [1, 1, 1]])) + xp_assert_equal(LUT_1.domain, [[0, 0, 0], [1, 1, 1]]) assert LUT_1.size == 4 assert LUT_1.comments == ["Adapted from a LUT generated by Foundry::LUT."] @@ -216,210 +212,208 @@ def test_write_LUT_SonySPI3D(self) -> None: if len(tokens) == 6: indexes.append(as_int_array(tokens[:3])) - np.testing.assert_array_equal( + xp_assert_equal( as_int_array(indexes)[:200, ...], - np.array( - [ - [0, 0, 0], - [0, 0, 1], - [0, 0, 2], - [0, 0, 3], - [0, 0, 4], - [0, 0, 5], - [0, 0, 6], - [0, 0, 7], - [0, 0, 8], - [0, 0, 9], - [0, 1, 0], - [0, 1, 1], - [0, 1, 2], - [0, 1, 3], - [0, 1, 4], - [0, 1, 5], - [0, 1, 6], - [0, 1, 7], - [0, 1, 8], - [0, 1, 9], - [0, 2, 0], - [0, 2, 1], - [0, 2, 2], - [0, 2, 3], - [0, 2, 4], - [0, 2, 5], - [0, 2, 6], - [0, 2, 7], - [0, 2, 8], - [0, 2, 9], - [0, 3, 0], - [0, 3, 1], - [0, 3, 2], - [0, 3, 3], - [0, 3, 4], - [0, 3, 5], - [0, 3, 6], - [0, 3, 7], - [0, 3, 8], - [0, 3, 9], - [0, 4, 0], - [0, 4, 1], - [0, 4, 2], - [0, 4, 3], - [0, 4, 4], - [0, 4, 5], - [0, 4, 6], - [0, 4, 7], - [0, 4, 8], - [0, 4, 9], - [0, 5, 0], - [0, 5, 1], - [0, 5, 2], - [0, 5, 3], - [0, 5, 4], - [0, 5, 5], - [0, 5, 6], - [0, 5, 7], - [0, 5, 8], - [0, 5, 9], - [0, 6, 0], - [0, 6, 1], - [0, 6, 2], - [0, 6, 3], - [0, 6, 4], - [0, 6, 5], - [0, 6, 6], - [0, 6, 7], - [0, 6, 8], - [0, 6, 9], - [0, 7, 0], - [0, 7, 1], - [0, 7, 2], - [0, 7, 3], - [0, 7, 4], - [0, 7, 5], - [0, 7, 6], - [0, 7, 7], - [0, 7, 8], - [0, 7, 9], - [0, 8, 0], - [0, 8, 1], - [0, 8, 2], - [0, 8, 3], - [0, 8, 4], - [0, 8, 5], - [0, 8, 6], - [0, 8, 7], - [0, 8, 8], - [0, 8, 9], - [0, 9, 0], - [0, 9, 1], - [0, 9, 2], - [0, 9, 3], - [0, 9, 4], - [0, 9, 5], - [0, 9, 6], - [0, 9, 7], - [0, 9, 8], - [0, 9, 9], - [1, 0, 0], - [1, 0, 1], - [1, 0, 2], - [1, 0, 3], - [1, 0, 4], - [1, 0, 5], - [1, 0, 6], - [1, 0, 7], - [1, 0, 8], - [1, 0, 9], - [1, 1, 0], - [1, 1, 1], - [1, 1, 2], - [1, 1, 3], - [1, 1, 4], - [1, 1, 5], - [1, 1, 6], - [1, 1, 7], - [1, 1, 8], - [1, 1, 9], - [1, 2, 0], - [1, 2, 1], - [1, 2, 2], - [1, 2, 3], - [1, 2, 4], - [1, 2, 5], - [1, 2, 6], - [1, 2, 7], - [1, 2, 8], - [1, 2, 9], - [1, 3, 0], - [1, 3, 1], - [1, 3, 2], - [1, 3, 3], - [1, 3, 4], - [1, 3, 5], - [1, 3, 6], - [1, 3, 7], - [1, 3, 8], - [1, 3, 9], - [1, 4, 0], - [1, 4, 1], - [1, 4, 2], - [1, 4, 3], - [1, 4, 4], - [1, 4, 5], - [1, 4, 6], - [1, 4, 7], - [1, 4, 8], - [1, 4, 9], - [1, 5, 0], - [1, 5, 1], - [1, 5, 2], - [1, 5, 3], - [1, 5, 4], - [1, 5, 5], - [1, 5, 6], - [1, 5, 7], - [1, 5, 8], - [1, 5, 9], - [1, 6, 0], - [1, 6, 1], - [1, 6, 2], - [1, 6, 3], - [1, 6, 4], - [1, 6, 5], - [1, 6, 6], - [1, 6, 7], - [1, 6, 8], - [1, 6, 9], - [1, 7, 0], - [1, 7, 1], - [1, 7, 2], - [1, 7, 3], - [1, 7, 4], - [1, 7, 5], - [1, 7, 6], - [1, 7, 7], - [1, 7, 8], - [1, 7, 9], - [1, 8, 0], - [1, 8, 1], - [1, 8, 2], - [1, 8, 3], - [1, 8, 4], - [1, 8, 5], - [1, 8, 6], - [1, 8, 7], - [1, 8, 8], - [1, 8, 9], - [1, 9, 0], - [1, 9, 1], - [1, 9, 2], - [1, 9, 3], - [1, 9, 4], - [1, 9, 5], - [1, 9, 6], - [1, 9, 7], - [1, 9, 8], - [1, 9, 9], - ] - ), + [ + [0, 0, 0], + [0, 0, 1], + [0, 0, 2], + [0, 0, 3], + [0, 0, 4], + [0, 0, 5], + [0, 0, 6], + [0, 0, 7], + [0, 0, 8], + [0, 0, 9], + [0, 1, 0], + [0, 1, 1], + [0, 1, 2], + [0, 1, 3], + [0, 1, 4], + [0, 1, 5], + [0, 1, 6], + [0, 1, 7], + [0, 1, 8], + [0, 1, 9], + [0, 2, 0], + [0, 2, 1], + [0, 2, 2], + [0, 2, 3], + [0, 2, 4], + [0, 2, 5], + [0, 2, 6], + [0, 2, 7], + [0, 2, 8], + [0, 2, 9], + [0, 3, 0], + [0, 3, 1], + [0, 3, 2], + [0, 3, 3], + [0, 3, 4], + [0, 3, 5], + [0, 3, 6], + [0, 3, 7], + [0, 3, 8], + [0, 3, 9], + [0, 4, 0], + [0, 4, 1], + [0, 4, 2], + [0, 4, 3], + [0, 4, 4], + [0, 4, 5], + [0, 4, 6], + [0, 4, 7], + [0, 4, 8], + [0, 4, 9], + [0, 5, 0], + [0, 5, 1], + [0, 5, 2], + [0, 5, 3], + [0, 5, 4], + [0, 5, 5], + [0, 5, 6], + [0, 5, 7], + [0, 5, 8], + [0, 5, 9], + [0, 6, 0], + [0, 6, 1], + [0, 6, 2], + [0, 6, 3], + [0, 6, 4], + [0, 6, 5], + [0, 6, 6], + [0, 6, 7], + [0, 6, 8], + [0, 6, 9], + [0, 7, 0], + [0, 7, 1], + [0, 7, 2], + [0, 7, 3], + [0, 7, 4], + [0, 7, 5], + [0, 7, 6], + [0, 7, 7], + [0, 7, 8], + [0, 7, 9], + [0, 8, 0], + [0, 8, 1], + [0, 8, 2], + [0, 8, 3], + [0, 8, 4], + [0, 8, 5], + [0, 8, 6], + [0, 8, 7], + [0, 8, 8], + [0, 8, 9], + [0, 9, 0], + [0, 9, 1], + [0, 9, 2], + [0, 9, 3], + [0, 9, 4], + [0, 9, 5], + [0, 9, 6], + [0, 9, 7], + [0, 9, 8], + [0, 9, 9], + [1, 0, 0], + [1, 0, 1], + [1, 0, 2], + [1, 0, 3], + [1, 0, 4], + [1, 0, 5], + [1, 0, 6], + [1, 0, 7], + [1, 0, 8], + [1, 0, 9], + [1, 1, 0], + [1, 1, 1], + [1, 1, 2], + [1, 1, 3], + [1, 1, 4], + [1, 1, 5], + [1, 1, 6], + [1, 1, 7], + [1, 1, 8], + [1, 1, 9], + [1, 2, 0], + [1, 2, 1], + [1, 2, 2], + [1, 2, 3], + [1, 2, 4], + [1, 2, 5], + [1, 2, 6], + [1, 2, 7], + [1, 2, 8], + [1, 2, 9], + [1, 3, 0], + [1, 3, 1], + [1, 3, 2], + [1, 3, 3], + [1, 3, 4], + [1, 3, 5], + [1, 3, 6], + [1, 3, 7], + [1, 3, 8], + [1, 3, 9], + [1, 4, 0], + [1, 4, 1], + [1, 4, 2], + [1, 4, 3], + [1, 4, 4], + [1, 4, 5], + [1, 4, 6], + [1, 4, 7], + [1, 4, 8], + [1, 4, 9], + [1, 5, 0], + [1, 5, 1], + [1, 5, 2], + [1, 5, 3], + [1, 5, 4], + [1, 5, 5], + [1, 5, 6], + [1, 5, 7], + [1, 5, 8], + [1, 5, 9], + [1, 6, 0], + [1, 6, 1], + [1, 6, 2], + [1, 6, 3], + [1, 6, 4], + [1, 6, 5], + [1, 6, 6], + [1, 6, 7], + [1, 6, 8], + [1, 6, 9], + [1, 7, 0], + [1, 7, 1], + [1, 7, 2], + [1, 7, 3], + [1, 7, 4], + [1, 7, 5], + [1, 7, 6], + [1, 7, 7], + [1, 7, 8], + [1, 7, 9], + [1, 8, 0], + [1, 8, 1], + [1, 8, 2], + [1, 8, 3], + [1, 8, 4], + [1, 8, 5], + [1, 8, 6], + [1, 8, 7], + [1, 8, 8], + [1, 8, 9], + [1, 9, 0], + [1, 9, 1], + [1, 9, 2], + [1, 9, 3], + [1, 9, 4], + [1, 9, 5], + [1, 9, 6], + [1, 9, 7], + [1, 9, 8], + [1, 9, 9], + ], ) diff --git a/colour/io/luts/tests/test_sony_spimtx.py b/colour/io/luts/tests/test_sony_spimtx.py index 88f839be50..0a2e20874f 100644 --- a/colour/io/luts/tests/test_sony_spimtx.py +++ b/colour/io/luts/tests/test_sony_spimtx.py @@ -6,10 +6,9 @@ import shutil import tempfile -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import read_LUT_SonySPImtx, write_LUT_SonySPImtx +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -41,61 +40,55 @@ def test_read_LUT_SonySPImtx(self) -> None: LUT_1 = read_LUT_SonySPImtx(os.path.join(ROOT_LUTS, "dt.spimtx")) - np.testing.assert_allclose( + xp_assert_close( LUT_1.matrix, - np.array( - [ - [0.864274, 0.000000, 0.000000, 0.000000], - [0.000000, 0.864274, 0.000000, 0.000000], - [0.000000, 0.000000, 0.864274, 0.000000], - [0.000000, 0.000000, 0.000000, 1.000000], - ] - ), + [ + [0.864274, 0.000000, 0.000000, 0.000000], + [0.000000, 0.864274, 0.000000, 0.000000], + [0.000000, 0.000000, 0.864274, 0.000000], + [0.000000, 0.000000, 0.000000, 1.000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_1.offset, - np.array([0.000000, 0.000000, 0.000000, 0.000000]), + [0.000000, 0.000000, 0.000000, 0.000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_1.name == "dt" LUT_2 = read_LUT_SonySPImtx(os.path.join(ROOT_LUTS, "p3_to_xyz16.spimtx")) - np.testing.assert_allclose( + xp_assert_close( LUT_2.matrix, - np.array( - [ - [0.44488, 0.27717, 0.17237, 0.00000], - [0.20936, 0.72170, 0.06895, 0.00000], - [0.00000, 0.04707, 0.90780, 0.00000], - [0.00000, 0.00000, 0.00000, 1.00000], - ] - ), + [ + [0.44488, 0.27717, 0.17237, 0.00000], + [0.20936, 0.72170, 0.06895, 0.00000], + [0.00000, 0.04707, 0.90780, 0.00000], + [0.00000, 0.00000, 0.00000, 1.00000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_2.offset, - np.array([0.000000, 0.000000, 0.000000, 0.000000]), + [0.000000, 0.000000, 0.000000, 0.000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_2.name == "p3 to xyz16" LUT_3 = read_LUT_SonySPImtx(os.path.join(ROOT_LUTS, "Matrix_Offset.spimtx")) - np.testing.assert_allclose( + xp_assert_close( LUT_3.matrix, - np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - ), + [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT_3.offset, - np.array([0.0, 0.0, 1.0, 0.0]), + [0.0, 0.0, 1.0, 0.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert LUT_3.name == "Matrix Offset" diff --git a/colour/io/tests/test_ctl.py b/colour/io/tests/test_ctl.py index 39d789ce53..c015a4619c 100644 --- a/colour/io/tests/test_ctl.py +++ b/colour/io/tests/test_ctl.py @@ -7,8 +7,6 @@ import tempfile import textwrap -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import ( ctl_render, @@ -17,7 +15,7 @@ template_ctl_transform_float, template_ctl_transform_float3, ) -from colour.utilities import full, is_ctlrender_installed +from colour.utilities import full, is_ctlrender_installed, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -91,7 +89,7 @@ def test_ctl_render(self) -> None: # pragma: no cover "-force", ) - np.testing.assert_allclose( + xp_assert_close( read_image(path_output)[..., 0:3], read_image(path_input) * [1, 2, 4], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -110,7 +108,7 @@ def test_ctl_render(self) -> None: # pragma: no cover env=dict(os.environ, CTL_MODULE_PATH=ROOT_RESOURCES), ) - np.testing.assert_allclose( + xp_assert_close( read_image(path_output)[..., 0:3], read_image(path_input) * 2, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -137,7 +135,7 @@ def test_process_image_ctl(self) -> None: # pragma: no cover parameters=["input float gain[3] = {1.0, 1.0, 1.0}"], ) - np.testing.assert_allclose( + xp_assert_close( process_image_ctl( 0.18, { @@ -147,43 +145,43 @@ def test_process_image_ctl(self) -> None: # pragma: no cover "-force", ), 0.18 / 2, - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( process_image_ctl( - np.array([0.18, 0.18, 0.18]), + [0.18, 0.18, 0.18], { ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], }, ), - np.array([0.18 / 2, 0.18, 0.18 * 2]), - atol=0.0001, + [0.18 / 2, 0.18, 0.18 * 2], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( process_image_ctl( - np.array([[0.18, 0.18, 0.18]]), + [[0.18, 0.18, 0.18]], { ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], }, ), - np.array([[0.18 / 2, 0.18, 0.18 * 2]]), - atol=0.0001, + [[0.18 / 2, 0.18, 0.18 * 2]], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( process_image_ctl( - np.array([[[0.18, 0.18, 0.18]]]), + [[[0.18, 0.18, 0.18]]], { ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], }, ), - np.array([[[0.18 / 2, 0.18, 0.18 * 2]]]), - atol=0.0001, + [[[0.18 / 2, 0.18, 0.18 * 2]]], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( + xp_assert_close( process_image_ctl( full([4, 2, 3], 0.18), { @@ -191,7 +189,7 @@ def test_process_image_ctl(self) -> None: # pragma: no cover }, ), full([4, 2, 3], 0.18) * [0.5, 1.0, 2.0], - atol=0.0001, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) diff --git a/colour/io/tests/test_fichet2021.py b/colour/io/tests/test_fichet2021.py index bed956ae69..39734bad9d 100644 --- a/colour/io/tests/test_fichet2021.py +++ b/colour/io/tests/test_fichet2021.py @@ -29,6 +29,7 @@ match_groups_to_nm, sds_and_msds_to_components_Fichet2021, ) +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -60,19 +61,19 @@ class TestMatchGroupsToNm: def test_match_groups_to_nm(self) -> None: """Test :func:`colour.io.fichet2021.match_groups_to_nm` definition.""" - np.testing.assert_allclose( + xp_assert_close( match_groups_to_nm("555.5", "n", "m"), 555.5, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( match_groups_to_nm("555.5", "", "m"), 555500000000.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( match_groups_to_nm(str(CONSTANT_LIGHT_SPEED / (555 * 1e-9)), "", "Hz"), 555.0, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -113,15 +114,15 @@ def test_spectrum_attribute_to_sd_Fichet2021(self) -> None: "300.00nm:0.03;305.00nm:1.66;310.00nm:3.29;315.00nm:11.77" ) - np.testing.assert_allclose( + xp_assert_close( sd.wavelengths, - np.array([300.0, 305.0, 310.0, 315.0]), + [300.0, 305.0, 310.0, 315.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd.values, - np.array([0.03, 1.66, 3.29, 11.77]), + [0.03, 1.66, 3.29, 11.77], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -148,13 +149,13 @@ def test_sds_and_msds_to_components_Fichet2021(self) -> None: assert "S0" in components - np.testing.assert_allclose( + xp_assert_close( components["S0"][0], SDS_ILLUMINANTS["D65"].wavelengths, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( components["S0"][1], np.reshape(SDS_ILLUMINANTS["D65"].values, (1, 1, -1)), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -185,9 +186,9 @@ def test_components_to_sRGB_Fichet2021(self) -> None: ) RGB, attributes = components_to_sRGB_Fichet2021(components, specification) - np.testing.assert_allclose( + xp_assert_close( cast("NDArrayFloat", RGB), - np.array([[[0.17998291, 0.18000802, 0.18000908]]]), + [[[0.17998291, 0.18000802, 0.18000908]]], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -203,7 +204,7 @@ def test_components_to_sRGB_Fichet2021(self) -> None: for attribute in attributes: if attribute.name == "X": sd_X = spectrum_attribute_to_sd_Fichet2021(attribute.value) - np.testing.assert_allclose( + xp_assert_close( sd_X.values, MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] .signals["x_bar"] @@ -212,7 +213,7 @@ def test_components_to_sRGB_Fichet2021(self) -> None: ) elif attribute.name == "illuminant": sd_illuminant = spectrum_attribute_to_sd_Fichet2021(attribute.value) - np.testing.assert_allclose( + xp_assert_close( sd_illuminant.values, SDS_ILLUMINANTS["E"].values, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -235,38 +236,36 @@ def test_components_to_sRGB_Fichet2021(self) -> None: ) RGB, attributes = components_to_sRGB_Fichet2021(components, specification) - np.testing.assert_allclose( + xp_assert_close( cast("NDArrayFloat", RGB), - np.array( + [ [ - [ - [0.17617566, 0.07822266, 0.05031637], - [0.55943028, 0.30875974, 0.22283237], - [0.11315875, 0.19922170, 0.33614049], - [0.09458646, 0.14840988, 0.04988729], - [0.23628263, 0.22587419, 0.44382286], - [0.13383963, 0.51702099, 0.40286142], - [0.70140973, 0.19925074, 0.02292392], - [0.06838428, 0.10600215, 0.37710859], - [0.55811797, 0.09062764, 0.12199424], - [0.10779019, 0.04434715, 0.14682113], - [0.34888054, 0.50195490, 0.04773998], - [0.79166868, 0.36502900, 0.02678776], - [0.02722027, 0.04781536, 0.30913913], - [0.06013188, 0.30558427, 0.06062012], - [0.44611192, 0.02849786, 0.04207225], - [0.85188200, 0.57960585, 0.01053590], - [0.50608734, 0.08898812, 0.29720873], - [-0.03338628, 0.24880620, 0.38541145], - [0.88687341, 0.88867240, 0.87460352], - [0.58637305, 0.58330907, 0.58216473], - [0.35827233, 0.35810703, 0.35873042], - [0.20316001, 0.20298624, 0.20353015], - [0.09106388, 0.09288101, 0.09424415], - [0.03266569, 0.03364008, 0.03526672], - ] + [0.17617566, 0.07822266, 0.05031637], + [0.55943028, 0.30875974, 0.22283237], + [0.11315875, 0.19922170, 0.33614049], + [0.09458646, 0.14840988, 0.04988729], + [0.23628263, 0.22587419, 0.44382286], + [0.13383963, 0.51702099, 0.40286142], + [0.70140973, 0.19925074, 0.02292392], + [0.06838428, 0.10600215, 0.37710859], + [0.55811797, 0.09062764, 0.12199424], + [0.10779019, 0.04434715, 0.14682113], + [0.34888054, 0.50195490, 0.04773998], + [0.79166868, 0.36502900, 0.02678776], + [0.02722027, 0.04781536, 0.30913913], + [0.06013188, 0.30558427, 0.06062012], + [0.44611192, 0.02849786, 0.04207225], + [0.85188200, 0.57960585, 0.01053590], + [0.50608734, 0.08898812, 0.29720873], + [-0.03338628, 0.24880620, 0.38541145], + [0.88687341, 0.88867240, 0.87460352], + [0.58637305, 0.58330907, 0.58216473], + [0.35827233, 0.35810703, 0.35873042], + [0.20316001, 0.20298624, 0.20353015], + [0.09106388, 0.09288101, 0.09424415], + [0.03266569, 0.03364008, 0.03526672], ] - ), + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -291,16 +290,16 @@ def _test_spectral_image_D65(path: str) -> None: assert "S0" in components - np.testing.assert_allclose( + xp_assert_close( components["S0"][0], SDS_ILLUMINANTS["D65"].wavelengths, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( components["S0"][1], np.reshape(SDS_ILLUMINANTS["D65"].values, (1, 1, -1)), - atol=0.05, + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, # OpenEXR *float16* storage precision. ) components, specification = read_spectral_image_Fichet2021( @@ -335,7 +334,7 @@ def _test_spectral_image_D65(path: str) -> None: assert attribute.value == "W.m^-2.sr^-1" elif attribute.name == "illuminant": sd_illuminant = spectrum_attribute_to_sd_Fichet2021(attribute.value) - np.testing.assert_allclose( + xp_assert_close( sd_illuminant.values, SDS_ILLUMINANTS["D65"].values, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -358,16 +357,16 @@ def _test_spectral_image_Ohta1997(path: str) -> None: ] ) - np.testing.assert_allclose( + xp_assert_close( components["T"][0], msds.wavelengths, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( components["T"][1], np.reshape(np.transpose(msds.values), (4, 6, -1)), - atol=0.0005, + atol=TOLERANCE_ABSOLUTE_TESTS * 5000, # OpenEXR *float16* mantissa precision. ) assert specification.is_emissive is False diff --git a/colour/io/tests/test_image.py b/colour/io/tests/test_image.py index cd4539c259..1b958bdbf9 100644 --- a/colour/io/tests/test_image.py +++ b/colour/io/tests/test_image.py @@ -23,7 +23,7 @@ write_image_Imageio, write_image_OpenImageIO, ) -from colour.utilities import attest, full +from colour.utilities import attest, full, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -89,10 +89,10 @@ def test_convert_bit_depth(self) -> None: a = np.around(np.linspace(0, 1, 10) * 255).astype("uint8") assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8") - np.testing.assert_equal(convert_bit_depth(a, "uint8"), a) + xp_assert_equal(convert_bit_depth(a, "uint8"), a) assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16") - np.testing.assert_equal( + xp_assert_equal( convert_bit_depth(a, "uint16"), np.array( [ @@ -111,42 +111,38 @@ def test_convert_bit_depth(self) -> None: ) assert convert_bit_depth(a, "float16").dtype is np.dtype("float16") - np.testing.assert_allclose( + xp_assert_close( convert_bit_depth(a, "float16"), - np.array( - [ - 0.0000, - 0.1098, - 0.2235, - 0.3333, - 0.443, - 0.5566, - 0.6665, - 0.7764, - 0.8900, - 1.0000, - ] - ), - atol=5e-4, + [ + 0.0000, + 0.1098, + 0.2235, + 0.3333, + 0.443, + 0.5566, + 0.6665, + 0.7764, + 0.8900, + 1.0000, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 5000, # *float16* mantissa precision. ) assert convert_bit_depth(a, "float32").dtype is np.dtype("float32") - np.testing.assert_allclose( + xp_assert_close( convert_bit_depth(a, "float32"), - np.array( - [ - 0.00000000, - 0.10980392, - 0.22352941, - 0.33333334, - 0.44313726, - 0.55686277, - 0.66666669, - 0.77647060, - 0.89019608, - 1.00000000, - ] - ), + [ + 0.00000000, + 0.10980392, + 0.22352941, + 0.33333334, + 0.44313726, + 0.55686277, + 0.66666669, + 0.77647060, + 0.89019608, + 1.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -157,51 +153,48 @@ def test_convert_bit_depth(self) -> None: a = np.around(np.linspace(0, 1, 10) * 65535).astype("uint16") assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8") - np.testing.assert_equal( + xp_assert_equal( convert_bit_depth(a, "uint8"), np.array([0, 28, 56, 85, 113, 141, 170, 198, 226, 255]), ) assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16") - np.testing.assert_equal(convert_bit_depth(a, "uint16"), a) + xp_assert_equal(convert_bit_depth(a, "uint16"), a) assert convert_bit_depth(a, "float16").dtype is np.dtype("float16") - np.testing.assert_allclose( + xp_assert_close( convert_bit_depth(a, "float16"), - np.array( - [ - 0.0000, - 0.1098, - 0.2235, - 0.3333, - 0.443, - 0.5566, - 0.6665, - 0.7764, - 0.8900, - 1.0000, - ] - ), - atol=5e-2, + [ + 0.0000, + 0.1098, + 0.2235, + 0.3333, + 0.443, + 0.5566, + 0.6665, + 0.7764, + 0.8900, + 1.0000, + ], + # Coarse *float16* quantisation across this batch. + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) assert convert_bit_depth(a, "float32").dtype is np.dtype("float32") - np.testing.assert_allclose( + xp_assert_close( convert_bit_depth(a, "float32"), - np.array( - [ - 0.00000000, - 0.11111620, - 0.22221714, - 0.33333334, - 0.44444954, - 0.55555046, - 0.66666669, - 0.77778286, - 0.88888383, - 1.00000000, - ] - ), + [ + 0.00000000, + 0.11111620, + 0.22221714, + 0.33333334, + 0.44444954, + 0.55555046, + 0.66666669, + 0.77778286, + 0.88888383, + 1.00000000, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -212,13 +205,13 @@ def test_convert_bit_depth(self) -> None: a = np.linspace(0, 1, 10, dtype=np.float64) assert convert_bit_depth(a, "uint8").dtype is np.dtype("uint8") - np.testing.assert_equal( + xp_assert_equal( convert_bit_depth(a, "uint8"), np.array([0, 28, 57, 85, 113, 142, 170, 198, 227, 255]), ) assert convert_bit_depth(a, "uint16").dtype is np.dtype("uint16") - np.testing.assert_equal( + xp_assert_equal( convert_bit_depth(a, "uint16"), np.array( [ @@ -237,27 +230,25 @@ def test_convert_bit_depth(self) -> None: ) assert convert_bit_depth(a, "float16").dtype is np.dtype("float16") - np.testing.assert_allclose( + xp_assert_close( convert_bit_depth(a, "float16"), - np.array( - [ - 0.0000, - 0.1111, - 0.2222, - 0.3333, - 0.4443, - 0.5557, - 0.6665, - 0.7780, - 0.8887, - 1.0000, - ] - ), - atol=5e-4, + [ + 0.0000, + 0.1111, + 0.2222, + 0.3333, + 0.4443, + 0.5557, + 0.6665, + 0.7780, + 0.8887, + 1.0000, + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 5000, # *float16* mantissa precision. ) assert convert_bit_depth(a, "float32").dtype is np.dtype("float32") - np.testing.assert_allclose( + xp_assert_close( convert_bit_depth(a, "float32"), a, atol=TOLERANCE_ABSOLUTE_TESTS ) @@ -377,25 +368,29 @@ def test_write_image_OpenImageIO(self) -> None: # pragma: no cover path = os.path.join(self._temporary_directory, "8-bit.png") RGB = full((1, 1, 3), 255, np.uint8) write_image_OpenImageIO(RGB, path, bit_depth="uint8") - image = read_image_OpenImageIO(path, bit_depth="uint8") - np.testing.assert_equal(np.squeeze(RGB), image) + image = read_image_OpenImageIO(path, bit_depth="uint8", additional_data=False) + xp_assert_equal(np.squeeze(RGB), image) path = os.path.join(self._temporary_directory, "16-bit.png") RGB = full((1, 1, 3), 65535, np.uint16) write_image_OpenImageIO(RGB, path, bit_depth="uint16") - image = read_image_OpenImageIO(path, bit_depth="uint16") - np.testing.assert_equal(np.squeeze(RGB), image) + image = read_image_OpenImageIO(path, bit_depth="uint16", additional_data=False) + xp_assert_equal(np.squeeze(RGB), image) source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png") - source_image = read_image_OpenImageIO(source_path, bit_depth="uint8") + source_image = read_image_OpenImageIO( + source_path, bit_depth="uint8", additional_data=False + ) target_path = os.path.join( self._temporary_directory, "Overflowing_Gradient.png" ) RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2 write_image_OpenImageIO(RGB, target_path, bit_depth="uint8") - target_image = read_image_OpenImageIO(source_path, bit_depth="uint8") - np.testing.assert_equal(source_image, target_image) - np.testing.assert_equal(np.squeeze(RGB), target_image) + target_image = read_image_OpenImageIO( + source_path, bit_depth="uint8", additional_data=False + ) + xp_assert_equal(source_image, target_image) + xp_assert_equal(np.squeeze(RGB), target_image) source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr") source_image = read_image_OpenImageIO( @@ -408,7 +403,7 @@ def test_write_image_OpenImageIO(self) -> None: # pragma: no cover target_path, additional_data=False, ) - np.testing.assert_equal(source_image, target_image) + xp_assert_equal(source_image, target_image) assert target_image.shape == (1267, 1274, 3) assert target_image.dtype is np.dtype("float32") @@ -439,7 +434,7 @@ def test_write_image_OpenImageIO(self) -> None: # pragma: no cover if write_attribute.name == read_attribute.name: attribute_exists = True if isinstance(write_attribute.value, tuple): - np.testing.assert_allclose( + xp_assert_close( write_attribute.value, read_attribute.value, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -534,8 +529,8 @@ def test_write_image_Imageio(self) -> None: RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2 write_image_Imageio(RGB, target_path, bit_depth="uint8") target_image = read_image_Imageio(target_path, bit_depth="uint8") - np.testing.assert_equal(np.squeeze(RGB), target_image) - np.testing.assert_equal(source_image, target_image) + xp_assert_equal(np.squeeze(RGB), target_image) + xp_assert_equal(source_image, target_image) @pytest.mark.skipif( platform.system() == "Linux", @@ -552,9 +547,7 @@ def test_write_image_Imageio_exr(self) -> None: target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr") write_image_Imageio(source_image, target_path) target_image = read_image_Imageio(target_path) - np.testing.assert_allclose( - source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS) assert target_image.shape == (1267, 1274, 3) assert target_image.dtype is np.dtype("float32") @@ -612,9 +605,7 @@ def test_write_image(self) -> None: target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr") write_image(source_image, target_path) target_image = read_image(target_path) - np.testing.assert_allclose( - source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS) assert target_image.shape == (1267, 1274, 3) assert target_image.dtype is np.dtype("float32") @@ -630,17 +621,17 @@ def test_as_3_channels_image(self) -> None: a = 0.18 b = np.array([[[0.18, 0.18, 0.18]]]) - np.testing.assert_equal(as_3_channels_image(a), b) + xp_assert_equal(as_3_channels_image(a), b) a = np.array([0.18]) - np.testing.assert_equal(as_3_channels_image(a), b) + xp_assert_equal(as_3_channels_image(a), b) a = np.array([0.18, 0.18, 0.18]) - np.testing.assert_equal(as_3_channels_image(a), b) + xp_assert_equal(as_3_channels_image(a), b) a = np.array([[0.18, 0.18, 0.18]]) - np.testing.assert_equal(as_3_channels_image(a), b) + xp_assert_equal(as_3_channels_image(a), b) a = np.array([[[0.18, 0.18, 0.18]]]) - np.testing.assert_equal(as_3_channels_image(a), b) + xp_assert_equal(as_3_channels_image(a), b) a = np.array([[[[0.18, 0.18, 0.18]]]]) - np.testing.assert_equal(as_3_channels_image(a), b) + xp_assert_equal(as_3_channels_image(a), b) a = np.array([[0.18, 0.18, 0.18], [0.20, 0.20, 0.20]]) result = as_3_channels_image(a) assert result.shape == (1, 2, 3) @@ -651,23 +642,19 @@ def test_raise_exception_as_3_channels_image(self) -> None: exception. """ - pytest.raises( - ValueError, - as_3_channels_image, - [ - [ - [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], - [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], - ], + with pytest.raises(ValueError): + as_3_channels_image( [ - [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], - [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], - ], - ], - ) + [ + [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], + [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], + ], + [ + [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], + [[0.18, 0.18, 0.18], [0.18, 0.18, 0.18]], + ], + ] + ) - pytest.raises( - ValueError, - as_3_channels_image, - [0.18, 0.18, 0.18, 0.18], - ) + with pytest.raises(ValueError): + as_3_channels_image([0.18, 0.18, 0.18, 0.18]) diff --git a/colour/io/tests/test_ocio.py b/colour/io/tests/test_ocio.py index 04ebc34cc2..561c5774f3 100644 --- a/colour/io/tests/test_ocio.py +++ b/colour/io/tests/test_ocio.py @@ -8,7 +8,7 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import process_image_OpenColorIO -from colour.utilities import full, is_opencolorio_installed +from colour.utilities import full, is_opencolorio_installed, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -45,34 +45,32 @@ def test_process_image_OpenColorIO(self) -> None: a = full([4, 2, 3], 0.18) - np.testing.assert_allclose( + xp_assert_close( process_image_OpenColorIO( a, "ACES - ACES2065-1", "ACES - ACEScct", config=config ), - np.array( + [ [ - [ - [0.41358781, 0.41358781, 0.41358781], - [0.41358781, 0.41358781, 0.41358781], - ], - [ - [0.41358781, 0.41358781, 0.41358781], - [0.41358781, 0.41358781, 0.41358781], - ], - [ - [0.41358781, 0.41358781, 0.41358781], - [0.41358781, 0.41358781, 0.41358781], - ], - [ - [0.41358781, 0.41358781, 0.41358781], - [0.41358781, 0.41358781, 0.41358781], - ], - ] - ), + [0.41358781, 0.41358781, 0.41358781], + [0.41358781, 0.41358781, 0.41358781], + ], + [ + [0.41358781, 0.41358781, 0.41358781], + [0.41358781, 0.41358781, 0.41358781], + ], + [ + [0.41358781, 0.41358781, 0.41358781], + [0.41358781, 0.41358781, 0.41358781], + ], + [ + [0.41358781, 0.41358781, 0.41358781], + [0.41358781, 0.41358781, 0.41358781], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( process_image_OpenColorIO( a, "ACES - ACES2065-1", @@ -81,26 +79,24 @@ def test_process_image_OpenColorIO(self) -> None: ocio.TRANSFORM_DIR_FORWARD, # pyright: ignore config=config, ), - np.array( + [ [ - [ - [0.35595229, 0.35595256, 0.35595250], - [0.35595229, 0.35595256, 0.35595250], - ], - [ - [0.35595229, 0.35595256, 0.35595250], - [0.35595229, 0.35595256, 0.35595250], - ], - [ - [0.35595229, 0.35595256, 0.35595250], - [0.35595229, 0.35595256, 0.35595250], - ], - [ - [0.35595229, 0.35595256, 0.35595250], - [0.35595229, 0.35595256, 0.35595250], - ], - ] - ), + [0.35595229, 0.35595256, 0.35595250], + [0.35595229, 0.35595256, 0.35595250], + ], + [ + [0.35595229, 0.35595256, 0.35595250], + [0.35595229, 0.35595256, 0.35595250], + ], + [ + [0.35595229, 0.35595256, 0.35595250], + [0.35595229, 0.35595256, 0.35595250], + ], + [ + [0.35595229, 0.35595256, 0.35595250], + [0.35595229, 0.35595256, 0.35595250], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -110,9 +106,7 @@ def test_process_image_OpenColorIO(self) -> None: a_scalar, "ACES - ACES2065-1", "ACES - ACEScct", config=config ) assert isinstance(result_scalar, (float, np.floating)) - np.testing.assert_allclose( - result_scalar, 0.41358781, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(result_scalar, 0.41358781, atol=TOLERANCE_ABSOLUTE_TESTS) # Test single-channel input a_single = np.array([0.18]) @@ -120,6 +114,4 @@ def test_process_image_OpenColorIO(self) -> None: a_single, "ACES - ACES2065-1", "ACES - ACEScct", config=config ) assert result_single.shape == (1,) - np.testing.assert_allclose( - result_single, np.array([0.41358781]), atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(result_single, [0.41358781], atol=TOLERANCE_ABSOLUTE_TESTS) diff --git a/colour/io/tests/test_tabular.py b/colour/io/tests/test_tabular.py index 7753a2dc9b..9a7c7d75f6 100644 --- a/colour/io/tests/test_tabular.py +++ b/colour/io/tests/test_tabular.py @@ -6,15 +6,16 @@ import shutil import tempfile -import numpy as np import pytest from colour.colorimetry import SpectralDistribution, SpectralShape +from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.io import ( read_sds_from_csv_file, read_spectral_data_from_csv_file, write_sds_to_csv_file, ) +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -210,10 +211,16 @@ def test_write_sds_to_csv_file(self) -> None: write_sds_to_csv_file(sds, colour_checker_n_ohta_test) sds_test = read_sds_from_csv_file(colour_checker_n_ohta_test) for key, value in sds.items(): - np.testing.assert_allclose( - value.wavelengths, sds_test[key].wavelengths, atol=1e-10 + xp_assert_close( + value.wavelengths, + sds_test[key].wavelengths, + atol=TOLERANCE_ABSOLUTE_TESTS * 0.001, + ) + xp_assert_close( + value.values, + sds_test[key].values, + atol=TOLERANCE_ABSOLUTE_TESTS * 0.001, ) - np.testing.assert_allclose(value.values, sds_test[key].values, atol=1e-10) def test_raise_exception_write_sds_to_csv_file(self) -> None: """ @@ -226,4 +233,5 @@ def test_raise_exception_write_sds_to_csv_file(self) -> None: key = next(iter(sds.keys())) sds[key] = sds[key].align(SpectralShape(400, 700, 10)) - pytest.raises(ValueError, write_sds_to_csv_file, sds, "") + with pytest.raises(ValueError): + write_sds_to_csv_file(sds, "") diff --git a/colour/io/tests/test_tm2714.py b/colour/io/tests/test_tm2714.py index 0f757001a0..4b89262da3 100644 --- a/colour/io/tests/test_tm2714.py +++ b/colour/io/tests/test_tm2714.py @@ -10,7 +10,6 @@ import typing from copy import deepcopy -import numpy as np import pytest from colour.colorimetry import SpectralDistribution @@ -21,7 +20,12 @@ from colour.hints import cast from colour.io.tm2714 import Header_IESTM2714, SpectralDistribution_IESTM2714 -from colour.utilities import is_scipy_installed, optional +from colour.utilities import ( + is_scipy_installed, + optional, + xp_assert_close, + xp_assert_equal, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -603,10 +607,8 @@ def test_read(self, sd: SpectralDistribution | None = None) -> None: sd_r = SpectralDistribution(FLUORESCENT_FILE_SPECTRAL_DATA) - np.testing.assert_array_equal(sd_r.domain, sd.domain) - np.testing.assert_allclose( - sd_r.values, sd.values, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_equal(sd_r.domain, sd.domain) + xp_assert_close(sd_r.values, sd.values, atol=TOLERANCE_ABSOLUTE_TESTS) test_read: List[ Tuple[dict, Header_IESTM2714 | SpectralDistribution_IESTM2714] @@ -627,7 +629,8 @@ def test_raise_exception_read(self) -> None: """ sd = SpectralDistribution_IESTM2714() - pytest.raises(ValueError, sd.read) + with pytest.raises(ValueError): + sd.read() with pytest.raises(ValueError): sd = SpectralDistribution_IESTM2714( @@ -680,4 +683,5 @@ def test_raise_exception_write(self) -> None: """ sd = SpectralDistribution_IESTM2714() - pytest.raises(ValueError, sd.write) + with pytest.raises(ValueError): + sd.write() diff --git a/colour/io/tests/test_uprtek_sekonic.py b/colour/io/tests/test_uprtek_sekonic.py index bcd0ba2de6..88f1b670fc 100644 --- a/colour/io/tests/test_uprtek_sekonic.py +++ b/colour/io/tests/test_uprtek_sekonic.py @@ -6,7 +6,6 @@ import os import typing -import numpy as np import pytest from colour.colorimetry import SpectralDistribution @@ -16,6 +15,7 @@ from colour.hints import Any from colour.io import SpectralDistribution_Sekonic, SpectralDistribution_UPRTek +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -96,10 +96,8 @@ def test_read(self) -> None: sd_r = SpectralDistribution(self._spectral_data) - np.testing.assert_array_equal(sd_r.domain, sd.domain) - np.testing.assert_allclose( - sd_r.values, sd.values, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_equal(sd_r.domain, sd.domain) + xp_assert_close(sd_r.values, sd.values, atol=TOLERANCE_ABSOLUTE_TESTS) for key, value in self._header.items(): for specification in sd.header.mapping.elements: diff --git a/colour/io/tm2714.py b/colour/io/tm2714.py index 7d7b895536..15b53f2751 100644 --- a/colour/io/tm2714.py +++ b/colour/io/tm2714.py @@ -946,7 +946,7 @@ def __init__( "BandwidthFWHM", "bandwidth_FWHM", read_conversion=( - lambda x: (None if x == "None" else as_float_scalar(x)) + lambda x: None if x == "None" else as_float_scalar(x) ), ), Element_Specification_IESTM2714( diff --git a/colour/models/cam02_ucs.py b/colour/models/cam02_ucs.py index 2cd8697ac7..8029216e18 100644 --- a/colour/models/cam02_ucs.py +++ b/colour/models/cam02_ucs.py @@ -30,8 +30,6 @@ import typing from dataclasses import dataclass -import numpy as np - from colour.algebra import cartesian_to_polar, polar_to_cartesian if typing.TYPE_CHECKING: @@ -49,6 +47,7 @@ from colour.utilities import ( CanonicalMapping, MixinDataclassIterable, + array_namespace, as_float_array, from_range_100, from_range_degrees, @@ -57,6 +56,8 @@ to_domain_degrees, tsplit, tstack, + xp_degrees, + xp_radians, ) __author__ = "Colour Developers" @@ -169,6 +170,7 @@ def JMh_CIECAM02_to_UCS_Luo2006( Examples -------- + >>> import numpy as np >>> from colour.appearance import ( ... VIEWING_CONDITIONS_CIECAM02, ... XYZ_to_CIECAM02, @@ -185,6 +187,8 @@ def JMh_CIECAM02_to_UCS_Luo2006( array([54.9043313..., -0.0845039..., -0.0685483...]) """ + xp = array_namespace(JMh) + J, M, h = tsplit(JMh) J = to_domain_100(J) M = to_domain_100(M) @@ -193,9 +197,9 @@ def JMh_CIECAM02_to_UCS_Luo2006( _K_L, c_1, c_2 = coefficients.values J_p = ((1 + 100 * c_1) * J) / (1 + c_1 * J) - M_p = (1 / c_2) * np.log1p(c_2 * M) + M_p = (1 / c_2) * xp.log1p(c_2 * M) - a_p, b_p = tsplit(polar_to_cartesian(tstack([M_p, np.radians(h)]))) + a_p, b_p = tsplit(polar_to_cartesian(tstack([M_p, xp_radians(h)]))) Jpapbp = tstack([J_p, a_p, b_p]) @@ -244,26 +248,32 @@ def UCS_Luo2006_to_JMh_CIECAM02( Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([54.90433134, -0.08450395, -0.06854831]) >>> UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) ... # doctest: +ELLIPSIS array([4.1731091...e+01, 1.0884217...e-01, 2.1904843...e+02]) """ - J_p, a_p, b_p = tsplit(to_domain_100(Jpapbp)) + Jpapbp = to_domain_100(Jpapbp) + + xp = array_namespace(Jpapbp) + + J_p, a_p, b_p = tsplit(Jpapbp) + _K_L, c_1, c_2 = coefficients.values J = -J_p / (c_1 * J_p - 1 - 100 * c_1) M_p, h = tsplit(cartesian_to_polar(tstack([a_p, b_p]))) - M = np.expm1(M_p / (1 / c_2)) / c_2 + M = xp.expm1(M_p * c_2) / c_2 return tstack( [ from_range_100(J), from_range_100(M), - from_range_degrees(np.degrees(h) % 360), + from_range_degrees(xp_degrees(h) % 360), ] ) @@ -311,6 +321,7 @@ def JMh_CIECAM02_to_CAM02LCD( Examples -------- + >>> import numpy as np >>> from colour.appearance import ( ... VIEWING_CONDITIONS_CIECAM02, ... XYZ_to_CIECAM02, @@ -374,6 +385,7 @@ def CAM02LCD_to_JMh_CIECAM02( Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([54.90433134, -0.08450395, -0.06854831]) >>> CAM02LCD_to_JMh_CIECAM02(Jpapbp) # doctest: +ELLIPSIS array([4.1731091...e+01, 1.0884217...e-01, 2.1904843...e+02]) @@ -427,6 +439,7 @@ def JMh_CIECAM02_to_CAM02SCD( Examples -------- + >>> import numpy as np >>> from colour.appearance import ( ... VIEWING_CONDITIONS_CIECAM02, ... XYZ_to_CIECAM02, @@ -491,6 +504,7 @@ def CAM02SCD_to_JMh_CIECAM02( Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([54.90433134, -0.08436178, -0.06843298]) >>> CAM02SCD_to_JMh_CIECAM02(Jpapbp) # doctest: +ELLIPSIS array([4.1731091...e+01, 1.0884217...e-01, 2.1904843...e+02]) @@ -544,6 +558,7 @@ def JMh_CIECAM02_to_CAM02UCS( Examples -------- + >>> import numpy as np >>> from colour.appearance import ( ... VIEWING_CONDITIONS_CIECAM02, ... XYZ_to_CIECAM02, @@ -607,6 +622,7 @@ def CAM02UCS_to_JMh_CIECAM02( Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([54.90433134, -0.08442362, -0.06848314]) >>> CAM02UCS_to_JMh_CIECAM02(Jpapbp) # doctest: +ELLIPSIS array([4.1731091...e+01, 1.0884217...e-01, 2.1904843...e+02]) @@ -672,6 +688,7 @@ def XYZ_to_UCS_Luo2006( Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) ... # doctest: +ELLIPSIS @@ -694,7 +711,6 @@ def XYZ_to_UCS_Luo2006( if domain_range_reference: XYZ = as_float_array(XYZ) * 100 - specification = XYZ_to_CIECAM02(XYZ, **settings) JMh = tstack( [ @@ -762,6 +778,7 @@ def UCS_Luo2006_to_XYZ( Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([46.61386154, 39.35760236, 15.96730435]) >>> UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) ... # doctest: +ELLIPSIS @@ -784,7 +801,6 @@ def UCS_Luo2006_to_XYZ( settings["XYZ_w"] = as_float_array(XYZ_w) * 100 J, M, h = tsplit(UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, coefficients)) - specification = CAM_Specification_CIECAM02(J=J, M=M, h=h) XYZ = CIECAM02_to_XYZ(specification, **settings) @@ -849,6 +865,7 @@ def XYZ_to_CAM02LCD(XYZ: Domain1, **kwargs: Any) -> Range100: Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_CAM02LCD(XYZ) # doctest: +ELLIPSIS array([46.6138615..., 39.3576023..., 15.9673043...]) @@ -911,6 +928,7 @@ def CAM02LCD_to_XYZ(Jpapbp: Domain100, **kwargs: Any) -> Range1: Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([46.61386154, 39.35760236, 15.96730435]) >>> CAM02LCD_to_XYZ(Jpapbp) # doctest: +ELLIPSIS array([0.2065400..., 0.1219722..., 0.0513695...]) @@ -975,6 +993,7 @@ def XYZ_to_CAM02SCD(XYZ: Domain1, **kwargs: Any) -> Range100: Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_CAM02SCD(XYZ) # doctest: +ELLIPSIS array([46.6138615..., 25.6287988..., 10.3975548...]) @@ -1037,6 +1056,7 @@ def CAM02SCD_to_XYZ(Jpapbp: Domain100, **kwargs: Any) -> Range1: Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([46.61386154, 25.62879882, 10.39755489]) >>> CAM02SCD_to_XYZ(Jpapbp) # doctest: +ELLIPSIS array([0.2065400..., 0.1219722..., 0.0513695...]) @@ -1101,6 +1121,7 @@ def XYZ_to_CAM02UCS(XYZ: Domain1, **kwargs: Any) -> Range100: Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_CAM02UCS(XYZ) # doctest: +ELLIPSIS array([46.6138615..., 29.8831001..., 12.1235168...]) @@ -1163,6 +1184,7 @@ def CAM02UCS_to_XYZ(Jpapbp: Domain100, **kwargs: Any) -> Range1: Examples -------- + >>> import numpy as np >>> Jpapbp = np.array([46.61386154, 29.88310013, 12.12351683]) >>> CAM02UCS_to_XYZ(Jpapbp) # doctest: +ELLIPSIS array([0.2065400..., 0.1219722..., 0.0513695...]) diff --git a/colour/models/cie_luv.py b/colour/models/cie_luv.py index e359a925b9..7c03fa70ca 100644 --- a/colour/models/cie_luv.py +++ b/colour/models/cie_luv.py @@ -30,8 +30,6 @@ from __future__ import annotations -import numpy as np - from colour.algebra import sdiv, sdiv_mode from colour.colorimetry import CCS_ILLUMINANTS, lightness_CIE1976, luminance_CIE1976 from colour.hints import ( # noqa: TC001 @@ -45,6 +43,8 @@ ) from colour.models import xy_to_xyY, xyY_to_XYZ from colour.utilities import ( + array_namespace, + as_float_array, domain_range_scale, from_range_1, from_range_100, @@ -54,6 +54,8 @@ to_domain_100, tsplit, tstack, + xp_as_float_array, + xp_resize, ) __author__ = "Colour Developers" @@ -329,12 +331,17 @@ def uv_to_Luv( array([41.5278752..., 96.8362609..., 17.7521029...]) """ - u, v = tsplit(uv) + uv = as_float_array(uv) L = to_domain_100( optional(L, 100 if get_domain_range_scale() == "reference" else 1) ) + xp = array_namespace(uv) + + u, v = tsplit(uv) + L = xp_as_float_array(L, xp=xp, like=uv) _X_r, Y_r, _Z_r = tsplit(xyY_to_XYZ(xy_to_xyY(illuminant))) + Y_r = xp_as_float_array(Y_r, xp=xp, like=uv) with domain_range_scale("ignore"): Y = luminance_CIE1976(L, Y_r) @@ -343,7 +350,7 @@ def uv_to_Luv( X = sdiv(9 * Y * u, 4 * v) Z = sdiv(Y * (-3 * u - 20 * v + 12), 4 * v) - XYZ = tstack([X, np.resize(Y, u.shape), Z]) + XYZ = tstack([X, xp_resize(Y, u.shape, xp=xp), Z]) return XYZ_to_Luv(from_range_1(XYZ), illuminant) diff --git a/colour/models/cie_ucs.py b/colour/models/cie_ucs.py index 6ec195482b..65301c0278 100644 --- a/colour/models/cie_ucs.py +++ b/colour/models/cie_ucs.py @@ -24,8 +24,6 @@ from __future__ import annotations -import numpy as np - from colour.algebra import sdiv, sdiv_mode from colour.hints import ( # noqa: TC001 ArrayLike, @@ -33,7 +31,15 @@ NDArrayFloat, Range1, ) -from colour.utilities import from_range_1, to_domain_1, tsplit, tstack +from colour.utilities import ( + array_namespace, + from_range_1, + to_domain_1, + tsplit, + tstack, + xp_as_float_array, + xp_resize, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -238,11 +244,21 @@ def uv_to_UCS(uv: ArrayLike, V: Domain1 = 1) -> Range1: array([1.1288911..., 1. , 0.8639104...]) """ - u, v = tsplit(uv) V = to_domain_1(V) + xp = array_namespace(uv, V) + + u, v = tsplit(uv) + V = xp_as_float_array(V, xp=xp, like=u) + with sdiv_mode(): - UVW = tstack([V * sdiv(u, v), np.resize(V, u.shape), -V * sdiv(u + v - 1, v)]) + UVW = tstack( + [ + V * sdiv(u, v), + xp_resize(V, u.shape, xp=xp), + -V * sdiv(u + v - 1, v), + ] + ) return from_range_1(UVW) diff --git a/colour/models/cie_uvw.py b/colour/models/cie_uvw.py index 1799cd47cc..3ac4e7427f 100644 --- a/colour/models/cie_uvw.py +++ b/colour/models/cie_uvw.py @@ -24,7 +24,14 @@ Range100, ) from colour.models import UCS_uv_to_xy, XYZ_to_xy, xy_to_UCS_uv, xyY_to_xy, xyY_to_XYZ -from colour.utilities import from_range_100, to_domain_100, tsplit, tstack +from colour.utilities import ( + array_namespace, + from_range_100, + to_domain_100, + tsplit, + tstack, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -92,12 +99,14 @@ def XYZ_to_UVW( XYZ = to_domain_100(XYZ) + xp = array_namespace(XYZ) + xy = XYZ_to_xy(XYZ / 100) xy_n = xyY_to_xy(illuminant) Y = XYZ[..., 1] uv = xy_to_UCS_uv(xy) - uv_0 = xy_to_UCS_uv(xy_n) + uv_0 = xp_as_float_array(xy_to_UCS_uv(xy_n), xp=xp, like=XYZ) W = 25 * spow(Y, 1 / 3) - 17 U, V = tsplit(13 * W[..., None] * (uv - uv_0)) @@ -162,7 +171,7 @@ def UVW_to_XYZ( u_0, v_0 = tsplit(xy_to_UCS_uv(xyY_to_xy(illuminant))) - Y = ((W + 17) / 25) ** 3 + Y = spow((W + 17) / 25, 3) with sdiv_mode(): u = sdiv(U, 13 * W) + u_0 diff --git a/colour/models/cie_xyy.py b/colour/models/cie_xyy.py index f8de178125..22658a2751 100644 --- a/colour/models/cie_xyy.py +++ b/colour/models/cie_xyy.py @@ -22,8 +22,6 @@ from __future__ import annotations -import numpy as np - from colour.algebra import sdiv, sdiv_mode from colour.hints import ( # noqa: TC001 ArrayLike, @@ -31,7 +29,16 @@ NDArrayFloat, Range1, ) -from colour.utilities import as_float_array, from_range_1, to_domain_1, tsplit, tstack +from colour.utilities import ( + array_namespace, + as_float_array, + from_range_1, + to_domain_1, + tsplit, + tstack, + xp_as_float_array, + xp_resize, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -85,6 +92,7 @@ def XYZ_to_xyY(XYZ: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_xyY(XYZ) # doctest: +ELLIPSIS array([0.5436955..., 0.3210794..., 0.1219722...]) @@ -138,6 +146,7 @@ def xyY_to_XYZ(xyY: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> xyY = np.array([0.54369557, 0.32107944, 0.12197225]) >>> xyY_to_XYZ(xyY) # doctest: +ELLIPSIS array([0.2065400..., 0.1219722..., 0.0513695...]) @@ -190,6 +199,7 @@ def xyY_to_xy(xyY: Domain1) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> xyY = np.array([0.54369557, 0.32107944, 0.12197225]) >>> xyY_to_xy(xyY) # doctest: +ELLIPSIS array([0.54369557..., 0.32107944...]) @@ -260,6 +270,7 @@ def xy_to_xyY(xy: ArrayLike, Y: Domain1 = 1) -> Range1: Examples -------- + >>> import numpy as np >>> xy = np.array([0.54369557, 0.32107944]) >>> xy_to_xyY(xy) # doctest: +ELLIPSIS array([0.5436955..., 0.3210794..., 1. ]) @@ -279,11 +290,14 @@ def xy_to_xyY(xy: ArrayLike, Y: Domain1 = 1) -> Range1: if xy.shape[-1] == 3: return xy + xp = array_namespace(xy, Y) + x, y = tsplit(xy) + Y = xp_as_float_array(Y, xp=xp, like=x) - xyY = tstack([x, y, np.resize(Y, x.shape)]) + xyY = tstack([x, y, xp_resize(Y, x.shape, xp=xp)]) - return from_range_1(xyY, np.array([1, 1, 100])) + return from_range_1(xyY, [1, 1, 100]) def XYZ_to_xy(XYZ: Domain1) -> NDArrayFloat: @@ -315,6 +329,7 @@ def XYZ_to_xy(XYZ: Domain1) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_xy(XYZ) # doctest: +ELLIPSIS array([0.5436955..., 0.3210794...]) @@ -357,6 +372,7 @@ def xy_to_XYZ(xy: ArrayLike) -> Range1: Examples -------- + >>> import numpy as np >>> xy = np.array([0.54369557, 0.32107944]) >>> xy_to_XYZ(xy) # doctest: +ELLIPSIS array([1.6933366..., 1. , 0.4211574...]) diff --git a/colour/models/common.py b/colour/models/common.py index b245c145a5..eb94c4af48 100644 --- a/colour/models/common.py +++ b/colour/models/common.py @@ -44,6 +44,8 @@ to_domain_degrees, tsplit, tstack, + xp_degrees, + xp_radians, ) from colour.utilities.documentation import DocstringTuple, is_documentation_building @@ -254,7 +256,7 @@ def Jab_to_JCh(Jab: Domain1) -> Annotated[NDArrayFloat, (1, 1, 360)]: C, h = tsplit(cartesian_to_polar(tstack([a, b]))) - return tstack([L, C, from_range_degrees(np.degrees(h) % 360)]) + return tstack([L, C, from_range_degrees(xp_degrees(h) % 360)]) def JCh_to_Jab( @@ -310,7 +312,7 @@ def JCh_to_Jab( L, C, h = tsplit(JCh) - a, b = tsplit(polar_to_cartesian(tstack([C, np.radians(to_domain_degrees(h))]))) + a, b = tsplit(polar_to_cartesian(tstack([C, xp_radians(to_domain_degrees(h))]))) return tstack([L, a, b]) diff --git a/colour/models/din99.py b/colour/models/din99.py index 32f08258c9..9c5e581a35 100644 --- a/colour/models/din99.py +++ b/colour/models/din99.py @@ -43,11 +43,14 @@ from colour.models import Lab_to_XYZ, XYZ_to_Lab from colour.utilities import ( CanonicalMapping, + array_namespace, from_range_100, to_domain_100, tsplit, tstack, validate_method, + xp_as_float_array, + xp_radians, ) __author__ = "Colour Developers" @@ -147,22 +150,26 @@ def Lab_to_DIN99( validate_method(method, tuple(DIN99_METHODS)) ] - L, a, b = tsplit(to_domain_100(Lab)) + Lab = to_domain_100(Lab) - cos_c = np.cos(np.radians(c_3)) - sin_c = np.sin(np.radians(c_3)) + xp = array_namespace(Lab) + + L, a, b = tsplit(Lab) + + cos_c = xp.cos(xp_as_float_array(xp_radians(c_3), xp=xp, like=L)) + sin_c = xp.sin(xp_as_float_array(xp_radians(c_3), xp=xp, like=L)) e = cos_c * a + sin_c * b f = c_4 * (-sin_c * a + cos_c * b) G = spow(e**2 + f**2, 0.5) - h_ef = np.arctan2(f, e) + np.radians(c_7) + h_ef = xp.atan2(f, e) + xp_radians(c_7) - C_99 = c_5 * (np.log1p(c_6 * G)) / (c_8 * k_CH * k_E) + C_99 = c_5 * (xp.log1p(c_6 * G)) / (c_8 * k_CH * k_E) # Hue angle is unused currently. # h_99 = np.degrees(h_ef) - a_99 = C_99 * np.cos(h_ef) - b_99 = C_99 * np.sin(h_ef) - L_99 = c_1 * (np.log1p(c_2 * L)) * k_E + a_99 = C_99 * xp.cos(h_ef) + b_99 = C_99 * xp.sin(h_ef) + L_99 = c_1 * (xp.log1p(c_2 * L)) * k_E Lab_99 = tstack([L_99, a_99, b_99]) @@ -230,22 +237,26 @@ def DIN99_to_Lab( validate_method(method, tuple(DIN99_METHODS)) ] - L_99, a_99, b_99 = tsplit(to_domain_100(Lab_99)) + Lab_99 = to_domain_100(Lab_99) + + xp = array_namespace(Lab_99) + + L_99, a_99, b_99 = tsplit(Lab_99) - cos = np.cos(np.radians(c_3)) - sin = np.sin(np.radians(c_3)) + cos = xp.cos(xp_as_float_array(xp_radians(c_3), xp=xp, like=L_99)) + sin = xp.sin(xp_as_float_array(xp_radians(c_3), xp=xp, like=L_99)) - h_99 = np.arctan2(b_99, a_99) - np.radians(c_7) + h_99 = xp.atan2(b_99, a_99) - xp_radians(c_7) - C_99 = np.hypot(a_99, b_99) - G = np.expm1((c_8 / c_5) * C_99 * k_CH * k_E) / c_6 + C_99 = xp.hypot(a_99, b_99) + G = xp.expm1((c_8 / c_5) * C_99 * k_CH * k_E) / c_6 - e = G * np.cos(h_99) - f = G * np.sin(h_99) + e = G * xp.cos(h_99) + f = G * xp.sin(h_99) a = e * cos - (f / c_4) * sin b = e * sin + (f / c_4) * cos - L = np.expm1(L_99 * k_E / c_1) / c_2 + L = xp.expm1(L_99 * k_E / c_1) / c_2 Lab = tstack([L, a, b]) diff --git a/colour/models/hdr_cie_lab.py b/colour/models/hdr_cie_lab.py index bdef3a5ed2..7f6d1f5cd7 100644 --- a/colour/models/hdr_cie_lab.py +++ b/colour/models/hdr_cie_lab.py @@ -22,10 +22,9 @@ from __future__ import annotations +import math import typing -import numpy as np - from colour.colorimetry import ( CCS_ILLUMINANTS, lightness_Fairchild2010, @@ -47,7 +46,7 @@ ) from colour.models import xy_to_xyY, xyY_to_XYZ from colour.utilities import ( - as_float_array, + array_namespace, domain_range_scale, from_range_1, from_range_100, @@ -56,6 +55,7 @@ tsplit, tstack, validate_method, + xp_as_float_array, ) from colour.utilities.documentation import DocstringTuple, is_documentation_building @@ -128,13 +128,16 @@ def exponent_hdr_CIELab( """ Y_s = to_domain_1(Y_s) - Y_abs = as_float_array(Y_abs) + + xp = array_namespace(Y_s) + + Y_abs = xp_as_float_array(Y_abs, xp=xp, like=Y_s) method = validate_method(method, HDR_CIELAB_METHODS) epsilon = 1.5 if method == "fairchild 2010" else 0.58 sf = 1.25 - 0.25 * (Y_s / 0.184) - lf = np.log(318) / np.log(Y_abs) + lf = math.log(318) / xp.log(Y_abs) if method == "fairchild 2010": epsilon *= sf * lf else: @@ -207,6 +210,7 @@ def XYZ_to_hdr_CIELab( Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_hdr_CIELab(XYZ) # doctest: +ELLIPSIS array([51.8700206..., 60.4763385..., 32.1455191...]) @@ -293,6 +297,7 @@ def hdr_CIELab_to_XYZ( Examples -------- + >>> import numpy as np >>> Lab_hdr = np.array([51.87002062, 60.4763385, 32.14551912]) >>> hdr_CIELab_to_XYZ(Lab_hdr) # doctest: +ELLIPSIS array([0.2065400..., 0.1219722..., 0.0513695...]) diff --git a/colour/models/hdr_ipt.py b/colour/models/hdr_ipt.py index e195ea81ec..98dbbbec8a 100644 --- a/colour/models/hdr_ipt.py +++ b/colour/models/hdr_ipt.py @@ -22,10 +22,9 @@ from __future__ import annotations +import math import typing -import numpy as np - from colour.algebra import vecmul from colour.colorimetry import ( lightness_Fairchild2010, @@ -52,13 +51,14 @@ MATRIX_IPT_XYZ_TO_LMS, ) from colour.utilities import ( - as_float_array, + array_namespace, domain_range_scale, from_range_1, from_range_100, to_domain_1, to_domain_100, validate_method, + xp_as_float_array, ) from colour.utilities.documentation import DocstringTuple, is_documentation_building @@ -131,12 +131,15 @@ def exponent_hdr_IPT( """ Y_s = to_domain_1(Y_s) - Y_abs = as_float_array(Y_abs) + + xp = array_namespace(Y_s) + + Y_abs = xp_as_float_array(Y_abs, xp=xp, like=Y_s) method = validate_method(method, HDR_IPT_METHODS) epsilon = 1.38 if method == "fairchild 2010" else 0.59 - lf = np.log(318) / np.log(Y_abs) + lf = math.log(318) / xp.log(Y_abs) sf = 1.25 - 0.25 * (Y_s / 0.184) if method == "fairchild 2010": epsilon *= sf * lf @@ -197,6 +200,7 @@ def XYZ_to_hdr_IPT( Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_hdr_IPT(XYZ) # doctest: +ELLIPSIS array([48.3937634..., 42.4499020..., 22.0195403...]) @@ -205,6 +209,9 @@ def XYZ_to_hdr_IPT( """ XYZ = to_domain_1(XYZ) + + xp = array_namespace(XYZ) + method = validate_method(method, HDR_IPT_METHODS) if method == "fairchild 2010": @@ -218,7 +225,7 @@ def XYZ_to_hdr_IPT( # Domain and range scaling has already been handled. with domain_range_scale("ignore"): - LMS_prime = np.sign(LMS) * np.abs(lightness_callable(LMS, e)) + LMS_prime = xp.sign(LMS) * xp.abs(lightness_callable(LMS, e)) IPT_hdr = vecmul(MATRIX_IPT_LMS_P_TO_IPT, LMS_prime) @@ -273,6 +280,7 @@ def hdr_IPT_to_XYZ( Examples -------- + >>> import numpy as np >>> IPT_hdr = np.array([48.39376346, 42.44990202, 22.01954033]) >>> hdr_IPT_to_XYZ(IPT_hdr) # doctest: +ELLIPSIS array([0.2065400..., 0.1219722..., 0.0513695...]) @@ -283,6 +291,9 @@ def hdr_IPT_to_XYZ( """ IPT_hdr = to_domain_100(IPT_hdr) + + xp = array_namespace(IPT_hdr) + method = validate_method(method, HDR_IPT_METHODS) if method == "fairchild 2010": @@ -296,7 +307,7 @@ def hdr_IPT_to_XYZ( # Domain and range scaling has already be handled. with domain_range_scale("ignore"): - LMS_prime = np.sign(LMS) * np.abs(luminance_callable(LMS, e)) + LMS_prime = xp.sign(LMS) * xp.abs(luminance_callable(LMS, e)) XYZ = vecmul(MATRIX_IPT_LMS_TO_XYZ, LMS_prime) diff --git a/colour/models/hunter_lab.py b/colour/models/hunter_lab.py index 4684b1a6e7..b7c4fa0435 100644 --- a/colour/models/hunter_lab.py +++ b/colour/models/hunter_lab.py @@ -20,8 +20,6 @@ from __future__ import annotations -import numpy as np - from colour.colorimetry import TVS_ILLUMINANTS_HUNTERLAB from colour.hints import ( # noqa: TC001 ArrayLike, @@ -30,12 +28,14 @@ Range100, ) from colour.utilities import ( + array_namespace, from_range_100, get_domain_range_scale, optional, to_domain_100, tsplit, tstack, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -75,15 +75,18 @@ def XYZ_to_K_ab_HunterLab1966(XYZ: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> XYZ = np.array([109.850, 100.000, 35.585]) >>> XYZ_to_K_ab_HunterLab1966(XYZ) # doctest: +ELLIPSIS array([185.2378721..., 38.4219142...]) """ + xp = array_namespace(XYZ) + X, _Y, Z = tsplit(XYZ) - K_a = 175 * np.sqrt(X / 98.043) - K_b = 70 * np.sqrt(Z / 118.115) + K_a = 175 * xp.sqrt(X / 98.043) + K_b = 70 * xp.sqrt(Z / 118.115) return tstack([K_a, K_b]) @@ -135,13 +138,18 @@ def XYZ_to_Hunter_Lab( Examples -------- + >>> import numpy as np >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 >>> D65 = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"]["D65"] >>> XYZ_to_Hunter_Lab(XYZ, D65.XYZ_n, D65.K_ab) # doctest: +ELLIPSIS array([34.9245257..., 47.0618985..., 14.3861510...]) """ - X, Y, Z = tsplit(to_domain_100(XYZ)) + XYZ = to_domain_100(XYZ) + + xp = array_namespace(XYZ) + + X, Y, Z = tsplit(XYZ) TVS_D65 = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"]["D65"] XYZ_n_default = XYZ_n is None XYZ_n = to_domain_100( @@ -152,12 +160,16 @@ def XYZ_to_Hunter_Lab( else TVS_D65.XYZ_n / 100, ) ) + XYZ_n = xp_as_float_array(XYZ_n, xp=xp, like=XYZ) X_n, Y_n, Z_n = tsplit(XYZ_n) K_ab = TVS_D65.K_ab if K_ab is None and XYZ_n_default else K_ab - K_a, K_b = tsplit(XYZ_to_K_ab_HunterLab1966(XYZ_n) if K_ab is None else K_ab) + K_ab = xp_as_float_array( + optional(K_ab, XYZ_to_K_ab_HunterLab1966(XYZ_n)), xp=xp, like=XYZ + ) + K_a, K_b = tsplit(K_ab) Y_Y_n = Y / Y_n - sqrt_Y_Y_n = np.sqrt(Y_Y_n) + sqrt_Y_Y_n = xp.sqrt(Y_Y_n) L = 100 * sqrt_Y_Y_n a = K_a * ((X / X_n - Y_Y_n) / sqrt_Y_Y_n) @@ -215,13 +227,18 @@ def Hunter_Lab_to_XYZ( Examples -------- + >>> import numpy as np >>> Lab = np.array([34.92452577, 47.06189858, 14.38615107]) >>> D65 = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"]["D65"] >>> Hunter_Lab_to_XYZ(Lab, D65.XYZ_n, D65.K_ab) array([20.654008, 12.197225, 5.136952]) """ - L, a, b = tsplit(to_domain_100(Lab)) + Lab = to_domain_100(Lab) + + xp = array_namespace(Lab) + + L, a, b = tsplit(Lab) d65 = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"]["D65"] XYZ_n_default = XYZ_n is None XYZ_n = to_domain_100( @@ -230,9 +247,13 @@ def Hunter_Lab_to_XYZ( d65.XYZ_n if get_domain_range_scale() == "reference" else d65.XYZ_n / 100, ) ) + XYZ_n = xp_as_float_array(XYZ_n, xp=xp, like=Lab) X_n, Y_n, Z_n = tsplit(XYZ_n) K_ab = d65.K_ab if K_ab is None and XYZ_n_default else K_ab - K_a, K_b = tsplit(XYZ_to_K_ab_HunterLab1966(XYZ_n) if K_ab is None else K_ab) + K_ab = xp_as_float_array( + optional(K_ab, XYZ_to_K_ab_HunterLab1966(XYZ_n)), xp=xp, like=Lab + ) + K_a, K_b = tsplit(K_ab) L_100 = L / 100 L_100_2 = L_100**2 diff --git a/colour/models/hunter_rdab.py b/colour/models/hunter_rdab.py index 1586c33647..78f1d4a375 100644 --- a/colour/models/hunter_rdab.py +++ b/colour/models/hunter_rdab.py @@ -25,12 +25,14 @@ ) from colour.models import XYZ_to_K_ab_HunterLab1966 from colour.utilities import ( + array_namespace, from_range_100, get_domain_range_scale, optional, to_domain_100, tsplit, tstack, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -101,7 +103,11 @@ def XYZ_to_Hunter_Rdab( array([12.197225..., 57.1253787..., 17.4624134...]) """ - X, Y, Z = tsplit(to_domain_100(XYZ)) + XYZ = to_domain_100(XYZ) + + xp = array_namespace(XYZ) + + X, Y, Z = tsplit(XYZ) d65 = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"]["D65"] XYZ_n_default = XYZ_n is None XYZ_n = to_domain_100( @@ -110,9 +116,14 @@ def XYZ_to_Hunter_Rdab( d65.XYZ_n if get_domain_range_scale() == "reference" else d65.XYZ_n / 100, ) ) + XYZ_n = xp_as_float_array(XYZ_n, xp=xp, like=XYZ) X_n, Y_n, Z_n = tsplit(XYZ_n) + K_ab = d65.K_ab if K_ab is None and XYZ_n_default else K_ab - K_a, K_b = tsplit(XYZ_to_K_ab_HunterLab1966(XYZ_n) if K_ab is None else K_ab) + K_ab = xp_as_float_array( + optional(K_ab, XYZ_to_K_ab_HunterLab1966(XYZ_n)), xp=xp, like=XYZ + ) + K_a, K_b = tsplit(K_ab) f = 0.51 * ((21 + 0.2 * Y) / (1 + 0.2 * Y)) Y_Yn = Y / Y_n @@ -180,7 +191,11 @@ def Hunter_Rdab_to_XYZ( array([20.654008, 12.197225, 5.136952]) """ - R_d, a_Rd, b_Rd = tsplit(to_domain_100(R_d_ab)) + R_d_ab = to_domain_100(R_d_ab) + + xp = array_namespace(R_d_ab) + + R_d, a_Rd, b_Rd = tsplit(R_d_ab) TVS_D65 = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"]["D65"] XYZ_n_default = XYZ_n is None XYZ_n = to_domain_100( @@ -191,9 +206,14 @@ def Hunter_Rdab_to_XYZ( else TVS_D65.XYZ_n / 100, ) ) + XYZ_n = xp_as_float_array(XYZ_n, xp=xp, like=R_d_ab) X_n, Y_n, Z_n = tsplit(XYZ_n) + K_ab = TVS_D65.K_ab if K_ab is None and XYZ_n_default else K_ab - K_a, K_b = tsplit(XYZ_to_K_ab_HunterLab1966(XYZ_n) if K_ab is None else K_ab) + K_ab = xp_as_float_array( + optional(K_ab, XYZ_to_K_ab_HunterLab1966(XYZ_n)), xp=xp, like=R_d_ab + ) + K_a, K_b = tsplit(K_ab) f = 0.51 * ((21 + 0.2 * R_d) / (1 + 0.2 * R_d)) Rd_Yn = R_d / Y_n diff --git a/colour/models/igpgtg.py b/colour/models/igpgtg.py index fb3689cd6b..ef7d4e6f78 100644 --- a/colour/models/igpgtg.py +++ b/colour/models/igpgtg.py @@ -26,6 +26,7 @@ Range1, ) from colour.models import Iab_to_XYZ, XYZ_to_Iab +from colour.utilities import array_namespace, xp_as_float_array __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -113,13 +114,17 @@ def XYZ_to_IgPgTg(XYZ: Domain1) -> Range1: array([0.4242125..., 0.1863249..., 0.1068922...]) """ - def LMS_to_LMS_p_callable(LMS: ArrayLike) -> NDArrayFloat: + def LMS_to_LMS_p_callable(LMS: NDArrayFloat) -> NDArrayFloat: """ Callable applying the forward non-linearity to the :math:`LMS` colourspace array. """ - return spow(LMS / np.array([18.36, 21.46, 19435]), 0.427) + xp = array_namespace(LMS) + + constants = xp_as_float_array([18.36, 21.46, 19435], xp=xp, like=LMS) + + return spow(LMS / constants, 0.427) return XYZ_to_Iab( XYZ, @@ -178,7 +183,11 @@ def LMS_p_to_LMS_callable(LMS_p: ArrayLike) -> NDArrayFloat: colourspace array. """ - return spow(LMS_p, 1 / 0.427) * np.array([18.36, 21.46, 19435]) + xp = array_namespace(LMS_p) + + constants = xp_as_float_array([18.36, 21.46, 19435], xp=xp, like=LMS_p) + + return spow(LMS_p, 1 / 0.427) * constants return Iab_to_XYZ( IgPgTg, diff --git a/colour/models/ipt.py b/colour/models/ipt.py index 9cf48de399..3c87fcf965 100644 --- a/colour/models/ipt.py +++ b/colour/models/ipt.py @@ -28,7 +28,14 @@ Range360, ) from colour.models import Iab_to_XYZ, XYZ_to_Iab -from colour.utilities import as_float, from_range_degrees, to_domain_1, tsplit +from colour.utilities import ( + array_namespace, + as_float, + from_range_degrees, + to_domain_1, + tsplit, + xp_degrees, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -211,8 +218,12 @@ def IPT_hue_angle(IPT: Domain1) -> Range360: np.float64(48.2852074...) """ - _I, P, T = tsplit(to_domain_1(IPT)) + IPT = to_domain_1(IPT) + + xp = array_namespace(IPT) + + _I, P, T = tsplit(IPT) - hue = np.degrees(np.arctan2(T, P)) % 360 + hue = xp_degrees(xp.atan2(T, P)) % 360 return as_float(from_range_degrees(hue)) diff --git a/colour/models/jzazbz.py b/colour/models/jzazbz.py index 0176972545..32f18730f5 100644 --- a/colour/models/jzazbz.py +++ b/colour/models/jzazbz.py @@ -379,8 +379,8 @@ def XYZ_to_Jzazbz( Lightness, :math:`a_z` is redness-greenness and :math:`b_z` is yellowness-blueness. - Warnings - -------- + Warnings + -------- The underlying *SMPTE ST 2084:2014* transfer function is an absolute transfer function. diff --git a/colour/models/osa_ucs.py b/colour/models/osa_ucs.py index abd83bf337..60d7d3c4ea 100644 --- a/colour/models/osa_ucs.py +++ b/colour/models/osa_ucs.py @@ -38,10 +38,14 @@ ) from colour.models import XYZ_to_xyY from colour.utilities import ( + array_namespace, from_range_100, to_domain_100, tsplit, tstack, + xp_as_float_array, + xp_matrix_transpose, + xp_reshape, ) __author__ = "Colour Developers" @@ -74,6 +78,18 @@ values (inverse of MATRIX_XYZ_TO_RGB_OSA_UCS). """ +VECTOR_J_OSA_UCS: NDArrayFloat = np.array([1.7, 8.0, -9.7]) +"""*OSA UCS* :math:`j` weight vector from *Schloemer (2019)* Eq. (4).""" + +VECTOR_G_OSA_UCS: NDArrayFloat = np.array([-13.7, 17.7, -4.0]) +"""*OSA UCS* :math:`g` weight vector from *Schloemer (2019)* Eq. (4).""" + +VECTOR_AUGMENT_OSA_UCS: NDArrayFloat = np.array([1.0, 0.0, 0.0]) +""" +Augmenting row used to make the *Schloemer (2019)* Eq. (4) ``(g, j)`` matrix +non-singular by setting ``w = cbrt(R)``. +""" + def XYZ_to_OSA_UCS(XYZ: Domain100) -> Range100: """ @@ -125,6 +141,9 @@ def XYZ_to_OSA_UCS(XYZ: Domain100) -> Range100: """ XYZ = to_domain_100(XYZ) + + xp = array_namespace(XYZ) + x, y, Y = tsplit(XYZ_to_xyY(XYZ)) Y_0 = Y * ( @@ -144,8 +163,8 @@ def XYZ_to_OSA_UCS(XYZ: Domain100) -> Range100: C = sdiv(Lambda, 5.9 * Y_0_es) L = (Lambda - 14.4) / spow(2, 1 / 2) - j = C * np.dot(RGB_3, np.array([1.7, 8, -9.7])) - g = C * np.dot(RGB_3, np.array([-13.7, 17.7, -4])) + j = C * xp.matmul(RGB_3, xp_as_float_array(VECTOR_J_OSA_UCS, xp=xp, like=RGB_3)) + g = C * xp.matmul(RGB_3, xp_as_float_array(VECTOR_G_OSA_UCS, xp=xp, like=RGB_3)) Ljg = tstack([L, j, g]) @@ -211,8 +230,11 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R """ Ljg = to_domain_100(Ljg) + + xp = array_namespace(Ljg) + shape = Ljg.shape - Ljg = np.atleast_1d(np.reshape(Ljg, (-1, 3))) + Ljg = xp_reshape(Ljg, (-1, 3), xp=xp) # Default optimization settings settings: dict[str, typing.Any] = { @@ -229,7 +251,7 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R # Forward: L = (Lambda - 14.4) / sqrt(2) # Backward: Lambda = L * sqrt(2) + 14.4 # But L' = Lambda in the intermediate calculation - sqrt_2 = np.sqrt(2) + sqrt_2 = 2.0**0.5 L_prime = L * sqrt_2 + 14.4 # Step 2: Solve for Y0 using Cardano's formula @@ -254,11 +276,11 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R with sdiv_mode(): t = ( -b / (3 * a) - + spow(-q / 2 + np.sqrt(discriminant), 1.0 / 3.0) - + spow(-q / 2 - np.sqrt(discriminant), 1.0 / 3.0) + + spow(-q / 2 + xp.sqrt(discriminant), 1.0 / 3.0) + + spow(-q / 2 - xp.sqrt(discriminant), 1.0 / 3.0) ) - Y0 = t**3 + Y0 = spow(t, 3) # Step 3: Compute C, a, b with sdiv_mode(): @@ -266,25 +288,27 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R a_coef = sdiv(g, C) b_coef = sdiv(j, C) - # Step 4: Solve for RGB using Newton iteration - # Matrix A from equation (4) - A = np.array([[-13.7, 17.7, -4.0], [1.7, 8.0, -9.7]]) - - # Augment A with [1, 0, 0] to make it non-singular (set w = cbrt(R)) - A_augmented = np.vstack([A, [1.0, 0.0, 0.0]]) - A_inv = np.linalg.inv(A_augmented) + # Step 4: Solve for RGB using Newton iteration. Matrix A from + # *Schloemer (2019)* Eq. (4) ``(g, j)`` rows, augmented with + # ``[1, 0, 0]`` to make it non-singular (set ``w = cbrt(R)``). + A_augmented = xp_as_float_array( + [VECTOR_G_OSA_UCS, VECTOR_J_OSA_UCS, VECTOR_AUGMENT_OSA_UCS], + xp=xp, + like=L, + ) + A_inv = xp.linalg.inv(A_augmented) # Initial guess for w (corresponds to cbrt(R)) # w0 = cbrt(79.9 + 41.94) from paper - w = np.full_like(L, (79.9 + 41.94) ** (1.0 / 3.0)) + w = xp.full_like(L, (79.9 + 41.94) ** (1.0 / 3.0)) # Newton iteration for _iteration in range(settings["iterations_maximum"]): # Solve for [cbrt(R), cbrt(G), cbrt(B)] given current w - ab_w = np.array([a_coef, b_coef, w]).T - RGB_cbrt = np.dot(ab_w, A_inv.T) + ab_w = xp.stack([a_coef, b_coef, w], axis=-1) + RGB_cbrt = xp.matmul(ab_w, xp_matrix_transpose(A_inv, xp=xp)) - RGB = RGB_cbrt**3 + RGB = spow(RGB_cbrt, 3) XYZ = vecmul(MATRIX_RGB_TO_XYZ_OSA_UCS, RGB) X, Y, Z = tsplit(XYZ) @@ -305,7 +329,7 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R Y0_computed = Y * K error = Y0_computed - Y0 - if np.all(np.abs(error) < settings["tolerance"]): + if xp.all(xp.abs(error) < settings["tolerance"]): break # Newton step: compute derivative and update w @@ -313,9 +337,9 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R epsilon = settings["epsilon"] w_plus = w + epsilon - ab_w_plus = np.array([a_coef, b_coef, w_plus]).T - RGB_cbrt_plus = np.dot(ab_w_plus, A_inv.T) - RGB_plus = RGB_cbrt_plus**3 + ab_w_plus = xp.stack([a_coef, b_coef, w_plus], axis=-1) + RGB_cbrt_plus = xp.matmul(ab_w_plus, xp_matrix_transpose(A_inv, xp=xp)) + RGB_plus = spow(RGB_cbrt_plus, 3) XYZ_plus = vecmul(MATRIX_RGB_TO_XYZ_OSA_UCS, RGB_plus) X_plus, Y_plus, Z_plus = tsplit(XYZ_plus) @@ -338,4 +362,4 @@ def OSA_UCS_to_XYZ(Ljg: Domain100, optimisation_kwargs: dict | None = None) -> R derivative = sdiv(Y0_computed_plus - Y0_computed, epsilon) w = w - sdiv(error, derivative) - return from_range_100(np.reshape(XYZ, shape)) + return from_range_100(xp_reshape(XYZ, shape, xp=xp)) diff --git a/colour/models/prolab.py b/colour/models/prolab.py index 8e2464d5de..b12129507d 100644 --- a/colour/models/prolab.py +++ b/colour/models/prolab.py @@ -26,7 +26,14 @@ Range1, ) from colour.models import xy_to_xyY, xyY_to_XYZ -from colour.utilities import as_float_array, from_range_1, ones, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float_array, + from_range_1, + to_domain_1, + xp_as_float_array, + xp_matrix_transpose, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -75,12 +82,15 @@ def projective_transformation(a: ArrayLike, Q: ArrayLike) -> NDArrayFloat: """ a = as_float_array(a) - Q = as_float_array(Q) - # Concatenate array with ones along last axis for homogeneous coordinates - M = np.concatenate([a, ones((*a.shape[:-1], 1))], axis=-1) + xp = array_namespace(a, Q) + + Q = xp_as_float_array(Q, xp=xp, like=a) - homography = np.dot(M, np.transpose(Q)) + # Concatenate array with ones along last axis for homogeneous coordinates. + M = xp.concat([a, xp.ones_like(a[..., :1])], axis=-1) + + homography = xp.matmul(M, xp_matrix_transpose(Q, xp=xp)) return homography[..., 0:-1] / homography[..., -1][..., None] @@ -133,7 +143,10 @@ def XYZ_to_ProLab( """ XYZ = to_domain_1(XYZ) - XYZ_n = xyY_to_XYZ(xy_to_xyY(illuminant)) + + xp = array_namespace(XYZ) + + XYZ_n = xp_as_float_array(xyY_to_XYZ(xy_to_xyY(illuminant)), xp=xp, like=XYZ) ProLab = projective_transformation(XYZ / XYZ_n, MATRIX_Q) @@ -188,7 +201,10 @@ def ProLab_to_XYZ( """ ProLab = to_domain_1(ProLab) - XYZ_n = xyY_to_XYZ(xy_to_xyY(illuminant)) + + xp = array_namespace(ProLab) + + XYZ_n = xp_as_float_array(xyY_to_XYZ(xy_to_xyY(illuminant)), xp=xp, like=ProLab) XYZ = projective_transformation(ProLab, MATRIX_INVERSE_Q) diff --git a/colour/models/rgb/cmyk.py b/colour/models/rgb/cmyk.py index 10cdbf3b32..ad4b145992 100644 --- a/colour/models/rgb/cmyk.py +++ b/colour/models/rgb/cmyk.py @@ -23,13 +23,11 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import from_range_1, to_domain_1, tsplit, tstack +from colour.utilities import array_namespace, from_range_1, to_domain_1, tsplit, tstack __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -80,6 +78,7 @@ def RGB_to_CMY(RGB: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> RGB = np.array([0.45620519, 0.03081071, 0.04091952]) >>> RGB_to_CMY(RGB) # doctest: +ELLIPSIS array([0.5437948..., 0.9691892..., 0.9590804...]) @@ -124,12 +123,15 @@ def CMY_to_RGB(CMY: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> CMY = np.array([0.54379481, 0.96918929, 0.95908048]) >>> CMY_to_RGB(CMY) # doctest: +ELLIPSIS array([0.4562051..., 0.0308107..., 0.0409195...]) """ - RGB = 1 - to_domain_1(CMY) + CMY = to_domain_1(CMY) + + RGB = 1 - CMY return from_range_1(RGB) @@ -168,23 +170,28 @@ def CMY_to_CMYK(CMY: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> CMY = np.array([0.54379481, 0.96918929, 0.95908048]) >>> CMY_to_CMYK(CMY) # doctest: +ELLIPSIS array([0. , 0.9324630..., 0.9103045..., 0.5437948...]) """ - C, M, Y = tsplit(to_domain_1(CMY)) + CMY = to_domain_1(CMY) + + xp = array_namespace(CMY) + + C, M, Y = tsplit(CMY) - K = np.where(C < 1, C, 1) - K = np.where(M < K, M, K) - K = np.where(Y < K, Y, K) + K = xp.where(C < 1, C, 1) + K = xp.where(M < K, M, K) + K = xp.where(Y < K, Y, K) K_1 = K == 1 - N = np.where(K_1, 1, 1 - K) + N = xp.where(K_1, 1, 1 - K) - C = np.where(K_1, 0, (C - K) / N) - M = np.where(K_1, 0, (M - K) / N) - Y = np.where(K_1, 0, (Y - K) / N) + C = xp.where(K_1, 0, (C - K) / N) + M = xp.where(K_1, 0, (M - K) / N) + Y = xp.where(K_1, 0, (Y - K) / N) CMYK = tstack([C, M, Y, K]) @@ -225,6 +232,7 @@ def CMYK_to_CMY(CMYK: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> CMYK = np.array([0.50000000, 0.00000000, 0.74400000, 0.01960784]) >>> CMYK_to_CMY(CMYK) # doctest: +ELLIPSIS array([0.5098039..., 0.0196078..., 0.7490196...]) diff --git a/colour/models/rgb/cylindrical.py b/colour/models/rgb/cylindrical.py index 1f3be8a539..d5288d2406 100644 --- a/colour/models/rgb/cylindrical.py +++ b/colour/models/rgb/cylindrical.py @@ -59,7 +59,17 @@ from colour.hints import Domain1, NDArrayBoolean, NDArrayFloat, Range1 from colour.hints import ArrayLike, cast -from colour.utilities import as_float_array, from_range_1, to_domain_1, tsplit, tstack +from colour.utilities import ( + array_namespace, + as_float_array, + as_int_array, + from_range_1, + to_domain_1, + tsplit, + tstack, + xp_radians, + xp_select, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -119,11 +129,11 @@ def RGB_to_HSV(RGB: Domain1) -> Range1: RGB = to_domain_1(RGB) - maximum = np.amax(RGB, -1) - delta = np.ptp(RGB, -1) + xp = array_namespace(RGB) + maximum = xp.max(RGB, axis=-1) + delta = maximum - xp.min(RGB, axis=-1) V = maximum - R, G, B = tsplit(RGB) with sdiv_mode(): @@ -134,11 +144,12 @@ def RGB_to_HSV(RGB: Domain1) -> Range1: delta_B = sdiv(((maximum - B) / 6) + (delta / 2), delta) H = delta_B - delta_G - H = np.where(maximum == G, (1 / 3) + delta_R - delta_B, H) - H = np.where(maximum == B, (2 / 3) + delta_G - delta_R, H) - H = np.where(H < 0, H + 1, H) - H = np.where(H > 1, H - 1, H) - H = np.where(delta == 0, 0, H) + + H = xp.where(maximum == G, (1 / 3) + delta_R - delta_B, H) + H = xp.where(maximum == B, (2 / 3) + delta_G - delta_R, H) + H = xp.where(H < 0, H + 1, H) + H = xp.where(H > 1, H - 1, H) + H = xp.where(delta == 0, 0, H) HSV = tstack([H, S, V]) @@ -184,20 +195,23 @@ def HSV_to_RGB(HSV: Domain1) -> Range1: array([0.4562051..., 0.0308107..., 0.0409195...]) """ - H, S, V = tsplit(to_domain_1(HSV)) + HSV = to_domain_1(HSV) - h = as_float_array(H * 6) - h = np.where(h == 6, 0, h) + xp = array_namespace(HSV) + + H, S, V = tsplit(HSV) - i = np.floor(h) + h = as_float_array(H * 6) + h = xp.where(h == 6, 0, h) + i = xp.floor(h) j = V * (1 - S) k = V * (1 - S * (h - i)) l = V * (1 - S * (1 - (h - i))) # noqa: E741 - i = tstack([i, i, i]).astype(np.uint8) + i = as_int_array(tstack([i, i, i])) - RGB = np.choose( - i, + RGB = xp_select( + [i == 0, i == 1, i == 2, i == 3, i == 4, i == 5], [ tstack([V, l, j]), tstack([k, V, j]), @@ -206,7 +220,7 @@ def HSV_to_RGB(HSV: Domain1) -> Range1: tstack([l, j, V]), tstack([V, j, k]), ], - mode="clip", + xp=xp, ) return from_range_1(RGB) @@ -253,16 +267,17 @@ def RGB_to_HSL(RGB: Domain1) -> Range1: RGB = to_domain_1(RGB) - minimum = np.amin(RGB, -1) - maximum = np.amax(RGB, -1) - delta = np.ptp(RGB, -1) + xp = array_namespace(RGB) + minimum = xp.min(RGB, axis=-1) + maximum = xp.max(RGB, axis=-1) + delta = maximum - minimum R, G, B = tsplit(RGB) L = (maximum + minimum) / 2 with sdiv_mode(): - S = np.where( + S = xp.where( L < 0.5, sdiv(delta, maximum + minimum), sdiv(delta, 2 - maximum - minimum), @@ -273,11 +288,12 @@ def RGB_to_HSL(RGB: Domain1) -> Range1: delta_B = sdiv(((maximum - B) / 6) + (delta / 2), delta) H = delta_B - delta_G - H = np.where(maximum == G, (1 / 3) + delta_R - delta_B, H) - H = np.where(maximum == B, (2 / 3) + delta_G - delta_R, H) - H = np.where(H < 0, H + 1, H) - H = np.where(H > 1, H - 1, H) - H = np.where(delta == 0, 0, H) + + H = xp.where(maximum == G, (1 / 3) + delta_R - delta_B, H) + H = xp.where(maximum == B, (2 / 3) + delta_G - delta_R, H) + H = xp.where(H < 0, H + 1, H) + H = xp.where(H > 1, H - 1, H) + H = xp.where(delta == 0, 0, H) HSL = tstack([H, S, L]) @@ -323,39 +339,49 @@ def HSL_to_RGB(HSL: Domain1) -> Range1: array([0.4562051..., 0.0308107..., 0.0409195...]) """ - H, S, L = tsplit(to_domain_1(HSL)) + HSL = to_domain_1(HSL) + + xp = array_namespace(HSL) + + H, S, L = tsplit(HSL) def H_to_RGB(vi: NDArrayFloat, vj: NDArrayFloat, vH: NDArrayFloat) -> NDArrayFloat: """Convert *hue* value to *RGB* colourspace.""" vH = as_float_array(vH) - vH = np.where(vH < 0, vH + 1, vH) - vH = np.where(vH > 1, vH - 1, vH) + vH = xp.where(vH < 0, vH + 1, vH) + vH = xp.where(vH > 1, vH - 1, vH) - v = np.where( + v = xp.where( 6 * vH < 1, vi + (vj - vi) * 6 * vH, - np.nan, + float("nan"), ) - v = np.where(np.logical_and(2 * vH < 1, np.isnan(v)), vj, v) - v = np.where( - np.logical_and(3 * vH < 2, np.isnan(v)), + + v = xp.where(xp.logical_and(2 * vH < 1, xp.isnan(v)), vj, v) + + v = xp.where( + xp.logical_and(3 * vH < 2, xp.isnan(v)), vi + (vj - vi) * ((2 / 3) - vH) * 6, v, ) - return np.where(np.isnan(v), vi, v) - j = np.where(L < 0.5, L * (1 + S), (L + S) - (S * L)) + return xp.where(xp.isnan(v), vi, v) + + j = xp.where(L < 0.5, L * (1 + S), (L + S) - (S * L)) + i = 2 * L - j R = H_to_RGB(i, j, H + (1 / 3)) + G = H_to_RGB(i, j, H) + B = H_to_RGB(i, j, H - (1 / 3)) - R = np.where(S == 0, L, R) - G = np.where(S == 0, L, G) - B = np.where(S == 0, L, B) + R = xp.where(S == 0, L, R) + G = xp.where(S == 0, L, G) + B = xp.where(S == 0, L, B) RGB = tstack([R, G, B]) @@ -410,43 +436,52 @@ def RGB_to_HCL(RGB: Domain1, gamma: float = 3, Y_0: float = 100) -> Range1: array([-0.0316785..., 0.2841715..., 0.2285964...]) """ - R, G, B = tsplit(to_domain_1(RGB)) + RGB = to_domain_1(RGB) + + xp = array_namespace(RGB) + + R, G, B = tsplit(RGB) + + Min = xp.minimum(xp.minimum(R, G), B) - Min = np.minimum(np.minimum(R, G), B) - Max = np.maximum(np.maximum(R, G), B) + Max = xp.maximum(xp.maximum(R, G), B) with sdiv_mode(): - Q = np.exp(sdiv(Min * gamma, Max * Y_0)) + Q = xp.exp(sdiv(Min * gamma, Max * Y_0)) L = (Q * Max + (Q - 1) * Min) / 2 R_G = R - G + G_B = G - B + B_R = B - R - C = Q * (np.abs(R_G) + np.abs(G_B) + np.abs(B_R)) / 3 + C = Q * (xp.abs(R_G) + xp.abs(G_B) + xp.abs(B_R)) / 3 with sdiv_mode("Ignore"): - H = np.arctan(sdiv(G_B, R_G)) + H = xp.atan(sdiv(G_B, R_G)) _2_H_3 = 2 * H / 3 + _4_H_3 = 4 * H / 3 - H = np.select( + H = xp_select( [ C == 0, - np.logical_and(R_G >= 0, G_B >= 0), - np.logical_and(R_G >= 0, G_B < 0), - np.logical_and(R_G < 0, G_B >= 0), - np.logical_and(R_G < 0, G_B < 0), + xp.logical_and(R_G >= 0, G_B >= 0), + xp.logical_and(R_G >= 0, G_B < 0), + xp.logical_and(R_G < 0, G_B >= 0), + xp.logical_and(R_G < 0, G_B < 0), ], [ 0, _2_H_3, _4_H_3, - np.pi + _4_H_3, - _2_H_3 - np.pi, + xp.pi + _4_H_3, + _2_H_3 - xp.pi, ], + xp=xp, ) HCL = tstack([H, C, L]) @@ -502,23 +537,28 @@ def HCL_to_RGB(HCL: Domain1, gamma: float = 3, Y_0: float = 100) -> Range1: array([0.4562033..., 0.0308104..., 0.0409192...]) """ - H, C, L = tsplit(to_domain_1(HCL)) + HCL = to_domain_1(HCL) + + xp = array_namespace(HCL) + + H, C, L = tsplit(HCL) with sdiv_mode(): - Q = np.exp((1 - sdiv(3 * C, 4 * L)) * gamma / Y_0) + Q = xp.exp((1 - sdiv(3 * C, 4 * L)) * gamma / Y_0) Min = sdiv(4 * L - 3 * C, 4 * Q - 2) + Max = Min + sdiv(3 * C, 2 * Q) - tan_3_2_H = np.tan(3 / 2 * H) - tan_3_4_H_MP = np.tan(3 / 4 * (H - np.pi)) - tan_3_4_H = np.tan(3 / 4 * H) - tan_3_2_H_PP = np.tan(3 / 2 * (H + np.pi)) + tan_3_2_H = xp.tan(3 / 2 * H) + tan_3_4_H_MP = xp.tan(3 / 4 * (H - xp.pi)) + tan_3_4_H = xp.tan(3 / 4 * H) + tan_3_2_H_PP = xp.tan(3 / 2 * (H + xp.pi)) - r_p60 = np.radians(60) - r_p120 = np.radians(120) - r_n60 = np.radians(-60) - r_n120 = np.radians(-120) + r_p60 = xp_radians(60) + r_p120 = xp_radians(120) + r_n60 = xp_radians(-60) + r_n120 = xp_radians(-120) def _1_2_3(a: ArrayLike) -> NDArrayBoolean: """Tail-stack specified :math:`a` array as a *bool* dtype.""" @@ -526,14 +566,14 @@ def _1_2_3(a: ArrayLike) -> NDArrayBoolean: return tstack(cast("ArrayLike", [a, a, a]), dtype=np.bool_) with sdiv_mode(): - RGB = np.select( + RGB = xp_select( [ - _1_2_3(np.logical_and(H >= 0, r_p60 >= H)), - _1_2_3(np.logical_and(r_p60 < H, r_p120 >= H)), - _1_2_3(np.logical_and(r_p120 < H, np.pi >= H)), - _1_2_3(np.logical_and(r_n60 <= H, H < 0)), - _1_2_3(np.logical_and(r_n120 <= H, r_n60 > H)), - _1_2_3(np.logical_and(-np.pi < H, r_n120 > H)), + _1_2_3(xp.logical_and(H >= 0, r_p60 >= H)), + _1_2_3(xp.logical_and(r_p60 < H, r_p120 >= H)), + _1_2_3(xp.logical_and(r_p120 < H, xp.pi >= H)), + _1_2_3(xp.logical_and(r_n60 <= H, H < 0)), + _1_2_3(xp.logical_and(r_n120 <= H, r_n60 > H)), + _1_2_3(xp.logical_and(-xp.pi < H, r_n120 > H)), ], [ tstack( @@ -579,6 +619,7 @@ def _1_2_3(a: ArrayLike) -> NDArrayBoolean: ] ), ], + xp=xp, ) return from_range_1(RGB) diff --git a/colour/models/rgb/datasets/tests/test__init__.py b/colour/models/rgb/datasets/tests/test__init__.py index c06966b42c..7893055942 100644 --- a/colour/models/rgb/datasets/tests/test__init__.py +++ b/colour/models/rgb/datasets/tests/test__init__.py @@ -2,14 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import pickle from copy import deepcopy import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import RGB_COLOURSPACES, normalised_primary_matrix -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,7 +41,8 @@ class TestRGB_COLOURSPACES: attribute unit tests methods. """ - def test_transformation_matrices(self) -> None: + @pytest.mark.mps_xfail("MPS float32; test uses hard-coded tolerance literals") + def test_transformation_matrices(self, xp: ModuleType) -> None: """ Test the transformations matrices from the :attr:`colour.models.rgb.datasets.RGB_COLOURSPACES` attribute @@ -51,29 +64,32 @@ def test_transformation_matrices(self) -> None: "sRGB": 1e-4, "V-Gamut": 1e-6, } - XYZ_r = np.reshape(np.array([0.5, 0.5, 0.5]), (3, 1)) + XYZ_r = xp_reshape(xp_as_array([0.5, 0.5, 0.5], xp=xp), (3, 1), xp=xp) for colourspace in RGB_COLOURSPACES.values(): - M = normalised_primary_matrix(colourspace.primaries, colourspace.whitepoint) + M = as_ndarray( + normalised_primary_matrix(colourspace.primaries, colourspace.whitepoint) + ) tolerance = tolerances.get(colourspace.name, 1e-7) - np.testing.assert_allclose( + xp_assert_close( colourspace.matrix_RGB_to_XYZ, M, atol=tolerance, ) - RGB = np.dot(colourspace.matrix_XYZ_to_RGB, XYZ_r) + RGB = np.dot(colourspace.matrix_XYZ_to_RGB, as_ndarray(XYZ_r)) XYZ = np.dot(colourspace.matrix_RGB_to_XYZ, RGB) - np.testing.assert_allclose(XYZ_r, XYZ, atol=tolerance) + xp_assert_close(XYZ_r, XYZ, atol=tolerance) # Derived transformation matrices. colourspace = deepcopy(colourspace) # noqa: PLW2901 colourspace.use_derived_transformation_matrices(True) - RGB = np.dot(colourspace.matrix_XYZ_to_RGB, XYZ_r) + RGB = np.dot(colourspace.matrix_XYZ_to_RGB, as_ndarray(XYZ_r)) XYZ = np.dot(colourspace.matrix_RGB_to_XYZ, RGB) - np.testing.assert_allclose(XYZ_r, XYZ, atol=tolerance) + xp_assert_close(XYZ_r, XYZ, atol=tolerance) - def test_cctf(self) -> None: + @pytest.mark.mps_xfail("MPS float32; test uses hard-coded tolerance literals") + def test_cctf(self, xp: ModuleType) -> None: """ Test colour component transfer functions from the :attr:`colour.models.rgb.datasets.RGB_COLOURSPACES` attribute @@ -84,8 +100,9 @@ def test_cctf(self) -> None: tolerance = {"DJI D-Gamut": 0.1, "F-Gamut": 1e-4, "N-Gamut": 1e-3} - samples = np.hstack( - [np.linspace(0, 1, int(1e5)), np.linspace(0, 65504, 65504 * 10)] + samples = xp_as_array( + np.hstack([np.linspace(0, 1, int(1e5)), np.linspace(0, 65504, 65504 * 10)]), + xp=xp, ) for colourspace in RGB_COLOURSPACES.values(): @@ -95,13 +112,14 @@ def test_cctf(self) -> None: cctf_encoding_s = colourspace.cctf_encoding(samples) cctf_decoding_s = colourspace.cctf_decoding(cctf_encoding_s) - np.testing.assert_allclose( + xp_assert_close( samples, - cctf_decoding_s, + as_ndarray(cctf_decoding_s), atol=tolerance.get(colourspace.name, TOLERANCE_ABSOLUTE_TESTS), ) - def test_n_dimensional_cctf(self) -> None: + @pytest.mark.mps_xfail("MPS float32; test uses hard-coded tolerance literals") + def test_n_dimensional_cctf(self, xp: ModuleType) -> None: """ Test colour component transfer functions from the :attr:`colour.models.rgb.datasets.RGB_COLOURSPACES` attribute @@ -112,34 +130,44 @@ def test_n_dimensional_cctf(self) -> None: for colourspace in RGB_COLOURSPACES.values(): value_cctf_encoding = 0.5 - value_cctf_decoding = colourspace.cctf_decoding( - colourspace.cctf_encoding(value_cctf_encoding) + value_cctf_decoding = as_ndarray( + colourspace.cctf_decoding( + colourspace.cctf_encoding(value_cctf_encoding) + ) ) - np.testing.assert_allclose( + xp_assert_close( value_cctf_encoding, value_cctf_decoding, atol=tolerance.get(colourspace.name, 1e-7), ) - value_cctf_encoding = np.tile(value_cctf_encoding, 6) - value_cctf_decoding = np.tile(value_cctf_decoding, 6) - np.testing.assert_allclose( + value_cctf_encoding = xp.tile(xp_as_array(value_cctf_encoding, xp=xp), (6,)) + value_cctf_decoding = xp.tile(xp_as_array(value_cctf_decoding, xp=xp), (6,)) + xp_assert_close( value_cctf_encoding, value_cctf_decoding, atol=tolerance.get(colourspace.name, 1e-7), ) - value_cctf_encoding = np.reshape(value_cctf_encoding, (3, 2)) - value_cctf_decoding = np.reshape(value_cctf_decoding, (3, 2)) - np.testing.assert_allclose( + value_cctf_encoding = xp_reshape( + xp_as_array(value_cctf_encoding, xp=xp), (3, 2), xp=xp + ) + value_cctf_decoding = xp_reshape( + xp_as_array(value_cctf_decoding, xp=xp), (3, 2), xp=xp + ) + xp_assert_close( value_cctf_encoding, value_cctf_decoding, atol=tolerance.get(colourspace.name, 1e-7), ) - value_cctf_encoding = np.reshape(value_cctf_encoding, (3, 2, 1)) - value_cctf_decoding = np.reshape(value_cctf_decoding, (3, 2, 1)) - np.testing.assert_allclose( + value_cctf_encoding = xp_reshape( + xp_as_array(value_cctf_encoding, xp=xp), (3, 2, 1), xp=xp + ) + value_cctf_decoding = xp_reshape( + xp_as_array(value_cctf_decoding, xp=xp), (3, 2, 1), xp=xp + ) + xp_assert_close( value_cctf_encoding, value_cctf_decoding, atol=tolerance.get(colourspace.name, 1e-7), diff --git a/colour/models/rgb/derivation.py b/colour/models/rgb/derivation.py index e683776883..b9fec8094e 100644 --- a/colour/models/rgb/derivation.py +++ b/colour/models/rgb/derivation.py @@ -27,8 +27,6 @@ import typing -import numpy as np - from colour.adaptation import chromatic_adaptation_VonKries if typing.TYPE_CHECKING: @@ -36,7 +34,17 @@ from colour.hints import ArrayLike, NDArrayFloat, Range1 # noqa: TC001 from colour.models import XYZ_to_xy, XYZ_to_xyY, xy_to_XYZ -from colour.utilities import as_float, as_float_array, ones, tsplit +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + ones, + tsplit, + xp_as_float_array, + xp_create_diagonal, + xp_matrix_transpose, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -72,12 +80,12 @@ def xy_to_z(xy: ArrayLike) -> float: Examples -------- + >>> import numpy as np >>> xy_to_z(np.array([0.25, 0.25])) np.float64(0.5) """ x, y = tsplit(xy) - return 1 - x - y @@ -107,6 +115,7 @@ def normalised_primary_matrix( Examples -------- + >>> import numpy as np >>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) >>> w = np.array([0.32168, 0.33767]) >>> normalised_primary_matrix(p, w) # doctest: +ELLIPSIS @@ -115,17 +124,21 @@ def normalised_primary_matrix( [ 0.0000000...e+00, 0.0000000...e+00, 1.0088251...e+00]]) """ - primaries = np.reshape(primaries, (3, 2)) + primaries = as_float_array(primaries) + + xp = array_namespace(primaries) + + primaries = xp_reshape(primaries, (3, 2), xp=xp) z = as_float_array(xy_to_z(primaries))[..., None] - primaries = np.transpose(np.hstack([primaries, z])) + primaries = xp_matrix_transpose(xp.concat([primaries, z], axis=1), xp=xp) whitepoint = xy_to_XYZ(whitepoint) - coefficients = np.dot(np.linalg.inv(primaries), whitepoint) - coefficients = np.diagflat(coefficients) + coefficients = xp.matmul(xp.linalg.inv(primaries), whitepoint) + coefficients = xp_create_diagonal(coefficients, xp=xp) - return np.dot(primaries, coefficients) + return xp.matmul(primaries, coefficients) def chromatically_adapted_primaries( @@ -160,6 +173,7 @@ def chromatically_adapted_primaries( Examples -------- + >>> import numpy as np >>> p = np.array([0.64, 0.33, 0.30, 0.60, 0.15, 0.06]) >>> w_t = np.array([0.31270, 0.32900]) >>> w_r = np.array([0.34570, 0.35850]) @@ -171,7 +185,11 @@ def chromatically_adapted_primaries( [0.1558932..., 0.0660492...]]) """ - primaries = np.reshape(primaries, (3, 2)) + primaries = as_float_array(primaries) + + xp = array_namespace(primaries) + + primaries = xp_reshape(primaries, (3, 2), xp=xp) XYZ_a = chromatic_adaptation_VonKries( xy_to_XYZ(primaries), @@ -204,6 +222,7 @@ def primaries_whitepoint(npm: ArrayLike) -> Tuple[NDArrayFloat, NDArrayFloat]: Examples -------- + >>> import numpy as np >>> npm = np.array( ... [ ... [9.52552396e-01, 0.00000000e00, 9.36786317e-05], @@ -220,10 +239,23 @@ def primaries_whitepoint(npm: ArrayLike) -> Tuple[NDArrayFloat, NDArrayFloat]: array([0.32168, 0.33767]) """ - npm = np.reshape(npm, (3, 3)) + npm = as_float_array(npm) + + xp = array_namespace(npm) - primaries = XYZ_to_xy(np.transpose(np.dot(npm, np.identity(3)))) - whitepoint = np.squeeze(XYZ_to_xy(np.transpose(np.dot(npm, ones((3, 1)))))) + npm = xp_reshape(npm, (3, 3), xp=xp) + + primaries = XYZ_to_xy(xp_matrix_transpose(npm, xp=xp)) + whitepoint = xp_reshape( + XYZ_to_xy( + xp_matrix_transpose( + xp.matmul(npm, xp_as_float_array(ones((3, 1)), xp=xp, like=npm)), + xp=xp, + ) + ), + (-1,), + xp=xp, + ) # TODO: Investigate if we return an ndarray here with primaries and # whitepoint stacked together. @@ -249,14 +281,19 @@ def RGB_luminance_equation(primaries: ArrayLike, whitepoint: ArrayLike) -> str: Examples -------- + >>> import numpy as np >>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) >>> whitepoint = np.array([0.32168, 0.33767]) >>> RGB_luminance_equation(p, whitepoint) # doctest: +ELLIPSIS 'Y = 0.3439664...(R) + 0.7281660...(G) + -0.0721325...(B)' """ + primaries = as_float_array(primaries) + + xp = array_namespace(primaries) + return "Y = {}(R) + {}(G) + {}(B)".format( - *np.ravel(normalised_primary_matrix(primaries, whitepoint))[3:6] + *xp_reshape(normalised_primary_matrix(primaries, whitepoint), (-1,), xp=xp)[3:6] ) @@ -291,6 +328,7 @@ def RGB_luminance( Examples -------- + >>> import numpy as np >>> RGB = np.array([0.21959402, 0.06986677, 0.04703877]) >>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) >>> whitepoint = np.array([0.32168, 0.33767]) @@ -298,6 +336,16 @@ def RGB_luminance( np.float64(0.1230145...) """ - Y = np.sum(normalised_primary_matrix(primaries, whitepoint)[1] * RGB, axis=-1) + RGB = as_float_array(RGB) + + xp = array_namespace(RGB) + + Y = xp.sum( + xp_as_float_array( + normalised_primary_matrix(primaries, whitepoint)[1], xp=xp, like=RGB + ) + * RGB, + axis=-1, + ) return as_float(Y) diff --git a/colour/models/rgb/hanbury2003.py b/colour/models/rgb/hanbury2003.py index 7f2a486d22..31b82e8585 100644 --- a/colour/models/rgb/hanbury2003.py +++ b/colour/models/rgb/hanbury2003.py @@ -28,7 +28,7 @@ NDArrayFloat, Range1, ) -from colour.utilities import from_range_1, to_domain_1, tsplit, tstack +from colour.utilities import array_namespace, from_range_1, to_domain_1, tsplit, tstack __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -95,20 +95,23 @@ def RGB_to_IHLS(RGB: Domain1) -> Range1: """ RGB = to_domain_1(RGB) + + xp = array_namespace(RGB) + R, G, B = tsplit(RGB) Y, C_1, C_2 = tsplit(vecmul(MATRIX_RGB_TO_YC_1_C_2, RGB)) - C = np.hypot(C_1, C_2) + C = xp.hypot(C_1, C_2) with sdiv_mode(): C_1_C = sdiv(C_1, C) - arcos_C_1_C_2 = np.where(C_1_C != 0, np.arccos(np.clip(C_1_C, -1, 1)), 0) + arcos_C_1_C_2 = xp.where(C_1_C != 0, xp.acos(xp.clip(C_1_C, -1, 1)), 0) - H = np.where(C_2 <= 0, arcos_C_1_C_2, (np.pi * 2) - arcos_C_1_C_2) + H = xp.where(C_2 <= 0, arcos_C_1_C_2, (xp.pi * 2) - arcos_C_1_C_2) - S = np.maximum(np.maximum(R, G), B) - np.minimum(np.minimum(R, G), B) + S = xp.maximum(xp.maximum(R, G), B) - xp.minimum(xp.minimum(R, G), B) HYS = tstack([H, Y, S]) @@ -154,16 +157,20 @@ def IHLS_to_RGB(HYS: Domain1) -> Range1: array([0.4559557..., 0.0303970..., 0.0408724...]) """ - H, Y, S = tsplit(to_domain_1(HYS)) + HYS = to_domain_1(HYS) + + xp = array_namespace(HYS) + + H, Y, S = tsplit(HYS) - pi_3 = np.pi / 3 + pi_3 = xp.pi / 3 - k = np.floor(H / pi_3) + k = xp.floor(H / pi_3) H_s = H - k * pi_3 - C = (np.sqrt(3) * S) / (2 * np.sin(2 * pi_3 - H_s)) + C = (3.0**0.5 * S) / (2 * xp.sin(2 * pi_3 - H_s)) - C_1 = C * np.cos(H) - C_2 = -C * np.sin(H) + C_1 = C * xp.cos(H) + C_2 = -C * xp.sin(H) RGB = vecmul(MATRIX_YC_1_C_2_TO_RGB, tstack([Y, C_1, C_2])) diff --git a/colour/models/rgb/itut_h_273.py b/colour/models/rgb/itut_h_273.py index c68f4abbd7..4ef3721899 100644 --- a/colour/models/rgb/itut_h_273.py +++ b/colour/models/rgb/itut_h_273.py @@ -147,7 +147,7 @@ oetf_H273_LogSqrt, oetf_SMPTE240M, ) -from colour.utilities import message_box, multiline_str +from colour.utilities import array_namespace, message_box, multiline_str from colour.utilities.documentation import DocstringDict, is_documentation_building __all__ = [ @@ -213,7 +213,9 @@ def _clipped_domain_function( def wrapped(x: ArrayLike, *args: Any, **kwargs: Any) -> Any: """Wrap specified function.""" - return function(np.clip(x, *domain), *args, **kwargs) + xp = array_namespace(x) + + return function(xp.clip(x, *domain), *args, **kwargs) return wrapped diff --git a/colour/models/rgb/prismatic.py b/colour/models/rgb/prismatic.py index 0f931c6b7d..040d420536 100644 --- a/colour/models/rgb/prismatic.py +++ b/colour/models/rgb/prismatic.py @@ -16,14 +16,12 @@ from __future__ import annotations -import numpy as np - from colour.algebra import sdiv, sdiv_mode from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import from_range_1, to_domain_1, tsplit, tstack +from colour.utilities import array_namespace, from_range_1, to_domain_1, tsplit, tstack __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -73,6 +71,7 @@ def RGB_to_Prismatic(RGB: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> RGB = np.array([0.25, 0.50, 0.75]) >>> RGB_to_Prismatic(RGB) # doctest: +ELLIPSIS array([0.75... , 0.1666666..., 0.3333333..., 0.5... ]) @@ -87,8 +86,10 @@ def RGB_to_Prismatic(RGB: Domain1) -> Range1: RGB = to_domain_1(RGB) - L = np.max(RGB, axis=-1) - s = np.sum(RGB, axis=-1) + xp = array_namespace(RGB) + + L = xp.max(RGB, axis=-1) + s = xp.sum(RGB, axis=-1) with sdiv_mode(): one_s = sdiv(1, s[..., None]) @@ -135,6 +136,7 @@ def Prismatic_to_RGB(Lrgb: Domain1) -> Range1: Examples -------- + >>> import numpy as np >>> Lrgb = np.array([0.75000000, 0.16666667, 0.33333333, 0.50000000]) >>> Prismatic_to_RGB(Lrgb) # doctest: +ELLIPSIS array([0.25... , 0.4999999..., 0.75... ]) @@ -142,8 +144,10 @@ def Prismatic_to_RGB(Lrgb: Domain1) -> Range1: Lrgb = to_domain_1(Lrgb) + xp = array_namespace(Lrgb) + rgb = Lrgb[..., 1:] - m = np.max(rgb, axis=-1) + m = xp.max(rgb, axis=-1) with sdiv_mode(): RGB = sdiv(Lrgb[..., 0][..., None], m[..., None]) diff --git a/colour/models/rgb/rgb_colourspace.py b/colour/models/rgb/rgb_colourspace.py index d493bfe28e..ee2f3798e1 100644 --- a/colour/models/rgb/rgb_colourspace.py +++ b/colour/models/rgb/rgb_colourspace.py @@ -49,6 +49,7 @@ from colour.models import xy_to_xyY, xy_to_XYZ, xyY_to_XYZ from colour.models.rgb import chromatically_adapted_primaries, normalised_primary_matrix from colour.utilities import ( + array_namespace, as_float_array, attest, domain_range_scale, @@ -60,6 +61,7 @@ to_domain_1, usage_warning, validate_method, + xp_reshape, ) __author__ = "Colour Developers" @@ -205,8 +207,8 @@ class RGB_Colourspace: -------- >>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) >>> whitepoint = np.array([0.32168, 0.33767]) - >>> matrix_RGB_to_XYZ = np.identity(3) - >>> matrix_XYZ_to_RGB = np.identity(3) + >>> matrix_RGB_to_XYZ = np.eye(3) + >>> matrix_XYZ_to_RGB = np.eye(3) >>> colourspace = RGB_Colourspace( ... "RGB Colourspace", ... p, @@ -340,7 +342,9 @@ def primaries(self, value: ArrayLike) -> None: value = as_float_array(value) - value = np.reshape(value, (3, 2)) + xp = array_namespace(value) + + value = xp_reshape(value, (3, 2), xp=xp) self._primaries = value @@ -657,8 +661,8 @@ def __str__(self) -> str: -------- >>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) >>> whitepoint = np.array([0.32168, 0.33767]) - >>> matrix_RGB_to_XYZ = np.identity(3) - >>> matrix_XYZ_to_RGB = np.identity(3) + >>> matrix_RGB_to_XYZ = np.eye(3) + >>> matrix_XYZ_to_RGB = np.eye(3) >>> cctf_encoding = lambda x: x >>> cctf_decoding = lambda x: x >>> print( # doctest: +ELLIPSIS @@ -749,8 +753,8 @@ def __repr__(self) -> str: >>> from colour.models import linear_function >>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) >>> whitepoint = np.array([0.32168, 0.33767]) - >>> matrix_RGB_to_XYZ = np.identity(3) - >>> matrix_XYZ_to_RGB = np.identity(3) + >>> matrix_RGB_to_XYZ = np.eye(3) + >>> matrix_XYZ_to_RGB = np.eye(3) >>> RGB_Colourspace( # doctest: +ELLIPSIS ... "RGB Colourspace", ... p, @@ -829,8 +833,10 @@ def _derive_transformation_matrices(self) -> None: if self._primaries is not None and self._whitepoint is not None: npm = normalised_primary_matrix(self._primaries, self._whitepoint) + xp = array_namespace(npm) + self._derived_matrix_RGB_to_XYZ = npm - self._derived_matrix_XYZ_to_RGB = np.linalg.inv(npm) + self._derived_matrix_XYZ_to_RGB = xp.linalg.inv(npm) def use_derived_transformation_matrices(self, usage: bool = True) -> None: """ @@ -1022,7 +1028,6 @@ def XYZ_to_RGB( from colour.models import RGB_COLOURSPACES # noqa: PLC0415 XYZ = to_domain_1(XYZ) - if not isinstance(colourspace, (RGB_Colourspace, str)): usage_warning( 'The "colour.XYZ_to_RGB" definition signature has changed with ' @@ -1156,7 +1161,6 @@ def RGB_to_XYZ( from colour.models import RGB_COLOURSPACES # noqa: PLC0415 RGB = to_domain_1(RGB) - if not isinstance(colourspace, (RGB_Colourspace, str)): usage_warning( 'The "colour.RGB_to_XYZ" definition signature has changed with ' @@ -1281,6 +1285,8 @@ def matrix_RGB_to_RGB( M = input_colourspace.matrix_RGB_to_XYZ + xp = array_namespace(M) + if chromatic_adaptation_transform is not None: M_CAT = matrix_chromatic_adaptation_VonKries( xy_to_XYZ(input_colourspace.whitepoint), @@ -1288,9 +1294,9 @@ def matrix_RGB_to_RGB( chromatic_adaptation_transform, ) - M = np.matmul(M_CAT, input_colourspace.matrix_RGB_to_XYZ) + M = xp.matmul(M_CAT, input_colourspace.matrix_RGB_to_XYZ) - return np.matmul(output_colourspace.matrix_XYZ_to_RGB, M) + return xp.matmul(output_colourspace.matrix_XYZ_to_RGB, M) def RGB_to_RGB( @@ -1387,7 +1393,6 @@ def RGB_to_RGB( ) RGB = to_domain_1(RGB) - if apply_cctf_decoding and input_colourspace.cctf_decoding is not None: with domain_range_scale("ignore"): RGB = input_colourspace.cctf_decoding( diff --git a/colour/models/rgb/tests/test_cmyk.py b/colour/models/rgb/tests/test_cmyk.py index 7dbe8a5b50..24dcf4e914 100644 --- a/colour/models/rgb/tests/test_cmyk.py +++ b/colour/models/rgb/tests/test_cmyk.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.cmyk import CMY_to_CMYK, CMY_to_RGB, CMYK_to_CMY, RGB_to_CMY -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,57 +43,57 @@ class TestRGB_to_CMY: methods. """ - def test_RGB_to_CMY(self) -> None: + def test_RGB_to_CMY(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cmyk.RGB_to_CMY` definition.""" - np.testing.assert_allclose( - RGB_to_CMY(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([0.54379481, 0.96918929, 0.95908048]), + xp_assert_close( + RGB_to_CMY(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [0.54379481, 0.96918929, 0.95908048], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_CMY(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([1.00000000, 1.00000000, 1.00000000]), + xp_assert_close( + RGB_to_CMY(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [1.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_CMY(np.array([1.00000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + RGB_to_CMY(xp_as_array([1.00000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_CMY(self) -> None: + def test_n_dimensional_RGB_to_CMY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.RGB_to_CMY` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - CMY = RGB_to_CMY(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + CMY = as_ndarray(RGB_to_CMY(RGB)) - RGB = np.tile(RGB, (6, 1)) - CMY = np.tile(CMY, (6, 1)) - np.testing.assert_allclose(RGB_to_CMY(RGB), CMY, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + CMY = xp.tile(xp_as_array(CMY, xp=xp), (6, 1)) + xp_assert_close(RGB_to_CMY(RGB), CMY, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - CMY = np.reshape(CMY, (2, 3, 3)) - np.testing.assert_allclose(RGB_to_CMY(RGB), CMY, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + CMY = xp_reshape(xp_as_array(CMY, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(RGB_to_CMY(RGB), CMY, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_CMY(self) -> None: + def test_domain_range_scale_RGB_to_CMY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.RGB_to_CMY` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - CMY = RGB_to_CMY(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + CMY = as_ndarray(RGB_to_CMY(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_CMY(RGB * factor), CMY * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -105,57 +117,57 @@ class TestCMY_to_RGB: methods. """ - def test_CMY_to_RGB(self) -> None: + def test_CMY_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cmyk.CMY_to_RGB` definition.""" - np.testing.assert_allclose( - CMY_to_RGB(np.array([0.54379481, 0.96918929, 0.95908048])), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + CMY_to_RGB(xp_as_array([0.54379481, 0.96918929, 0.95908048], xp=xp)), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CMY_to_RGB(np.array([1.00000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + CMY_to_RGB(xp_as_array([1.00000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CMY_to_RGB(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([1.00000000, 1.00000000, 1.00000000]), + xp_assert_close( + CMY_to_RGB(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [1.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CMY_to_RGB(self) -> None: + def test_n_dimensional_CMY_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.CMY_to_RGB` definition n-dimensional arrays support. """ - CMY = np.array([0.54379481, 0.96918929, 0.95908048]) - RGB = CMY_to_RGB(CMY) + CMY = xp_as_array([0.54379481, 0.96918929, 0.95908048], xp=xp) + RGB = as_ndarray(CMY_to_RGB(CMY)) - CMY = np.tile(CMY, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose(CMY_to_RGB(CMY), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + CMY = xp.tile(xp_as_array(CMY, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(CMY_to_RGB(CMY), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - CMY = np.reshape(CMY, (2, 3, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose(CMY_to_RGB(CMY), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + CMY = xp_reshape(xp_as_array(CMY, xp=xp), (2, 3, 3), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(CMY_to_RGB(CMY), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_CMY_to_RGB(self) -> None: + def test_domain_range_scale_CMY_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.CMY_to_RGB` definition domain and range scale support. """ - CMY = np.array([0.54379481, 0.96918929, 0.95908048]) - RGB = CMY_to_RGB(CMY) + CMY = xp_as_array([0.54379481, 0.96918929, 0.95908048], xp=xp) + RGB = as_ndarray(CMY_to_RGB(CMY)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( CMY_to_RGB(CMY * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -176,61 +188,57 @@ class TestCMY_to_CMYK: methods. """ - def test_CMY_to_CMYK(self) -> None: + def test_CMY_to_CMYK(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cmyk.CMY_to_CMYK` definition.""" - np.testing.assert_allclose( - CMY_to_CMYK(np.array([0.54379481, 0.96918929, 0.95908048])), - np.array([0.00000000, 0.93246304, 0.91030457, 0.54379481]), + xp_assert_close( + CMY_to_CMYK(xp_as_array([0.54379481, 0.96918929, 0.95908048], xp=xp)), + [0.00000000, 0.93246304, 0.91030457, 0.54379481], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CMY_to_CMYK(np.array([0.15000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 1.00000000, 1.00000000, 0.15000000]), + xp_assert_close( + CMY_to_CMYK(xp_as_array([0.15000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 1.00000000, 1.00000000, 0.15000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CMY_to_CMYK(np.array([0.15000000, 0.00000000, 0.00000000])), - np.array([0.15000000, 0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + CMY_to_CMYK(xp_as_array([0.15000000, 0.00000000, 0.00000000], xp=xp)), + [0.15000000, 0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CMY_to_CMYK(self) -> None: + def test_n_dimensional_CMY_to_CMYK(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.CMY_to_CMYK` definition n-dimensional arrays support. """ - CMY = np.array([0.54379481, 0.96918929, 0.95908048]) - CMYK = CMY_to_CMYK(CMY) + CMY = xp_as_array([0.54379481, 0.96918929, 0.95908048], xp=xp) + CMYK = as_ndarray(CMY_to_CMYK(CMY)) - CMY = np.tile(CMY, (6, 1)) - CMYK = np.tile(CMYK, (6, 1)) - np.testing.assert_allclose( - CMY_to_CMYK(CMY), CMYK, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CMY = xp.tile(xp_as_array(CMY, xp=xp), (6, 1)) + CMYK = xp.tile(xp_as_array(CMYK, xp=xp), (6, 1)) + xp_assert_close(CMY_to_CMYK(CMY), CMYK, atol=TOLERANCE_ABSOLUTE_TESTS) - CMY = np.reshape(CMY, (2, 3, 3)) - CMYK = np.reshape(CMYK, (2, 3, 4)) - np.testing.assert_allclose( - CMY_to_CMYK(CMY), CMYK, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CMY = xp_reshape(xp_as_array(CMY, xp=xp), (2, 3, 3), xp=xp) + CMYK = xp_reshape(xp_as_array(CMYK, xp=xp), (2, 3, 4), xp=xp) + xp_assert_close(CMY_to_CMYK(CMY), CMYK, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_CMY_to_CMYK(self) -> None: + def test_domain_range_scale_CMY_to_CMYK(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.CMY_to_CMYK` definition domain and range scale support. """ - CMY = np.array([0.54379481, 0.96918929, 0.95908048]) - CMYK = CMY_to_CMYK(CMY) + CMY = xp_as_array([0.54379481, 0.96918929, 0.95908048], xp=xp) + CMYK = as_ndarray(CMY_to_CMYK(CMY)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( CMY_to_CMYK(CMY * factor), CMYK * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -254,61 +262,63 @@ class TestCMYK_to_CMY: methods. """ - def test_CMYK_to_CMY(self) -> None: + def test_CMYK_to_CMY(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cmyk.CMYK_to_CMY` definition.""" - np.testing.assert_allclose( - CMYK_to_CMY(np.array([0.00000000, 0.93246304, 0.91030457, 0.54379481])), - np.array([0.54379481, 0.96918929, 0.95908048]), + xp_assert_close( + CMYK_to_CMY( + xp_as_array([0.00000000, 0.93246304, 0.91030457, 0.54379481], xp=xp) + ), + [0.54379481, 0.96918929, 0.95908048], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CMYK_to_CMY(np.array([0.00000000, 1.00000000, 1.00000000, 0.15000000])), - np.array([0.15000000, 1.00000000, 1.00000000]), + xp_assert_close( + CMYK_to_CMY( + xp_as_array([0.00000000, 1.00000000, 1.00000000, 0.15000000], xp=xp) + ), + [0.15000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CMYK_to_CMY(np.array([0.15000000, 0.00000000, 0.00000000, 0.00000000])), - np.array([0.15000000, 0.00000000, 0.00000000]), + xp_assert_close( + CMYK_to_CMY( + xp_as_array([0.15000000, 0.00000000, 0.00000000, 0.00000000], xp=xp) + ), + [0.15000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CMYK_to_CMY(self) -> None: + def test_n_dimensional_CMYK_to_CMY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.CMYK_to_CMY` definition n-dimensional arrays support. """ - CMYK = np.array([0.00000000, 0.93246304, 0.91030457, 0.54379481]) - CMY = CMYK_to_CMY(CMYK) + CMYK = xp_as_array([0.00000000, 0.93246304, 0.91030457, 0.54379481], xp=xp) + CMY = as_ndarray(CMYK_to_CMY(CMYK)) - CMYK = np.tile(CMYK, (6, 1)) - CMY = np.tile(CMY, (6, 1)) - np.testing.assert_allclose( - CMYK_to_CMY(CMYK), CMY, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CMYK = xp.tile(xp_as_array(CMYK, xp=xp), (6, 1)) + CMY = xp.tile(xp_as_array(CMY, xp=xp), (6, 1)) + xp_assert_close(CMYK_to_CMY(CMYK), CMY, atol=TOLERANCE_ABSOLUTE_TESTS) - CMYK = np.reshape(CMYK, (2, 3, 4)) - CMY = np.reshape(CMY, (2, 3, 3)) - np.testing.assert_allclose( - CMYK_to_CMY(CMYK), CMY, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CMYK = xp_reshape(xp_as_array(CMYK, xp=xp), (2, 3, 4), xp=xp) + CMY = xp_reshape(xp_as_array(CMY, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(CMYK_to_CMY(CMYK), CMY, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_CMYK_to_CMY(self) -> None: + def test_domain_range_scale_CMYK_to_CMY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cmyk.CMYK_to_CMY` definition domain and range scale support. """ - CMYK = np.array([0.00000000, 0.93246304, 0.91030457, 0.54379481]) - CMY = CMYK_to_CMY(CMYK) + CMYK = xp_as_array([0.00000000, 0.93246304, 0.91030457, 0.54379481], xp=xp) + CMY = as_ndarray(CMYK_to_CMY(CMYK)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( CMYK_to_CMY(CMYK * factor), CMY * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/tests/test_common.py b/colour/models/rgb/tests/test_common.py index 667a43a5ce..64c13c9c17 100644 --- a/colour/models/rgb/tests/test_common.py +++ b/colour/models/rgb/tests/test_common.py @@ -2,10 +2,15 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import XYZ_to_sRGB, sRGB_to_XYZ +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -26,55 +31,55 @@ class TestXYZ_to_sRGB: methods. """ - def test_XYZ_to_sRGB(self) -> None: + def test_XYZ_to_sRGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.common.XYZ_to_sRGB` definition.""" - np.testing.assert_allclose( - XYZ_to_sRGB(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.70573936, 0.19248266, 0.22354169]), + xp_assert_close( + XYZ_to_sRGB(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.70573936, 0.19248266, 0.22354169], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_sRGB(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.25847003, 0.58276102, 0.29718877]), + xp_assert_close( + XYZ_to_sRGB(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.25847003, 0.58276102, 0.29718877], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_sRGB( - np.array([0.07818780, 0.06157201, 0.28099326]), - np.array([0.34570, 0.35850]), + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp), + [0.34570, 0.35850], ), - np.array([0.09838967, 0.25404426, 0.65130925]), + [0.09838967, 0.25404426, 0.65130925], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_sRGB( - np.array([0.00000000, 0.00000000, 0.00000000]), - np.array([0.44757, 0.40745]), + xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp), + [0.44757, 0.40745], ), - np.array([0.00000000, 0.00000000, 0.00000000]), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_sRGB( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + [0.44757, 0.40745], chromatic_adaptation_transform="Bradford", ), - np.array([0.60873814, 0.23259548, 0.43714892]), + [0.60873814, 0.23259548, 0.43714892], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_sRGB( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), apply_cctf_encoding=False, ), - np.array([0.45620520, 0.03081070, 0.04091953]), + [0.45620520, 0.03081070, 0.04091953], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -85,54 +90,54 @@ class TestsRGB_to_XYZ: methods. """ - def test_sRGB_to_XYZ(self) -> None: + def test_sRGB_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.common.sRGB_to_XYZ` definition.""" - np.testing.assert_allclose( - sRGB_to_XYZ(np.array([0.70573936, 0.19248266, 0.22354169])), - np.array([0.20654290, 0.12197943, 0.05137140]), + xp_assert_close( + sRGB_to_XYZ(xp_as_array([0.70573936, 0.19248266, 0.22354169], xp=xp)), + [0.20654290, 0.12197943, 0.05137140], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sRGB_to_XYZ(np.array([0.25847003, 0.58276102, 0.29718877])), - np.array([0.14222582, 0.23043727, 0.10496290]), + xp_assert_close( + sRGB_to_XYZ(xp_as_array([0.25847003, 0.58276102, 0.29718877], xp=xp)), + [0.14222582, 0.23043727, 0.10496290], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sRGB_to_XYZ( - np.array([0.09838967, 0.25404426, 0.65130925]), - np.array([0.34570, 0.35850]), + xp_as_array([0.09838967, 0.25404426, 0.65130925], xp=xp), + [0.34570, 0.35850], ), - np.array([0.07819162, 0.06157356, 0.28099475]), + [0.07819162, 0.06157356, 0.28099475], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sRGB_to_XYZ( - np.array([0.00000000, 0.00000000, 0.00000000]), - np.array([0.44757, 0.40745]), + xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp), + [0.44757, 0.40745], ), - np.array([0.00000000, 0.00000000, 0.00000000]), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sRGB_to_XYZ( - np.array([0.60873814, 0.23259548, 0.43714892]), - np.array([0.44757, 0.40745]), + xp_as_array([0.60873814, 0.23259548, 0.43714892], xp=xp), + [0.44757, 0.40745], chromatic_adaptation_transform="Bradford", ), - np.array([0.20654449, 0.12197792, 0.05137030]), + [0.20654449, 0.12197792, 0.05137030], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sRGB_to_XYZ( - np.array([0.45620520, 0.03081070, 0.04091953]), + xp_as_array([0.45620520, 0.03081070, 0.04091953], xp=xp), apply_cctf_decoding=False, ), - np.array([0.20654291, 0.12197943, 0.05137141]), + [0.20654291, 0.12197943, 0.05137141], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/tests/test_cylindrical.py b/colour/models/rgb/tests/test_cylindrical.py index 2373776b48..a6ce785055 100644 --- a/colour/models/rgb/tests/test_cylindrical.py +++ b/colour/models/rgb/tests/test_cylindrical.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -15,7 +20,14 @@ RGB_to_HSL, RGB_to_HSV, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -40,63 +52,63 @@ class TestRGB_to_HSV: tests methods. """ - def test_RGB_to_HSV(self) -> None: + def test_RGB_to_HSV(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cylindrical.RGB_to_HSV` definition.""" - np.testing.assert_allclose( - RGB_to_HSV(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([0.99603944, 0.93246304, 0.45620519]), + xp_assert_close( + RGB_to_HSV(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [0.99603944, 0.93246304, 0.45620519], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HSV(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + RGB_to_HSV(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HSV(np.array([1.00000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 0.00000000, 1.00000000]), + xp_assert_close( + RGB_to_HSV(xp_as_array([1.00000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 0.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HSV(np.array([0.00000000, 1.00000000, 1.00000000])), - np.array([0.50000000, 1.00000000, 1.00000000]), + xp_assert_close( + RGB_to_HSV(xp_as_array([0.00000000, 1.00000000, 1.00000000], xp=xp)), + [0.50000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_HSV(self) -> None: + def test_n_dimensional_RGB_to_HSV(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.RGB_to_HSV` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HSV = RGB_to_HSV(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HSV = as_ndarray(RGB_to_HSV(RGB)) - RGB = np.tile(RGB, (6, 1)) - HSV = np.tile(HSV, (6, 1)) - np.testing.assert_allclose(RGB_to_HSV(RGB), HSV, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + HSV = xp.tile(xp_as_array(HSV, xp=xp), (6, 1)) + xp_assert_close(RGB_to_HSV(RGB), HSV, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - HSV = np.reshape(HSV, (2, 3, 3)) - np.testing.assert_allclose(RGB_to_HSV(RGB), HSV, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + HSV = xp_reshape(xp_as_array(HSV, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(RGB_to_HSV(RGB), HSV, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_HSV(self) -> None: + def test_domain_range_scale_RGB_to_HSV(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.RGB_to_HSV` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HSV = RGB_to_HSV(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HSV = as_ndarray(RGB_to_HSV(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_HSV(RGB * factor), HSV * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -120,63 +132,63 @@ class TestHSV_to_RGB: tests methods. """ - def test_HSV_to_RGB(self) -> None: + def test_HSV_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cylindrical.HSV_to_RGB` definition.""" - np.testing.assert_allclose( - HSV_to_RGB(np.array([0.99603944, 0.93246304, 0.45620519])), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + HSV_to_RGB(xp_as_array([0.99603944, 0.93246304, 0.45620519], xp=xp)), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HSV_to_RGB(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + HSV_to_RGB(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HSV_to_RGB(np.array([0.00000000, 0.00000000, 1.00000000])), - np.array([1.00000000, 1.00000000, 1.00000000]), + xp_assert_close( + HSV_to_RGB(xp_as_array([0.00000000, 0.00000000, 1.00000000], xp=xp)), + [1.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HSV_to_RGB(np.array([0.50000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 1.00000000, 1.00000000]), + xp_assert_close( + HSV_to_RGB(xp_as_array([0.50000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_HSV_to_RGB(self) -> None: + def test_n_dimensional_HSV_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.HSV_to_RGB` definition n-dimensional arrays support. """ - HSV = np.array([0.99603944, 0.93246304, 0.45620519]) - RGB = HSV_to_RGB(HSV) + HSV = xp_as_array([0.99603944, 0.93246304, 0.45620519], xp=xp) + RGB = as_ndarray(HSV_to_RGB(HSV)) - HSV = np.tile(HSV, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose(HSV_to_RGB(HSV), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HSV = xp.tile(xp_as_array(HSV, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(HSV_to_RGB(HSV), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - HSV = np.reshape(HSV, (2, 3, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose(HSV_to_RGB(HSV), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HSV = xp_reshape(xp_as_array(HSV, xp=xp), (2, 3, 3), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(HSV_to_RGB(HSV), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_HSV_to_RGB(self) -> None: + def test_domain_range_scale_HSV_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.HSV_to_RGB` definition domain and range scale support. """ - HSV = np.array([0.99603944, 0.93246304, 0.45620519]) - RGB = HSV_to_RGB(HSV) + HSV = xp_as_array([0.99603944, 0.93246304, 0.45620519], xp=xp) + RGB = as_ndarray(HSV_to_RGB(HSV)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( HSV_to_RGB(HSV * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -200,63 +212,63 @@ class TestRGB_to_HSL: tests methods. """ - def test_RGB_to_HSL(self) -> None: + def test_RGB_to_HSL(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cylindrical.RGB_to_HSL` definition.""" - np.testing.assert_allclose( - RGB_to_HSL(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([0.99603944, 0.87347144, 0.24350795]), + xp_assert_close( + RGB_to_HSL(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [0.99603944, 0.87347144, 0.24350795], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HSL(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + RGB_to_HSL(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HSL(np.array([1.00000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 0.00000000, 1.00000000]), + xp_assert_close( + RGB_to_HSL(xp_as_array([1.00000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 0.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HSL(np.array([1.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 1.00000000, 0.50000000]), + xp_assert_close( + RGB_to_HSL(xp_as_array([1.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 1.00000000, 0.50000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_HSL(self) -> None: + def test_n_dimensional_RGB_to_HSL(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.RGB_to_HSL` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HSL = RGB_to_HSL(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HSL = as_ndarray(RGB_to_HSL(RGB)) - RGB = np.tile(RGB, (6, 1)) - HSL = np.tile(HSL, (6, 1)) - np.testing.assert_allclose(RGB_to_HSL(RGB), HSL, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + HSL = xp.tile(xp_as_array(HSL, xp=xp), (6, 1)) + xp_assert_close(RGB_to_HSL(RGB), HSL, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - HSL = np.reshape(HSL, (2, 3, 3)) - np.testing.assert_allclose(RGB_to_HSL(RGB), HSL, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + HSL = xp_reshape(xp_as_array(HSL, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(RGB_to_HSL(RGB), HSL, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_HSL(self) -> None: + def test_domain_range_scale_RGB_to_HSL(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.RGB_to_HSL` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HSL = RGB_to_HSL(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HSL = as_ndarray(RGB_to_HSL(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_HSL(RGB * factor), HSL * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -280,63 +292,63 @@ class TestHSL_to_RGB: tests methods. """ - def test_HSL_to_RGB(self) -> None: + def test_HSL_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cylindrical.HSL_to_RGB` definition.""" - np.testing.assert_allclose( - HSL_to_RGB(np.array([0.99603944, 0.87347144, 0.24350795])), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + HSL_to_RGB(xp_as_array([0.99603944, 0.87347144, 0.24350795], xp=xp)), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HSL_to_RGB(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + HSL_to_RGB(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HSL_to_RGB(np.array([0.00000000, 0.00000000, 1.00000000])), - np.array([1.00000000, 1.00000000, 1.00000000]), + xp_assert_close( + HSL_to_RGB(xp_as_array([0.00000000, 0.00000000, 1.00000000], xp=xp)), + [1.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HSL_to_RGB(np.array([0.00000000, 1.00000000, 0.50000000])), - np.array([1.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + HSL_to_RGB(xp_as_array([0.00000000, 1.00000000, 0.50000000], xp=xp)), + [1.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_HSL_to_RGB(self) -> None: + def test_n_dimensional_HSL_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.HSL_to_RGB` definition n-dimensional arrays support. """ - HSL = np.array([0.99603944, 0.87347144, 0.24350795]) - RGB = HSL_to_RGB(HSL) + HSL = xp_as_array([0.99603944, 0.87347144, 0.24350795], xp=xp) + RGB = as_ndarray(HSL_to_RGB(HSL)) - HSL = np.tile(HSL, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose(HSL_to_RGB(HSL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HSL = xp.tile(xp_as_array(HSL, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(HSL_to_RGB(HSL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - HSL = np.reshape(HSL, (2, 3, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose(HSL_to_RGB(HSL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HSL = xp_reshape(xp_as_array(HSL, xp=xp), (2, 3, 3), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(HSL_to_RGB(HSL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_HSL_to_RGB(self) -> None: + def test_domain_range_scale_HSL_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.HSL_to_RGB` definition domain and range scale support. """ - HSL = np.array([0.99603944, 0.87347144, 0.24350795]) - RGB = HSL_to_RGB(HSL) + HSL = xp_as_array([0.99603944, 0.87347144, 0.24350795], xp=xp) + RGB = as_ndarray(HSL_to_RGB(HSL)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( HSL_to_RGB(HSL * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -360,63 +372,63 @@ class TestRGB_to_HCL: tests methods. """ - def test_RGB_to_HCL(self) -> None: + def test_RGB_to_HCL(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cylindrical.RGB_to_HCL` definition.""" - np.testing.assert_allclose( - RGB_to_HCL(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([-0.03167854, 0.2841715, 0.22859647]), + xp_assert_close( + RGB_to_HCL(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [-0.03167854, 0.2841715, 0.22859647], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HCL(np.array([1.00000000, 2.00000000, 0.50000000])), - np.array([1.83120102, 1.0075282, 1.00941024]), + xp_assert_close( + RGB_to_HCL(xp_as_array([1.00000000, 2.00000000, 0.50000000], xp=xp)), + [1.83120102, 1.0075282, 1.00941024], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HCL(np.array([2.00000000, 1.00000000, 0.50000000])), - np.array([0.30909841, 1.0075282, 1.00941024]), + xp_assert_close( + RGB_to_HCL(xp_as_array([2.00000000, 1.00000000, 0.50000000], xp=xp)), + [0.30909841, 1.0075282, 1.00941024], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_HCL(np.array([0.50000000, 1.00000000, 2.00000000])), - np.array([-2.40349351, 1.0075282, 1.00941024]), + xp_assert_close( + RGB_to_HCL(xp_as_array([0.50000000, 1.00000000, 2.00000000], xp=xp)), + [-2.40349351, 1.0075282, 1.00941024], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_HCL(self) -> None: + def test_n_dimensional_RGB_to_HCL(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.RGB_to_HCL` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HCL = RGB_to_HCL(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HCL = as_ndarray(RGB_to_HCL(RGB)) - RGB = np.tile(RGB, (6, 1)) - HCL = np.tile(HCL, (6, 1)) - np.testing.assert_allclose(RGB_to_HCL(RGB), HCL, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + HCL = xp.tile(xp_as_array(HCL, xp=xp), (6, 1)) + xp_assert_close(RGB_to_HCL(RGB), HCL, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - HCL = np.reshape(HCL, (2, 3, 3)) - np.testing.assert_allclose(RGB_to_HCL(RGB), HCL, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + HCL = xp_reshape(xp_as_array(HCL, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(RGB_to_HCL(RGB), HCL, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_HCL(self) -> None: + def test_domain_range_scale_RGB_to_HCL(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.RGB_to_HCL` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HCL = RGB_to_HCL(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HCL = as_ndarray(RGB_to_HCL(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_HCL(RGB * factor), HCL * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -440,63 +452,63 @@ class TestHCL_to_RGB: tests methods. """ - def test_HCL_to_RGB(self) -> None: + def test_HCL_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.cylindrical.HCL_to_RGB` definition.""" - np.testing.assert_allclose( - HCL_to_RGB(np.array([-0.03167854, 0.28417150, 0.22859647])), - np.array([0.45620333, 0.03081048, 0.04091925]), + xp_assert_close( + HCL_to_RGB(xp_as_array([-0.03167854, 0.28417150, 0.22859647], xp=xp)), + [0.45620333, 0.03081048, 0.04091925], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HCL_to_RGB(np.array([1.00000000, 2.00000000, 0.50000000])), - np.array([0.92186029, 0.71091922, -2.26364935]), + xp_assert_close( + HCL_to_RGB(xp_as_array([1.00000000, 2.00000000, 0.50000000], xp=xp)), + [0.92186029, 0.71091922, -2.26364935], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HCL_to_RGB(np.array([2.00000000, 1.00000000, 0.50000000])), - np.array([-0.31368585, 1.00732462, -0.51534497]), + xp_assert_close( + HCL_to_RGB(xp_as_array([2.00000000, 1.00000000, 0.50000000], xp=xp)), + [-0.31368585, 1.00732462, -0.51534497], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - HCL_to_RGB(np.array([0.50000000, 1.00000000, 2.00000000])), - np.array([3.88095422, 3.11881934, 2.40881719]), + xp_assert_close( + HCL_to_RGB(xp_as_array([0.50000000, 1.00000000, 2.00000000], xp=xp)), + [3.88095422, 3.11881934, 2.40881719], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_HCL_to_RGB(self) -> None: + def test_n_dimensional_HCL_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.HCL_to_RGB` definition n-dimensional arrays support. """ - HCL = np.array([0.99603944, 0.87347144, 0.24350795]) - RGB = HCL_to_RGB(HCL) + HCL = xp_as_array([0.99603944, 0.87347144, 0.24350795], xp=xp) + RGB = as_ndarray(HCL_to_RGB(HCL)) - HCL = np.tile(HCL, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose(HCL_to_RGB(HCL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HCL = xp.tile(xp_as_array(HCL, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(HCL_to_RGB(HCL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - HCL = np.reshape(HCL, (2, 3, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose(HCL_to_RGB(HCL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HCL = xp_reshape(xp_as_array(HCL, xp=xp), (2, 3, 3), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(HCL_to_RGB(HCL), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_HCL_to_RGB(self) -> None: + def test_domain_range_scale_HCL_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.cylindrical.HCL_to_RGB` definition domain and range scale support. """ - HCL = np.array([0.99603944, 0.87347144, 0.24350795]) - RGB = HCL_to_RGB(HCL) + HCL = xp_as_array([0.99603944, 0.87347144, 0.24350795], xp=xp) + RGB = as_ndarray(HCL_to_RGB(HCL)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( HCL_to_RGB(HCL * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/tests/test_derivation.py b/colour/models/rgb/tests/test_derivation.py index 5a50b6abd5..bbefe7fc65 100644 --- a/colour/models/rgb/tests/test_derivation.py +++ b/colour/models/rgb/tests/test_derivation.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import contextlib import re from itertools import product @@ -18,7 +23,13 @@ primaries_whitepoint, ) from colour.models.rgb.derivation import xy_to_z -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -43,46 +54,46 @@ class Testxy_to_z: tests methods. """ - def test_xy_to_z(self) -> None: + def test_xy_to_z(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.derivation.xy_to_z` definition.""" - np.testing.assert_allclose( - xy_to_z(np.array([0.2500, 0.2500])), + xp_assert_close( + xy_to_z(xp_as_array([0.2500, 0.2500], xp=xp)), 0.50000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_z(np.array([0.0001, -0.0770])), + xp_assert_close( + xy_to_z(xp_as_array([0.0001, -0.0770], xp=xp)), 1.07690000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_z(np.array([0.0000, 1.0000])), + xp_assert_close( + xy_to_z(xp_as_array([0.0000, 1.0000], xp=xp)), 0.00000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_z(self) -> None: + def test_n_dimensional_xy_to_z(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.xy_to_z` definition n-dimensional arrays support. """ - xy = np.array([0.25, 0.25]) - z = xy_to_z(xy) + xy = xp_as_array([0.25, 0.25], xp=xp) + z = as_ndarray(xy_to_z(xy)) - xy = np.tile(xy, (6, 1)) - z = np.tile( - z, - 6, + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + z = xp.tile( + xp_as_array(z, xp=xp), + (6,), ) - np.testing.assert_allclose(xy_to_z(xy), z, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xy_to_z(xy), z, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - z = np.reshape(z, (2, 3)) - np.testing.assert_allclose(xy_to_z(xy), z, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + z = xp_reshape(xp_as_array(z, xp=xp), (2, 3), xp=xp) + xp_assert_close(xy_to_z(xy), z, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_z(self) -> None: @@ -102,39 +113,37 @@ class TestNormalisedPrimaryMatrix: definition unit tests methods. """ - def test_normalised_primary_matrix(self) -> None: + def test_normalised_primary_matrix(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.normalised_primary_matrix` definition. """ - np.testing.assert_allclose( + xp_assert_close( normalised_primary_matrix( - np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]), - np.array([0.32168, 0.33767]), - ), - np.array( - [ - [0.95255240, 0.00000000, 0.00009368], - [0.34396645, 0.72816610, -0.07213255], - [0.00000000, 0.00000000, 1.00882518], - ] + xp_as_array( + [0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700], xp=xp + ), + xp_as_array([0.32168, 0.33767], xp=xp), ), + [ + [0.95255240, 0.00000000, 0.00009368], + [0.34396645, 0.72816610, -0.07213255], + [0.00000000, 0.00000000, 1.00882518], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalised_primary_matrix( - np.array([0.640, 0.330, 0.300, 0.600, 0.150, 0.060]), - np.array([0.3127, 0.3290]), - ), - np.array( - [ - [0.41239080, 0.35758434, 0.18048079], - [0.21263901, 0.71516868, 0.07219232], - [0.01933082, 0.11919478, 0.95053215], - ] + xp_as_array([0.640, 0.330, 0.300, 0.600, 0.150, 0.060], xp=xp), + xp_as_array([0.3127, 0.3290], xp=xp), ), + [ + [0.41239080, 0.35758434, 0.18048079], + [0.21263901, 0.71516868, 0.07219232], + [0.01933082, 0.11919478, 0.95053215], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -160,58 +169,54 @@ class TestChromaticallyAdaptedPrimaries: chromatically_adapted_primaries` definition unit tests methods. """ - def test_chromatically_adapted_primaries(self) -> None: + def test_chromatically_adapted_primaries(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.\ chromatically_adapted_primaries` definition. """ - np.testing.assert_allclose( + xp_assert_close( chromatically_adapted_primaries( - np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]), - np.array([0.32168, 0.33767]), - np.array([0.34570, 0.35850]), - ), - np.array( - [ - [0.73431182, 0.26694964], - [0.02211963, 0.98038009], - [-0.05880375, -0.12573056], - ] + xp_as_array( + [0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700], xp=xp + ), + xp_as_array([0.32168, 0.33767], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), + [ + [0.73431182, 0.26694964], + [0.02211963, 0.98038009], + [-0.05880375, -0.12573056], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatically_adapted_primaries( - np.array([0.640, 0.330, 0.300, 0.600, 0.150, 0.060]), - np.array([0.31270, 0.32900]), - np.array([0.34570, 0.35850]), - ), - np.array( - [ - [0.64922534, 0.33062196], - [0.32425276, 0.60237128], - [0.15236177, 0.06118676], - ] + xp_as_array([0.640, 0.330, 0.300, 0.600, 0.150, 0.060], xp=xp), + xp_as_array([0.31270, 0.32900], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), + [ + [0.64922534, 0.33062196], + [0.32425276, 0.60237128], + [0.15236177, 0.06118676], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( chromatically_adapted_primaries( - np.array([0.640, 0.330, 0.300, 0.600, 0.150, 0.060]), - np.array([0.31270, 0.32900]), - np.array([0.34570, 0.35850]), + xp_as_array([0.640, 0.330, 0.300, 0.600, 0.150, 0.060], xp=xp), + xp_as_array([0.31270, 0.32900], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), "Bradford", ), - np.array( - [ - [0.64844144, 0.33085331], - [0.32119518, 0.59784434], - [0.15589322, 0.06604921], - ] - ), + [ + [0.64844144, 0.33085331], + [0.32119518, 0.59784434], + [0.15589322, 0.06604921], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -236,59 +241,59 @@ class TestPrimariesWhitepoint: definition unit tests methods. """ - def test_primaries_whitepoint(self) -> None: + def test_primaries_whitepoint(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.primaries_whitepoint` definition. """ P, W = primaries_whitepoint( - np.array( + xp_as_array( [ [0.95255240, 0.00000000, 0.00009368], [0.34396645, 0.72816610, -0.07213255], [0.00000000, 0.00000000, 1.00882518], - ] + ], + xp=xp, ) ) - np.testing.assert_allclose( + xp_assert_close( P, - np.array( - [ - [0.73470, 0.26530], - [0.00000, 1.00000], - [0.00010, -0.07700], - ] - ), + [ + [0.73470, 0.26530], + [0.00000, 1.00000], + [0.00010, -0.07700], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - W, np.array([0.32168, 0.33767]), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + W, + [0.32168, 0.33767], + atol=TOLERANCE_ABSOLUTE_TESTS, ) P, W = primaries_whitepoint( - np.array( + xp_as_array( [ [0.41240000, 0.35760000, 0.18050000], [0.21260000, 0.71520000, 0.07220000], [0.01930000, 0.11920000, 0.95050000], - ] + ], + xp=xp, ) ) - np.testing.assert_allclose( + xp_assert_close( P, - np.array( - [ - [0.64007450, 0.32997051], - [0.30000000, 0.60000000], - [0.15001662, 0.06000665], - ] - ), + [ + [0.64007450, 0.32997051], + [0.30000000, 0.60000000], + [0.15001662, 0.06000665], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( W, - np.array([0.31271591, 0.32900148]), + [0.31271591, 0.32900148], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -312,7 +317,7 @@ class TestRGBLuminanceEquation: definition unit tests methods. """ - def test_RGB_luminance_equation(self) -> None: + def test_RGB_luminance_equation(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.RGB_luminance_equation` definition. @@ -320,8 +325,10 @@ def test_RGB_luminance_equation(self) -> None: assert isinstance( RGB_luminance_equation( - np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]), - np.array([0.32168, 0.33767]), + xp_as_array( + [0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700], xp=xp + ), + xp_as_array([0.32168, 0.33767], xp=xp), ), str, ) @@ -333,10 +340,10 @@ def test_RGB_luminance_equation(self) -> None: "\\(G\\)\\s?[+-]\\s?[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?." "\\(B\\)" ) - P = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]) + P = xp_as_array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700], xp=xp) assert re.match( pattern, - RGB_luminance_equation(P, np.array([0.32168, 0.33767])), + RGB_luminance_equation(P, xp_as_array([0.32168, 0.33767], xp=xp)), ) @@ -346,64 +353,66 @@ class TestRGBLuminance: unit tests methods. """ - def test_RGB_luminance(self) -> None: + def test_RGB_luminance(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.RGB_luminance` definition. """ - np.testing.assert_allclose( + xp_assert_close( RGB_luminance( - np.array([0.18, 0.18, 0.18]), - np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]), - np.array([0.32168, 0.33767]), + xp_as_array([0.18, 0.18, 0.18], xp=xp), + xp_as_array( + [0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700], xp=xp + ), + xp_as_array([0.32168, 0.33767], xp=xp), ), 0.18000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_luminance( - np.array([0.21959402, 0.06986677, 0.04703877]), - np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]), - np.array([0.32168, 0.33767]), + xp_as_array([0.21959402, 0.06986677, 0.04703877], xp=xp), + xp_as_array( + [0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700], xp=xp + ), + xp_as_array([0.32168, 0.33767], xp=xp), ), 0.123014562384318, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_luminance( - np.array([0.45620519, 0.03081071, 0.04091952]), - np.array([0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600]), - np.array([0.31270, 0.32900]), + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), + xp_as_array([0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600], xp=xp), + xp_as_array([0.31270, 0.32900], xp=xp), ), 0.121995947729870, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_luminance(self) -> None: + def test_n_dimensional_RGB_luminance(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.derivation.RGB_luminance` definition n_dimensional arrays support. """ - RGB = (np.array([0.18, 0.18, 0.18]),) - P = (np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]),) - W = np.array([0.32168, 0.33767]) - Y = RGB_luminance(RGB, P, W) - - RGB = np.tile(RGB, (6, 1)) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - RGB_luminance(RGB, P, W), Y, atol=TOLERANCE_ABSOLUTE_TESTS + RGB = xp_as_array([[0.18, 0.18, 0.18]], xp=xp) + P = xp_as_array( + [[0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]], xp=xp ) + W = xp_as_array([0.32168, 0.33767], xp=xp) + Y = as_ndarray(RGB_luminance(RGB, P, W)) - RGB = np.reshape(RGB, (2, 3, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - RGB_luminance(RGB, P, W), Y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(RGB, (6, 1)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close(RGB_luminance(RGB, P, W), Y, atol=TOLERANCE_ABSOLUTE_TESTS) + + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close(RGB_luminance(RGB, P, W), Y, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_RGB_luminance(self) -> None: diff --git a/colour/models/rgb/tests/test_hanbury2003.py b/colour/models/rgb/tests/test_hanbury2003.py index 7490cb9a2c..06ced132e0 100644 --- a/colour/models/rgb/tests/test_hanbury2003.py +++ b/colour/models/rgb/tests/test_hanbury2003.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb import IHLS_to_RGB, RGB_to_IHLS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,57 +41,57 @@ class TestRGB_to_IHLS: tests methods. """ - def test_RGB_to_IHLS(self) -> None: + def test_RGB_to_IHLS(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.hanbury2003.RGB_to_IHLS` definition.""" - np.testing.assert_allclose( - RGB_to_IHLS(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([6.26236117, 0.12197943, 0.42539448]), + xp_assert_close( + RGB_to_IHLS(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [6.26236117, 0.12197943, 0.42539448], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_IHLS(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + RGB_to_IHLS(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_IHLS(np.array([1.00000000, 1.00000000, 1.00000000])), - np.array([0.00000000, 1.00000000, 0.00000000]), + xp_assert_close( + RGB_to_IHLS(xp_as_array([1.00000000, 1.00000000, 1.00000000], xp=xp)), + [0.00000000, 1.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_IHLS(self) -> None: + def test_n_dimensional_RGB_to_IHLS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.hanbury2003.RGB_to_IHLS` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HYS = RGB_to_IHLS(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HYS = as_ndarray(RGB_to_IHLS(RGB)) - RGB = np.tile(RGB, (6, 1)) - HYS = np.tile(HYS, (6, 1)) - np.testing.assert_allclose(RGB_to_IHLS(RGB), HYS, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + HYS = xp.tile(xp_as_array(HYS, xp=xp), (6, 1)) + xp_assert_close(RGB_to_IHLS(RGB), HYS, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - HYS = np.reshape(HYS, (2, 3, 3)) - np.testing.assert_allclose(RGB_to_IHLS(RGB), HYS, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + HYS = xp_reshape(xp_as_array(HYS, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(RGB_to_IHLS(RGB), HYS, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_IHLS(self) -> None: + def test_domain_range_scale_RGB_to_IHLS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.hanbury2003.RGB_to_IHLS` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - HYS = RGB_to_IHLS(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + HYS = as_ndarray(RGB_to_IHLS(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_IHLS(RGB * factor), HYS * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -103,57 +115,57 @@ class TestIHLS_to_RGB: tests methods. """ - def test_IHLS_to_RGB(self) -> None: + def test_IHLS_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.hanbury2003.IHLS_to_RGB` definition.""" - np.testing.assert_allclose( - IHLS_to_RGB(np.array([6.26236117, 0.12197943, 0.42539448])), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + IHLS_to_RGB(xp_as_array([6.26236117, 0.12197943, 0.42539448], xp=xp)), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IHLS_to_RGB(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + IHLS_to_RGB(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IHLS_to_RGB(np.array([0.00000000, 1.00000000, 0.00000000])), - np.array([1.00000000, 1.00000000, 1.00000000]), + xp_assert_close( + IHLS_to_RGB(xp_as_array([0.00000000, 1.00000000, 0.00000000], xp=xp)), + [1.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_IHLS_to_RGB(self) -> None: + def test_n_dimensional_IHLS_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.hanbury2003.IHLS_to_RGB` definition n-dimensional arrays support. """ - HYS = np.array([6.26236117, 0.12197943, 0.42539448]) - RGB = IHLS_to_RGB(HYS) + HYS = xp_as_array([6.26236117, 0.12197943, 0.42539448], xp=xp) + RGB = as_ndarray(IHLS_to_RGB(HYS)) - HYS = np.tile(HYS, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose(IHLS_to_RGB(HYS), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HYS = xp.tile(xp_as_array(HYS, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(IHLS_to_RGB(HYS), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - HYS = np.reshape(HYS, (2, 3, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose(IHLS_to_RGB(HYS), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + HYS = xp_reshape(xp_as_array(HYS, xp=xp), (2, 3, 3), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(IHLS_to_RGB(HYS), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_IHLS_to_RGB(self) -> None: + def test_domain_range_scale_IHLS_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.hanbury2003.IHLS_to_RGB` definition domain and range scale support. """ - HYS = np.array([6.26236117, 0.12197943, 0.42539448]) - RGB = IHLS_to_RGB(HYS) + HYS = xp_as_array([6.26236117, 0.12197943, 0.42539448], xp=xp) + RGB = as_ndarray(IHLS_to_RGB(HYS)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( IHLS_to_RGB(HYS * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/tests/test_ictcp.py b/colour/models/rgb/tests/test_ictcp.py index 572a8c0431..8f51897595 100644 --- a/colour/models/rgb/tests/test_ictcp.py +++ b/colour/models/rgb/tests/test_ictcp.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb import ICtCp_to_RGB, ICtCp_to_XYZ, RGB_to_ICtCp, XYZ_to_ICtCp -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,97 +43,97 @@ class TestRGB_to_ICtCp: tests methods. """ - def test_RGB_to_ICtCp(self) -> None: + def test_RGB_to_ICtCp(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ictcp.RGB_to_ICtCp` definition.""" - np.testing.assert_allclose( - RGB_to_ICtCp(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([0.07351364, 0.00475253, 0.09351596]), + xp_assert_close( + RGB_to_ICtCp(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [0.07351364, 0.00475253, 0.09351596], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_ICtCp(np.array([0.45620519, 0.03081071, 0.04091952]), L_p=4000), - np.array([0.10516931, 0.00514031, 0.12318730]), + xp_assert_close( + RGB_to_ICtCp( + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), L_p=4000 + ), + [0.10516931, 0.00514031, 0.12318730], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_ICtCp(np.array([0.45620519, 0.03081071, 0.04091952]), L_p=1000), - np.array([0.17079612, 0.00485580, 0.17431356]), + xp_assert_close( + RGB_to_ICtCp( + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), L_p=1000 + ), + [0.17079612, 0.00485580, 0.17431356], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_ICtCp( - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), method="ITU-R BT.2100-1 PQ", ), - np.array([0.07351364, 0.00475253, 0.09351596]), + [0.07351364, 0.00475253, 0.09351596], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_ICtCp( - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), method="ITU-R BT.2100-2 PQ", ), - np.array([0.07351364, 0.00475253, 0.09351596]), + [0.07351364, 0.00475253, 0.09351596], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_ICtCp( - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), method="ITU-R BT.2100-1 HLG", ), - np.array([0.62567899, -0.03622422, 0.67786522]), + [0.62567899, -0.03622422, 0.67786522], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_ICtCp( - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp), method="ITU-R BT.2100-2 HLG", ), - np.array([0.62567899, -0.01984490, 0.35911259]), + [0.62567899, -0.01984490, 0.35911259], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_ICtCp(self) -> None: + def test_n_dimensional_RGB_to_ICtCp(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.RGB_to_ICtCp` definition n-dimensional support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - ICtCp = RGB_to_ICtCp(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + ICtCp = as_ndarray(RGB_to_ICtCp(RGB)) - RGB = np.tile(RGB, (6, 1)) - ICtCp = np.tile(ICtCp, (6, 1)) - np.testing.assert_allclose( - RGB_to_ICtCp(RGB), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + ICtCp = xp.tile(xp_as_array(ICtCp, xp=xp), (6, 1)) + xp_assert_close(RGB_to_ICtCp(RGB), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - ICtCp = np.reshape(ICtCp, (2, 3, 3)) - np.testing.assert_allclose( - RGB_to_ICtCp(RGB), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + ICtCp = xp_reshape(xp_as_array(ICtCp, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(RGB_to_ICtCp(RGB), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_ICtCp(self) -> None: + def test_domain_range_scale_RGB_to_ICtCp(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.RGB_to_ICtCp` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - ICtCp = RGB_to_ICtCp(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + ICtCp = as_ndarray(RGB_to_ICtCp(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_ICtCp(RGB * factor), ICtCp * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -145,97 +157,97 @@ class TestICtCp_to_RGB: methods. """ - def test_ICtCp_to_RGB(self) -> None: + def test_ICtCp_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ictcp.ICtCp_to_RGB` definition.""" - np.testing.assert_allclose( - ICtCp_to_RGB(np.array([0.07351364, 0.00475253, 0.09351596])), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + ICtCp_to_RGB(xp_as_array([0.07351364, 0.00475253, 0.09351596], xp=xp)), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICtCp_to_RGB(np.array([0.10516931, 0.00514031, 0.12318730]), L_p=4000), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + ICtCp_to_RGB( + xp_as_array([0.10516931, 0.00514031, 0.12318730], xp=xp), L_p=4000 + ), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICtCp_to_RGB(np.array([0.17079612, 0.00485580, 0.17431356]), L_p=1000), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + ICtCp_to_RGB( + xp_as_array([0.17079612, 0.00485580, 0.17431356], xp=xp), L_p=1000 + ), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_RGB( - np.array([0.07351364, 0.00475253, 0.09351596]), + xp_as_array([0.07351364, 0.00475253, 0.09351596], xp=xp), method="ITU-R BT.2100-1 PQ", ), - np.array([0.45620519, 0.03081071, 0.04091952]), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_RGB( - np.array([0.07351364, 0.00475253, 0.09351596]), + xp_as_array([0.07351364, 0.00475253, 0.09351596], xp=xp), method="ITU-R BT.2100-2 PQ", ), - np.array([0.45620519, 0.03081071, 0.04091952]), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_RGB( - np.array([0.62567899, -0.03622422, 0.67786522]), + xp_as_array([0.62567899, -0.03622422, 0.67786522], xp=xp), method="ITU-R BT.2100-1 HLG", ), - np.array([0.45620519, 0.03081071, 0.04091952]), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_RGB( - np.array([0.62567899, -0.01984490, 0.35911259]), + xp_as_array([0.62567899, -0.01984490, 0.35911259], xp=xp), method="ITU-R BT.2100-2 HLG", ), - np.array([0.45620519, 0.03081071, 0.04091952]), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ICtCp_to_RGB(self) -> None: + def test_n_dimensional_ICtCp_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.ICtCp_to_RGB` definition n-dimensional support. """ - ICtCp = np.array([0.07351364, 0.00475253, 0.09351596]) - RGB = ICtCp_to_RGB(ICtCp) + ICtCp = xp_as_array([0.07351364, 0.00475253, 0.09351596], xp=xp) + RGB = as_ndarray(ICtCp_to_RGB(ICtCp)) - ICtCp = np.tile(ICtCp, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose( - ICtCp_to_RGB(ICtCp), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICtCp = xp.tile(xp_as_array(ICtCp, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(ICtCp_to_RGB(ICtCp), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - ICtCp = np.reshape(ICtCp, (2, 3, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose( - ICtCp_to_RGB(ICtCp), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICtCp = xp_reshape(xp_as_array(ICtCp, xp=xp), (2, 3, 3), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(ICtCp_to_RGB(ICtCp), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ICtCp_to_RGB(self) -> None: + def test_domain_range_scale_ICtCp_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.ICtCp_to_RGB` definition domain and range scale support. """ - ICtCp = np.array([0.07351364, 0.00475253, 0.09351596]) - RGB = ICtCp_to_RGB(ICtCp) + ICtCp = xp_as_array([0.07351364, 0.00475253, 0.09351596], xp=xp) + RGB = as_ndarray(ICtCp_to_RGB(ICtCp)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_RGB(ICtCp * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -259,116 +271,116 @@ class TestXYZ_to_ICtCp: tests methods. """ - def test_XYZ_to_ICtCp(self) -> None: + def test_XYZ_to_ICtCp(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ictcp.XYZ_to_ICtCp` definition.""" - np.testing.assert_allclose( - XYZ_to_ICtCp(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.06858097, -0.00283842, 0.06020983]), + xp_assert_close( + XYZ_to_ICtCp(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.06858097, -0.00283842, 0.06020983], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + [0.34570, 0.35850], ), - np.array([0.06792437, 0.00452089, 0.05514480]), + [0.06792437, 0.00452089, 0.05514480], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + [0.34570, 0.35850], chromatic_adaptation_transform="Bradford", ), - np.array([0.06783951, 0.00476111, 0.05523093]), + [0.06783951, 0.00476111, 0.05523093], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ICtCp(np.array([0.20654008, 0.12197225, 0.05136952]), L_p=4000), - np.array([0.09871102, -0.00447247, 0.07984812]), + xp_assert_close( + XYZ_to_ICtCp( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), L_p=4000 + ), + [0.09871102, -0.00447247, 0.07984812], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ICtCp(np.array([0.20654008, 0.12197225, 0.05136952]), L_p=1000), - np.array([0.16173872, -0.00792543, 0.11409458]), + xp_assert_close( + XYZ_to_ICtCp( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), L_p=1000 + ), + [0.16173872, -0.00792543, 0.11409458], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="ITU-R BT.2100-1 PQ", ), - np.array([0.06858097, -0.00283842, 0.06020983]), + [0.06858097, -0.00283842, 0.06020983], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="ITU-R BT.2100-2 PQ", ), - np.array([0.06858097, -0.00283842, 0.06020983]), + [0.06858097, -0.00283842, 0.06020983], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="ITU-R BT.2100-1 HLG", ), - np.array([0.59242792, -0.06824263, 0.47421473]), + [0.59242792, -0.06824263, 0.47421473], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="ITU-R BT.2100-2 HLG", ), - np.array([0.59242792, -0.03740730, 0.25122675]), + [0.59242792, -0.03740730, 0.25122675], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_ICtCp(self) -> None: + def test_n_dimensional_XYZ_to_ICtCp(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.XYZ_to_ICtCp` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - ICtCp = XYZ_to_ICtCp(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + ICtCp = as_ndarray(XYZ_to_ICtCp(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - ICtCp = np.tile(ICtCp, (6, 1)) - np.testing.assert_allclose( - XYZ_to_ICtCp(XYZ), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + ICtCp = xp.tile(xp_as_array(ICtCp, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_ICtCp(XYZ), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - ICtCp = np.reshape(ICtCp, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_ICtCp(XYZ), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + ICtCp = xp_reshape(xp_as_array(ICtCp, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_ICtCp(XYZ), ICtCp, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_ICtCp(self) -> None: + def test_domain_range_scale_XYZ_to_ICtCp(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.XYZ_to_ICtCp` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - ICtCp = XYZ_to_ICtCp(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + ICtCp = as_ndarray(XYZ_to_ICtCp(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICtCp(XYZ * factor), ICtCp * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -392,116 +404,116 @@ class TestICtCp_to_XYZ: methods. """ - def test_ICtCp_to_XYZ(self) -> None: + def test_ICtCp_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ictcp.ICtCp_to_XYZ` definition.""" - np.testing.assert_allclose( - ICtCp_to_XYZ(np.array([0.06858097, -0.00283842, 0.06020983])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + ICtCp_to_XYZ(xp_as_array([0.06858097, -0.00283842, 0.06020983], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ( - np.array([0.06792437, 0.00452089, 0.05514480]), - np.array([0.34570, 0.35850]), + xp_as_array([0.06792437, 0.00452089, 0.05514480], xp=xp), + [0.34570, 0.35850], ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ( - np.array([0.06783951, 0.00476111, 0.05523093]), - np.array([0.34570, 0.35850]), + xp_as_array([0.06783951, 0.00476111, 0.05523093], xp=xp), + [0.34570, 0.35850], chromatic_adaptation_transform="Bradford", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICtCp_to_XYZ(np.array([0.09871102, -0.00447247, 0.07984812]), L_p=4000), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + ICtCp_to_XYZ( + xp_as_array([0.09871102, -0.00447247, 0.07984812], xp=xp), L_p=4000 + ), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICtCp_to_XYZ(np.array([0.16173872, -0.00792543, 0.11409458]), L_p=1000), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + ICtCp_to_XYZ( + xp_as_array([0.16173872, -0.00792543, 0.11409458], xp=xp), L_p=1000 + ), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ( - np.array([0.06858097, -0.00283842, 0.06020983]), + xp_as_array([0.06858097, -0.00283842, 0.06020983], xp=xp), method="ITU-R BT.2100-1 PQ", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ( - np.array([0.06858097, -0.00283842, 0.06020983]), + xp_as_array([0.06858097, -0.00283842, 0.06020983], xp=xp), method="ITU-R BT.2100-2 PQ", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ( - np.array([0.59242792, -0.06824263, 0.47421473]), + xp_as_array([0.59242792, -0.06824263, 0.47421473], xp=xp), method="ITU-R BT.2100-1 HLG", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ( - np.array([0.59242792, -0.03740730, 0.25122675]), + xp_as_array([0.59242792, -0.03740730, 0.25122675], xp=xp), method="ITU-R BT.2100-2 HLG", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ICtCp_to_XYZ(self) -> None: + def test_n_dimensional_ICtCp_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.ICtCp_to_XYZ` definition n-dimensional support. """ - ICtCp = np.array([0.06858097, -0.00283842, 0.06020983]) - XYZ = ICtCp_to_XYZ(ICtCp) + ICtCp = xp_as_array([0.06858097, -0.00283842, 0.06020983], xp=xp) + XYZ = as_ndarray(ICtCp_to_XYZ(ICtCp)) - ICtCp = np.tile(ICtCp, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - ICtCp_to_XYZ(ICtCp), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICtCp = xp.tile(xp_as_array(ICtCp, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(ICtCp_to_XYZ(ICtCp), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - ICtCp = np.reshape(ICtCp, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - ICtCp_to_XYZ(ICtCp), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICtCp = xp_reshape(xp_as_array(ICtCp, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(ICtCp_to_XYZ(ICtCp), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ICtCp_to_XYZ(self) -> None: + def test_domain_range_scale_ICtCp_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ictcp.ICtCp_to_XYZ` definition domain and range scale support. """ - ICtCp = np.array([0.06858097, -0.00283842, 0.06020983]) - XYZ = ICtCp_to_XYZ(ICtCp) + ICtCp = xp_as_array([0.06858097, -0.00283842, 0.06020983], xp=xp) + XYZ = as_ndarray(ICtCp_to_XYZ(ICtCp)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( ICtCp_to_XYZ(ICtCp * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/tests/test_itut_h_273.py b/colour/models/rgb/tests/test_itut_h_273.py index 660fce04c9..4c3b808b9b 100644 --- a/colour/models/rgb/tests/test_itut_h_273.py +++ b/colour/models/rgb/tests/test_itut_h_273.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.models import ( describe_video_signal_colour_primaries, describe_video_signal_matrix_coefficients, @@ -45,7 +50,10 @@ class TestDescribeVideoSignalTransferCharacteristics: describe_video_signal_transfer_characteristics` definition unit tests methods. """ - def test_describe_video_signal_transfer_characteristics(self) -> None: + def test_describe_video_signal_transfer_characteristics( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.models.rgb.itut_h_273.\ describe_video_signal_transfer_characteristics` definition. diff --git a/colour/models/rgb/tests/test_prismatic.py b/colour/models/rgb/tests/test_prismatic.py index 339bb4baa5..a1a5531874 100644 --- a/colour/models/rgb/tests/test_prismatic.py +++ b/colour/models/rgb/tests/test_prismatic.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb import Prismatic_to_RGB, RGB_to_Prismatic -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,55 +41,51 @@ class TestRGB_to_Prismatic: unit tests methods. """ - def test_RGB_to_Prismatic(self) -> None: + def test_RGB_to_Prismatic(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.prismatic.RGB_to_Prismatic` definition.""" - np.testing.assert_allclose( - RGB_to_Prismatic(np.array([0.0, 0.0, 0.0])), - np.array([0.0, 0.0, 0.0, 0.0]), + xp_assert_close( + RGB_to_Prismatic(xp_as_array([0.0, 0.0, 0.0], xp=xp)), + [0.0, 0.0, 0.0, 0.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_Prismatic(np.array([0.25, 0.50, 0.75])), - np.array([0.7500000, 0.1666667, 0.3333333, 0.5000000]), + xp_assert_close( + RGB_to_Prismatic(xp_as_array([0.25, 0.50, 0.75], xp=xp)), + [0.7500000, 0.1666667, 0.3333333, 0.5000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_Prismatic(self) -> None: + def test_n_dimensional_RGB_to_Prismatic(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.RGB_to_Prismatic` definition n-dimensional support. """ - RGB = np.array([0.25, 0.50, 0.75]) - Lrgb = RGB_to_Prismatic(RGB) + RGB = xp_as_array([0.25, 0.50, 0.75], xp=xp) + Lrgb = as_ndarray(RGB_to_Prismatic(RGB)) - RGB = np.tile(RGB, (6, 1)) - Lrgb = np.tile(Lrgb, (6, 1)) - np.testing.assert_allclose( - RGB_to_Prismatic(RGB), Lrgb, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + Lrgb = xp.tile(xp_as_array(Lrgb, xp=xp), (6, 1)) + xp_assert_close(RGB_to_Prismatic(RGB), Lrgb, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.reshape(RGB, (2, 3, 3)) - Lrgb = np.reshape(Lrgb, (2, 3, 4)) - np.testing.assert_allclose( - RGB_to_Prismatic(RGB), Lrgb, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + Lrgb = xp_reshape(xp_as_array(Lrgb, xp=xp), (2, 3, 4), xp=xp) + xp_assert_close(RGB_to_Prismatic(RGB), Lrgb, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_Prismatic(self) -> None: + def test_domain_range_scale_RGB_to_Prismatic(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.RGB_to_Prismatic` definition domain and range scale support. """ - RGB = np.array([0.25, 0.50, 0.75]) - Lrgb = RGB_to_Prismatic(RGB) + RGB = xp_as_array([0.25, 0.50, 0.75], xp=xp) + Lrgb = as_ndarray(RGB_to_Prismatic(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_Prismatic(RGB * factor), Lrgb * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -101,55 +109,53 @@ class TestPrismatic_to_RGB: unit tests methods. """ - def test_Prismatic_to_RGB(self) -> None: + def test_Prismatic_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.prismatic.Prismatic_to_RGB` definition.""" - np.testing.assert_allclose( - Prismatic_to_RGB(np.array([0.0, 0.0, 0.0, 0.0])), - np.array([0.0, 0.0, 0.0]), + xp_assert_close( + Prismatic_to_RGB(xp_as_array([0.0, 0.0, 0.0, 0.0], xp=xp)), + [0.0, 0.0, 0.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Prismatic_to_RGB(np.array([0.7500000, 0.1666667, 0.3333333, 0.5000000])), - np.array([0.25, 0.50, 0.75]), + xp_assert_close( + Prismatic_to_RGB( + xp_as_array([0.7500000, 0.1666667, 0.3333333, 0.5000000], xp=xp) + ), + [0.25, 0.50, 0.75], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Prismatic_to_RGB(self) -> None: + def test_n_dimensional_Prismatic_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.Prismatic_to_RGB` definition n-dimensional support. """ - Lrgb = np.array([0.7500000, 0.1666667, 0.3333333, 0.5000000]) - RGB = Prismatic_to_RGB(Lrgb) + Lrgb = xp_as_array([0.7500000, 0.1666667, 0.3333333, 0.5000000], xp=xp) + RGB = as_ndarray(Prismatic_to_RGB(Lrgb)) - Lrgb = np.tile(Lrgb, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose( - Prismatic_to_RGB(Lrgb), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lrgb = xp.tile(xp_as_array(Lrgb, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close(Prismatic_to_RGB(Lrgb), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - Lrgb = np.reshape(Lrgb, (2, 3, 4)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose( - Prismatic_to_RGB(Lrgb), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lrgb = xp_reshape(xp_as_array(Lrgb, xp=xp), (2, 3, 4), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Prismatic_to_RGB(Lrgb), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Prismatic_to_RGB(self) -> None: + def test_domain_range_scale_Prismatic_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.Prismatic_to_RGB` definition domain and range scale support. """ - Lrgb = np.array([0.7500000, 0.1666667, 0.3333333, 0.5000000]) - RGB = Prismatic_to_RGB(Lrgb) + Lrgb = xp_as_array([0.7500000, 0.1666667, 0.3333333, 0.5000000], xp=xp) + RGB = as_ndarray(Prismatic_to_RGB(Lrgb)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Prismatic_to_RGB(Lrgb * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/tests/test_rgb_colourspace.py b/colour/models/rgb/tests/test_rgb_colourspace.py index cb43bc2d6a..060ff2882f 100644 --- a/colour/models/rgb/tests/test_rgb_colourspace.py +++ b/colour/models/rgb/tests/test_rgb_colourspace.py @@ -4,6 +4,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import re import textwrap from itertools import product @@ -26,7 +31,15 @@ matrix_RGB_to_RGB, normalised_primary_matrix, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -174,46 +187,34 @@ def test_use_derived_transformation_matrices(self) -> None: use_derived_transformation_matrices` method. """ - np.testing.assert_array_equal( - self._colourspace.matrix_RGB_to_XYZ, np.identity(3) - ) - np.testing.assert_array_equal( - self._colourspace.matrix_XYZ_to_RGB, np.identity(3) - ) + xp_assert_equal(self._colourspace.matrix_RGB_to_XYZ, np.identity(3)) + xp_assert_equal(self._colourspace.matrix_XYZ_to_RGB, np.identity(3)) self._colourspace.use_derived_transformation_matrices() - np.testing.assert_allclose( + xp_assert_close( self._colourspace.matrix_RGB_to_XYZ, - np.array( - [ - [0.95255240, 0.00000000, 0.00009368], - [0.34396645, 0.72816610, -0.07213255], - [0.00000000, 0.00000000, 1.00882518], - ] - ), + [ + [0.95255240, 0.00000000, 0.00009368], + [0.34396645, 0.72816610, -0.07213255], + [0.00000000, 0.00000000, 1.00882518], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._colourspace.matrix_XYZ_to_RGB, - np.array( - [ - [1.04981102, 0.00000000, -0.00009748], - [-0.49590302, 1.37331305, 0.09824004], - [0.00000000, 0.00000000, 0.99125202], - ] - ), + [ + [1.04981102, 0.00000000, -0.00009748], + [-0.49590302, 1.37331305, 0.09824004], + [0.00000000, 0.00000000, 0.99125202], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) self._colourspace.use_derived_matrix_RGB_to_XYZ = False - np.testing.assert_array_equal( - self._colourspace.matrix_RGB_to_XYZ, np.identity(3) - ) + xp_assert_equal(self._colourspace.matrix_RGB_to_XYZ, np.identity(3)) self._colourspace.use_derived_matrix_XYZ_to_RGB = False - np.testing.assert_array_equal( - self._colourspace.matrix_XYZ_to_RGB, np.identity(3) - ) + xp_assert_equal(self._colourspace.matrix_XYZ_to_RGB, np.identity(3)) def test_chromatically_adapt(self) -> None: """ @@ -226,24 +227,22 @@ def test_chromatically_adapt(self) -> None: whitepoint_t, "D50", "Bradford" ) - np.testing.assert_allclose( + xp_assert_close( colourspace.primaries, - np.array( - [ - [0.73485524, 0.26422533], - [-0.00617091, 1.01131496], - [0.01596756, -0.06423550], - ] - ), + [ + [0.73485524, 0.26422533], + [-0.00617091, 1.01131496], + [0.01596756, -0.06423550], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colourspace.whitepoint, whitepoint_t, atol=TOLERANCE_ABSOLUTE_TESTS ) assert colourspace.whitepoint_name == "D50" - np.testing.assert_allclose( + xp_assert_close( colourspace.primaries, chromatically_adapted_primaries( self._colourspace.primaries, @@ -254,13 +253,13 @@ def test_chromatically_adapt(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colourspace.matrix_RGB_to_XYZ, normalised_primary_matrix(colourspace.primaries, colourspace.whitepoint), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colourspace.matrix_XYZ_to_RGB, np.linalg.inv( normalised_primary_matrix(colourspace.primaries, colourspace.whitepoint) @@ -283,183 +282,189 @@ class TestXYZ_to_RGB: unit tests methods. """ - def test_XYZ_to_RGB(self) -> None: + def test_XYZ_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.XYZ_to_RGB` definition. """ - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), RGB_COLOURSPACE_sRGB, - np.array([0.34570, 0.35850]), + [0.34570, 0.35850], "Bradford", True, ), - np.array([0.70556403, 0.19112904, 0.22341005]), + [0.70556403, 0.19112904, 0.22341005], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), RGB_COLOURSPACE_sRGB, apply_cctf_encoding=True, ), - np.array([0.72794351, 0.18184112, 0.17951801]), + [0.72794351, 0.18184112, 0.17951801], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), RGB_COLOURSPACE_ACES2065_1, - np.array([0.34570, 0.35850]), + [0.34570, 0.35850], ), - np.array([0.21959099, 0.06985815, 0.04703704]), + [0.21959099, 0.06985815, 0.04703704], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), "sRGB", - np.array([0.34570, 0.35850]), + [0.34570, 0.35850], "Bradford", True, ), - XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), - RGB_COLOURSPACE_sRGB, - np.array([0.34570, 0.35850]), - "Bradford", - True, + as_ndarray( + XYZ_to_RGB( + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), + RGB_COLOURSPACE_sRGB, + [0.34570, 0.35850], + "Bradford", + True, + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) # TODO: Remove tests when dropping deprecated signature support. - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), - np.array([0.34570, 0.35850]), # pyright: ignore - np.array([0.31270, 0.32900]), - np.array( # pyright: ignore + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), + [0.34570, 0.35850], # pyright: ignore + [0.31270, 0.32900], + xp_as_array( # pyright: ignore [ [3.24062548, -1.53720797, -0.49862860], [-0.96893071, 1.87575606, 0.04151752], [0.05571012, -0.20402105, 1.05699594], - ] + ], + xp=xp, ), "Bradford", # pyright: ignore eotf_inverse_sRGB, ), - np.array([0.70556599, 0.19109268, 0.22340812]), + [0.70556599, 0.19109268, 0.22340812], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), - np.array([0.34570, 0.35850]), # pyright: ignore - np.array([0.31270, 0.32900]), - np.array( # pyright: ignore + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), + [0.34570, 0.35850], # pyright: ignore + [0.31270, 0.32900], + xp_as_array( # pyright: ignore [ [3.24062548, -1.53720797, -0.49862860], [-0.96893071, 1.87575606, 0.04151752], [0.05571012, -0.20402105, 1.05699594], - ] + ], + xp=xp, ), None, # pyright: ignore eotf_inverse_sRGB, ), - np.array([0.72794579, 0.18180021, 0.17951580]), + [0.72794579, 0.18180021, 0.17951580], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), - np.array([0.34570, 0.35850]), # pyright: ignore - np.array([0.32168, 0.33767]), - np.array( # pyright: ignore + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), + [0.34570, 0.35850], # pyright: ignore + [0.32168, 0.33767], + xp_as_array( # pyright: ignore [ [1.04981102, 0.00000000, -0.00009748], [-0.49590302, 1.37331305, 0.09824004], [0.00000000, 0.00000000, 0.99125202], - ] + ], + xp=xp, ), ), - np.array([0.21959099, 0.06985815, 0.04703704]), + [0.21959099, 0.06985815, 0.04703704], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB( - np.array([0.21638819, 0.12570000, 0.03847493]), - np.array([0.34570, 0.35850]), # pyright: ignore - np.array([0.31270, 0.32900, 1.00000]), - np.array( # pyright: ignore + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), + [0.34570, 0.35850], # pyright: ignore + [0.31270, 0.32900, 1.00000], + xp_as_array( # pyright: ignore [ [3.24062548, -1.53720797, -0.49862860], [-0.96893071, 1.87575606, 0.04151752], [0.05571012, -0.20402105, 1.05699594], - ] + ], + xp=xp, ), ), - np.array([0.45620801, 0.03079991, 0.04091883]), + [0.45620801, 0.03079991, 0.04091883], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_RGB(self) -> None: + def test_n_dimensional_XYZ_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.XYZ_to_RGB` definition n-dimensional support. """ - XYZ = np.array([0.21638819, 0.12570000, 0.03847493]) - W_R = np.array([0.34570, 0.35850]) - RGB = XYZ_to_RGB(XYZ, "sRGB", W_R, "Bradford", True) + XYZ = xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp) + W_R = xp_as_array([0.34570, 0.35850], xp=xp) + RGB = as_ndarray(XYZ_to_RGB(XYZ, "sRGB", W_R, "Bradford", True)) - XYZ = np.tile(XYZ, (6, 1)) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_RGB(XYZ, "sRGB", W_R, "Bradford", True), RGB, atol=TOLERANCE_ABSOLUTE_TESTS, ) - W_R = np.tile(W_R, (6, 1)) - np.testing.assert_allclose( + W_R = xp.tile(xp_as_array(W_R, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_RGB(XYZ, "sRGB", W_R, "Bradford", True), RGB, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - W_R = np.reshape(W_R, (2, 3, 2)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + W_R = xp_reshape(xp_as_array(W_R, xp=xp), (2, 3, 2), xp=xp) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_RGB(XYZ, "sRGB", W_R, "Bradford", True), RGB, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_RGB(self) -> None: + def test_domain_range_scale_XYZ_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.XYZ_to_RGB` definition domain and range scale support. """ - XYZ = np.array([0.21638819, 0.12570000, 0.03847493]) - W_R = np.array([0.34570, 0.35850]) - RGB = XYZ_to_RGB(XYZ, "sRGB", W_R) + XYZ = xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp) + W_R = xp_as_array([0.34570, 0.35850], xp=xp) + RGB = as_ndarray(XYZ_to_RGB(XYZ, "sRGB", W_R)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_RGB(XYZ * factor, "sRGB", W_R), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -483,183 +488,189 @@ class TestRGB_to_XYZ: unit tests methods. """ - def test_RGB_to_XYZ(self) -> None: + def test_RGB_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.RGB_to_XYZ` definition. """ - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.70556403, 0.19112904, 0.22341005]), + xp_as_array([0.70556403, 0.19112904, 0.22341005], xp=xp), RGB_COLOURSPACE_sRGB, - np.array([0.34570, 0.35850]), + [0.34570, 0.35850], "Bradford", True, ), - np.array([0.21639121, 0.12570714, 0.03847642]), + [0.21639121, 0.12570714, 0.03847642], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.72794351, 0.18184112, 0.17951801]), + xp_as_array([0.72794351, 0.18184112, 0.17951801], xp=xp), RGB_COLOURSPACE_sRGB, apply_cctf_decoding=True, ), - np.array([0.21639100, 0.12570754, 0.03847682]), + [0.21639100, 0.12570754, 0.03847682], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.21959099, 0.06985815, 0.04703704]), + xp_as_array([0.21959099, 0.06985815, 0.04703704], xp=xp), RGB_COLOURSPACE_ACES2065_1, - np.array([0.34570, 0.35850]), + [0.34570, 0.35850], ), - np.array([0.21638819, 0.12570000, 0.03847493]), + [0.21638819, 0.12570000, 0.03847493], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.21638819, 0.12570000, 0.03847493]), + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), "sRGB", - np.array([0.34570, 0.35850]), + [0.34570, 0.35850], "Bradford", True, ), - RGB_to_XYZ( - np.array([0.21638819, 0.12570000, 0.03847493]), - RGB_COLOURSPACE_sRGB, - np.array([0.34570, 0.35850]), - "Bradford", - True, + as_ndarray( + RGB_to_XYZ( + xp_as_array([0.21638819, 0.12570000, 0.03847493], xp=xp), + RGB_COLOURSPACE_sRGB, + [0.34570, 0.35850], + "Bradford", + True, + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) # TODO: Remove tests when dropping deprecated signature support. - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.70556599, 0.19109268, 0.22340812]), - np.array([0.31270, 0.32900]), # pyright: ignore - np.array([0.34570, 0.35850]), - np.array( # pyright: ignore + xp_as_array([0.70556599, 0.19109268, 0.22340812], xp=xp), + [0.31270, 0.32900], # pyright: ignore + [0.34570, 0.35850], + xp_as_array( # pyright: ignore [ [0.41240000, 0.35760000, 0.18050000], [0.21260000, 0.71520000, 0.07220000], [0.01930000, 0.11920000, 0.95050000], - ] + ], + xp=xp, ), "Bradford", # pyright: ignore eotf_sRGB, ), - np.array([0.21638819, 0.12570000, 0.03847493]), + [0.21638819, 0.12570000, 0.03847493], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.72794579, 0.18180021, 0.17951580]), - np.array([0.31270, 0.32900]), # pyright: ignore - np.array([0.34570, 0.35850]), - np.array( # pyright: ignore + xp_as_array([0.72794579, 0.18180021, 0.17951580], xp=xp), + [0.31270, 0.32900], # pyright: ignore + [0.34570, 0.35850], + xp_as_array( # pyright: ignore [ [0.41240000, 0.35760000, 0.18050000], [0.21260000, 0.71520000, 0.07220000], [0.01930000, 0.11920000, 0.95050000], - ] + ], + xp=xp, ), None, # pyright: ignore eotf_sRGB, ), - np.array([0.21638819, 0.12570000, 0.03847493]), + [0.21638819, 0.12570000, 0.03847493], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.21959099, 0.06985815, 0.04703704]), - np.array([0.32168, 0.33767]), # pyright: ignore - np.array([0.34570, 0.35850]), - np.array( # pyright: ignore + xp_as_array([0.21959099, 0.06985815, 0.04703704], xp=xp), + [0.32168, 0.33767], # pyright: ignore + [0.34570, 0.35850], + xp_as_array( # pyright: ignore [ [0.95255240, 0.00000000, 0.00009368], [0.34396645, 0.72816610, -0.07213255], [0.00000000, 0.00000000, 1.00882518], - ] + ], + xp=xp, ), ), - np.array([0.21638819, 0.12570000, 0.03847493]), + [0.21638819, 0.12570000, 0.03847493], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ( - np.array([0.45620801, 0.03079991, 0.04091883]), - np.array([0.31270, 0.32900, 1.00000]), # pyright: ignore - np.array([0.34570, 0.35850]), - np.array( # pyright: ignore + xp_as_array([0.45620801, 0.03079991, 0.04091883], xp=xp), + [0.31270, 0.32900, 1.00000], # pyright: ignore + [0.34570, 0.35850], + xp_as_array( # pyright: ignore [ [0.41240000, 0.35760000, 0.18050000], [0.21260000, 0.71520000, 0.07220000], [0.01930000, 0.11920000, 0.95050000], - ] + ], + xp=xp, ), ), - np.array([0.21638819, 0.12570000, 0.03847493]), + [0.21638819, 0.12570000, 0.03847493], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_XYZ(self) -> None: + def test_n_dimensional_RGB_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.RGB_to_XYZ` definition n-dimensional support. """ - RGB = np.array([0.70556599, 0.19109268, 0.22340812]) - W_R = np.array([0.31270, 0.32900]) - XYZ = RGB_to_XYZ(RGB, "sRGB", W_R, "Bradford", True) + RGB = xp_as_array([0.70556599, 0.19109268, 0.22340812], xp=xp) + W_R = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(RGB_to_XYZ(RGB, "sRGB", W_R, "Bradford", True)) - RGB = np.tile(RGB, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + RGB = xp.tile(xp_as_array(RGB, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( RGB_to_XYZ(RGB, "sRGB", W_R, "Bradford", True), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - W_R = np.tile(W_R, (6, 1)) - np.testing.assert_allclose( + W_R = xp.tile(xp_as_array(W_R, xp=xp), (6, 1)) + xp_assert_close( RGB_to_XYZ(RGB, "sRGB", W_R, "Bradford", True), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB = np.reshape(RGB, (2, 3, 3)) - W_R = np.reshape(W_R, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (2, 3, 3), xp=xp) + W_R = xp_reshape(xp_as_array(W_R, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( RGB_to_XYZ(RGB, "sRGB", W_R, "Bradford", True), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_RGB(self) -> None: + def test_domain_range_scale_XYZ_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.RGB_to_XYZ` definition domain and range scale support. """ - RGB = np.array([0.45620801, 0.03079991, 0.04091883]) - W_R = np.array([0.31270, 0.32900]) - XYZ = RGB_to_XYZ(RGB, "sRGB", W_R) + RGB = xp_as_array([0.45620801, 0.03079991, 0.04091883], xp=xp) + W_R = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(RGB_to_XYZ(RGB, "sRGB", W_R)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_XYZ(RGB * factor, "sRGB", W_R), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -693,67 +704,57 @@ def test_matrix_RGB_to_RGB(self) -> None: aces_cg_colourspace = RGB_COLOURSPACES["ACEScg"] sRGB_colourspace = RGB_COLOURSPACES["sRGB"] - np.testing.assert_allclose( + xp_assert_close( matrix_RGB_to_RGB(aces_2065_1_colourspace, sRGB_colourspace), - np.array( - [ - [2.52164943, -1.13688855, -0.38491759], - [-0.27521355, 1.36970515, -0.09439245], - [-0.01592501, -0.14780637, 1.16380582], - ] - ), + [ + [2.52164943, -1.13688855, -0.38491759], + [-0.27521355, 1.36970515, -0.09439245], + [-0.01592501, -0.14780637, 1.16380582], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_RGB_to_RGB(sRGB_colourspace, aces_2065_1_colourspace), - np.array( - [ - [0.43958564, 0.38392940, 0.17653274], - [0.08953957, 0.81474984, 0.09568361], - [0.01738718, 0.10873911, 0.87382059], - ] - ), + [ + [0.43958564, 0.38392940, 0.17653274], + [0.08953957, 0.81474984, 0.09568361], + [0.01738718, 0.10873911, 0.87382059], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_RGB_to_RGB(aces_2065_1_colourspace, aces_cg_colourspace, "Bradford"), - np.array( - [ - [1.45143932, -0.23651075, -0.21492857], - [-0.07655377, 1.17622970, -0.09967593], - [0.00831615, -0.00603245, 0.99771630], - ] - ), + [ + [1.45143932, -0.23651075, -0.21492857], + [-0.07655377, 1.17622970, -0.09967593], + [0.00831615, -0.00603245, 0.99771630], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_RGB_to_RGB(aces_2065_1_colourspace, sRGB_colourspace, "Bradford"), - np.array( - [ - [2.52140089, -1.13399575, -0.38756186], - [-0.27621406, 1.37259557, -0.09628236], - [-0.01532020, -0.15299256, 1.16838720], - ] - ), + [ + [2.52140089, -1.13399575, -0.38756186], + [-0.27621406, 1.37259557, -0.09628236], + [-0.01532020, -0.15299256, 1.16838720], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_RGB_to_RGB(aces_2065_1_colourspace, sRGB_colourspace, None), - np.array( - [ - [2.55809607, -1.11933692, -0.39181451], - [-0.27771575, 1.36589396, -0.09353075], - [-0.01711199, -0.14854588, 1.08104848], - ] - ), + [ + [2.55809607, -1.11933692, -0.39181451], + [-0.27771575, 1.36589396, -0.09353075], + [-0.01711199, -0.14854588, 1.08104848], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( matrix_RGB_to_RGB(aces_2065_1_colourspace, sRGB_colourspace), matrix_RGB_to_RGB("ACES2065-1", "sRGB"), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -766,107 +767,111 @@ class TestRGB_to_RGB: unit tests methods. """ - def test_RGB_to_RGB(self) -> None: + def test_RGB_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.rgb_colourspace.RGB_to_RGB` definition.""" aces_2065_1_colourspace = RGB_COLOURSPACES["ACES2065-1"] sRGB_colourspace = RGB_COLOURSPACES["sRGB"] - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), aces_2065_1_colourspace, sRGB_colourspace, ), - np.array([0.45595289, 0.03040780, 0.04087313]), + [0.45595289, 0.03040780, 0.04087313], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.45595571, 0.03039702, 0.04087245]), + xp_as_array([0.45595571, 0.03039702, 0.04087245], xp=xp), sRGB_colourspace, aces_2065_1_colourspace, ), - np.array([0.21931722, 0.06950287, 0.04694832]), + [0.21931722, 0.06950287, 0.04694832], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), aces_2065_1_colourspace, sRGB_colourspace, "Bradford", ), - np.array([0.45597530, 0.03030054, 0.04086041]), + [0.45597530, 0.03030054, 0.04086041], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), aces_2065_1_colourspace, sRGB_colourspace, None, ), - np.array([0.46484236, 0.02963459, 0.03667609]), + [0.46484236, 0.02963459, 0.03667609], atol=TOLERANCE_ABSOLUTE_TESTS, ) aces_cg_colourspace = RGB_COLOURSPACES["ACEScg"] aces_cc_colourspace = RGB_COLOURSPACES["ACEScc"] - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), aces_cg_colourspace, aces_cc_colourspace, apply_cctf_decoding=True, apply_cctf_encoding=True, ), - np.array([0.42985679, 0.33522924, 0.30292336]), + [0.42985679, 0.33522924, 0.30292336], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.46956438, 0.48137533, 0.43788601]), + xp_as_array([0.46956438, 0.48137533, 0.43788601], xp=xp), aces_cc_colourspace, sRGB_colourspace, apply_cctf_decoding=True, apply_cctf_encoding=True, ), - np.array([0.60983062, 0.67896356, 0.50435764]), + [0.60983062, 0.67896356, 0.50435764], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_equal( - RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), - aces_2065_1_colourspace, - RGB_COLOURSPACES["ProPhoto RGB"], - apply_cctf_encoding=True, - out_int=True, + xp_assert_equal( + as_ndarray( + RGB_to_RGB( + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), + aces_2065_1_colourspace, + RGB_COLOURSPACES["ProPhoto RGB"], + apply_cctf_encoding=True, + out_int=True, + ) ), - np.array([120, 59, 46]), + [120, 59, 46], ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), aces_2065_1_colourspace, sRGB_colourspace, ), - RGB_to_RGB( - np.array([0.21931722, 0.06950287, 0.04694832]), - "ACES2065-1", - "sRGB", + as_ndarray( + RGB_to_RGB( + xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp), + "ACES2065-1", + "sRGB", + ) ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_RGB(self) -> None: + def test_n_dimensional_RGB_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.RGB_to_RGB` definition n-dimensional support. @@ -874,26 +879,26 @@ def test_n_dimensional_RGB_to_RGB(self) -> None: aces_2065_1_colourspace = RGB_COLOURSPACES["ACES2065-1"] sRGB_colourspace = RGB_COLOURSPACES["sRGB"] - RGB_i = np.array([0.21931722, 0.06950287, 0.04694832]) - RGB_o = RGB_to_RGB(RGB_i, aces_2065_1_colourspace, sRGB_colourspace) + RGB_i = xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp) + RGB_o = as_ndarray(RGB_to_RGB(RGB_i, aces_2065_1_colourspace, sRGB_colourspace)) - RGB_i = np.tile(RGB_i, (6, 1)) - RGB_o = np.tile(RGB_o, (6, 1)) - np.testing.assert_allclose( + RGB_i = xp.tile(xp_as_array(RGB_i, xp=xp), (6, 1)) + RGB_o = xp.tile(xp_as_array(RGB_o, xp=xp), (6, 1)) + xp_assert_close( RGB_to_RGB(RGB_i, aces_2065_1_colourspace, sRGB_colourspace), RGB_o, atol=TOLERANCE_ABSOLUTE_TESTS, ) - RGB_i = np.reshape(RGB_i, (2, 3, 3)) - RGB_o = np.reshape(RGB_o, (2, 3, 3)) - np.testing.assert_allclose( + RGB_i = xp_reshape(xp_as_array(RGB_i, xp=xp), (2, 3, 3), xp=xp) + RGB_o = xp_reshape(xp_as_array(RGB_o, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( RGB_to_RGB(RGB_i, aces_2065_1_colourspace, sRGB_colourspace), RGB_o, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_RGB(self) -> None: + def test_domain_range_scale_XYZ_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.rgb_colourspace.RGB_to_RGB` definition domain and range scale support. @@ -901,13 +906,13 @@ def test_domain_range_scale_XYZ_to_RGB(self) -> None: aces_2065_1_colourspace = RGB_COLOURSPACES["ACES2065-1"] sRGB_colourspace = RGB_COLOURSPACES["sRGB"] - RGB_i = np.array([0.21931722, 0.06950287, 0.04694832]) - RGB_o = RGB_to_RGB(RGB_i, aces_2065_1_colourspace, sRGB_colourspace) + RGB_i = xp_as_array([0.21931722, 0.06950287, 0.04694832], xp=xp) + RGB_o = as_ndarray(RGB_to_RGB(RGB_i, aces_2065_1_colourspace, sRGB_colourspace)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_RGB( RGB_i * factor, aces_2065_1_colourspace, diff --git a/colour/models/rgb/tests/test_ycbcr.py b/colour/models/rgb/tests/test_ycbcr.py index 8e7e60152b..06b1e70ccf 100644 --- a/colour/models/rgb/tests/test_ycbcr.py +++ b/colour/models/rgb/tests/test_ycbcr.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -19,7 +24,15 @@ ranges_YCbCr, round_BT2100, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -46,12 +59,12 @@ class TestRoundBT2100: methods. """ - def test_round_BT2100(self) -> None: + def test_round_BT2100(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycbcr.round_BT2100` definition.""" - np.testing.assert_array_equal( - round_BT2100([-0.6, -0.5, -0.4, 0.4, 0.5, 0.6]), - np.array([-1.0, -1.0, -0.0, 0.0, 1.0, 1.0]), + xp_assert_equal( + round_BT2100(xp_as_array([-0.6, -0.5, -0.4, 0.4, 0.5, 0.6], xp=xp)), + [-1.0, -1.0, -0.0, 0.0, 1.0, 1.0], ) @@ -64,51 +77,51 @@ class TestRangeYCbCr: def test_ranges_YCbCr(self) -> None: """Test :func:`colour.models.rgb.ycbcr.ranges_YCbCr` definition.""" - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(8, True, True), - np.array([16.00000000, 235.00000000, 16.00000000, 240.00000000]), + [16.00000000, 235.00000000, 16.00000000, 240.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(8, True, False), - np.array([0.06274510, 0.92156863, 0.06274510, 0.94117647]), + [0.06274510, 0.92156863, 0.06274510, 0.94117647], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(8, False, True), - np.array([0.00000000, 255.00000000, 0.50000000, 255.50000000]), + [0.00000000, 255.00000000, 0.50000000, 255.50000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(8, False, False), - np.array([0.00000000, 1.00000000, -0.50000000, 0.50000000]), + [0.00000000, 1.00000000, -0.50000000, 0.50000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(10, True, True), - np.array([64.00000000, 940.00000000, 64.00000000, 960.00000000]), + [64.00000000, 940.00000000, 64.00000000, 960.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(10, True, False), - np.array([0.06256109, 0.91886608, 0.06256109, 0.93841642]), + [0.06256109, 0.91886608, 0.06256109, 0.93841642], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(10, False, True), - np.array([0.00000000, 1023.00000000, 0.50000000, 1023.50000000]), + [0.00000000, 1023.00000000, 0.50000000, 1023.50000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( ranges_YCbCr(10, False, False), - np.array([0.00000000, 1.00000000, -0.50000000, 0.50000000]), + [0.00000000, 1.00000000, -0.50000000, 0.50000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -119,78 +132,75 @@ class TestMatrixYCbCr: methods. """ - def test_matrix_YCbCr(self) -> None: + def test_matrix_YCbCr(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycbcr.matrix_YCbCr` definition.""" - np.testing.assert_allclose( - matrix_YCbCr(), - np.array( - [ - [1.00000000, 0.00000000, 1.57480000], - [1.00000000, -0.18732427, -0.46812427], - [1.00000000, 1.85560000, 0.00000000], - ] - ), + xp_assert_close( + matrix_YCbCr(K=xp_as_array(WEIGHTS_YCBCR["ITU-R BT.709"], xp=xp)), + [ + [1.00000000, 0.00000000, 1.57480000], + [1.00000000, -0.18732427, -0.46812427], + [1.00000000, 1.85560000, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - matrix_YCbCr(K=WEIGHTS_YCBCR["ITU-R BT.601"]), - np.array( - [ - [1.00000000, 0.00000000, 1.40200000], - [1.00000000, -0.34413629, -0.71413629], - [1.00000000, 1.77200000, -0.00000000], - ] - ), + xp_assert_close( + matrix_YCbCr(K=xp_as_array(WEIGHTS_YCBCR["ITU-R BT.601"], xp=xp)), + [ + [1.00000000, 0.00000000, 1.40200000], + [1.00000000, -0.34413629, -0.71413629], + [1.00000000, 1.77200000, -0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - matrix_YCbCr(is_legal=True), - np.array( - [ - [1.16438356, 0.00000000, 1.79274107], - [1.16438356, -0.21324861, -0.53290933], - [1.16438356, 2.11240179, -0.00000000], - ] + xp_assert_close( + matrix_YCbCr( + K=xp_as_array(WEIGHTS_YCBCR["ITU-R BT.709"], xp=xp), is_legal=True ), + [ + [1.16438356, 0.00000000, 1.79274107], + [1.16438356, -0.21324861, -0.53290933], + [1.16438356, 2.11240179, -0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - matrix_YCbCr(bits=10), - np.array( - [ - [1.00000000, 0.00000000, 1.57480000], - [1.00000000, -0.18732427, -0.46812427], - [1.00000000, 1.85560000, 0.00000000], - ] - ), + xp_assert_close( + matrix_YCbCr(K=xp_as_array(WEIGHTS_YCBCR["ITU-R BT.709"], xp=xp), bits=10), + [ + [1.00000000, 0.00000000, 1.57480000], + [1.00000000, -0.18732427, -0.46812427], + [1.00000000, 1.85560000, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - matrix_YCbCr(bits=10, is_int=True), - np.array( - [ - [0.00097752, 0.00000000, 0.00153939], - [0.00097752, -0.00018311, -0.00045760], - [0.00097752, 0.00181388, 0.00000000], - ] + xp_assert_close( + matrix_YCbCr( + K=xp_as_array(WEIGHTS_YCBCR["ITU-R BT.709"], xp=xp), + bits=10, + is_int=True, ), + [ + [0.00097752, 0.00000000, 0.00153939], + [0.00097752, -0.00018311, -0.00045760], + [0.00097752, 0.00181388, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - matrix_YCbCr(S=SCALES_YCBCR["Y'UV"]), - np.array( - [ - [1.00000000, 0.00000000, 1.28032520], - [1.00000000, -0.21482141, -0.38058884], - [1.00000000, 2.12798165, 0.00000000], - ] + xp_assert_close( + matrix_YCbCr( + K=xp_as_array(WEIGHTS_YCBCR["ITU-R BT.709"], xp=xp), + S=xp_as_array(SCALES_YCBCR["Y'UV"], xp=xp), ), + [ + [1.00000000, 0.00000000, 1.28032520], + [1.00000000, -0.21482141, -0.38058884], + [1.00000000, 2.12798165, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -204,27 +214,27 @@ class TestOffsetYCbCr: def test_offset_YCbCr(self) -> None: """Test :func:`colour.models.rgb.ycbcr.offset_YCbCr` definition.""" - np.testing.assert_allclose( + xp_assert_close( offset_YCbCr(), - np.array([0.00000000, 0.00000000, 0.00000000]), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( offset_YCbCr(is_legal=True), - np.array([0.06274510, 0.50196078, 0.50196078]), + [0.06274510, 0.50196078, 0.50196078], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( offset_YCbCr(bits=10), - np.array([0.00000000, 0.00000000, 0.00000000]), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( offset_YCbCr(bits=10, is_int=True), - np.array([0.00000000, 512.00000000, 512.00000000]), + [0.00000000, 512.00000000, 512.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -235,99 +245,111 @@ class TestRGB_to_YCbCr: methods. """ - def test_RGB_to_YCbCr(self) -> None: + def test_RGB_to_YCbCr(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycbcr.RGB_to_YCbCr` definition.""" - np.testing.assert_allclose( - RGB_to_YCbCr(np.array([0.75, 0.75, 0.0])), - np.array([0.66035745, 0.17254902, 0.53216593]), + xp_assert_close( + RGB_to_YCbCr(xp_as_array([0.75, 0.75, 0.0], xp=xp)), + [0.66035745, 0.17254902, 0.53216593], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_YCbCr( - np.array([0.25, 0.5, 0.75]), + xp_as_array([0.25, 0.5, 0.75], xp=xp), K=WEIGHTS_YCBCR["ITU-R BT.601"], out_int=True, out_legal=True, out_bits=10, ), - np.array([461, 662, 382]), + [461, 662, 382], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_YCbCr( - np.array([0.0, 0.75, 0.75]), + xp_as_array([0.0, 0.75, 0.75], xp=xp), K=WEIGHTS_YCBCR["ITU-R BT.2020"], out_int=False, out_legal=False, ), - np.array([0.55297500, 0.10472255, -0.37500000]), + [0.55297500, 0.10472255, -0.37500000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_YCbCr( - np.array([0.75, 0.0, 0.75]), + xp_as_array([0.75, 0.0, 0.75], xp=xp), K=WEIGHTS_YCBCR["ITU-R BT.709"], out_range=(16 / 255, 235 / 255, 15.5 / 255, 239.5 / 255), ), - np.array([0.24618980, 0.75392897, 0.79920662]), + [0.24618980, 0.75392897, 0.79920662], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_YCbCr( - np.array([0.75, 0.5, 0.25]), + xp_as_array([0.75, 0.5, 0.25], xp=xp), S=SCALES_YCBCR["Y'UV"], out_legal=False, out_int=False, ), - np.array([0.53510000, -0.13397672, 0.16784798]), + [0.53510000, -0.13397672, 0.16784798], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_YCbCr(self) -> None: + def test_n_dimensional_RGB_to_YCbCr(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ycbcr.RGB_to_YCbCr` definition n-dimensional arrays support. """ - RGB = np.array([0.75, 0.5, 0.25]) - YCbCr = RGB_to_YCbCr(RGB) + RGB = xp_as_array([0.75, 0.5, 0.25], xp=xp) + YCbCr = as_ndarray(RGB_to_YCbCr(RGB)) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 3)) - YCbCr = np.tile(YCbCr, 4) - YCbCr = np.reshape(YCbCr, (4, 3)) - np.testing.assert_allclose(RGB_to_YCbCr(RGB), YCbCr) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 3), xp=xp) + YCbCr = xp.tile(xp_as_array(YCbCr, xp=xp), (4,)) + YCbCr = xp_reshape(xp_as_array(YCbCr, xp=xp), (4, 3), xp=xp) + xp_assert_close( + RGB_to_YCbCr(RGB), + YCbCr, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 3)) - YCbCr = np.tile(YCbCr, 4) - YCbCr = np.reshape(YCbCr, (4, 4, 3)) - np.testing.assert_allclose(RGB_to_YCbCr(RGB), YCbCr) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 3), xp=xp) + YCbCr = xp.tile(xp_as_array(YCbCr, xp=xp), (4,)) + YCbCr = xp_reshape(xp_as_array(YCbCr, xp=xp), (4, 4, 3), xp=xp) + xp_assert_close( + RGB_to_YCbCr(RGB), + YCbCr, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 4, 3)) - YCbCr = np.tile(YCbCr, 4) - YCbCr = np.reshape(YCbCr, (4, 4, 4, 3)) - np.testing.assert_allclose(RGB_to_YCbCr(RGB), YCbCr) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 4, 3), xp=xp) + YCbCr = xp.tile(xp_as_array(YCbCr, xp=xp), (4,)) + YCbCr = xp_reshape(xp_as_array(YCbCr, xp=xp), (4, 4, 4, 3), xp=xp) + xp_assert_close( + RGB_to_YCbCr(RGB), + YCbCr, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_domain_range_scale_RGB_to_YCbCr(self) -> None: + def test_domain_range_scale_RGB_to_YCbCr(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.RGB_to_YCbCr` definition domain and range scale support. """ - RGB = np.array([0.75, 0.5, 0.25]) - YCbCr = RGB_to_YCbCr(RGB) + RGB = xp_as_array([0.75, 0.5, 0.25], xp=xp) + YCbCr = as_ndarray(RGB_to_YCbCr(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_YCbCr(RGB * factor), YCbCr * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -351,29 +373,29 @@ class TestYCbCr_to_RGB: methods. """ - def test_YCbCr_to_RGB(self) -> None: + def test_YCbCr_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycbcr.YCbCr_to_RGB` definition.""" - np.testing.assert_allclose( - YCbCr_to_RGB(np.array([0.66035745, 0.17254902, 0.53216593])), - np.array([0.75, 0.75, 0.0]), + xp_assert_close( + YCbCr_to_RGB(xp_as_array([0.66035745, 0.17254902, 0.53216593], xp=xp)), + [0.75, 0.75, 0.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( YCbCr_to_RGB( - np.array([471, 650, 390]), + xp_as_array([471, 650, 390], xp=xp), in_bits=10, in_legal=True, in_int=True, ), - np.array([0.25018598, 0.49950072, 0.75040741]), + [0.25018598, 0.49950072, 0.75040741], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( YCbCr_to_RGB( - np.array([150, 99, 175]), + xp_as_array([150, 99, 175], xp=xp), in_bits=8, in_legal=False, in_int=True, @@ -381,74 +403,86 @@ def test_YCbCr_to_RGB(self) -> None: out_legal=True, out_int=True, ), - np.array([208, 131, 99]), + [208, 131, 99], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( YCbCr_to_RGB( - np.array([0.53510000, -0.13397672, 0.16784798]), + xp_as_array([0.53510000, -0.13397672, 0.16784798], xp=xp), S=SCALES_YCBCR["Y'UV"], in_legal=False, in_int=False, ), - np.array([0.75, 0.5, 0.25]), + [0.75, 0.5, 0.25], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_roundtrip_YCbCr_YUV(self) -> None: + def test_roundtrip_YCbCr_YUV(self, xp: ModuleType) -> None: """Test *Y'UV* roundtrip with :func:`colour.models.rgb.ycbcr.RGB_to_YCbCr` and :func:`colour.models.rgb.ycbcr.YCbCr_to_RGB` definitions. """ - RGB = np.array([0.75, 0.5, 0.25]) + RGB = xp_as_array([0.75, 0.5, 0.25], xp=xp) YUV = RGB_to_YCbCr(RGB, S=SCALES_YCBCR["Y'UV"], out_legal=False, out_int=False) - np.testing.assert_allclose( + xp_assert_close( YCbCr_to_RGB(YUV, S=SCALES_YCBCR["Y'UV"], in_legal=False, in_int=False), - RGB, + [0.75, 0.5, 0.25], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_YCbCr_to_RGB(self) -> None: + def test_n_dimensional_YCbCr_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ycbcr.YCbCr_to_RGB` definition n-dimensional arrays support. """ - YCbCr = np.array([0.52230157, 0.36699593, 0.62183309]) - RGB = YCbCr_to_RGB(YCbCr) + YCbCr = xp_as_array([0.52230157, 0.36699593, 0.62183309], xp=xp) + RGB = as_ndarray(YCbCr_to_RGB(YCbCr)) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 3)) - YCbCr = np.tile(YCbCr, 4) - YCbCr = np.reshape(YCbCr, (4, 3)) - np.testing.assert_allclose(YCbCr_to_RGB(YCbCr), RGB) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 3), xp=xp) + YCbCr = xp.tile(xp_as_array(YCbCr, xp=xp), (4,)) + YCbCr = xp_reshape(xp_as_array(YCbCr, xp=xp), (4, 3), xp=xp) + xp_assert_close( + YCbCr_to_RGB(YCbCr), + RGB, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 3)) - YCbCr = np.tile(YCbCr, 4) - YCbCr = np.reshape(YCbCr, (4, 4, 3)) - np.testing.assert_allclose(YCbCr_to_RGB(YCbCr), RGB) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 3), xp=xp) + YCbCr = xp.tile(xp_as_array(YCbCr, xp=xp), (4,)) + YCbCr = xp_reshape(xp_as_array(YCbCr, xp=xp), (4, 4, 3), xp=xp) + xp_assert_close( + YCbCr_to_RGB(YCbCr), + RGB, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 4, 3)) - YCbCr = np.tile(YCbCr, 4) - YCbCr = np.reshape(YCbCr, (4, 4, 4, 3)) - np.testing.assert_allclose(YCbCr_to_RGB(YCbCr), RGB) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 4, 3), xp=xp) + YCbCr = xp.tile(xp_as_array(YCbCr, xp=xp), (4,)) + YCbCr = xp_reshape(xp_as_array(YCbCr, xp=xp), (4, 4, 4, 3), xp=xp) + xp_assert_close( + YCbCr_to_RGB(YCbCr), + RGB, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_domain_range_scale_YCbCr_to_RGB(self) -> None: + def test_domain_range_scale_YCbCr_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.YCbCr_to_RGB` definition domain and range scale support. """ - YCbCr = np.array([0.52230157, 0.36699593, 0.62183309]) - RGB = YCbCr_to_RGB(YCbCr) + YCbCr = xp_as_array([0.52230157, 0.36699593, 0.62183309], xp=xp) + RGB = as_ndarray(YCbCr_to_RGB(YCbCr)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( YCbCr_to_RGB(YCbCr * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -472,73 +506,67 @@ class TestRGB_to_YcCbcCrc: tests methods. """ - def test_RGB_to_YcCbcCrc(self) -> None: + def test_RGB_to_YcCbcCrc(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycbcr.RGB_to_YcCbcCrc` definition.""" - np.testing.assert_allclose( - RGB_to_YcCbcCrc(np.array([0.45620519, 0.03081071, 0.04091952])), - np.array([0.37020379, 0.41137200, 0.77704674]), + xp_assert_close( + RGB_to_YcCbcCrc(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)), + [0.37020379, 0.41137200, 0.77704674], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_YcCbcCrc( - np.array([0.18, 0.18, 0.18]), + xp_as_array([0.18, 0.18, 0.18], xp=xp), out_bits=10, out_legal=True, out_int=True, is_12_bits_system=False, ), - np.array([422, 512, 512]), + [422, 512, 512], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_RGB_to_YcCbcCrc(self) -> None: + def test_n_dimensional_RGB_to_YcCbcCrc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ycbcr.RGB_to_YcCbcCrc` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - YcCbcCrc = RGB_to_YcCbcCrc(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + YcCbcCrc = as_ndarray(RGB_to_YcCbcCrc(RGB)) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 3)) - YcCbcCrc = np.tile(YcCbcCrc, 4) - YcCbcCrc = np.reshape(YcCbcCrc, (4, 3)) - np.testing.assert_allclose( - RGB_to_YcCbcCrc(RGB), YcCbcCrc, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 3), xp=xp) + YcCbcCrc = xp.tile(xp_as_array(YcCbcCrc, xp=xp), (4,)) + YcCbcCrc = xp_reshape(xp_as_array(YcCbcCrc, xp=xp), (4, 3), xp=xp) + xp_assert_close(RGB_to_YcCbcCrc(RGB), YcCbcCrc, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 3)) - YcCbcCrc = np.tile(YcCbcCrc, 4) - YcCbcCrc = np.reshape(YcCbcCrc, (4, 4, 3)) - np.testing.assert_allclose( - RGB_to_YcCbcCrc(RGB), YcCbcCrc, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 3), xp=xp) + YcCbcCrc = xp.tile(xp_as_array(YcCbcCrc, xp=xp), (4,)) + YcCbcCrc = xp_reshape(xp_as_array(YcCbcCrc, xp=xp), (4, 4, 3), xp=xp) + xp_assert_close(RGB_to_YcCbcCrc(RGB), YcCbcCrc, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 4, 3)) - YcCbcCrc = np.tile(YcCbcCrc, 4) - YcCbcCrc = np.reshape(YcCbcCrc, (4, 4, 4, 3)) - np.testing.assert_allclose( - RGB_to_YcCbcCrc(RGB), YcCbcCrc, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 4, 3), xp=xp) + YcCbcCrc = xp.tile(xp_as_array(YcCbcCrc, xp=xp), (4,)) + YcCbcCrc = xp_reshape(xp_as_array(YcCbcCrc, xp=xp), (4, 4, 4, 3), xp=xp) + xp_assert_close(RGB_to_YcCbcCrc(RGB), YcCbcCrc, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_RGB_to_YcCbcCrc(self) -> None: + def test_domain_range_scale_RGB_to_YcCbcCrc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.RGB_to_YcCbcCrc` definition domain and range scale support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) - YcCbcCrc = RGB_to_YcCbcCrc(RGB) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) + YcCbcCrc = as_ndarray(RGB_to_YcCbcCrc(RGB)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( RGB_to_YcCbcCrc(RGB * factor), YcCbcCrc * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -562,73 +590,67 @@ class TestYcCbcCrc_to_RGB: methods. """ - def test_YcCbcCrc_to_RGB(self) -> None: + def test_YcCbcCrc_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycbcr.YCbCr_to_RGB` definition.""" - np.testing.assert_allclose( - YcCbcCrc_to_RGB(np.array([0.37020379, 0.41137200, 0.77704674])), - np.array([0.45620519, 0.03081071, 0.04091952]), + xp_assert_close( + YcCbcCrc_to_RGB(xp_as_array([0.37020379, 0.41137200, 0.77704674], xp=xp)), + [0.45620519, 0.03081071, 0.04091952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( YcCbcCrc_to_RGB( - np.array([1689, 2048, 2048]), + xp_as_array([1689, 2048, 2048], xp=xp), in_bits=12, in_legal=True, in_int=True, is_12_bits_system=True, ), - np.array([0.18009037, 0.18009037, 0.18009037]), + [0.18009037, 0.18009037, 0.18009037], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_YcCbcCrc_to_RGB(self) -> None: + def test_n_dimensional_YcCbcCrc_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ycbcr.YcCbcCrc_to_RGB` definition n-dimensional arrays support. """ - YcCbcCrc = np.array([0.37020379, 0.41137200, 0.77704674]) - RGB = YcCbcCrc_to_RGB(YcCbcCrc) + YcCbcCrc = xp_as_array([0.37020379, 0.41137200, 0.77704674], xp=xp) + RGB = as_ndarray(YcCbcCrc_to_RGB(YcCbcCrc)) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 3)) - YcCbcCrc = np.tile(YcCbcCrc, 4) - YcCbcCrc = np.reshape(YcCbcCrc, (4, 3)) - np.testing.assert_allclose( - YcCbcCrc_to_RGB(YcCbcCrc), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 3), xp=xp) + YcCbcCrc = xp.tile(xp_as_array(YcCbcCrc, xp=xp), (4,)) + YcCbcCrc = xp_reshape(xp_as_array(YcCbcCrc, xp=xp), (4, 3), xp=xp) + xp_assert_close(YcCbcCrc_to_RGB(YcCbcCrc), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 3)) - YcCbcCrc = np.tile(YcCbcCrc, 4) - YcCbcCrc = np.reshape(YcCbcCrc, (4, 4, 3)) - np.testing.assert_allclose( - YcCbcCrc_to_RGB(YcCbcCrc), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 3), xp=xp) + YcCbcCrc = xp.tile(xp_as_array(YcCbcCrc, xp=xp), (4,)) + YcCbcCrc = xp_reshape(xp_as_array(YcCbcCrc, xp=xp), (4, 4, 3), xp=xp) + xp_assert_close(YcCbcCrc_to_RGB(YcCbcCrc), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 4, 3)) - YcCbcCrc = np.tile(YcCbcCrc, 4) - YcCbcCrc = np.reshape(YcCbcCrc, (4, 4, 4, 3)) - np.testing.assert_allclose( - YcCbcCrc_to_RGB(YcCbcCrc), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 4, 3), xp=xp) + YcCbcCrc = xp.tile(xp_as_array(YcCbcCrc, xp=xp), (4,)) + YcCbcCrc = xp_reshape(xp_as_array(YcCbcCrc, xp=xp), (4, 4, 4, 3), xp=xp) + xp_assert_close(YcCbcCrc_to_RGB(YcCbcCrc), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_YcCbcCrc_to_RGB(self) -> None: + def test_domain_range_scale_YcCbcCrc_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.prismatic.YcCbcCrc_to_RGB` definition domain and range scale support. """ - YcCbcCrc = np.array([0.69943807, 0.38814348, 0.61264549]) - RGB = YcCbcCrc_to_RGB(YcCbcCrc) + YcCbcCrc = xp_as_array([0.69943807, 0.38814348, 0.61264549], xp=xp) + RGB = as_ndarray(YcCbcCrc_to_RGB(YcCbcCrc)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( YcCbcCrc_to_RGB(YcCbcCrc * factor), RGB * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/tests/test_ycocg.py b/colour/models/rgb/tests/test_ycocg.py index 3bd5d40696..1c958672ae 100644 --- a/colour/models/rgb/tests/test_ycocg.py +++ b/colour/models/rgb/tests/test_ycocg.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb import RGB_to_YCoCg, YCoCg_to_RGB -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,56 +41,50 @@ class TestRGB_to_YCoCg: methods. """ - def test_RGB_to_YCoCg(self) -> None: + def test_RGB_to_YCoCg(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycocg.RGB_to_YCoCg` definition.""" - np.testing.assert_array_equal( - RGB_to_YCoCg(np.array([0.75, 0.75, 0.0])), - np.array([0.5625, 0.375, 0.1875]), + xp_assert_equal( + RGB_to_YCoCg(xp_as_array([0.75, 0.75, 0.0], xp=xp)), + [0.5625, 0.375, 0.1875], ) - np.testing.assert_array_equal( - RGB_to_YCoCg(np.array([0.25, 0.5, 0.75])), - np.array([0.5, -0.25, 0.0]), + xp_assert_equal( + RGB_to_YCoCg(xp_as_array([0.25, 0.5, 0.75], xp=xp)), + [0.5, -0.25, 0.0], ) - np.testing.assert_array_equal( - RGB_to_YCoCg(np.array([0.0, 0.75, 0.75])), - np.array([0.5625, -0.375, 0.1875]), + xp_assert_equal( + RGB_to_YCoCg(xp_as_array([0.0, 0.75, 0.75], xp=xp)), + [0.5625, -0.375, 0.1875], ) - def test_n_dimensional_RGB_to_YCoCg(self) -> None: + def test_n_dimensional_RGB_to_YCoCg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ycocg.RGB_to_YCoCg` definition n-dimensional arrays support. """ - RGB = np.array([0.75, 0.75, 0.0]) - YCoCg = RGB_to_YCoCg(RGB) + RGB = xp_as_array([0.75, 0.75, 0.0], xp=xp) + YCoCg = as_ndarray(RGB_to_YCoCg(RGB)) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 3)) - YCoCg = np.tile(YCoCg, 4) - YCoCg = np.reshape(YCoCg, (4, 3)) - np.testing.assert_allclose( - RGB_to_YCoCg(RGB), YCoCg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 3), xp=xp) + YCoCg = xp.tile(xp_as_array(YCoCg, xp=xp), (4,)) + YCoCg = xp_reshape(xp_as_array(YCoCg, xp=xp), (4, 3), xp=xp) + xp_assert_close(RGB_to_YCoCg(RGB), YCoCg, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 3)) - YCoCg = np.tile(YCoCg, 4) - YCoCg = np.reshape(YCoCg, (4, 4, 3)) - np.testing.assert_allclose( - RGB_to_YCoCg(RGB), YCoCg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 3), xp=xp) + YCoCg = xp.tile(xp_as_array(YCoCg, xp=xp), (4,)) + YCoCg = xp_reshape(xp_as_array(YCoCg, xp=xp), (4, 4, 3), xp=xp) + xp_assert_close(RGB_to_YCoCg(RGB), YCoCg, atol=TOLERANCE_ABSOLUTE_TESTS) - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 4, 3)) - YCoCg = np.tile(YCoCg, 4) - YCoCg = np.reshape(YCoCg, (4, 4, 4, 3)) - np.testing.assert_allclose( - RGB_to_YCoCg(RGB), YCoCg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 4, 3), xp=xp) + YCoCg = xp.tile(xp_as_array(YCoCg, xp=xp), (4,)) + YCoCg = xp_reshape(xp_as_array(YCoCg, xp=xp), (4, 4, 4, 3), xp=xp) + xp_assert_close(RGB_to_YCoCg(RGB), YCoCg, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_RGB_to_YCoCg(self) -> None: @@ -98,56 +104,50 @@ class TestYCoCg_to_RGB: methods. """ - def test_YCoCg_to_RGB(self) -> None: + def test_YCoCg_to_RGB(self, xp: ModuleType) -> None: """Test :func:`colour.models.rgb.ycocg.YCoCg_to_RGB` definition.""" - np.testing.assert_array_equal( - YCoCg_to_RGB(np.array([0.5625, 0.375, 0.1875])), - np.array([0.75, 0.75, 0.0]), + xp_assert_equal( + YCoCg_to_RGB(xp_as_array([0.5625, 0.375, 0.1875], xp=xp)), + [0.75, 0.75, 0.0], ) - np.testing.assert_array_equal( - YCoCg_to_RGB(np.array([0.5, -0.25, 0.0])), - np.array([0.25, 0.5, 0.75]), + xp_assert_equal( + YCoCg_to_RGB(xp_as_array([0.5, -0.25, 0.0], xp=xp)), + [0.25, 0.5, 0.75], ) - np.testing.assert_array_equal( - YCoCg_to_RGB(np.array([0.5625, -0.375, 0.1875])), - np.array([0.0, 0.75, 0.75]), + xp_assert_equal( + YCoCg_to_RGB(xp_as_array([0.5625, -0.375, 0.1875], xp=xp)), + [0.0, 0.75, 0.75], ) - def test_n_dimensional_YCoCg_to_RGB(self) -> None: + def test_n_dimensional_YCoCg_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.ycocg.YCoCg_to_RGB` definition n-dimensional arrays support. """ - YCoCg = np.array([0.5625, 0.375, 0.1875]) - RGB = YCoCg_to_RGB(YCoCg) - - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 3)) - YCoCg = np.tile(YCoCg, 4) - YCoCg = np.reshape(YCoCg, (4, 3)) - np.testing.assert_allclose( - YCoCg_to_RGB(YCoCg), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) - - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 3)) - YCoCg = np.tile(YCoCg, 4) - YCoCg = np.reshape(YCoCg, (4, 4, 3)) - np.testing.assert_allclose( - YCoCg_to_RGB(YCoCg), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) - - RGB = np.tile(RGB, 4) - RGB = np.reshape(RGB, (4, 4, 4, 3)) - YCoCg = np.tile(YCoCg, 4) - YCoCg = np.reshape(YCoCg, (4, 4, 4, 3)) - np.testing.assert_allclose( - YCoCg_to_RGB(YCoCg), RGB, atol=TOLERANCE_ABSOLUTE_TESTS - ) + YCoCg = xp_as_array([0.5625, 0.375, 0.1875], xp=xp) + RGB = as_ndarray(YCoCg_to_RGB(YCoCg)) + + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 3), xp=xp) + YCoCg = xp.tile(xp_as_array(YCoCg, xp=xp), (4,)) + YCoCg = xp_reshape(xp_as_array(YCoCg, xp=xp), (4, 3), xp=xp) + xp_assert_close(YCoCg_to_RGB(YCoCg), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 3), xp=xp) + YCoCg = xp.tile(xp_as_array(YCoCg, xp=xp), (4,)) + YCoCg = xp_reshape(xp_as_array(YCoCg, xp=xp), (4, 4, 3), xp=xp) + xp_assert_close(YCoCg_to_RGB(YCoCg), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + + RGB = xp.tile(xp_as_array(RGB, xp=xp), (4,)) + RGB = xp_reshape(xp_as_array(RGB, xp=xp), (4, 4, 4, 3), xp=xp) + YCoCg = xp.tile(xp_as_array(YCoCg, xp=xp), (4,)) + YCoCg = xp_reshape(xp_as_array(YCoCg, xp=xp), (4, 4, 4, 3), xp=xp) + xp_assert_close(YCoCg_to_RGB(YCoCg), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_YCoCg_to_RGB(self) -> None: diff --git a/colour/models/rgb/transfer_functions/aces.py b/colour/models/rgb/transfer_functions/aces.py index d94fb812fa..5dd5196c40 100644 --- a/colour/models/rgb/transfer_functions/aces.py +++ b/colour/models/rgb/transfer_functions/aces.py @@ -49,10 +49,9 @@ from __future__ import annotations +import math import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import Literal, NDArrayInt @@ -64,6 +63,7 @@ ) from colour.utilities import ( Structure, + array_namespace, as_float, as_int, from_range_1, @@ -183,6 +183,9 @@ def log_encoding_ACESproxy( """ lin_AP1 = to_domain_1(lin_AP1) + + xp = array_namespace(lin_AP1) + constants = optional(constants, CONSTANTS_ACES_PROXY) CV_min = constants[bit_depth].CV_min @@ -194,18 +197,18 @@ def log_encoding_ACESproxy( def float_2_cv(x: float) -> float: """Convert specified numeric to code value.""" - return np.maximum(CV_min, np.minimum(CV_max, np.round(x))) + return xp.clip(xp.round(x), min=CV_min, max=CV_max) - ACESproxy = np.where( + ACESproxy = xp.where( lin_AP1 > 2**-9.72, float_2_cv( - (np.log2(lin_AP1) + mid_log_offset) * steps_per_stop + mid_CV_offset + (xp.log2(lin_AP1) + mid_log_offset) * steps_per_stop + mid_CV_offset ), - np.resize(CV_min, lin_AP1.shape), + CV_min, ) if out_int: - return as_int(np.round(ACESproxy)) + return as_int(xp.round(ACESproxy)) return as_float(from_range_1(ACESproxy / (2**bit_depth - 1))) @@ -326,14 +329,16 @@ def log_encoding_ACEScc(lin_AP1: Domain1) -> Range1: lin_AP1 = to_domain_1(lin_AP1) - ACEScc = np.where( + xp = array_namespace(lin_AP1) + + ACEScc = xp.where( lin_AP1 < 0, - (np.log2(2**-16) + 9.72) / 17.52, - (np.log2(2**-16 + lin_AP1 * 0.5) + 9.72) / 17.52, + (math.log2(2**-16) + 9.72) / 17.52, + (xp.log2(2**-16 + lin_AP1 * 0.5) + 9.72) / 17.52, ) - ACEScc = np.where( + ACEScc = xp.where( lin_AP1 >= 2**-15, - (np.log2(lin_AP1) + 9.72) / 17.52, + (xp.log2(lin_AP1) + 9.72) / 17.52, ACEScc, ) @@ -383,13 +388,15 @@ def log_decoding_ACEScc(ACEScc: Domain1) -> Range1: ACEScc = to_domain_1(ACEScc) - lin_AP1 = np.where( + xp = array_namespace(ACEScc) + + lin_AP1 = xp.where( ACEScc < (9.72 - 15) / 17.52, (2 ** (ACEScc * 17.52 - 9.72) - 2**-16) * 2, 2 ** (ACEScc * 17.52 - 9.72), ) - lin_AP1 = np.where( - ACEScc >= (np.log2(65504) + 9.72) / 17.52, + lin_AP1 = xp.where( + ACEScc >= (math.log2(65504) + 9.72) / 17.52, 65504, lin_AP1, ) @@ -443,12 +450,15 @@ def log_encoding_ACEScct( """ lin_AP1 = to_domain_1(lin_AP1) + + xp = array_namespace(lin_AP1) + constants = optional(constants, CONSTANTS_ACES_CCT) - ACEScct = np.where( + ACEScct = xp.where( lin_AP1 <= constants.X_BRK, constants.A * lin_AP1 + constants.B, - (np.log2(lin_AP1) + 9.72) / 17.52, + (xp.log2(lin_AP1) + 9.72) / 17.52, ) return as_float(from_range_1(ACEScct)) @@ -500,9 +510,12 @@ def log_decoding_ACEScct( """ ACEScct = to_domain_1(ACEScct) + + xp = array_namespace(ACEScct) + constants = optional(constants, CONSTANTS_ACES_CCT) - lin_AP1 = np.where( + lin_AP1 = xp.where( ACEScct > constants.Y_BRK, 2 ** (ACEScct * 17.52 - 9.72), (ACEScct - constants.B) / constants.A, diff --git a/colour/models/rgb/transfer_functions/apple_log_profile.py b/colour/models/rgb/transfer_functions/apple_log_profile.py index bb891fa7a2..75b3d0bfb0 100644 --- a/colour/models/rgb/transfer_functions/apple_log_profile.py +++ b/colour/models/rgb/transfer_functions/apple_log_profile.py @@ -22,13 +22,19 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, + xp_select, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -104,6 +110,9 @@ def log_encoding_AppleLogProfile( """ R = to_domain_1(R) + + xp = array_namespace(R) + constants = optional(constants, CONSTANTS_APPLE_LOG_PROFILE) R_0 = constants.R_0 @@ -113,17 +122,18 @@ def log_encoding_AppleLogProfile( gamma = constants.gamma delta = constants.delta - P = np.select( + P = xp_select( [ R >= R_t, # noqa: SIM300 - np.logical_and(R_0 <= R, R < R_t), # noqa: SIM300 + xp.logical_and(R_0 <= R, R < R_t), # noqa: SIM300 R < R_0, ], [ - gamma * np.log2(R + beta) + delta, + gamma * xp.log2(R + beta) + delta, sigma * (R - R_0) ** 2, 0, ], + xp=xp, ) return as_float(from_range_1(P)) @@ -178,6 +188,9 @@ def log_decoding_AppleLogProfile( """ P = to_domain_1(P) + + xp = array_namespace(P) + constants = optional(constants, CONSTANTS_APPLE_LOG_PROFILE) R_0 = constants.R_0 @@ -189,17 +202,18 @@ def log_decoding_AppleLogProfile( P_t = sigma * (R_t - R_0) ** 2 - R = np.select( + R = xp_select( [ P >= P_t, # noqa: SIM300 - np.logical_and(0 <= P, P < P_t), # noqa: SIM300 + xp.logical_and(0 <= P, P < P_t), # noqa: SIM300 P < 0, ], [ 2 ** ((P - delta) / gamma) - beta, - np.sqrt(P / sigma) + R_0, + xp.sqrt(P / sigma) + R_0, R_0, ], + xp=xp, ) return as_float(from_range_1(R)) diff --git a/colour/models/rgb/transfer_functions/arib_std_b67.py b/colour/models/rgb/transfer_functions/arib_std_b67.py index d89e4f76c9..0eafae8854 100644 --- a/colour/models/rgb/transfer_functions/arib_std_b67.py +++ b/colour/models/rgb/transfer_functions/arib_std_b67.py @@ -19,8 +19,6 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 ArrayLike, Domain1, @@ -29,12 +27,13 @@ from colour.models.rgb.transfer_functions import gamma_function from colour.utilities import ( Structure, + array_namespace, as_float, - as_float_array, domain_range_scale, from_range_1, optional, to_domain_1, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -108,14 +107,17 @@ def oetf_ARIBSTDB67( """ E = to_domain_1(E) - r = as_float_array(r) + + xp = array_namespace(E) + + r = xp_as_float_array(r, xp=xp, like=E) constants = optional(constants, CONSTANTS_ARIBSTDB67) a = constants.a b = constants.b c = constants.c - E_p = np.where(E <= 1, r * gamma_function(E, 0.5, "mirror"), a * np.log(E - b) + c) + E_p = xp.where(E <= 1, r * gamma_function(E, 0.5, "mirror"), a * xp.log(E - b) + c) return as_float(from_range_1(E_p)) @@ -174,6 +176,11 @@ def oetf_inverse_ARIBSTDB67( """ E_p = to_domain_1(E_p) + + xp = array_namespace(E_p) + + r = xp_as_float_array(r, xp=xp, like=E_p) + constants = optional(constants, CONSTANTS_ARIBSTDB67) a = constants.a @@ -181,10 +188,10 @@ def oetf_inverse_ARIBSTDB67( c = constants.c with domain_range_scale("ignore"): - E = np.where( + E = xp.where( E_p <= oetf_ARIBSTDB67(1), gamma_function((E_p / r), 2, "mirror"), - np.exp((E_p - c) / a) + b, + xp.exp((E_p - c) / a) + b, ) return as_float(from_range_1(E)) diff --git a/colour/models/rgb/transfer_functions/arri.py b/colour/models/rgb/transfer_functions/arri.py index 7e20f2dcf0..21004f41cb 100644 --- a/colour/models/rgb/transfer_functions/arri.py +++ b/colour/models/rgb/transfer_functions/arri.py @@ -35,6 +35,7 @@ from colour.utilities import ( CanonicalMapping, Structure, + array_namespace, as_float, from_range_1, optional, @@ -613,6 +614,9 @@ def log_encoding_ARRILogC3( """ x = to_domain_1(x) + + xp = array_namespace(x) + firmware = validate_method(firmware, ("SUP 3.x", "SUP 2.x")) method = validate_method( method, ("Linear Scene Exposure Factor", "Normalised Sensor Signal") @@ -622,7 +626,7 @@ def log_encoding_ARRILogC3( method ][EI] - t = np.where(x > cut, c * np.log10(a * x + b) + d, e * x + f) + t = xp.where(x > cut, c * xp.log10(a * x + b) + d, e * x + f) return as_float(from_range_1(t)) @@ -680,6 +684,9 @@ def log_decoding_ARRILogC3( """ t = to_domain_1(t) + + xp = array_namespace(t) + method = validate_method( method, ("Linear Scene Exposure Factor", "Normalised Sensor Signal") ) @@ -688,7 +695,7 @@ def log_decoding_ARRILogC3( method ][EI] - x = np.where(t > e * cut + f, (10 ** ((t - d) / c) - b) / a, (t - f) / e) + x = xp.where(t > e * cut + f, (10 ** ((t - d) / c) - b) / a, (t - f) / e) return as_float(from_range_1(x)) @@ -754,6 +761,9 @@ def log_encoding_ARRILogC4( """ E_scene = to_domain_1(E_scene) + + xp = array_namespace(E_scene) + constants = optional(constants, CONSTANTS_ARRILOGC4) a = constants.a @@ -762,9 +772,9 @@ def log_encoding_ARRILogC4( s = constants.s t = constants.t - E_p = np.where( + E_p = xp.where( E_scene >= t, - (np.log2(a * E_scene + 64) - 6) / 14 * b + c, + (xp.log2(a * E_scene + 64) - 6) / 14 * b + c, (E_scene - t) / s, ) @@ -816,6 +826,9 @@ def log_decoding_ARRILogC4( """ E_p = to_domain_1(E_p) + + xp = array_namespace(E_p) + constants = optional(constants, CONSTANTS_ARRILOGC4) a = constants.a @@ -824,7 +837,7 @@ def log_decoding_ARRILogC4( s = constants.s t = constants.t - E_scene = np.where( + E_scene = xp.where( E_p >= 0, (2 ** (14 * ((E_p - c) / b) + 6) - 64) / a, E_p * s + t, diff --git a/colour/models/rgb/transfer_functions/blackmagic_design.py b/colour/models/rgb/transfer_functions/blackmagic_design.py index 255dc718d4..d4423c080c 100644 --- a/colour/models/rgb/transfer_functions/blackmagic_design.py +++ b/colour/models/rgb/transfer_functions/blackmagic_design.py @@ -16,13 +16,18 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -93,6 +98,9 @@ def oetf_BlackmagicFilmGeneration5( """ x = to_domain_1(x) + + xp = array_namespace(x) + constants = optional(constants, CONSTANTS_BLACKMAGIC_FILM_GENERATION_5) A = constants.A @@ -102,10 +110,10 @@ def oetf_BlackmagicFilmGeneration5( E = constants.E LIN_CUT = constants.LIN_CUT - V_out = np.where( + V_out = xp.where( x < LIN_CUT, D * x + E, - A * np.log(x + B) + C, + A * xp.log(x + B) + C, ) return as_float(from_range_1(V_out)) @@ -157,6 +165,9 @@ def oetf_inverse_BlackmagicFilmGeneration5( """ y = to_domain_1(y) + + xp = array_namespace(y) + constants = optional(constants, CONSTANTS_BLACKMAGIC_FILM_GENERATION_5) A = constants.A @@ -168,9 +179,9 @@ def oetf_inverse_BlackmagicFilmGeneration5( LOG_CUT = D * LIN_CUT + E - x = np.where( + x = xp.where( y < LOG_CUT, (y - E) / D, - np.exp((y - C) / A) - B, + xp.exp((y - C) / A) - B, ) return as_float(from_range_1(x)) diff --git a/colour/models/rgb/transfer_functions/canon.py b/colour/models/rgb/transfer_functions/canon.py index 6541d9bc19..e2f77b3e1c 100644 --- a/colour/models/rgb/transfer_functions/canon.py +++ b/colour/models/rgb/transfer_functions/canon.py @@ -43,8 +43,6 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import Literal @@ -55,11 +53,13 @@ from colour.models.rgb.transfer_functions import full_to_legal, legal_to_full from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, domain_range_scale, from_range_1, to_domain_1, validate_method, + xp_select, ) __author__ = "Colour Developers" @@ -143,6 +143,7 @@ def log_encoding_CanonLog_v1( Examples -------- + >>> import numpy as np >>> log_encoding_CanonLog_v1(0.18) * 100 # doctest: +ELLIPSIS np.float64(34.3389651...) @@ -158,14 +159,16 @@ def log_encoding_CanonLog_v1( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 with domain_range_scale("ignore"): - clog = np.where( + clog = xp.where( x < log_decoding_CanonLog_v1(0.0730597, bit_depth, False), - -(0.529136 * (np.log10(-x * 10.1596 + 1)) - 0.0730597), - 0.529136 * np.log10(10.1596 * x + 1) + 0.0730597, + -(0.529136 * (xp.log10(-x * 10.1596 + 1)) - 0.0730597), + 0.529136 * xp.log10(10.1596 * x + 1) + 0.0730597, ) clog_cv = full_to_legal(clog, bit_depth) if out_normalised_code_value else clog @@ -226,9 +229,11 @@ def log_decoding_CanonLog_v1( clog = to_domain_1(clog) + xp = array_namespace(clog) + clog = legal_to_full(clog, bit_depth) if in_normalised_code_value else clog - x = np.where( + x = xp.where( clog < 0.0730597, -(10 ** ((0.0730597 - clog) / 0.529136) - 1) / 10.1596, (10 ** ((clog - 0.0730597) / 0.529136) - 1) / 10.1596, @@ -292,14 +297,16 @@ def log_encoding_CanonLog_v1_2( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 with domain_range_scale("ignore"): - clog = np.where( + clog = xp.where( x < (log_decoding_CanonLog_v1_2(0.12512248, bit_depth, True)), - -(0.45310179 * (np.log10(-x * 10.1596 + 1)) - 0.12512248), - 0.45310179 * np.log10(10.1596 * x + 1) + 0.12512248, + -(0.45310179 * (xp.log10(-x * 10.1596 + 1)) - 0.12512248), + 0.45310179 * xp.log10(10.1596 * x + 1) + 0.12512248, ) # NOTE: *Canon Log* v1.2 constants are expressed in legal range @@ -364,11 +371,13 @@ def log_decoding_CanonLog_v1_2( clog = to_domain_1(clog) + xp = array_namespace(clog) + # NOTE: *Canon Log* v1.2 constants are expressed in legal range # (studio swing). clog = clog if in_normalised_code_value else full_to_legal(clog, bit_depth) - x = np.where( + x = xp.where( clog < 0.12512248, -(10 ** ((0.12512248 - clog) / 0.45310179) - 1) / 10.1596, (10 ** ((clog - 0.12512248) / 0.45310179) - 1) / 10.1596, @@ -445,6 +454,7 @@ def log_encoding_CanonLog( Examples -------- + >>> import numpy as np >>> log_encoding_CanonLog(0.18) * 100 # doctest: +ELLIPSIS np.float64(34.3389649...) >>> log_encoding_CanonLog(0.18, method="v1") * 100 # doctest: +ELLIPSIS @@ -598,14 +608,16 @@ def log_encoding_CanonLog2_v1( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 with domain_range_scale("ignore"): - clog2 = np.where( + clog2 = xp.where( x < log_decoding_CanonLog2_v1(0.035388128, bit_depth, False), - -(0.281863093 * (np.log10(-x * 87.09937546 + 1)) - 0.035388128), - 0.281863093 * np.log10(x * 87.09937546 + 1) + 0.035388128, + -(0.281863093 * (xp.log10(-x * 87.09937546 + 1)) - 0.035388128), + 0.281863093 * xp.log10(x * 87.09937546 + 1) + 0.035388128, ) clog2_cv = full_to_legal(clog2, bit_depth) if out_normalised_code_value else clog2 @@ -666,9 +678,11 @@ def log_decoding_CanonLog2_v1( clog2 = to_domain_1(clog2) + xp = array_namespace(clog2) + clog2 = legal_to_full(clog2, bit_depth) if in_normalised_code_value else clog2 - x = np.where( + x = xp.where( clog2 < 0.035388128, -(10 ** ((0.035388128 - clog2) / 0.281863093) - 1) / 87.09937546, (10 ** ((clog2 - 0.035388128) / 0.281863093) - 1) / 87.09937546, @@ -733,14 +747,16 @@ def log_encoding_CanonLog2_v1_2( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 with domain_range_scale("ignore"): - clog2 = np.where( + clog2 = xp.where( x < (log_decoding_CanonLog2_v1_2(0.092864125, bit_depth, True)), - -(0.24136077 * (np.log10(-x * 87.09937546 + 1)) - 0.092864125), - 0.24136077 * np.log10(x * 87.09937546 + 1) + 0.092864125, + -(0.24136077 * (xp.log10(-x * 87.09937546 + 1)) - 0.092864125), + 0.24136077 * xp.log10(x * 87.09937546 + 1) + 0.092864125, ) # NOTE: *Canon Log 2* v1.2 constants are expressed in legal range @@ -804,11 +820,13 @@ def log_decoding_CanonLog2_v1_2( clog2 = to_domain_1(clog2) + xp = array_namespace(clog2) + # NOTE: *Canon Log 2* v1.2 constants are expressed in legal range # (studio swing). clog2 = clog2 if in_normalised_code_value else full_to_legal(clog2, bit_depth) - x = np.where( + x = xp.where( clog2 < 0.092864125, -(10 ** ((0.092864125 - clog2) / 0.24136077) - 1) / 87.09937546, (10 ** ((clog2 - 0.092864125) / 0.24136077) - 1) / 87.09937546, @@ -1037,21 +1055,24 @@ def log_encoding_CanonLog3_v1( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 with domain_range_scale("ignore"): - clog3 = np.select( + clog3 = xp_select( ( x < log_decoding_CanonLog3_v1(0.04076162, bit_depth, False, False), x <= log_decoding_CanonLog3_v1(0.105357102, bit_depth, False, False), x > log_decoding_CanonLog3_v1(0.105357102, bit_depth, False, False), ), ( - -0.42889912 * np.log10(-x * 14.98325 + 1) + 0.07623209, + -0.42889912 * xp.log10(-x * 14.98325 + 1) + 0.07623209, 2.3069815 * x + 0.073059361, - 0.42889912 * np.log10(x * 14.98325 + 1) + 0.069886632, + 0.42889912 * xp.log10(x * 14.98325 + 1) + 0.069886632, ), + xp=xp, ) clog3_cv = full_to_legal(clog3, bit_depth) if out_normalised_code_value else clog3 @@ -1112,15 +1133,18 @@ def log_decoding_CanonLog3_v1( clog3 = to_domain_1(clog3) + xp = array_namespace(clog3) + clog3 = legal_to_full(clog3, bit_depth) if in_normalised_code_value else clog3 - x = np.select( + x = xp_select( (clog3 < 0.04076162, clog3 <= 0.105357102, clog3 > 0.105357102), ( -(10 ** ((0.07623209 - clog3) / 0.42889912) - 1) / 14.98325, (clog3 - 0.073059361) / 2.3069815, (10 ** ((clog3 - 0.069886632) / 0.42889912) - 1) / 14.98325, ), + xp=xp, ) if out_reflection: @@ -1182,21 +1206,24 @@ def log_encoding_CanonLog3_v1_2( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 with domain_range_scale("ignore"): - clog3 = np.select( + clog3 = xp_select( ( x < log_decoding_CanonLog3_v1_2(0.097465473, bit_depth, True, False), x <= log_decoding_CanonLog3_v1_2(0.15277891, bit_depth, True, False), x > log_decoding_CanonLog3_v1_2(0.15277891, bit_depth, True, False), ), ( - -0.36726845 * np.log10(-x * 14.98325 + 1) + 0.12783901, + -0.36726845 * xp.log10(-x * 14.98325 + 1) + 0.12783901, 1.9754798 * x + 0.12512219, - 0.36726845 * np.log10(x * 14.98325 + 1) + 0.12240537, + 0.36726845 * xp.log10(x * 14.98325 + 1) + 0.12240537, ), + xp=xp, ) # NOTE: *Canon Log 3* v1.2 constants are expressed in legal range @@ -1260,17 +1287,20 @@ def log_decoding_CanonLog3_v1_2( clog3 = to_domain_1(clog3) + xp = array_namespace(clog3) + # NOTE: *Canon Log 3* v1.2 constants are expressed in legal range # (studio swing). clog3 = clog3 if in_normalised_code_value else full_to_legal(clog3, bit_depth) - x = np.select( + x = xp_select( (clog3 < 0.097465473, clog3 <= 0.15277891, clog3 > 0.15277891), ( -(10 ** ((0.12783901 - clog3) / 0.36726845) - 1) / 14.98325, (clog3 - 0.12512219) / 1.9754798, (10 ** ((clog3 - 0.12240537) / 0.36726845) - 1) / 14.98325, ), + xp=xp, ) if out_reflection: diff --git a/colour/models/rgb/transfer_functions/cineon.py b/colour/models/rgb/transfer_functions/cineon.py index cb19c21831..00d5eb6910 100644 --- a/colour/models/rgb/transfer_functions/cineon.py +++ b/colour/models/rgb/transfer_functions/cineon.py @@ -17,14 +17,18 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 ArrayLike, Domain1, Range1, ) -from colour.utilities import as_float, as_float_array, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + from_range_1, + to_domain_1, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -83,9 +87,12 @@ def log_encoding_Cineon( """ x = to_domain_1(x) - black_offset = as_float_array(black_offset) - y = (685 + 300 * np.log10(x * (1 - black_offset) + black_offset)) / 1023 + xp = array_namespace(x) + + black_offset = xp_as_float_array(black_offset, xp=xp, like=x) + + y = (685 + 300 * xp.log10(x * (1 - black_offset) + black_offset)) / 1023 return as_float(from_range_1(y)) @@ -134,7 +141,10 @@ def log_decoding_Cineon( """ y = to_domain_1(y) - black_offset = as_float_array(black_offset) + + xp = array_namespace(y) + + black_offset = xp_as_float_array(black_offset, xp=xp, like=y) x = (10 ** ((1023 * y - 685) / 300) - black_offset) / (1 - black_offset) diff --git a/colour/models/rgb/transfer_functions/common.py b/colour/models/rgb/transfer_functions/common.py index ef7d5cce16..d5598dc1e3 100644 --- a/colour/models/rgb/transfer_functions/common.py +++ b/colour/models/rgb/transfer_functions/common.py @@ -9,12 +9,16 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayReal -from colour.utilities import as_float, as_float_array, as_int, as_int_array +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + as_int, + as_int_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -63,10 +67,9 @@ def CV_range( """ if is_legal: - ranges = np.array([16, 235]) - ranges *= 2 ** (bit_depth - 8) + ranges = as_int_array([16, 235]) * 2 ** (bit_depth - 8) else: - ranges = np.array([0, 2**bit_depth - 1]) + ranges = as_int_array([0, 2**bit_depth - 1]) if not is_int: ranges = as_float_array(ranges) / (2**bit_depth - 1) @@ -127,16 +130,18 @@ def legal_to_full( CV = as_float_array(CV) + xp = array_namespace(CV) + MV = 2**bit_depth - 1 - CV_full = as_int_array(np.round(CV)) if in_int else CV * MV + CV_full = as_int_array(xp.round(CV)) if in_int else CV * MV B, W = CV_range(bit_depth, True, True) CV_full = (CV_full - B) / (W - B) if out_int: - return as_int(np.round(CV_full * MV)) + return as_int(xp.round(CV_full * MV)) return as_float(CV_full) @@ -194,15 +199,17 @@ def full_to_legal( CV = as_float_array(CV) + xp = array_namespace(CV) + MV = 2**bit_depth - 1 - CV_legal = as_int_array(np.round(CV / MV)) if in_int else CV + CV_legal = as_int_array(xp.round(CV / MV)) if in_int else CV B, W = CV_range(bit_depth, True, True) CV_legal = (W - B) * CV_legal + B if out_int: - return as_int(np.round(CV_legal)) + return as_int(xp.round(CV_legal)) return as_float(CV_legal / MV) diff --git a/colour/models/rgb/transfer_functions/davinci_intermediate.py b/colour/models/rgb/transfer_functions/davinci_intermediate.py index d9af7b5762..b1cba12029 100644 --- a/colour/models/rgb/transfer_functions/davinci_intermediate.py +++ b/colour/models/rgb/transfer_functions/davinci_intermediate.py @@ -18,13 +18,18 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -94,6 +99,9 @@ def oetf_DaVinciIntermediate( """ L = to_domain_1(L) + + xp = array_namespace(L) + constants = optional(constants, CONSTANTS_DAVINCI_INTERMEDIATE) DI_LIN_CUT = constants.DI_LIN_CUT @@ -102,10 +110,10 @@ def oetf_DaVinciIntermediate( DI_C = constants.DI_C DI_M = constants.DI_M - V_out = np.where( + V_out = xp.where( L <= DI_LIN_CUT, L * DI_M, - DI_C * (np.log2(L + DI_A) + DI_B), + DI_C * (xp.log2(L + DI_A) + DI_B), ) return as_float(from_range_1(V_out)) @@ -157,6 +165,9 @@ def oetf_inverse_DaVinciIntermediate( """ V = to_domain_1(V) + + xp = array_namespace(V) + constants = optional(constants, CONSTANTS_DAVINCI_INTERMEDIATE) DI_LOG_CUT = constants.DI_LOG_CUT @@ -165,7 +176,7 @@ def oetf_inverse_DaVinciIntermediate( DI_C = constants.DI_C DI_M = constants.DI_M - L_out = np.where( + L_out = xp.where( V <= DI_LOG_CUT, V / DI_M, 2 ** ((V / DI_C) - DI_B) - DI_A, diff --git a/colour/models/rgb/transfer_functions/dcdm.py b/colour/models/rgb/transfer_functions/dcdm.py index 61fed58142..ca9ee36502 100644 --- a/colour/models/rgb/transfer_functions/dcdm.py +++ b/colour/models/rgb/transfer_functions/dcdm.py @@ -23,14 +23,12 @@ import typing -import numpy as np - from colour.algebra import spow if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat, NDArrayReal -from colour.utilities import as_float, as_float_array, as_int +from colour.utilities import array_namespace, as_float, as_float_array, as_int __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -101,10 +99,12 @@ def eotf_inverse_DCDM(XYZ: ArrayLike, out_int: bool = False) -> NDArrayReal: XYZ = as_float_array(XYZ) + xp = array_namespace(XYZ) + XYZ_p = spow(XYZ / 52.37, 1 / 2.6) if out_int: - return as_int(np.round(4095 * XYZ_p)) + return as_int(xp.round(4095 * XYZ_p)) return as_float(XYZ_p) diff --git a/colour/models/rgb/transfer_functions/dicom_gsdf.py b/colour/models/rgb/transfer_functions/dicom_gsdf.py index eff103b86d..c90da1a324 100644 --- a/colour/models/rgb/transfer_functions/dicom_gsdf.py +++ b/colour/models/rgb/transfer_functions/dicom_gsdf.py @@ -27,8 +27,6 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import NDArrayReal @@ -39,6 +37,7 @@ ) from colour.utilities import ( Structure, + array_namespace, as_float, as_int, from_range_1, @@ -134,9 +133,12 @@ def eotf_inverse_DICOMGSDF( """ L = to_domain_1(L) + + xp = array_namespace(L) + constants = optional(constants, CONSTANTS_DICOMGSDF) - L_lg = np.log10(L) + L_lg = xp.log10(L) A = constants.A B = constants.B @@ -161,7 +163,7 @@ def eotf_inverse_DICOMGSDF( ) if out_int: - return as_int(np.round(J)) + return as_int(xp.round(J)) return as_float(from_range_1(J / 1023)) @@ -217,6 +219,9 @@ def eotf_DICOMGSDF( """ J = to_domain_1(J) + + xp = array_namespace(J) + constants = optional(constants, CONSTANTS_DICOMGSDF) if not in_int: @@ -233,7 +238,7 @@ def eotf_DICOMGSDF( k = constants.k m = constants.m - J_ln = np.log(J) + J_ln = xp.log(J) J_ln2 = J_ln**2 J_ln3 = J_ln**3 J_ln4 = J_ln**4 diff --git a/colour/models/rgb/transfer_functions/dji_d_log.py b/colour/models/rgb/transfer_functions/dji_d_log.py index 336f230c7d..81f7da42f3 100644 --- a/colour/models/rgb/transfer_functions/dji_d_log.py +++ b/colour/models/rgb/transfer_functions/dji_d_log.py @@ -17,13 +17,11 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -78,10 +76,12 @@ def log_encoding_DJIDLog(x: Domain1) -> Range1: x = to_domain_1(x) - y = np.where( + xp = array_namespace(x) + + y = xp.where( x <= 0.0078, 6.025 * x + 0.0929, - (np.log10(x * 0.9892 + 0.0108)) * 0.256663 + 0.584555, + (xp.log10(x * 0.9892 + 0.0108)) * 0.256663 + 0.584555, ) return as_float(from_range_1(y)) @@ -127,7 +127,9 @@ def log_decoding_DJIDLog(y: Domain1) -> Range1: y = to_domain_1(y) - x = np.where( + xp = array_namespace(y) + + x = xp.where( y <= 0.14, (y - 0.0929) / 6.025, (10 ** (3.89616 * y - 2.27752) - 0.0108) / 0.9892, diff --git a/colour/models/rgb/transfer_functions/exponent.py b/colour/models/rgb/transfer_functions/exponent.py index 284f4027b0..a466b4485a 100644 --- a/colour/models/rgb/transfer_functions/exponent.py +++ b/colour/models/rgb/transfer_functions/exponent.py @@ -20,14 +20,18 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode if typing.TYPE_CHECKING: from colour.hints import ArrayLike, Literal, NDArrayFloat -from colour.utilities import as_float, as_float_array, validate_method +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + validate_method, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -144,7 +148,11 @@ def exponent_function_basic( """ x = as_float_array(x) - exponent = as_float_array(exponent) + + xp = array_namespace(x) + + exponent = xp_as_float_array(exponent, xp=xp, like=x) + style = validate_method( style, ( @@ -166,21 +174,21 @@ def exponent_forward(x: NDArrayFloat) -> NDArrayFloat: def exponent_reverse(y: NDArrayFloat) -> NDArrayFloat: """Return the input raised to the inverse exponent value.""" - return y ** (as_float_array(1) / exponent) + return y ** (1 / exponent) m_x = x >= 0 if style == "basicfwd": - y = np.where(m_x, exponent_forward(x), 0) + y = xp.where(m_x, exponent_forward(x), 0) elif style == "basicrev": - y = np.where(m_x, exponent_reverse(x), 0) + y = xp.where(m_x, exponent_reverse(x), 0) elif style == "basicmirrorfwd": - y = np.where(m_x, exponent_forward(x), -exponent_forward(-x)) + y = xp.where(m_x, exponent_forward(x), -exponent_forward(-x)) elif style == "basicmirrorrev": - y = np.where(m_x, exponent_reverse(x), -exponent_reverse(-x)) + y = xp.where(m_x, exponent_reverse(x), -exponent_reverse(-x)) elif style == "basicpassthrufwd": - y = np.where(m_x, exponent_forward(x), x) + y = xp.where(m_x, exponent_forward(x), x) else: # style == 'basicpassthrurev' - y = np.where(m_x, exponent_reverse(x), x) + y = xp.where(m_x, exponent_reverse(x), x) return as_float(y) @@ -270,8 +278,12 @@ def exponent_function_monitor_curve( """ x = as_float_array(x) - exponent = as_float_array(exponent) - offset = as_float_array(offset) + + xp = array_namespace(x) + + exponent = xp_as_float_array(exponent, xp=xp, like=x) + offset = xp_as_float_array(offset, xp=xp, like=x) + style = validate_method( style, ( @@ -284,7 +296,7 @@ def exponent_function_monitor_curve( ) with sdiv_mode(): - s = as_float_array( + s = ( sdiv(exponent - 1, offset) * sdiv(exponent * offset, (exponent - 1) * (offset + 1)) ** exponent ) @@ -297,9 +309,9 @@ def monitor_curve_forward( with sdiv_mode(): x_break = sdiv(offset, exponent - 1) - y = as_float_array(x * s) + y = x * s - return np.where(x >= x_break, ((x + offset) / (1 + offset)) ** exponent, y) + return xp.where(x >= x_break, ((x + offset) / (1 + offset)) ** exponent, y) def monitor_curve_reverse( y: NDArrayFloat, offset: NDArrayFloat, exponent: NDArrayFloat @@ -311,9 +323,9 @@ def monitor_curve_reverse( sdiv(exponent * offset, (exponent - 1) * (1 + offset)) ) ** exponent - x = as_float_array(y / s) + x = y / s - return np.where( + return xp.where( y >= y_break, ((1 + offset) * (y ** (1 / exponent))) - offset, x ) @@ -323,13 +335,13 @@ def monitor_curve_reverse( elif style == "moncurverev": y = monitor_curve_reverse(x, offset, exponent) elif style == "moncurvemirrorfwd": - y = np.where( + y = xp.where( m_x, monitor_curve_forward(x, offset, exponent), -monitor_curve_forward(-x, offset, exponent), ) else: # style == 'moncurvemirrorrev' - y = np.where( + y = xp.where( m_x, monitor_curve_reverse(x, offset, exponent), -monitor_curve_reverse(-x, offset, exponent), diff --git a/colour/models/rgb/transfer_functions/filmic_pro.py b/colour/models/rgb/transfer_functions/filmic_pro.py index ad68d6182d..885d9dde32 100644 --- a/colour/models/rgb/transfer_functions/filmic_pro.py +++ b/colour/models/rgb/transfer_functions/filmic_pro.py @@ -15,14 +15,13 @@ from __future__ import annotations -import numpy as np - from colour.algebra import Extrapolator, LinearInterpolator +from colour.constants import EPSILON from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -88,7 +87,9 @@ def log_encoding_FilmicPro6(t: Domain1) -> Range1: t = to_domain_1(t) - y = 0.371 * (np.sqrt(t) + 0.28257 * np.log(t) + 1.69542) + xp = array_namespace(t) + + y = 0.371 * (xp.sqrt(t) + 0.28257 * xp.log(t) + 1.69542) return as_float(from_range_1(y)) @@ -110,10 +111,13 @@ def _log_decoding_FilmicPro6_interpolator() -> Extrapolator: global _CACHE_LOG_DECODING_FILMICPRO_INTERPOLATOR # noqa: PLW0603 - t = np.arange(0, 1, 0.0001) + xp = array_namespace() + + t = xp.arange(EPSILON, 1, 0.0001) if _CACHE_LOG_DECODING_FILMICPRO_INTERPOLATOR is None: _CACHE_LOG_DECODING_FILMICPRO_INTERPOLATOR = Extrapolator( - LinearInterpolator(log_encoding_FilmicPro6(t), t) + LinearInterpolator(log_encoding_FilmicPro6(t), t), + left=0, ) return _CACHE_LOG_DECODING_FILMICPRO_INTERPOLATOR @@ -164,7 +168,7 @@ def log_decoding_FilmicPro6(y: Domain1) -> Range1: Examples -------- >>> log_decoding_FilmicPro6(0.6066345199247033) # doctest: +ELLIPSIS - np.float64(0.1800000...) + np.float64(0.179999...) """ y = to_domain_1(y) diff --git a/colour/models/rgb/transfer_functions/filmlight_t_log.py b/colour/models/rgb/transfer_functions/filmlight_t_log.py index d2d62d57a7..1b82129737 100644 --- a/colour/models/rgb/transfer_functions/filmlight_t_log.py +++ b/colour/models/rgb/transfer_functions/filmlight_t_log.py @@ -15,13 +15,13 @@ from __future__ import annotations -import numpy as np +import math from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -111,20 +111,22 @@ def log_encoding_FilmLightTLog( x = to_domain_1(x) - b = 1 / (0.7107 + 1.2359 * np.log(w * g)) + xp = array_namespace(x) + + b = 1 / (0.7107 + 1.2359 * math.log(w * g)) gs = g / (1 - o) C = b / gs - a = 1 - b * np.log(w + C) - y0 = a + b * np.log(C) + a = 1 - b * math.log(w + C) + y0 = a + b * math.log(C) s = (1 - o) / (1 - y0) A = 1 + (a - 1) * s B = b * s G = gs * s - t = np.where( + t = xp.where( x < 0.0, G * x + o, - np.log(x + C) * B + A, + xp.log(x + C) * B + A, ) return as_float(from_range_1(t)) @@ -207,20 +209,22 @@ def log_decoding_FilmLightTLog( t = to_domain_1(t) - b = 1 / (0.7107 + 1.2359 * np.log(w * g)) + xp = array_namespace(t) + + b = 1 / (0.7107 + 1.2359 * math.log(w * g)) gs = g / (1 - o) C = b / gs - a = 1 - b * np.log(w + C) - y0 = a + b * np.log(C) + a = 1 - b * math.log(w + C) + y0 = a + b * math.log(C) s = (1 - o) / (1 - y0) A = 1 + (a - 1) * s B = b * s G = gs * s - x = np.where( + x = xp.where( t < o, (t - o) / G, - np.exp((t - A) / B) - C, + xp.exp((t - A) / B) - C, ) return as_float(from_range_1(x)) diff --git a/colour/models/rgb/transfer_functions/fujifilm_f_log.py b/colour/models/rgb/transfer_functions/fujifilm_f_log.py index 961a336093..e040bcfe22 100644 --- a/colour/models/rgb/transfer_functions/fujifilm_f_log.py +++ b/colour/models/rgb/transfer_functions/fujifilm_f_log.py @@ -19,14 +19,19 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) from colour.models.rgb.transfer_functions import full_to_legal, legal_to_full -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -118,6 +123,7 @@ def log_encoding_FLog( Examples -------- + >>> import numpy as np >>> log_encoding_FLog(0.18) # doctest: +ELLIPSIS np.float64(0.4593184...) @@ -132,6 +138,9 @@ def log_encoding_FLog( """ in_r = to_domain_1(in_r) + + xp = array_namespace(in_r) + constants = optional(constants, CONSTANTS_FLOG) if not in_reflection: @@ -145,10 +154,10 @@ def log_encoding_FLog( e = constants.e f = constants.f - out_r = np.where( + out_r = xp.where( in_r < cut1, e * in_r + f, - c * np.log10(a * in_r + b) + d, + c * xp.log10(a * in_r + b) + d, ) out_r_cv = out_r if out_normalised_code_value else legal_to_full(out_r, bit_depth) @@ -212,6 +221,9 @@ def log_decoding_FLog( """ out_r = to_domain_1(out_r) + + xp = array_namespace(out_r) + constants = optional(constants, CONSTANTS_FLOG) out_r = out_r if in_normalised_code_value else full_to_legal(out_r, bit_depth) @@ -224,7 +236,7 @@ def log_decoding_FLog( e = constants.e f = constants.f - in_r = np.where( + in_r = xp.where( out_r < cut2, (out_r - f) / e, (10 ** ((out_r - d) / c)) / a - b / a, @@ -285,6 +297,7 @@ def log_encoding_FLog2( Examples -------- + >>> import numpy as np >>> log_encoding_FLog2(0.18) # doctest: +ELLIPSIS np.float64(0.3910072...) diff --git a/colour/models/rgb/transfer_functions/gamma.py b/colour/models/rgb/transfer_functions/gamma.py index 548d09e658..b886bf6d42 100644 --- a/colour/models/rgb/transfer_functions/gamma.py +++ b/colour/models/rgb/transfer_functions/gamma.py @@ -12,14 +12,18 @@ import typing -import numpy as np - from colour.algebra import spow if typing.TYPE_CHECKING: from colour.hints import ArrayLike, Literal, NDArrayFloat -from colour.utilities import as_float, as_float_array, validate_method +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + validate_method, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -85,7 +89,10 @@ def gamma_function( """ a = as_float_array(a) - exponent = as_float_array(exponent) + + xp = array_namespace(a) + + exponent = xp_as_float_array(exponent, xp=xp, like=a) negative_number_handling = validate_method( negative_number_handling, ("Indeterminate", "Mirror", "Preserve", "Clamp"), @@ -99,7 +106,7 @@ def gamma_function( return spow(a, exponent) if negative_number_handling == "preserve": - return as_float(np.where(a <= 0, a, a**exponent)) + return as_float(xp.where(a <= 0, a, a**exponent)) # negative_number_handling == 'clamp': - return as_float(np.where(a <= 0, 0, a**exponent)) + return as_float(xp.where(a <= 0, 0, a**exponent)) diff --git a/colour/models/rgb/transfer_functions/gopro.py b/colour/models/rgb/transfer_functions/gopro.py index 0b483c0b5b..c5dca35331 100644 --- a/colour/models/rgb/transfer_functions/gopro.py +++ b/colour/models/rgb/transfer_functions/gopro.py @@ -17,13 +17,13 @@ from __future__ import annotations -import numpy as np +import math from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -78,7 +78,9 @@ def log_encoding_Protune(x: Domain1) -> Range1: x = to_domain_1(x) - y = np.log1p(x * 112) / np.log(113) + xp = array_namespace(x) + + y = xp.log1p(x * 112) / math.log(113) return as_float(from_range_1(y)) diff --git a/colour/models/rgb/transfer_functions/itur_bt_1361.py b/colour/models/rgb/transfer_functions/itur_bt_1361.py index b7f5344372..27aa95f604 100644 --- a/colour/models/rgb/transfer_functions/itur_bt_1361.py +++ b/colour/models/rgb/transfer_functions/itur_bt_1361.py @@ -19,15 +19,19 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) from colour.models.rgb.transfer_functions import oetf_BT709, oetf_inverse_BT709 -from colour.utilities import as_float, domain_range_scale, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + domain_range_scale, + from_range_1, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -87,11 +91,13 @@ def oetf_BT1361(L: Domain1) -> Range1: L = to_domain_1(L) + xp = array_namespace(L) + with domain_range_scale("ignore"): - E_p = np.where( + E_p = xp.where( L >= 0, oetf_BT709(L), - np.where( + xp.where( L <= -0.0045, # L in [-0.25, -0.0045] range -(1.099 * spow(-4 * L, 0.45) - 0.099) / 4, @@ -148,11 +154,13 @@ def oetf_inverse_BT1361(E_p: Domain1) -> Range1: E_p = to_domain_1(E_p) + xp = array_namespace(E_p) + with domain_range_scale("ignore"): - L = np.where( + L = xp.where( E_p >= 0, oetf_inverse_BT709(E_p), - np.where( + xp.where( E_p <= 4.500 * -0.0045, # L in [-0.25, -0.0045] range -spow((-4 * E_p + 0.099) / 1.099, 1 / 0.45) / 4, diff --git a/colour/models/rgb/transfer_functions/itur_bt_1886.py b/colour/models/rgb/transfer_functions/itur_bt_1886.py index 1a9e7c1d0e..ac128b9c12 100644 --- a/colour/models/rgb/transfer_functions/itur_bt_1886.py +++ b/colour/models/rgb/transfer_functions/itur_bt_1886.py @@ -20,14 +20,12 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -152,12 +150,14 @@ def eotf_BT1886(V: Domain1, L_B: float = 0, L_W: float = 1) -> Range1: V = to_domain_1(V) + xp = array_namespace(V) + gamma = 2.40 gamma_d = 1 / gamma n = L_W**gamma_d - L_B**gamma_d a = n**gamma b = L_B**gamma_d / n - L = a * spow(np.maximum(V + b, 0), gamma) + L = a * spow(xp.clip(V + b, min=0), gamma) return as_float(from_range_1(L)) diff --git a/colour/models/rgb/transfer_functions/itur_bt_2020.py b/colour/models/rgb/transfer_functions/itur_bt_2020.py index d6bcb4d6eb..4aa7229bf9 100644 --- a/colour/models/rgb/transfer_functions/itur_bt_2020.py +++ b/colour/models/rgb/transfer_functions/itur_bt_2020.py @@ -20,8 +20,6 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, @@ -29,6 +27,7 @@ ) from colour.utilities import ( Structure, + array_namespace, as_float, domain_range_scale, from_range_1, @@ -121,12 +120,15 @@ def oetf_BT2020( """ E = to_domain_1(E) + + xp = array_namespace(E) + constants = optional(constants, CONSTANTS_BT2020) a = constants.alpha(is_12_bits_system) b = constants.beta(is_12_bits_system) - E_p = np.where(b > E, E * 4.5, a * spow(E, 0.45) - (a - 1)) + E_p = xp.where(b > E, E * 4.5, a * spow(E, 0.45) - (a - 1)) return as_float(from_range_1(E_p)) @@ -182,13 +184,16 @@ def oetf_inverse_BT2020( """ E_p = to_domain_1(E_p) + + xp = array_namespace(E_p) + constants = optional(constants, CONSTANTS_BT2020) a = constants.alpha(is_12_bits_system) b = constants.beta(is_12_bits_system) with domain_range_scale("ignore"): - E = np.where( + E = xp.where( E_p < oetf_BT2020(b), E_p / 4.5, spow((E_p + (a - 1)) / a, 1 / 0.45), diff --git a/colour/models/rgb/transfer_functions/itur_bt_2100.py b/colour/models/rgb/transfer_functions/itur_bt_2100.py index a46d18b958..0be8f3e8b2 100644 --- a/colour/models/rgb/transfer_functions/itur_bt_2100.py +++ b/colour/models/rgb/transfer_functions/itur_bt_2100.py @@ -53,6 +53,7 @@ from __future__ import annotations +import math import typing import numpy as np @@ -82,6 +83,7 @@ from colour.utilities import ( CanonicalMapping, Structure, + array_namespace, as_float, as_float_array, as_float_scalar, @@ -94,6 +96,8 @@ tstack, usage_warning, validate_method, + xp_as_float_array, + xp_atleast_1d, ) __author__ = "Colour Developers" @@ -450,9 +454,10 @@ def gamma_function_BT2100_HLG(L_W: float = 1000) -> float: np.float64(1.3264325...) >>> gamma_function_BT2100_HLG(4000) # doctest: +ELLIPSIS np.float64(1.4528651...) + """ - gamma = 1.2 + 0.42 * np.log10(L_W / 1000) + gamma = 1.2 + 0.42 * math.log10(L_W / 1000) return as_float_scalar(gamma) @@ -592,7 +597,7 @@ def black_level_lift_BT2100_HLG( gamma = optional(gamma, gamma_function_BT2100_HLG(L_W)) - beta = np.sqrt(3 * spow((L_B / L_W), 1 / gamma)) + beta = math.sqrt(3 * spow((L_B / L_W), 1 / gamma)) return as_float_scalar(beta) @@ -1122,7 +1127,9 @@ def ootf_BT2100_HLG_1( E = to_domain_1(E) - is_single_channel = np.atleast_1d(E).shape[-1] != 3 + xp = array_namespace(E) + + is_single_channel = xp_atleast_1d(E, xp=xp).shape[-1] != 3 if is_single_channel: usage_warning( @@ -1138,13 +1145,17 @@ def ootf_BT2100_HLG_1( alpha = L_W - L_B beta = L_B - Y_S = np.sum(WEIGHTS_BT2100_HLG * tstack([R_S, G_S, B_S]), axis=-1) + Y_S = xp.sum( + xp_as_float_array(WEIGHTS_BT2100_HLG, xp=xp, like=R_S) + * tstack([R_S, G_S, B_S]), + axis=-1, + ) gamma = optional(gamma, gamma_function_BT2100_HLG(L_W)) - R_D = alpha * R_S * np.abs(Y_S) ** (gamma - 1) + beta - G_D = alpha * G_S * np.abs(Y_S) ** (gamma - 1) + beta - B_D = alpha * B_S * np.abs(Y_S) ** (gamma - 1) + beta + R_D = alpha * R_S * xp.abs(Y_S) ** (gamma - 1) + beta + G_D = alpha * G_S * xp.abs(Y_S) ** (gamma - 1) + beta + B_D = alpha * B_S * xp.abs(Y_S) ** (gamma - 1) + beta if is_single_channel: return as_float(from_range_1(R_D)) @@ -1210,7 +1221,9 @@ def ootf_BT2100_HLG_2( E = to_domain_1(E) - is_single_channel = np.atleast_1d(E).shape[-1] != 3 + xp = array_namespace(E) + + is_single_channel = xp_atleast_1d(E, xp=xp).shape[-1] != 3 if is_single_channel: usage_warning( @@ -1225,13 +1238,17 @@ def ootf_BT2100_HLG_2( alpha = L_W - Y_S = np.sum(WEIGHTS_BT2100_HLG * tstack([R_S, G_S, B_S]), axis=-1) + Y_S = xp.sum( + xp_as_float_array(WEIGHTS_BT2100_HLG, xp=xp, like=R_S) + * tstack([R_S, G_S, B_S]), + axis=-1, + ) gamma = optional(gamma, gamma_function_BT2100_HLG(L_W)) - R_D = alpha * R_S * np.abs(Y_S) ** (gamma - 1) - G_D = alpha * G_S * np.abs(Y_S) ** (gamma - 1) - B_D = alpha * B_S * np.abs(Y_S) ** (gamma - 1) + R_D = alpha * R_S * xp.abs(Y_S) ** (gamma - 1) + G_D = alpha * G_S * xp.abs(Y_S) ** (gamma - 1) + B_D = alpha * B_S * xp.abs(Y_S) ** (gamma - 1) if is_single_channel: return as_float(from_range_1(R_D)) @@ -1391,7 +1408,9 @@ def ootf_inverse_BT2100_HLG_1( F_D = to_domain_1(F_D) - is_single_channel = np.atleast_1d(F_D).shape[-1] != 3 + xp = array_namespace(F_D) + + is_single_channel = xp_atleast_1d(F_D, xp=xp).shape[-1] != 3 if is_single_channel: usage_warning( @@ -1404,26 +1423,30 @@ def ootf_inverse_BT2100_HLG_1( else: R_D, G_D, B_D = tsplit(F_D) - Y_D = np.sum(WEIGHTS_BT2100_HLG * tstack([R_D, G_D, B_D]), axis=-1) + Y_D = xp.sum( + xp_as_float_array(WEIGHTS_BT2100_HLG, xp=xp, like=R_D) + * tstack([R_D, G_D, B_D]), + axis=-1, + ) alpha = L_W - L_B beta = L_B gamma = optional(gamma, gamma_function_BT2100_HLG(L_W)) - Y_D_beta = np.abs((Y_D - beta) / alpha) ** ((1 - gamma) / gamma) + Y_D_beta = xp.abs((Y_D - beta) / alpha) ** ((1 - gamma) / gamma) - R_S = np.where( + R_S = xp.where( beta == Y_D, 0.0, Y_D_beta * (R_D - beta) / alpha, ) - G_S = np.where( + G_S = xp.where( beta == Y_D, 0.0, Y_D_beta * (G_D - beta) / alpha, ) - B_S = np.where( + B_S = xp.where( beta == Y_D, 0.0, Y_D_beta * (B_D - beta) / alpha, @@ -1494,7 +1517,9 @@ def ootf_inverse_BT2100_HLG_2( F_D = to_domain_1(F_D) - is_single_channel = np.atleast_1d(F_D).shape[-1] != 3 + xp = array_namespace(F_D) + + is_single_channel = xp_atleast_1d(F_D, xp=xp).shape[-1] != 3 if is_single_channel: usage_warning( @@ -1507,25 +1532,29 @@ def ootf_inverse_BT2100_HLG_2( else: R_D, G_D, B_D = tsplit(F_D) - Y_D = np.sum(WEIGHTS_BT2100_HLG * tstack([R_D, G_D, B_D]), axis=-1) + Y_D = xp.sum( + xp_as_float_array(WEIGHTS_BT2100_HLG, xp=xp, like=R_D) + * tstack([R_D, G_D, B_D]), + axis=-1, + ) alpha = L_W gamma = optional(gamma, gamma_function_BT2100_HLG(L_W)) - Y_D_alpha = np.abs(Y_D / alpha) ** ((1 - gamma) / gamma) + Y_D_alpha = xp.abs(Y_D / alpha) ** ((1 - gamma) / gamma) - R_S = np.where( + R_S = xp.where( Y_D == 0, 0.0, Y_D_alpha * R_D / alpha, ) - G_S = np.where( + G_S = xp.where( Y_D == 0, 0.0, Y_D_alpha * G_D / alpha, ) - B_S = np.where( + B_S = xp.where( Y_D == 0, 0.0, Y_D_alpha * B_D / alpha, diff --git a/colour/models/rgb/transfer_functions/itur_bt_601.py b/colour/models/rgb/transfer_functions/itur_bt_601.py index 06f9ed9bd5..637e48623d 100644 --- a/colour/models/rgb/transfer_functions/itur_bt_601.py +++ b/colour/models/rgb/transfer_functions/itur_bt_601.py @@ -20,14 +20,18 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, domain_range_scale, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + domain_range_scale, + from_range_1, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -83,7 +87,9 @@ def oetf_BT601(L: Domain1) -> Range1: L = to_domain_1(L) - E = np.where(L < 0.018, L * 4.5, 1.099 * spow(L, 0.45) - 0.099) + xp = array_namespace(L) + + E = xp.where(L < 0.018, L * 4.5, 1.099 * spow(L, 0.45) - 0.099) return as_float(from_range_1(E)) @@ -129,8 +135,10 @@ def oetf_inverse_BT601(E: Domain1) -> Range1: E = to_domain_1(E) + xp = array_namespace(E) + with domain_range_scale("ignore"): - L = np.where( + L = xp.where( oetf_BT601(0.018) > E, E / 4.5, spow((E + 0.099) / 1.099, 1 / 0.45), diff --git a/colour/models/rgb/transfer_functions/itut_h_273.py b/colour/models/rgb/transfer_functions/itut_h_273.py index 7ee87b47e0..e90608a1d3 100644 --- a/colour/models/rgb/transfer_functions/itut_h_273.py +++ b/colour/models/rgb/transfer_functions/itut_h_273.py @@ -27,7 +27,7 @@ from __future__ import annotations -import numpy as np +import math from colour.algebra import spow from colour.hints import ( # noqa: TC001 @@ -40,7 +40,13 @@ eotf_inverse_sRGB, eotf_sRGB, ) -from colour.utilities import as_float, as_float_array, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + from_range_1, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -112,10 +118,12 @@ def oetf_H273_Log(L_c: Domain1) -> Range1: L_c = to_domain_1(L_c) - V = np.where( + xp = array_namespace(L_c) + + V = xp.where( L_c >= 0.01, # L_c in [0.01, 1] range - 1 + np.log10(L_c) / 2, + 1 + xp.log10(L_c) / 2, # L_c in [0, 0.01] range 0, ) @@ -173,7 +181,9 @@ def oetf_inverse_H273_Log(V: Domain1) -> Range1: V = to_domain_1(V) - L_c = np.where( + xp = array_namespace(V) + + L_c = xp.where( oetf_H273_Log(0.01) <= V, # L_c in [0.01, 1] range spow(10, (V - 1) * 2), @@ -237,10 +247,12 @@ def oetf_H273_LogSqrt(L_c: Domain1) -> Range1: L_c = to_domain_1(L_c) - V = np.where( - L_c >= np.sqrt(10) / 1000, + xp = array_namespace(L_c) + + V = xp.where( + L_c >= math.sqrt(10) / 1000, # L_c in [sqrt(10)/1000, 1] range - 1 + np.log10(L_c) / 2.5, + 1 + xp.log10(L_c) / 2.5, # L_c in [0, sqrt(10)/1000] range 0, ) @@ -297,8 +309,10 @@ def oetf_inverse_H273_LogSqrt(V: Domain1) -> Range1: V = to_domain_1(V) - L_c = np.where( - oetf_H273_LogSqrt(np.sqrt(10) / 1000) <= V, + xp = array_namespace(V) + + L_c = xp.where( + oetf_H273_LogSqrt(math.sqrt(10) / 1000) <= V, # L_c in [sqrt(10)/1000, 1] range spow(10, (V - 1) * 2.5), # L_c in [0, sqrt(10)/1000] range @@ -359,7 +373,9 @@ def oetf_H273_IEC61966_2(L_c: Domain1) -> Range1: L_c = as_float_array(L_c) - V = np.where( + xp = array_namespace(L_c) + + V = xp.where( L_c >= 0, eotf_inverse_sRGB(L_c), -eotf_inverse_sRGB(-L_c), @@ -423,7 +439,9 @@ def oetf_inverse_H273_IEC61966_2( V = as_float_array(V) - L_c = np.where( + xp = array_namespace(V) + + L_c = xp.where( V >= 0, eotf_sRGB(V), -eotf_sRGB(-V), diff --git a/colour/models/rgb/transfer_functions/leica_l_log.py b/colour/models/rgb/transfer_functions/leica_l_log.py index fc6b2425ed..c2e87c1bce 100644 --- a/colour/models/rgb/transfer_functions/leica_l_log.py +++ b/colour/models/rgb/transfer_functions/leica_l_log.py @@ -16,15 +16,20 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) from colour.models.rgb.transfer_functions import full_to_legal, legal_to_full -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -106,6 +111,9 @@ def log_encoding_LLog( """ LSR = to_domain_1(LSR) + + xp = array_namespace(LSR) + constants = optional(constants, CONSTANTS_LLOG) if not in_reflection: @@ -119,10 +127,10 @@ def log_encoding_LLog( e = constants.e f = constants.f - LLog = np.where( + LLog = xp.where( cut1 >= LSR, a * LSR + b, - c * np.log10(d * LSR + e) + f, + c * xp.log10(d * LSR + e) + f, ) LLog_cv = LLog if out_normalised_code_value else legal_to_full(LLog, bit_depth) @@ -185,6 +193,9 @@ def log_decoding_LLog( """ LLog = to_domain_1(LLog) + + xp = array_namespace(LLog) + constants = optional(constants, CONSTANTS_LLOG) LLog = LLog if in_normalised_code_value else full_to_legal(LLog, bit_depth) @@ -197,7 +208,7 @@ def log_decoding_LLog( e = constants.e f = constants.f - LSR = np.where( + LSR = xp.where( LLog <= cut2, (LLog - b) / a, (spow(10, (LLog - f) / c) - e) / d, diff --git a/colour/models/rgb/transfer_functions/log.py b/colour/models/rgb/transfer_functions/log.py index 9625428a00..70a957f297 100644 --- a/colour/models/rgb/transfer_functions/log.py +++ b/colour/models/rgb/transfer_functions/log.py @@ -37,10 +37,9 @@ from __future__ import annotations +import math import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode if typing.TYPE_CHECKING: @@ -52,6 +51,7 @@ from colour.hints import cast from colour.utilities import ( + array_namespace, as_float, as_float_array, optional, @@ -133,6 +133,9 @@ def logarithmic_function_basic( """ x = as_float_array(x) + + xp = array_namespace(x) + style = validate_method( style, ("log10", "antiLog10", "log2", "antiLog2", "logB", "antiLogB"), @@ -140,19 +143,19 @@ def logarithmic_function_basic( ) if style == "log10": - return as_float(np.where(x >= FLT_MIN, np.log10(x), np.log10(FLT_MIN))) + return as_float(xp.where(x >= FLT_MIN, xp.log10(x), math.log10(FLT_MIN))) if style == "antilog10": return as_float(10**x) if style == "log2": - return as_float(np.where(x >= FLT_MIN, np.log2(x), np.log2(FLT_MIN))) + return as_float(xp.where(x >= FLT_MIN, xp.log2(x), math.log2(FLT_MIN))) if style == "antilog2": return as_float(2**x) if style == "logb": - return as_float(np.log(x) / np.log(base)) + return as_float(xp.log(x) / math.log(base)) # style == 'antilogb' return as_float(base**x) @@ -215,6 +218,9 @@ def logarithmic_function_quasilog( """ x = as_float_array(x) + + xp = array_namespace(x) + style = validate_method( style, ("lintolog", "logtolin"), @@ -225,8 +231,8 @@ def logarithmic_function_quasilog( y = ( log_side_slope * ( - np.log(np.maximum(lin_side_slope * x + lin_side_offset, FLT_MIN)) - / np.log(base) + xp.log(xp.clip(lin_side_slope * x + lin_side_offset, min=FLT_MIN)) + / math.log(base) ) + log_side_offset ) @@ -310,6 +316,9 @@ def logarithmic_function_camera( """ x = as_float_array(x) + + xp = array_namespace(x) + style = validate_method( style, ("cameraLinToLog", "cameraLogToLin"), @@ -318,7 +327,7 @@ def logarithmic_function_camera( log_side_break = ( log_side_slope - * (np.log(lin_side_slope * lin_side_break + lin_side_offset) / np.log(base)) + * (math.log(lin_side_slope * lin_side_break + lin_side_offset) / math.log(base)) + log_side_offset ) @@ -333,7 +342,7 @@ def logarithmic_function_camera( sdiv( lin_side_slope, (lin_side_slope * lin_side_break + lin_side_offset) - * np.log(base), + * math.log(base), ) ) ), @@ -344,7 +353,7 @@ def logarithmic_function_camera( if style == "cameralintolog": m_x = x <= lin_side_break - y = np.where( + y = xp.where( m_x, linear_slope * x + linear_offset, logarithmic_function_quasilog( @@ -360,7 +369,7 @@ def logarithmic_function_camera( else: # style == 'cameralogtolin' with sdiv_mode(): m_x = x <= log_side_break - y = np.where( + y = xp.where( m_x, sdiv(x - linear_offset, linear_slope), logarithmic_function_quasilog( @@ -439,7 +448,9 @@ def log_encoding_Log2( lin = as_float_array(lin) - lg2 = np.log2(lin / middle_grey) + xp = array_namespace(lin) + + lg2 = xp.log2(lin / middle_grey) log_norm = (lg2 - min_exposure) / (max_exposure - min_exposure) return as_float(log_norm) diff --git a/colour/models/rgb/transfer_functions/nikon_n_log.py b/colour/models/rgb/transfer_functions/nikon_n_log.py index 4eca968fc6..bc880f85e9 100644 --- a/colour/models/rgb/transfer_functions/nikon_n_log.py +++ b/colour/models/rgb/transfer_functions/nikon_n_log.py @@ -17,15 +17,20 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) from colour.models.rgb.transfer_functions import full_to_legal, legal_to_full -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -106,6 +111,9 @@ def log_encoding_NLog( """ y = to_domain_1(y) + + xp = array_namespace(y) + constants = optional(constants, CONSTANTS_NLOG) if not in_reflection: @@ -117,10 +125,10 @@ def log_encoding_NLog( c = constants.c d = constants.d - x = np.where( + x = xp.where( y < cut1, a * spow(y + b, 1 / 3), - c * np.log(y) + d, + c * xp.log(y) + d, ) x_cv = x if out_normalised_code_value else legal_to_full(x, bit_depth) @@ -184,6 +192,9 @@ def log_decoding_NLog( """ x = to_domain_1(x) + + xp = array_namespace(x) + constants = optional(constants, CONSTANTS_NLOG) x = x if in_normalised_code_value else full_to_legal(x, bit_depth) @@ -194,10 +205,10 @@ def log_decoding_NLog( c = constants.c d = constants.d - y = np.where( + y = xp.where( x < cut2, spow(x / a, 3) - b, - np.exp((x - d) / c), + xp.exp((x - d) / c), ) if not out_reflection: diff --git a/colour/models/rgb/transfer_functions/oppo_o_log.py b/colour/models/rgb/transfer_functions/oppo_o_log.py index 6fe437583a..2e82a61dfa 100644 --- a/colour/models/rgb/transfer_functions/oppo_o_log.py +++ b/colour/models/rgb/transfer_functions/oppo_o_log.py @@ -16,13 +16,18 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -95,13 +100,16 @@ def log_encoding_OPPOOLog( """ R = to_domain_1(R) + + xp = array_namespace(R) + constants = optional(constants, CONSTANTS_OPPO_O_LOG) gamma = constants.gamma beta = constants.beta delta = constants.delta - P = gamma * np.log(R + beta) + delta + P = gamma * xp.log(R + beta) + delta return as_float(from_range_1(P)) @@ -154,12 +162,15 @@ def log_decoding_OPPOOLog( """ P = to_domain_1(P) + + xp = array_namespace(P) + constants = optional(constants, CONSTANTS_OPPO_O_LOG) gamma = constants.gamma beta = constants.beta delta = constants.delta - R = np.exp((P - delta) / gamma) - beta + R = xp.exp((P - delta) / gamma) - beta return as_float(from_range_1(R)) diff --git a/colour/models/rgb/transfer_functions/panalog.py b/colour/models/rgb/transfer_functions/panalog.py index 48c3af67b0..a8643140fd 100644 --- a/colour/models/rgb/transfer_functions/panalog.py +++ b/colour/models/rgb/transfer_functions/panalog.py @@ -17,14 +17,18 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 ArrayLike, Domain1, Range1, ) -from colour.utilities import as_float, as_float_array, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + from_range_1, + to_domain_1, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -88,9 +92,12 @@ def log_encoding_Panalog( """ x = to_domain_1(x) - black_offset = as_float_array(black_offset) - y = (681 + 444 * np.log10(x * (1 - black_offset) + black_offset)) / 1023 + xp = array_namespace(x) + + black_offset = xp_as_float_array(black_offset, xp=xp, like=x) + + y = (681 + 444 * xp.log10(x * (1 - black_offset) + black_offset)) / 1023 return as_float(from_range_1(y)) @@ -144,7 +151,10 @@ def log_decoding_Panalog( """ y = to_domain_1(y) - black_offset = as_float_array(black_offset) + + xp = array_namespace(y) + + black_offset = xp_as_float_array(black_offset, xp=xp, like=y) x = (10 ** ((1023 * y - 681) / 444) - black_offset) / (1 - black_offset) diff --git a/colour/models/rgb/transfer_functions/panasonic_v_log.py b/colour/models/rgb/transfer_functions/panasonic_v_log.py index 355aed679b..446623e36e 100644 --- a/colour/models/rgb/transfer_functions/panasonic_v_log.py +++ b/colour/models/rgb/transfer_functions/panasonic_v_log.py @@ -16,14 +16,19 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) from colour.models.rgb.transfer_functions import full_to_legal, legal_to_full -from colour.utilities import Structure, as_float, from_range_1, optional, to_domain_1 +from colour.utilities import ( + Structure, + array_namespace, + as_float, + from_range_1, + optional, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -93,6 +98,7 @@ def log_encoding_VLog( Examples -------- + >>> import numpy as np >>> log_encoding_VLog(0.18) # doctest: +ELLIPSIS np.float64(0.4233114...) @@ -113,6 +119,9 @@ def log_encoding_VLog( """ L_in = to_domain_1(L_in) + + xp = array_namespace(L_in) + constants = optional(constants, CONSTANTS_VLOG) if not in_reflection: @@ -123,10 +132,10 @@ def log_encoding_VLog( c = constants.c d = constants.d - V_out = np.where( + V_out = xp.where( L_in < cut1, 5.6 * L_in + 0.125, - c * np.log10(L_in + b) + d, + c * xp.log10(L_in + b) + d, ) V_out_cv = V_out if out_normalised_code_value else legal_to_full(V_out, bit_depth) @@ -190,6 +199,9 @@ def log_decoding_VLog( """ V_out = to_domain_1(V_out) + + xp = array_namespace(V_out) + constants = optional(constants, CONSTANTS_VLOG) V_out = V_out if in_normalised_code_value else full_to_legal(V_out, bit_depth) @@ -199,7 +211,7 @@ def log_decoding_VLog( c = constants.c d = constants.d - L_in = np.where( + L_in = xp.where( V_out < cut2, (V_out - 0.125) / 5.6, 10 ** ((V_out - d) / c) - b, diff --git a/colour/models/rgb/transfer_functions/pivoted_log.py b/colour/models/rgb/transfer_functions/pivoted_log.py index 35859b57bd..e0f3cb7773 100644 --- a/colour/models/rgb/transfer_functions/pivoted_log.py +++ b/colour/models/rgb/transfer_functions/pivoted_log.py @@ -17,13 +17,11 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -100,9 +98,11 @@ def log_encoding_PivotedLog( x = to_domain_1(x) + xp = array_namespace(x) + y = ( log_reference - + np.log10(x / linear_reference) / (density_per_code_value / negative_gamma) + + xp.log10(x / linear_reference) / (density_per_code_value / negative_gamma) ) / 1023 return as_float(from_range_1(y)) diff --git a/colour/models/rgb/transfer_functions/red.py b/colour/models/rgb/transfer_functions/red.py index a28602b88c..cf37ed6f4a 100644 --- a/colour/models/rgb/transfer_functions/red.py +++ b/colour/models/rgb/transfer_functions/red.py @@ -38,8 +38,6 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import Literal @@ -54,11 +52,12 @@ ) from colour.utilities import ( CanonicalMapping, + array_namespace, as_float, - as_float_array, from_range_1, to_domain_1, validate_method, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -132,9 +131,12 @@ def log_encoding_REDLog( """ x = to_domain_1(x) - black_offset = as_float_array(black_offset) - y = (1023 + 511 * np.log10(x * (1 - black_offset) + black_offset)) / 1023 + xp = array_namespace(x) + + black_offset = xp_as_float_array(black_offset, xp=xp, like=x) + + y = (1023 + 511 * xp.log10(x * (1 - black_offset) + black_offset)) / 1023 return as_float(from_range_1(y)) @@ -183,7 +185,10 @@ def log_decoding_REDLog( """ y = to_domain_1(y) - black_offset = as_float_array(black_offset) + + xp = array_namespace(y) + + black_offset = xp_as_float_array(black_offset, xp=xp, like=y) x = ((10 ** ((1023 * y - 1023) / 511)) - black_offset) / (1 - black_offset) @@ -325,7 +330,9 @@ def log_encoding_Log3G10_v1(x: Domain1) -> Range1: x = to_domain_1(x) - y = np.sign(x) * 0.222497 * np.log10((np.abs(x) * 169.379333) + 1) + xp = array_namespace(x) + + y = xp.sign(x) * 0.222497 * xp.log10((xp.abs(x) * 169.379333) + 1) return as_float(from_range_1(y)) @@ -373,7 +380,9 @@ def log_decoding_Log3G10_v1(y: Domain1) -> Range1: y = to_domain_1(y) - x = np.sign(y) * (10.0 ** (np.abs(y) / 0.222497) - 1) / 169.379333 + xp = array_namespace(y) + + x = xp.sign(y) * (10.0 ** (xp.abs(y) / 0.222497) - 1) / 169.379333 return as_float(from_range_1(x)) @@ -420,7 +429,9 @@ def log_encoding_Log3G10_v2(x: Domain1) -> Range1: x = to_domain_1(x) - y = np.sign(x + 0.01) * 0.224282 * np.log10((np.abs(x + 0.01) * 155.975327) + 1) + xp = array_namespace(x) + + y = xp.sign(x + 0.01) * 0.224282 * xp.log10((xp.abs(x + 0.01) * 155.975327) + 1) return as_float(from_range_1(y)) @@ -468,7 +479,9 @@ def log_decoding_Log3G10_v2(y: Domain1) -> Range1: y = to_domain_1(y) - x = (np.sign(y) * (10.0 ** (np.abs(y) / 0.224282) - 1) / 155.975327) - 0.01 + xp = array_namespace(y) + + x = (xp.sign(y) * (10.0 ** (xp.abs(y) / 0.224282) - 1) / 155.975327) - 0.01 return as_float(from_range_1(x)) @@ -520,9 +533,11 @@ def log_encoding_Log3G10_v3(x: Domain1) -> Range1: x = to_domain_1(x) + xp = array_namespace(x) + x = x + c - y = np.where(x < 0.0, x * g, np.sign(x) * a * np.log10((np.abs(x) * b) + 1.0)) + y = xp.where(x < 0.0, x * g, xp.sign(x) * a * xp.log10((xp.abs(x) * b) + 1.0)) return as_float(from_range_1(y)) @@ -575,10 +590,12 @@ def log_decoding_Log3G10_v3(y: Domain1) -> Range1: y = to_domain_1(y) - x = np.where( + xp = array_namespace(y) + + x = xp.where( y < 0.0, (y / g) - c, - np.sign(y) * (10 ** (np.abs(y) / a) - 1.0) / b - c, + xp.sign(y) * (10 ** (xp.abs(y) / a) - 1.0) / b - c, ) return as_float(from_range_1(x)) @@ -787,7 +804,9 @@ def log_encoding_Log3G12(x: Domain1) -> Range1: x = to_domain_1(x) - y = np.sign(x) * 0.184904 * np.log10((np.abs(x) * 347.189667) + 1) + xp = array_namespace(x) + + y = xp.sign(x) * 0.184904 * xp.log10((xp.abs(x) * 347.189667) + 1) return as_float(from_range_1(y)) @@ -832,6 +851,8 @@ def log_decoding_Log3G12(y: Domain1) -> Range1: y = to_domain_1(y) - x = np.sign(y) * (10.0 ** (np.abs(y) / 0.184904) - 1) / 347.189667 + xp = array_namespace(y) + + x = xp.sign(y) * (10.0 ** (xp.abs(y) / 0.184904) - 1) / 347.189667 return as_float(from_range_1(x)) diff --git a/colour/models/rgb/transfer_functions/rimm_romm_rgb.py b/colour/models/rgb/transfer_functions/rimm_romm_rgb.py index 0b5b91e226..92bdd6e0a2 100644 --- a/colour/models/rgb/transfer_functions/rimm_romm_rgb.py +++ b/colour/models/rgb/transfer_functions/rimm_romm_rgb.py @@ -25,10 +25,9 @@ from __future__ import annotations +import math import typing -import numpy as np - from colour.algebra import spow if typing.TYPE_CHECKING: @@ -40,13 +39,16 @@ Range1, ) from colour.utilities import ( + array_namespace, as_float, + as_float_array, as_float_scalar, as_int, copy_definition, domain_range_scale, from_range_1, to_domain_1, + xp_select, ) __author__ = "Colour Developers" @@ -121,14 +123,16 @@ def cctf_encoding_ROMMRGB( X = to_domain_1(X) + xp = array_namespace(X) + I_max = 2**bit_depth - 1 E_t = 16 ** (1.8 / (1 - 1.8)) - X_p = np.where(E_t > X, X * 16 * I_max, spow(X, 1 / 1.8) * I_max) + X_p = xp.where(E_t > X, X * 16 * I_max, spow(X, 1 / 1.8) * I_max) if out_int: - return as_int(np.round(X_p)) + return as_int(xp.round(X_p)) return as_float(from_range_1(X_p / I_max)) @@ -188,6 +192,8 @@ def cctf_decoding_ROMMRGB( X_p = to_domain_1(X_p) + xp = array_namespace(X_p) + I_max = 2**bit_depth - 1 if not in_int: @@ -195,7 +201,7 @@ def cctf_decoding_ROMMRGB( E_t = 16 ** (1.8 / (1 - 1.8)) - X = np.where( + X = xp.where( X_p < 16 * E_t * I_max, X_p / (16 * I_max), spow(X_p / I_max, 1.8), @@ -280,18 +286,21 @@ def cctf_encoding_RIMMRGB( X = to_domain_1(X) + xp = array_namespace(X) + I_max = 2**bit_depth - 1 V_clip = 1.099 * spow(E_clip, 0.45) - 0.099 q = I_max / V_clip - X_p = q * np.select( + X_p = q * xp_select( [X < 0.0, X < 0.018, X >= 0.018, E_clip < X], [0, 4.5 * X, 1.099 * spow(X, 0.45) - 0.099, I_max], + xp=xp, ) if out_int: - return as_int(np.round(X_p)) + return as_int(xp.round(X_p)) return as_float(from_range_1(X_p / I_max)) @@ -354,6 +363,8 @@ def cctf_decoding_RIMMRGB( X_p = to_domain_1(X_p) + xp = array_namespace(X_p) + I_max = as_float_scalar(2**bit_depth - 1) if not in_int: @@ -364,7 +375,7 @@ def cctf_decoding_RIMMRGB( m = V_clip * X_p / I_max with domain_range_scale("ignore"): - X = np.where( + X = xp.where( X_p / I_max < cctf_encoding_RIMMRGB(0.018, bit_depth, E_clip=E_clip), m / 4.5, spow((m + 0.099) / 1.099, 1 / 0.45), @@ -433,14 +444,17 @@ def log_encoding_ERIMMRGB( X = to_domain_1(X) + xp = array_namespace(X) + I_max = 2**bit_depth - 1 - E_t = np.exp(1) * E_min + E_t = math.e * E_min + + l_E_t = math.log(E_t) + l_E_min = math.log(float(E_min)) + l_E_clip = math.log(float(E_clip)) - l_E_t = np.log(E_t) - l_E_min = np.log(E_min) - l_E_clip = np.log(E_clip) - X_p = np.select( + X_p = xp_select( [ X < 0.0, E_t >= X, @@ -450,13 +464,14 @@ def log_encoding_ERIMMRGB( [ 0, I_max * ((l_E_t - l_E_min) / (l_E_clip - l_E_min)) * X / E_t, - I_max * ((np.log(X) - l_E_min) / (l_E_clip - l_E_min)), + I_max * ((xp.log(X) - l_E_min) / (l_E_clip - l_E_min)), I_max, ], + xp=xp, ) if out_int: - return as_int(np.round(X_p)) + return as_int(xp.round(X_p)) return as_float(from_range_1(X_p / I_max)) @@ -521,20 +536,24 @@ def log_decoding_ERIMMRGB( X_p = to_domain_1(X_p) + xp = array_namespace(X_p) + I_max = 2**bit_depth - 1 if not in_int: X_p = X_p * I_max - E_t = np.exp(1) * E_min + X_p = as_float_array(X_p) + + E_t = math.e * E_min - l_E_t = np.log(E_t) - l_E_min = np.log(E_min) - l_E_clip = np.log(E_clip) - X = np.where( + l_E_t = math.log(E_t) + l_E_min = math.log(float(E_min)) + l_E_clip = math.log(float(E_clip)) + X = xp.where( X_p <= I_max * ((l_E_t - l_E_min) / (l_E_clip - l_E_min)), ((l_E_clip - l_E_min) / (l_E_t - l_E_min)) * ((X_p * E_t) / I_max), - np.exp((X_p / I_max) * (l_E_clip - l_E_min) + l_E_min), + xp.exp((X_p / I_max) * (l_E_clip - l_E_min) + l_E_min), ) return as_float(from_range_1(X)) diff --git a/colour/models/rgb/transfer_functions/smpte_240m.py b/colour/models/rgb/transfer_functions/smpte_240m.py index 78f1026bd7..e7a6f60da8 100644 --- a/colour/models/rgb/transfer_functions/smpte_240m.py +++ b/colour/models/rgb/transfer_functions/smpte_240m.py @@ -19,14 +19,18 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, domain_range_scale, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + domain_range_scale, + from_range_1, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -83,7 +87,9 @@ def oetf_SMPTE240M(L_c: Domain1) -> Range1: L_c = to_domain_1(L_c) - V_c = np.where(L_c < 0.0228, 4 * L_c, 1.1115 * spow(L_c, 0.45) - 0.1115) + xp = array_namespace(L_c) + + V_c = xp.where(L_c < 0.0228, 4 * L_c, 1.1115 * spow(L_c, 0.45) - 0.1115) return as_float(from_range_1(V_c)) @@ -130,8 +136,10 @@ def eotf_SMPTE240M(V_r: Domain1) -> Range1: V_r = to_domain_1(V_r) + xp = array_namespace(V_r) + with domain_range_scale("ignore"): - L_r = np.where( + L_r = xp.where( V_r < oetf_SMPTE240M(0.0228), V_r / 4, spow((V_r + 0.1115) / 1.1115, 1 / 0.45), diff --git a/colour/models/rgb/transfer_functions/sony.py b/colour/models/rgb/transfer_functions/sony.py index f25cf4bc49..5aea7be5df 100644 --- a/colour/models/rgb/transfer_functions/sony.py +++ b/colour/models/rgb/transfer_functions/sony.py @@ -24,14 +24,13 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) from colour.models.rgb.transfer_functions import full_to_legal, legal_to_full from colour.utilities import ( + array_namespace, as_float, as_float_array, domain_range_scale, @@ -104,6 +103,7 @@ def log_encoding_SLog( Examples -------- + >>> import numpy as np >>> log_encoding_SLog(0.18) # doctest: +ELLIPSIS np.float64(0.3849708...) @@ -119,12 +119,14 @@ def log_encoding_SLog( x = to_domain_1(x) + xp = array_namespace(x) + if in_reflection: x = x / 0.9 - y = np.where( + y = xp.where( x >= 0, - ((0.432699 * np.log10(x + 0.037584) + 0.616596) + 0.03), + ((0.432699 * xp.log10(x + 0.037584) + 0.616596) + 0.03), x * 5 + 0.030001222851889303, ) @@ -187,10 +189,12 @@ def log_decoding_SLog( y = to_domain_1(y) + xp = array_namespace(y) + x = legal_to_full(y, bit_depth) if in_normalised_code_value else y with domain_range_scale("ignore"): - x = np.where( + x = xp.where( y >= log_encoding_SLog(0.0, bit_depth, in_normalised_code_value), 10 ** ((x - 0.616596 - 0.03) / 0.432699) - 0.037584, (x - 0.030001222851889303) / 5.0, @@ -250,6 +254,7 @@ def log_encoding_SLog2( Examples -------- + >>> import numpy as np >>> log_encoding_SLog2(0.18) # doctest: +ELLIPSIS np.float64(0.3395325...) @@ -377,6 +382,7 @@ def log_encoding_SLog3( Examples -------- + >>> import numpy as np >>> log_encoding_SLog3(0.18) # doctest: +ELLIPSIS np.float64(0.4105571...) @@ -392,12 +398,14 @@ def log_encoding_SLog3( x = to_domain_1(x) + xp = array_namespace(x) + if not in_reflection: x = x * 0.9 - y = np.where( + y = xp.where( x >= 0.01125000, - (420 + np.log10((x + 0.01) / (0.18 + 0.01)) * 261.5) / 1023, + (420 + xp.log10((x + 0.01) / (0.18 + 0.01)) * 261.5) / 1023, (x * (171.2102946929 - 95) / 0.01125000 + 95) / 1023, ) @@ -460,9 +468,11 @@ def log_decoding_SLog3( y = to_domain_1(y) + xp = array_namespace(y) + y = y if in_normalised_code_value else full_to_legal(y, bit_depth) - x = np.where( + x = xp.where( y >= 171.2102946929 / 1023, ((10 ** ((y * 1023 - 420) / 261.5)) * (0.18 + 0.01) - 0.01), (y * 1023 - 95) * 0.01125000 / (171.2102946929 - 95), diff --git a/colour/models/rgb/transfer_functions/srgb.py b/colour/models/rgb/transfer_functions/srgb.py index c065d8cc72..b9cfc8bd28 100644 --- a/colour/models/rgb/transfer_functions/srgb.py +++ b/colour/models/rgb/transfer_functions/srgb.py @@ -25,14 +25,18 @@ from __future__ import annotations -import numpy as np - from colour.algebra import spow from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, domain_range_scale, from_range_1, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float, + domain_range_scale, + from_range_1, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -89,7 +93,9 @@ def eotf_inverse_sRGB(L: Domain1) -> Range1: L = to_domain_1(L) - V = np.where(L <= 0.0031308, L * 12.92, 1.055 * spow(L, 1 / 2.4) - 0.055) + xp = array_namespace(L) + + V = xp.where(L <= 0.0031308, L * 12.92, 1.055 * spow(L, 1 / 2.4) - 0.055) return as_float(from_range_1(V)) @@ -136,8 +142,10 @@ def eotf_sRGB(V: Domain1) -> Range1: V = to_domain_1(V) + xp = array_namespace(V) + with domain_range_scale("ignore"): - L = np.where( + L = xp.where( eotf_inverse_sRGB(0.0031308) >= V, V / 12.92, spow((V + 0.055) / 1.055, 2.4), diff --git a/colour/models/rgb/transfer_functions/st_2084.py b/colour/models/rgb/transfer_functions/st_2084.py index 89134b3a80..c61da164a6 100644 --- a/colour/models/rgb/transfer_functions/st_2084.py +++ b/colour/models/rgb/transfer_functions/st_2084.py @@ -24,14 +24,18 @@ import typing -import numpy as np - from colour.algebra import spow if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import Structure, as_float, as_float_array, optional +from colour.utilities import ( + Structure, + array_namespace, + as_float, + as_float_array, + optional, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -202,6 +206,9 @@ def eotf_ST2084( """ N = as_float_array(N) + + xp = array_namespace(N) + constants = optional(constants, CONSTANTS_ST2084) c_1 = constants.c_1 @@ -214,7 +221,7 @@ def eotf_ST2084( m_2_d = 1 / m_2 V_p = spow(N, m_2_d) - n = np.maximum(0, V_p - c_1) + n = xp.clip(V_p - c_1, min=0) L = spow((n / (c_2 - c_3 * V_p)), m_1_d) C = L_p * L diff --git a/colour/models/rgb/transfer_functions/tests/test__init__.py b/colour/models/rgb/transfer_functions/tests/test__init__.py index 723e64eb31..9ae7b081fc 100644 --- a/colour/models/rgb/transfer_functions/tests/test__init__.py +++ b/colour/models/rgb/transfer_functions/tests/test__init__.py @@ -21,7 +21,7 @@ cctf_decoding, cctf_encoding, ) -from colour.utilities import ColourUsageWarning +from colour.utilities import ColourUsageWarning, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -49,18 +49,10 @@ def test_raise_exception_cctf_encoding(self) -> None: log_encoding_ACESproxy` definition raised exception. """ - pytest.warns( - ColourUsageWarning, - cctf_encoding, - 0.18, - function="ITU-R BT.2100 HLG", - ) - pytest.warns( - ColourUsageWarning, - cctf_encoding, - 0.18, - function="ITU-R BT.2100 PQ", - ) + with pytest.warns(ColourUsageWarning): + cctf_encoding(0.18, function="ITU-R BT.2100 HLG") + with pytest.warns(ColourUsageWarning): + cctf_encoding(0.18, function="ITU-R BT.2100 PQ") class TestCctfDecoding: @@ -75,18 +67,10 @@ def test_raise_exception_cctf_decoding(self) -> None: log_encoding_ACESproxy` definition raised exception. """ - pytest.warns( - ColourUsageWarning, - cctf_decoding, - 0.18, - function="ITU-R BT.2100 HLG", - ) - pytest.warns( - ColourUsageWarning, - cctf_decoding, - 0.18, - function="ITU-R BT.2100 PQ", - ) + with pytest.warns(ColourUsageWarning): + cctf_decoding(0.18, function="ITU-R BT.2100 HLG") + with pytest.warns(ColourUsageWarning): + cctf_decoding(0.18, function="ITU-R BT.2100 PQ") class TestTransferFunctions: @@ -131,7 +115,7 @@ def test_transfer_functions(self) -> None: samples_e = CCTF_ENCODINGS[name](samples_r) samples_d = CCTF_DECODINGS[name](samples_e) - np.testing.assert_allclose( + xp_assert_close( samples_r, samples_d, atol=tolerance.get(name, TOLERANCE_ABSOLUTE_TESTS), diff --git a/colour/models/rgb/transfer_functions/tests/test_aces.py b/colour/models/rgb/transfer_functions/tests/test_aces.py index 8611f61636..b9b6bf1c1b 100644 --- a/colour/models/rgb/transfer_functions/tests/test_aces.py +++ b/colour/models/rgb/transfer_functions/tests/test_aces.py @@ -3,6 +3,10 @@ module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -14,7 +18,17 @@ log_encoding_ACEScct, log_encoding_ACESproxy, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -39,85 +53,88 @@ class TestLogEncoding_ACESproxy: definition unit tests methods. """ - def test_log_encoding_ACESproxy(self) -> None: + def test_log_encoding_ACESproxy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACESproxy` definition. """ - np.testing.assert_allclose( - log_encoding_ACESproxy(0.0), + xp_assert_close( + log_encoding_ACESproxy(xp_as_array(0.0, xp=xp)), 0.062561094819159, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACESproxy(0.18), + xp_assert_close( + log_encoding_ACESproxy(xp_as_array(0.18, xp=xp)), 0.416422287390029, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACESproxy(0.18, 12), + xp_assert_close( + log_encoding_ACESproxy(xp_as_array(0.18, xp=xp), 12), 0.416361416361416, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACESproxy(1.0), + xp_assert_close( + log_encoding_ACESproxy(xp_as_array(1.0, xp=xp)), 0.537634408602151, atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert log_encoding_ACESproxy(0.18, out_int=True) == 426 + assert ( + as_ndarray(log_encoding_ACESproxy(xp_as_array(0.18, xp=xp), out_int=True)) + == 426 + ) - def test_n_dimensional_log_encoding_ACESproxy(self) -> None: + def test_n_dimensional_log_encoding_ACESproxy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACESproxy` definition n-dimensional arrays support. """ lin_AP1 = 0.18 - ACESproxy = log_encoding_ACESproxy(lin_AP1) + ACESproxy = as_ndarray(log_encoding_ACESproxy(xp_as_array(lin_AP1, xp=xp))) - lin_AP1 = np.tile(lin_AP1, 6) - ACESproxy = np.tile(ACESproxy, 6) - np.testing.assert_allclose( + lin_AP1 = xp.tile(xp_as_array(lin_AP1, xp=xp), (6,)) + ACESproxy = xp.tile(xp_as_array(ACESproxy, xp=xp), (6,)) + xp_assert_close( log_encoding_ACESproxy(lin_AP1), ACESproxy, atol=TOLERANCE_ABSOLUTE_TESTS, ) - lin_AP1 = np.reshape(lin_AP1, (2, 3)) - ACESproxy = np.reshape(ACESproxy, (2, 3)) - np.testing.assert_allclose( + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3), xp=xp) + ACESproxy = xp_reshape(xp_as_array(ACESproxy, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_encoding_ACESproxy(lin_AP1), ACESproxy, atol=TOLERANCE_ABSOLUTE_TESTS, ) - lin_AP1 = np.reshape(lin_AP1, (2, 3, 1)) - ACESproxy = np.reshape(ACESproxy, (2, 3, 1)) - np.testing.assert_allclose( + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3, 1), xp=xp) + ACESproxy = xp_reshape(xp_as_array(ACESproxy, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_encoding_ACESproxy(lin_AP1), ACESproxy, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_ACESproxy(self) -> None: + def test_domain_range_scale_log_encoding_ACESproxy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACESproxy` definition domain and range scale support. """ lin_AP1 = 0.18 - ACESproxy = log_encoding_ACESproxy(lin_AP1) + ACESproxy = as_ndarray(log_encoding_ACESproxy(xp_as_array(lin_AP1, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ACESproxy(lin_AP1 * factor), + xp_assert_close( + log_encoding_ACESproxy(xp_as_array(lin_AP1 * factor, xp=xp)), ACESproxy * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -139,89 +156,89 @@ class TestLogDecoding_ACESproxy: definition unit tests methods. """ - def test_log_decoding_ACESproxy(self) -> None: + def test_log_decoding_ACESproxy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACESproxy` definition. """ - np.testing.assert_allclose( - log_decoding_ACESproxy(0.062561094819159), + xp_assert_close( + log_decoding_ACESproxy(xp_as_array(0.062561094819159, xp=xp)), 0.0, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - np.testing.assert_allclose( - log_decoding_ACESproxy(0.416422287390029), + xp_assert_close( + log_decoding_ACESproxy(xp_as_array(0.416422287390029, xp=xp)), 0.18, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - np.testing.assert_allclose( - log_decoding_ACESproxy(0.416361416361416, 12), + xp_assert_close( + log_decoding_ACESproxy(xp_as_array(0.416361416361416, xp=xp), 12), 0.18, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - np.testing.assert_allclose( - log_decoding_ACESproxy(0.537634408602151), + xp_assert_close( + log_decoding_ACESproxy(xp_as_array(0.537634408602151, xp=xp)), 1.0, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - np.testing.assert_allclose( - log_decoding_ACESproxy(426, in_int=True), + xp_assert_close( + log_decoding_ACESproxy(xp_as_array(426, xp=xp), in_int=True), 0.18, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - def test_n_dimensional_log_decoding_ACESproxy(self) -> None: + def test_n_dimensional_log_decoding_ACESproxy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACESproxy` definition n-dimensional arrays support. """ ACESproxy = 0.416422287390029 - lin_AP1 = log_decoding_ACESproxy(ACESproxy) + lin_AP1 = as_ndarray(log_decoding_ACESproxy(xp_as_array(ACESproxy, xp=xp))) - ACESproxy = np.tile(ACESproxy, 6) - lin_AP1 = np.tile(lin_AP1, 6) - np.testing.assert_allclose( + ACESproxy = xp.tile(xp_as_array(ACESproxy, xp=xp), (6,)) + lin_AP1 = xp.tile(xp_as_array(lin_AP1, xp=xp), (6,)) + xp_assert_close( log_decoding_ACESproxy(ACESproxy), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - ACESproxy = np.reshape(ACESproxy, (2, 3)) - lin_AP1 = np.reshape(lin_AP1, (2, 3)) - np.testing.assert_allclose( + ACESproxy = xp_reshape(xp_as_array(ACESproxy, xp=xp), (2, 3), xp=xp) + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_decoding_ACESproxy(ACESproxy), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - ACESproxy = np.reshape(ACESproxy, (2, 3, 1)) - lin_AP1 = np.reshape(lin_AP1, (2, 3, 1)) - np.testing.assert_allclose( + ACESproxy = xp_reshape(xp_as_array(ACESproxy, xp=xp), (2, 3, 1), xp=xp) + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_decoding_ACESproxy(ACESproxy), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_ACESproxy(self) -> None: + def test_domain_range_scale_log_decoding_ACESproxy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACESproxy` definition domain and range scale support. """ ACESproxy = 426.0 - lin_AP1 = log_decoding_ACESproxy(ACESproxy) + lin_AP1 = as_ndarray(log_decoding_ACESproxy(xp_as_array(ACESproxy, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ACESproxy(ACESproxy * factor), + xp_assert_close( + log_decoding_ACESproxy(xp_as_array(ACESproxy * factor, xp=xp)), lin_AP1 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -242,71 +259,77 @@ class TestLogEncoding_ACEScc: log_encoding_ACEScc` definition unit tests methods. """ - def test_log_encoding_ACEScc(self) -> None: + def test_log_encoding_ACEScc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScc` definition. """ - np.testing.assert_allclose( - log_encoding_ACEScc(0.0), + xp_assert_close( + log_encoding_ACEScc(xp_as_array(0.0, xp=xp)), -0.358447488584475, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACEScc(0.18), + xp_assert_close( + log_encoding_ACEScc(xp_as_array(0.18, xp=xp)), 0.413588402492442, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACEScc(1.0), + xp_assert_close( + log_encoding_ACEScc(xp_as_array(1.0, xp=xp)), 0.554794520547945, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_ACEScc(self) -> None: + def test_n_dimensional_log_encoding_ACEScc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScc` definition n-dimensional arrays support. """ lin_AP1 = 0.18 - ACEScc = log_encoding_ACEScc(lin_AP1) + ACEScc = as_ndarray(log_encoding_ACEScc(xp_as_array(lin_AP1, xp=xp))) - lin_AP1 = np.tile(lin_AP1, 6) - ACEScc = np.tile(ACEScc, 6) - np.testing.assert_allclose( - log_encoding_ACEScc(lin_AP1), ACEScc, atol=TOLERANCE_ABSOLUTE_TESTS + lin_AP1 = xp.tile(xp_as_array(lin_AP1, xp=xp), (6,)) + ACEScc = xp.tile(xp_as_array(ACEScc, xp=xp), (6,)) + xp_assert_close( + log_encoding_ACEScc(lin_AP1), + ACEScc, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - lin_AP1 = np.reshape(lin_AP1, (2, 3)) - ACEScc = np.reshape(ACEScc, (2, 3)) - np.testing.assert_allclose( - log_encoding_ACEScc(lin_AP1), ACEScc, atol=TOLERANCE_ABSOLUTE_TESTS + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3), xp=xp) + ACEScc = xp_reshape(xp_as_array(ACEScc, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_encoding_ACEScc(lin_AP1), + ACEScc, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - lin_AP1 = np.reshape(lin_AP1, (2, 3, 1)) - ACEScc = np.reshape(ACEScc, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_ACEScc(lin_AP1), ACEScc, atol=TOLERANCE_ABSOLUTE_TESTS + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3, 1), xp=xp) + ACEScc = xp_reshape(xp_as_array(ACEScc, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_encoding_ACEScc(lin_AP1), + ACEScc, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_ACEScc(self) -> None: + def test_domain_range_scale_log_encoding_ACEScc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScc` definition domain and range scale support. """ lin_AP1 = 0.18 - ACEScc = log_encoding_ACEScc(lin_AP1) + ACEScc = as_ndarray(log_encoding_ACEScc(xp_as_array(lin_AP1, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ACEScc(lin_AP1 * factor), + xp_assert_close( + log_encoding_ACEScc(xp_as_array(lin_AP1 * factor, xp=xp)), ACEScc * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -327,71 +350,77 @@ class TestLogDecoding_ACEScc: log_decoding_ACEScc` definition unit tests methods. """ - def test_log_decoding_ACEScc(self) -> None: + def test_log_decoding_ACEScc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScc` definition. """ - np.testing.assert_allclose( - log_decoding_ACEScc(-0.358447488584475), + xp_assert_close( + log_decoding_ACEScc(xp_as_array(-0.358447488584475, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ACEScc(0.413588402492442), + xp_assert_close( + log_decoding_ACEScc(xp_as_array(0.413588402492442, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ACEScc(0.554794520547945), + xp_assert_close( + log_decoding_ACEScc(xp_as_array(0.554794520547945, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_ACEScc(self) -> None: + def test_n_dimensional_log_decoding_ACEScc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScc` definition n-dimensional arrays support. """ ACEScc = 0.413588402492442 - lin_AP1 = log_decoding_ACEScc(ACEScc) + lin_AP1 = as_ndarray(log_decoding_ACEScc(xp_as_array(ACEScc, xp=xp))) - ACEScc = np.tile(ACEScc, 6) - lin_AP1 = np.tile(lin_AP1, 6) - np.testing.assert_allclose( - log_decoding_ACEScc(ACEScc), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS + ACEScc = xp.tile(xp_as_array(ACEScc, xp=xp), (6,)) + lin_AP1 = xp.tile(xp_as_array(lin_AP1, xp=xp), (6,)) + xp_assert_close( + log_decoding_ACEScc(ACEScc), + lin_AP1, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - ACEScc = np.reshape(ACEScc, (2, 3)) - lin_AP1 = np.reshape(lin_AP1, (2, 3)) - np.testing.assert_allclose( - log_decoding_ACEScc(ACEScc), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS + ACEScc = xp_reshape(xp_as_array(ACEScc, xp=xp), (2, 3), xp=xp) + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_decoding_ACEScc(ACEScc), + lin_AP1, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - ACEScc = np.reshape(ACEScc, (2, 3, 1)) - lin_AP1 = np.reshape(lin_AP1, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_ACEScc(ACEScc), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS + ACEScc = xp_reshape(xp_as_array(ACEScc, xp=xp), (2, 3, 1), xp=xp) + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_decoding_ACEScc(ACEScc), + lin_AP1, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_ACEScc(self) -> None: + def test_domain_range_scale_log_decoding_ACEScc(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScc` definition domain and range scale support. """ ACEScc = 0.413588402492442 - lin_AP1 = log_decoding_ACEScc(ACEScc) + lin_AP1 = as_ndarray(log_decoding_ACEScc(xp_as_array(ACEScc, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ACEScc(ACEScc * factor), + xp_assert_close( + log_decoding_ACEScc(xp_as_array(ACEScc * factor, xp=xp)), lin_AP1 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -412,82 +441,82 @@ class TestLogEncoding_ACEScct: log_encoding_ACEScct` definition unit tests methods. """ - def test_log_encoding_ACEScct(self) -> None: + def test_log_encoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScct` definition. """ - np.testing.assert_allclose( - log_encoding_ACEScct(0.0), + xp_assert_close( + log_encoding_ACEScct(xp_as_array(0.0, xp=xp)), 0.072905534195835495, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACEScct(0.18), + xp_assert_close( + log_encoding_ACEScct(xp_as_array(0.18, xp=xp)), 0.413588402492442, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ACEScct(1.0), + xp_assert_close( + log_encoding_ACEScct(xp_as_array(1.0, xp=xp)), 0.554794520547945, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_ACEScct(self) -> None: + def test_n_dimensional_log_encoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScct` definition n-dimensional arrays support. """ lin_AP1 = 0.18 - ACEScct = log_encoding_ACEScct(lin_AP1) + ACEScct = as_ndarray(log_encoding_ACEScct(xp_as_array(lin_AP1, xp=xp))) - lin_AP1 = np.tile(lin_AP1, 6) - ACEScct = np.tile(ACEScct, 6) - np.testing.assert_allclose( + lin_AP1 = xp.tile(xp_as_array(lin_AP1, xp=xp), (6,)) + ACEScct = xp.tile(xp_as_array(ACEScct, xp=xp), (6,)) + xp_assert_close( log_encoding_ACEScct(lin_AP1), ACEScct, atol=TOLERANCE_ABSOLUTE_TESTS, ) - lin_AP1 = np.reshape(lin_AP1, (2, 3)) - ACEScct = np.reshape(ACEScct, (2, 3)) - np.testing.assert_allclose( + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3), xp=xp) + ACEScct = xp_reshape(xp_as_array(ACEScct, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_encoding_ACEScct(lin_AP1), ACEScct, atol=TOLERANCE_ABSOLUTE_TESTS, ) - lin_AP1 = np.reshape(lin_AP1, (2, 3, 1)) - ACEScct = np.reshape(ACEScct, (2, 3, 1)) - np.testing.assert_allclose( + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3, 1), xp=xp) + ACEScct = xp_reshape(xp_as_array(ACEScct, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_encoding_ACEScct(lin_AP1), ACEScct, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_ACEScct(self) -> None: + def test_domain_range_scale_log_encoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScct` definition domain and range scale support. """ lin_AP1 = 0.18 - ACEScct = log_encoding_ACEScct(lin_AP1) + ACEScct = as_ndarray(log_encoding_ACEScct(xp_as_array(lin_AP1, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ACEScct(lin_AP1 * factor), + xp_assert_close( + log_encoding_ACEScct(xp_as_array(lin_AP1 * factor, xp=xp)), ACEScct * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_ACEScc_equivalency_log_encoding_ACEScct(self) -> None: + def test_ACEScc_equivalency_log_encoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_encoding_ACEScct` definition ACEScc equivalency, and explicit requirement @@ -497,9 +526,9 @@ def test_ACEScc_equivalency_log_encoding_ACEScct(self) -> None: """ equiv = np.linspace(0.0078125, 222.86094420380761, 100) - np.testing.assert_allclose( - log_encoding_ACEScct(equiv), - log_encoding_ACEScc(equiv), + xp_assert_close( + log_encoding_ACEScct(xp_as_array(equiv, xp=xp)), + as_ndarray(log_encoding_ACEScc(xp_as_array(equiv, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -519,82 +548,82 @@ class TestLogDecoding_ACEScct: log_decoding_ACEScct` definition unit tests methods. """ - def test_log_decoding_ACEScct(self) -> None: + def test_log_decoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScct` definition. """ - np.testing.assert_allclose( - log_decoding_ACEScct(0.072905534195835495), + xp_assert_close( + log_decoding_ACEScct(xp_as_array(0.072905534195835495, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ACEScct(0.41358840249244228), + xp_assert_close( + log_decoding_ACEScct(xp_as_array(0.41358840249244228, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ACEScct(0.554794520547945), + xp_assert_close( + log_decoding_ACEScct(xp_as_array(0.554794520547945, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_ACEScct(self) -> None: + def test_n_dimensional_log_decoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScct` definition n-dimensional arrays support. """ ACEScct = 0.413588402492442 - lin_AP1 = log_decoding_ACEScct(ACEScct) + lin_AP1 = as_ndarray(log_decoding_ACEScct(xp_as_array(ACEScct, xp=xp))) - ACEScct = np.tile(ACEScct, 6) - lin_AP1 = np.tile(lin_AP1, 6) - np.testing.assert_allclose( + ACEScct = xp.tile(xp_as_array(ACEScct, xp=xp), (6,)) + lin_AP1 = xp.tile(xp_as_array(lin_AP1, xp=xp), (6,)) + xp_assert_close( log_decoding_ACEScct(ACEScct), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - ACEScct = np.reshape(ACEScct, (2, 3)) - lin_AP1 = np.reshape(lin_AP1, (2, 3)) - np.testing.assert_allclose( + ACEScct = xp_reshape(xp_as_array(ACEScct, xp=xp), (2, 3), xp=xp) + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_decoding_ACEScct(ACEScct), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - ACEScct = np.reshape(ACEScct, (2, 3, 1)) - lin_AP1 = np.reshape(lin_AP1, (2, 3, 1)) - np.testing.assert_allclose( + ACEScct = xp_reshape(xp_as_array(ACEScct, xp=xp), (2, 3, 1), xp=xp) + lin_AP1 = xp_reshape(xp_as_array(lin_AP1, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_decoding_ACEScct(ACEScct), lin_AP1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_ACEScct(self) -> None: + def test_domain_range_scale_log_decoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScct` definition domain and range scale support. """ ACEScc = 0.413588402492442 - lin_AP1 = log_decoding_ACEScct(ACEScc) + lin_AP1 = as_ndarray(log_decoding_ACEScct(xp_as_array(ACEScc, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ACEScct(ACEScc * factor), + xp_assert_close( + log_decoding_ACEScct(xp_as_array(ACEScc * factor, xp=xp)), lin_AP1 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_ACEScc_equivalency_log_decoding_ACEScct(self) -> None: + def test_ACEScc_equivalency_log_decoding_ACEScct(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.aces.\ log_decoding_ACEScct` definition ACEScc equivalency, and explicit requirement @@ -604,9 +633,9 @@ def test_ACEScc_equivalency_log_decoding_ACEScct(self) -> None: """ equiv = np.linspace(0.15525114155251146, 1.0, 100) - np.testing.assert_allclose( - log_decoding_ACEScct(equiv), - log_decoding_ACEScc(equiv), + xp_assert_close( + log_decoding_ACEScct(xp_as_array(equiv, xp=xp)), + as_ndarray(log_decoding_ACEScc(xp_as_array(equiv, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_apple_log_profile.py b/colour/models/rgb/transfer_functions/tests/test_apple_log_profile.py index 19e64508f3..12d4af54f1 100644 --- a/colour/models/rgb/transfer_functions/tests/test_apple_log_profile.py +++ b/colour/models/rgb/transfer_functions/tests/test_apple_log_profile.py @@ -3,6 +3,10 @@ apple_log_profile` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_AppleLogProfile, log_encoding_AppleLogProfile, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +45,77 @@ class TestLogEncoding_AppleLogProfile: log_encoding_AppleLogProfile` definition unit tests methods. """ - def test_log_encoding_AppleLogProfile(self) -> None: + def test_log_encoding_AppleLogProfile(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.apple_log_profile.\ log_encoding_AppleLogProfile` definition. """ - np.testing.assert_allclose( - log_encoding_AppleLogProfile(0.0), + xp_assert_close( + log_encoding_AppleLogProfile(xp_as_array(0.0, xp=xp)), 0.150476452300913, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_AppleLogProfile(0.18), + xp_assert_close( + log_encoding_AppleLogProfile(xp_as_array(0.18, xp=xp)), 0.488272458526868, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_AppleLogProfile(1.0), + xp_assert_close( + log_encoding_AppleLogProfile(xp_as_array(1.0, xp=xp)), 0.694552983055191, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_DLog(self) -> None: + def test_n_dimensional_log_encoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.apple_log_profile.\ log_encoding_AppleLogProfile` definition n-dimensional arrays support. """ R = 0.18 - P = log_encoding_AppleLogProfile(R) + P = as_ndarray(log_encoding_AppleLogProfile(xp_as_array(R, xp=xp))) - R = np.tile(R, 6) - P = np.tile(P, 6) - np.testing.assert_allclose( - log_encoding_AppleLogProfile(R), P, atol=TOLERANCE_ABSOLUTE_TESTS + R = xp.tile(xp_as_array(R, xp=xp), (6,)) + P = xp.tile(xp_as_array(P, xp=xp), (6,)) + xp_assert_close( + log_encoding_AppleLogProfile(R), + P, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - R = np.reshape(R, (2, 3)) - P = np.reshape(P, (2, 3)) - np.testing.assert_allclose( - log_encoding_AppleLogProfile(R), P, atol=TOLERANCE_ABSOLUTE_TESTS + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3), xp=xp) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_encoding_AppleLogProfile(R), + P, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - R = np.reshape(R, (2, 3, 1)) - P = np.reshape(P, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_AppleLogProfile(R), P, atol=TOLERANCE_ABSOLUTE_TESTS + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3, 1), xp=xp) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_encoding_AppleLogProfile(R), + P, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_DLog(self) -> None: + def test_domain_range_scale_log_encoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.apple_log_profile.\ log_encoding_AppleLogProfile` definition domain and range scale support. """ R = 0.18 - P = log_encoding_AppleLogProfile(R) + P = as_ndarray(log_encoding_AppleLogProfile(xp_as_array(R, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_AppleLogProfile(R * factor), + xp_assert_close( + log_encoding_AppleLogProfile(xp_as_array(R * factor, xp=xp)), P * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -118,71 +138,77 @@ class TestLogDecoding_AppleLogProfile: log_decoding_AppleLogProfile` definition unit tests methods. """ - def test_log_decoding_AppleLogProfile(self) -> None: + def test_log_decoding_AppleLogProfile(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.apple_log_profile.\ log_decoding_AppleLogProfile` definition. """ - np.testing.assert_allclose( - log_decoding_AppleLogProfile(0.150476452300913), + xp_assert_close( + log_decoding_AppleLogProfile(xp_as_array(0.150476452300913, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_AppleLogProfile(0.488272458526868), + xp_assert_close( + log_decoding_AppleLogProfile(xp_as_array(0.488272458526868, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_AppleLogProfile(0.694552983055191), + xp_assert_close( + log_decoding_AppleLogProfile(xp_as_array(0.694552983055191, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_DLog(self) -> None: + def test_n_dimensional_log_decoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.apple_log_profile.\ log_decoding_AppleLogProfile` definition n-dimensional arrays support. """ P = 0.398764556189331 - R = log_decoding_AppleLogProfile(P) + R = as_ndarray(log_decoding_AppleLogProfile(xp_as_array(P, xp=xp))) - P = np.tile(P, 6) - R = np.tile(R, 6) - np.testing.assert_allclose( - log_decoding_AppleLogProfile(P), R, atol=TOLERANCE_ABSOLUTE_TESTS + P = xp.tile(xp_as_array(P, xp=xp), (6,)) + R = xp.tile(xp_as_array(R, xp=xp), (6,)) + xp_assert_close( + log_decoding_AppleLogProfile(P), + R, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - P = np.reshape(P, (2, 3)) - R = np.reshape(R, (2, 3)) - np.testing.assert_allclose( - log_decoding_AppleLogProfile(P), R, atol=TOLERANCE_ABSOLUTE_TESTS + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3), xp=xp) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_decoding_AppleLogProfile(P), + R, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - P = np.reshape(P, (2, 3, 1)) - R = np.reshape(R, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_AppleLogProfile(P), R, atol=TOLERANCE_ABSOLUTE_TESTS + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3, 1), xp=xp) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_decoding_AppleLogProfile(P), + R, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_DLog(self) -> None: + def test_domain_range_scale_log_decoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.apple_log_profile.\ log_decoding_AppleLogProfile` definition domain and range scale support. """ P = 0.398764556189331 - R = log_decoding_AppleLogProfile(P) + R = as_ndarray(log_decoding_AppleLogProfile(xp_as_array(P, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_AppleLogProfile(P * factor), + xp_assert_close( + log_decoding_AppleLogProfile(xp_as_array(P * factor, xp=xp)), R * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_arib_std_b67.py b/colour/models/rgb/transfer_functions/tests/test_arib_std_b67.py index f63e41d363..90b7dd28e3 100644 --- a/colour/models/rgb/transfer_functions/tests/test_arib_std_b67.py +++ b/colour/models/rgb/transfer_functions/tests/test_arib_std_b67.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.arib_std_b67` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ oetf_ARIBSTDB67, oetf_inverse_ARIBSTDB67, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,77 +45,77 @@ class TestOetf_ARIBSTDB67: oetf_ARIBSTDB67` definition unit tests methods. """ - def test_oetf_ARIBSTDB67(self) -> None: + def test_oetf_ARIBSTDB67(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arib_std_b67.\ oetf_ARIBSTDB67` definition. """ - np.testing.assert_allclose( - oetf_ARIBSTDB67(-0.25), -0.25, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_ARIBSTDB67(xp_as_array(-0.25, xp=xp)), + -0.25, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_ARIBSTDB67(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_ARIBSTDB67(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_ARIBSTDB67(0.18), + xp_assert_close( + oetf_ARIBSTDB67(xp_as_array(0.18, xp=xp)), 0.212132034355964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_ARIBSTDB67(1.0), 0.5, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_ARIBSTDB67(xp_as_array(1.0, xp=xp)), + 0.5, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_ARIBSTDB67(64.0), + xp_assert_close( + oetf_ARIBSTDB67(xp_as_array(64.0, xp=xp)), 1.302858098046995, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_ARIBSTDB67(self) -> None: + def test_n_dimensional_oetf_ARIBSTDB67(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arib_std_b67.\ oetf_ARIBSTDB67` definition n-dimensional arrays support. """ E = 0.18 - E_p = oetf_ARIBSTDB67(E) + E_p = as_ndarray(oetf_ARIBSTDB67(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - oetf_ARIBSTDB67(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_ARIBSTDB67(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - oetf_ARIBSTDB67(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_ARIBSTDB67(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - oetf_ARIBSTDB67(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_ARIBSTDB67(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_ARIBSTDB67(self) -> None: + def test_domain_range_scale_oetf_ARIBSTDB67(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arib_std_b67.\ oetf_ARIBSTDB67` definition domain and range scale support. """ E = 0.18 - E_p = oetf_ARIBSTDB67(E) + E_p = as_ndarray(oetf_ARIBSTDB67(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_ARIBSTDB67(E * factor), + xp_assert_close( + oetf_ARIBSTDB67(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -122,79 +136,77 @@ class TestOetf_inverse_ARIBSTDB67: oetf_inverse_ARIBSTDB67` definition unit tests methods. """ - def test_oetf_inverse_ARIBSTDB67(self) -> None: + def test_oetf_inverse_ARIBSTDB67(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arib_std_b67.\ oetf_inverse_ARIBSTDB67` definition. """ - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(-0.25), + xp_assert_close( + oetf_inverse_ARIBSTDB67(xp_as_array(-0.25, xp=xp)), -0.25, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_ARIBSTDB67(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(0.212132034355964), + xp_assert_close( + oetf_inverse_ARIBSTDB67(xp_as_array(0.212132034355964, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(0.5), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_ARIBSTDB67(xp_as_array(0.5, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(1.302858098046995), + xp_assert_close( + oetf_inverse_ARIBSTDB67(xp_as_array(1.302858098046995, xp=xp)), 64.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_ARIBSTDB67(self) -> None: + def test_n_dimensional_oetf_inverse_ARIBSTDB67(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arib_std_b67.\ oetf_inverse_ARIBSTDB67` definition n-dimensional arrays support. """ E_p = 0.212132034355964 - E = oetf_inverse_ARIBSTDB67(E_p) + E = as_ndarray(oetf_inverse_ARIBSTDB67(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(oetf_inverse_ARIBSTDB67(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_ARIBSTDB67(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_ARIBSTDB67(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_ARIBSTDB67(self) -> None: + def test_domain_range_scale_oetf_inverse_ARIBSTDB67(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arib_std_b67.\ oetf_inverse_ARIBSTDB67` definition domain and range scale support. """ E_p = 0.212132034355964 - E = oetf_inverse_ARIBSTDB67(E_p) + E = as_ndarray(oetf_inverse_ARIBSTDB67(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_ARIBSTDB67(E_p * factor), + xp_assert_close( + oetf_inverse_ARIBSTDB67(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_arri.py b/colour/models/rgb/transfer_functions/tests/test_arri.py index feffb36c3b..1fb841496c 100644 --- a/colour/models/rgb/transfer_functions/tests/test_arri.py +++ b/colour/models/rgb/transfer_functions/tests/test_arri.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.arri` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -12,7 +16,17 @@ log_encoding_ARRILogC3, log_encoding_ARRILogC4, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -35,71 +49,65 @@ class TestLogEncoding_ARRILogC3: log_encoding_ARRILogC3` definition unit tests methods. """ - def test_log_encoding_ARRILogC3(self) -> None: + def test_log_encoding_ARRILogC3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_encoding_ARRILogC3` definition. """ - np.testing.assert_allclose( - log_encoding_ARRILogC3(0.0), + xp_assert_close( + log_encoding_ARRILogC3(xp_as_array(0.0, xp=xp)), 0.092809000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ARRILogC3(0.18), + xp_assert_close( + log_encoding_ARRILogC3(xp_as_array(0.18, xp=xp)), 0.391006832034084, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ARRILogC3(1.0), + xp_assert_close( + log_encoding_ARRILogC3(xp_as_array(1.0, xp=xp)), 0.570631558120417, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_ARRILogC3(self) -> None: + def test_n_dimensional_log_encoding_ARRILogC3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_encoding_ARRILogC3` definition n-dimensional arrays support. """ x = 0.18 - t = log_encoding_ARRILogC3(x) + t = as_ndarray(log_encoding_ARRILogC3(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - t = np.tile(t, 6) - np.testing.assert_allclose( - log_encoding_ARRILogC3(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + t = xp.tile(xp_as_array(t, xp=xp), (6,)) + xp_assert_close(log_encoding_ARRILogC3(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - t = np.reshape(t, (2, 3)) - np.testing.assert_allclose( - log_encoding_ARRILogC3(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_ARRILogC3(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - t = np.reshape(t, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_ARRILogC3(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_ARRILogC3(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_ARRILogC3(self) -> None: + def test_domain_range_scale_log_encoding_ARRILogC3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_encoding_ARRILogC3` definition domain and range scale support. """ x = 0.18 - t = log_encoding_ARRILogC3(x) + t = as_ndarray(log_encoding_ARRILogC3(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ARRILogC3(x * factor), + xp_assert_close( + log_encoding_ARRILogC3(xp_as_array(x * factor, xp=xp)), t * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -120,71 +128,65 @@ class TestLogDecoding_ARRILogC3: log_decoding_ARRILogC3` definition unit tests methods. """ - def test_log_decoding_ARRILogC3(self) -> None: + def test_log_decoding_ARRILogC3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_decoding_ARRILogC3` definition. """ - np.testing.assert_allclose( - log_decoding_ARRILogC3(0.092809), + xp_assert_close( + log_decoding_ARRILogC3(xp_as_array(0.092809, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ARRILogC3(0.391006832034084), + xp_assert_close( + log_decoding_ARRILogC3(xp_as_array(0.391006832034084, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ARRILogC3(0.570631558120417), + xp_assert_close( + log_decoding_ARRILogC3(xp_as_array(0.570631558120417, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_ARRILogC3(self) -> None: + def test_n_dimensional_log_decoding_ARRILogC3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_decoding_ARRILogC3` definition n-dimensional arrays support. """ t = 0.391006832034084 - x = log_decoding_ARRILogC3(t) + x = as_ndarray(log_decoding_ARRILogC3(xp_as_array(t, xp=xp))) - t = np.tile(t, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_ARRILogC3(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp.tile(xp_as_array(t, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_ARRILogC3(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - t = np.reshape(t, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_ARRILogC3(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_ARRILogC3(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - t = np.reshape(t, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_ARRILogC3(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_ARRILogC3(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_ARRILogC3(self) -> None: + def test_domain_range_scale_log_decoding_ARRILogC3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_decoding_ARRILogC3` definition domain and range scale support. """ t = 0.391006832034084 - x = log_decoding_ARRILogC3(t) + x = as_ndarray(log_decoding_ARRILogC3(xp_as_array(t, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ARRILogC3(t * factor), + xp_assert_close( + log_decoding_ARRILogC3(xp_as_array(t * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -205,71 +207,65 @@ class TestLogEncoding_ARRILogC4: log_encoding_ARRILogC4` definition unit tests methods. """ - def test_log_encoding_ARRILogC4(self) -> None: + def test_log_encoding_ARRILogC4(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_encoding_ARRILogC4` definition. """ - np.testing.assert_allclose( - log_encoding_ARRILogC4(0.0), + xp_assert_close( + log_encoding_ARRILogC4(xp_as_array(0.0, xp=xp)), 0.092864125122190, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ARRILogC4(0.18), + xp_assert_close( + log_encoding_ARRILogC4(xp_as_array(0.18, xp=xp)), 0.278395836548265, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ARRILogC4(1.0), + xp_assert_close( + log_encoding_ARRILogC4(xp_as_array(1.0, xp=xp)), 0.427519364835306, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_ARRILogC4(self) -> None: + def test_n_dimensional_log_encoding_ARRILogC4(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_encoding_ARRILogC4` definition n-dimensional arrays support. """ x = 0.18 - t = log_encoding_ARRILogC4(x) + t = as_ndarray(log_encoding_ARRILogC4(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - t = np.tile(t, 6) - np.testing.assert_allclose( - log_encoding_ARRILogC4(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + t = xp.tile(xp_as_array(t, xp=xp), (6,)) + xp_assert_close(log_encoding_ARRILogC4(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - t = np.reshape(t, (2, 3)) - np.testing.assert_allclose( - log_encoding_ARRILogC4(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_ARRILogC4(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - t = np.reshape(t, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_ARRILogC4(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_ARRILogC4(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_ARRILogC4(self) -> None: + def test_domain_range_scale_log_encoding_ARRILogC4(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_encoding_ARRILogC4` definition domain and range scale support. """ x = 0.18 - t = log_encoding_ARRILogC4(x) + t = as_ndarray(log_encoding_ARRILogC4(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ARRILogC4(x * factor), + xp_assert_close( + log_encoding_ARRILogC4(xp_as_array(x * factor, xp=xp)), t * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -290,71 +286,65 @@ class TestLogDecoding_ARRILogC4: log_decoding_ARRILogC4` definition unit tests methods. """ - def test_log_decoding_ARRILogC4(self) -> None: + def test_log_decoding_ARRILogC4(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_decoding_ARRILogC4` definition. """ - np.testing.assert_allclose( - log_decoding_ARRILogC4(0.092864125122190), + xp_assert_close( + log_decoding_ARRILogC4(xp_as_array(0.092864125122190, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ARRILogC4(0.278395836548265), + xp_assert_close( + log_decoding_ARRILogC4(xp_as_array(0.278395836548265, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ARRILogC4(0.427519364835306), + xp_assert_close( + log_decoding_ARRILogC4(xp_as_array(0.427519364835306, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_ARRILogC4(self) -> None: + def test_n_dimensional_log_decoding_ARRILogC4(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_decoding_ARRILogC4` definition n-dimensional arrays support. """ t = 0.278395836548265 - x = log_decoding_ARRILogC4(t) + x = as_ndarray(log_decoding_ARRILogC4(xp_as_array(t, xp=xp))) - t = np.tile(t, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_ARRILogC4(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp.tile(xp_as_array(t, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_ARRILogC4(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - t = np.reshape(t, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_ARRILogC4(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_ARRILogC4(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - t = np.reshape(t, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_ARRILogC4(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_ARRILogC4(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_ARRILogC4(self) -> None: + def test_domain_range_scale_log_decoding_ARRILogC4(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.arri.\ log_decoding_ARRILogC4` definition domain and range scale support. """ t = 0.278395836548265 - x = log_decoding_ARRILogC4(t) + x = as_ndarray(log_decoding_ARRILogC4(xp_as_array(t, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ARRILogC4(t * factor), + xp_assert_close( + log_decoding_ARRILogC4(xp_as_array(t * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_blackmagic_design.py b/colour/models/rgb/transfer_functions/tests/test_blackmagic_design.py index 84318a28d5..cb89432065 100644 --- a/colour/models/rgb/transfer_functions/tests/test_blackmagic_design.py +++ b/colour/models/rgb/transfer_functions/tests/test_blackmagic_design.py @@ -3,6 +3,10 @@ blackmagic_design` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ oetf_BlackmagicFilmGeneration5, oetf_inverse_BlackmagicFilmGeneration5, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,43 +45,43 @@ class TestOetf_BlackmagicFilmGeneration5: oetf_BlackmagicFilmGeneration5` definition unit tests methods. """ - def test_oetf_BlackmagicFilmGeneration5(self) -> None: + def test_oetf_BlackmagicFilmGeneration5(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ blackmagic_design.oetf_BlackmagicFilmGeneration5` definition. """ - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(0.0), + xp_assert_close( + oetf_BlackmagicFilmGeneration5(xp_as_array(0.0, xp=xp)), 0.092465753424658, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(0.18), + xp_assert_close( + oetf_BlackmagicFilmGeneration5(xp_as_array(0.18, xp=xp)), 0.383561643835617, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(1.0), + xp_assert_close( + oetf_BlackmagicFilmGeneration5(xp_as_array(1.0, xp=xp)), 0.530489624957305, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(100.0), + xp_assert_close( + oetf_BlackmagicFilmGeneration5(xp_as_array(100.0, xp=xp)), 0.930339851899973, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(222.86), + xp_assert_close( + oetf_BlackmagicFilmGeneration5(xp_as_array(222.86, xp=xp)), 0.999999631713769, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_BlackmagicFilmGeneration5(self) -> None: + def test_n_dimensional_oetf_BlackmagicFilmGeneration5(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ blackmagic_design.oetf_BlackmagicFilmGeneration5` definition n-dimensional @@ -75,27 +89,35 @@ def test_n_dimensional_oetf_BlackmagicFilmGeneration5(self) -> None: """ L = 0.18 - V = oetf_BlackmagicFilmGeneration5(L) + V = as_ndarray(oetf_BlackmagicFilmGeneration5(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - V = np.tile(V, 6) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(L), V, atol=TOLERANCE_ABSOLUTE_TESTS + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + xp_assert_close( + oetf_BlackmagicFilmGeneration5(L), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - L = np.reshape(L, (2, 3)) - V = np.reshape(V, (2, 3)) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(L), V, atol=TOLERANCE_ABSOLUTE_TESTS + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + xp_assert_close( + oetf_BlackmagicFilmGeneration5(L), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - L = np.reshape(L, (2, 3, 1)) - V = np.reshape(V, (2, 3, 1)) - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(L), V, atol=TOLERANCE_ABSOLUTE_TESTS + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + oetf_BlackmagicFilmGeneration5(L), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_oetf_BlackmagicFilmGeneration5(self) -> None: + def test_domain_range_scale_oetf_BlackmagicFilmGeneration5( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ blackmagic_design.oetf_BlackmagicFilmGeneration5` definition domain and range @@ -103,13 +125,13 @@ def test_domain_range_scale_oetf_BlackmagicFilmGeneration5(self) -> None: """ L = 0.18 - V = oetf_BlackmagicFilmGeneration5(L) + V = as_ndarray(oetf_BlackmagicFilmGeneration5(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BlackmagicFilmGeneration5(L * factor), + xp_assert_close( + oetf_BlackmagicFilmGeneration5(xp_as_array(L * factor, xp=xp)), V * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -133,43 +155,55 @@ class TestOetf_inverse_BlackmagicFilmGeneration5: methods. """ - def test_oetf_inverse_BlackmagicFilmGeneration5(self) -> None: + def test_oetf_inverse_BlackmagicFilmGeneration5(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ blackmagic_design.oetf_inverse_BlackmagicFilmGeneration5` definition. """ - np.testing.assert_allclose( - oetf_inverse_BlackmagicFilmGeneration5(0.092465753424658), + xp_assert_close( + oetf_inverse_BlackmagicFilmGeneration5( + xp_as_array(0.092465753424658, xp=xp) + ), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BlackmagicFilmGeneration5(0.383561643835617), + xp_assert_close( + oetf_inverse_BlackmagicFilmGeneration5( + xp_as_array(0.383561643835617, xp=xp) + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BlackmagicFilmGeneration5(0.530489624957305), + xp_assert_close( + oetf_inverse_BlackmagicFilmGeneration5( + xp_as_array(0.530489624957305, xp=xp) + ), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BlackmagicFilmGeneration5(0.930339851899973), + xp_assert_close( + oetf_inverse_BlackmagicFilmGeneration5( + xp_as_array(0.930339851899973, xp=xp) + ), 100.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BlackmagicFilmGeneration5(0.999999631713769), + xp_assert_close( + oetf_inverse_BlackmagicFilmGeneration5( + xp_as_array(0.999999631713769, xp=xp) + ), 222.86, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BlackmagicFilmGeneration5(self) -> None: + def test_n_dimensional_oetf_inverse_BlackmagicFilmGeneration5( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ blackmagic_design.oetf_inverse_BlackmagicFilmGeneration5` definition @@ -177,33 +211,35 @@ def test_n_dimensional_oetf_inverse_BlackmagicFilmGeneration5(self) -> None: """ V = 0.383561643835617 - L = oetf_inverse_BlackmagicFilmGeneration5(V) + L = as_ndarray(oetf_inverse_BlackmagicFilmGeneration5(xp_as_array(V, xp=xp))) - V = np.tile(V, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close( oetf_inverse_BlackmagicFilmGeneration5(V), L, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close( oetf_inverse_BlackmagicFilmGeneration5(V), L, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( oetf_inverse_BlackmagicFilmGeneration5(V), L, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_oetf_inverse_BlackmagicFilmGeneration5(self) -> None: + def test_domain_range_scale_oetf_inverse_BlackmagicFilmGeneration5( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ blackmagic_design.oetf_inverse_BlackmagicFilmGeneration5` definition domain and @@ -211,13 +247,15 @@ def test_domain_range_scale_oetf_inverse_BlackmagicFilmGeneration5(self) -> None """ V = 0.383561643835617 - L = oetf_inverse_BlackmagicFilmGeneration5(V) + L = as_ndarray(oetf_inverse_BlackmagicFilmGeneration5(xp_as_array(V, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BlackmagicFilmGeneration5(V * factor), + xp_assert_close( + oetf_inverse_BlackmagicFilmGeneration5( + xp_as_array(V * factor, xp=xp) + ), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_canon.py b/colour/models/rgb/transfer_functions/tests/test_canon.py index 8f44c0f0d2..39f3bd42e3 100644 --- a/colour/models/rgb/transfer_functions/tests/test_canon.py +++ b/colour/models/rgb/transfer_functions/tests/test_canon.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.canon` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -20,7 +24,17 @@ log_encoding_CanonLog_v1, log_encoding_CanonLog_v1_2, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -51,95 +65,95 @@ class TestLogEncoding_CanonLog_v1: log_encoding_CanonLog_v1` definition unit tests methods. """ - def test_log_encoding_CanonLog_v1(self) -> None: + def test_log_encoding_CanonLog_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog_v1` definition. """ - np.testing.assert_allclose( - log_encoding_CanonLog_v1(-0.1), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(-0.1, xp=xp)), -0.023560122781997, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(0.0), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(0.0, xp=xp)), 0.125122480156403, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(0.18), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(0.18, xp=xp)), 0.343389651726069, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(0.18, 12), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(0.18, xp=xp), 12), 0.343138084215647, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(0.18, 10, False), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(0.18, xp=xp), 10, False), 0.327953896935809, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(0.18, 10, False, False), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(0.18, xp=xp), 10, False, False), 0.312012855550395, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(1.0), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(1.0, xp=xp)), 0.618775485598649, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_CanonLog_v1(self) -> None: + def test_n_dimensional_log_encoding_CanonLog_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog_v1` definition n-dimensional arrays support. """ x = 0.18 - clog = log_encoding_CanonLog_v1(x) + clog = as_ndarray(log_encoding_CanonLog_v1(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - clog = np.tile(clog, 6) - np.testing.assert_allclose( + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + clog = xp.tile(xp_as_array(clog, xp=xp), (6,)) + xp_assert_close( log_encoding_CanonLog_v1(x), clog, atol=TOLERANCE_ABSOLUTE_TESTS ) - x = np.reshape(x, (2, 3)) - clog = np.reshape(clog, (2, 3)) - np.testing.assert_allclose( + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_encoding_CanonLog_v1(x), clog, atol=TOLERANCE_ABSOLUTE_TESTS ) - x = np.reshape(x, (2, 3, 1)) - clog = np.reshape(clog, (2, 3, 1)) - np.testing.assert_allclose( + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_encoding_CanonLog_v1(x), clog, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_log_encoding_CanonLog_v1(self) -> None: + def test_domain_range_scale_log_encoding_CanonLog_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog_v1` definition domain and range scale support. """ x = 0.18 - clog = log_encoding_CanonLog_v1(x) + clog = as_ndarray(log_encoding_CanonLog_v1(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_CanonLog_v1(x * factor), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(x * factor, xp=xp)), clog * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -160,95 +174,97 @@ class TestLogDecoding_CanonLog_v1: log_decoding_CanonLog_v1` definition unit tests methods. """ - def test_log_decoding_CanonLog_v1(self) -> None: + def test_log_decoding_CanonLog_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog_v1` definition. """ - np.testing.assert_allclose( - log_decoding_CanonLog_v1(-0.023560122781997), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(-0.023560122781997, xp=xp)), -0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(0.125122480156403), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(0.125122480156403, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(0.343389651726069), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(0.343389651726069, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(0.343138084215647, 12), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(0.343138084215647, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(0.327953896935809, 10, False), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(0.327953896935809, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(0.312012855550395, 10, False, False), + xp_assert_close( + log_decoding_CanonLog_v1( + xp_as_array(0.312012855550395, xp=xp), 10, False, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(0.618775485598649), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(0.618775485598649, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_CanonLog_v1(self) -> None: + def test_n_dimensional_log_decoding_CanonLog_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog_v1` definition n-dimensional arrays support. """ clog = 0.343389651726069 - x = log_decoding_CanonLog_v1(clog) + x = as_ndarray(log_decoding_CanonLog_v1(xp_as_array(clog, xp=xp))) - clog = np.tile(clog, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( + clog = xp.tile(xp_as_array(clog, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close( log_decoding_CanonLog_v1(clog), x, atol=TOLERANCE_ABSOLUTE_TESTS ) - clog = np.reshape(clog, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_decoding_CanonLog_v1(clog), x, atol=TOLERANCE_ABSOLUTE_TESTS ) - clog = np.reshape(clog, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_decoding_CanonLog_v1(clog), x, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_log_decoding_CanonLog_v1(self) -> None: + def test_domain_range_scale_log_decoding_CanonLog_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog_v1` definition domain and range scale support. """ clog = 0.343389651726069 - x = log_decoding_CanonLog_v1(clog) + x = as_ndarray(log_decoding_CanonLog_v1(xp_as_array(clog, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_CanonLog_v1(clog * factor), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(clog * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -269,120 +285,138 @@ class TestLogEncoding_CanonLog_v1_2: log_encoding_CanonLog_v1_2` definition unit tests methods. """ - def test_log_encoding_CanonLog_v1_2(self) -> None: + def test_log_encoding_CanonLog_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog_v1_2` definition. """ - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(-0.1), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(-0.1, xp=xp)), -0.023560121389098, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(0.0), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(0.0, xp=xp)), 0.125122480000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(0.18), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(0.18, xp=xp)), 0.343389649295280, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(0.18, 12), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(0.18, xp=xp), 12), 0.343389649295281, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(0.18, 10, False), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(0.18, xp=xp), 10, False), 0.327953894097114, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(0.18, 10, False, False), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(0.18, xp=xp), 10, False, False), 0.312012852877809, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(1.0), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(1.0, xp=xp)), 0.618775480298287, atol=TOLERANCE_ABSOLUTE_TESTS, ) samples = np.linspace(0, 1, 10000) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(samples), - log_encoding_CanonLog_v1_2(samples), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_encoding_CanonLog_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(samples), - log_encoding_CanonLog_v1_2(samples), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_encoding_CanonLog_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(samples, out_normalised_code_value=False), - log_encoding_CanonLog_v1_2(samples, out_normalised_code_value=False), + xp_assert_close( + log_encoding_CanonLog_v1( + xp_as_array(samples, xp=xp), out_normalised_code_value=False + ), + as_ndarray( + log_encoding_CanonLog_v1_2( + xp_as_array(samples, xp=xp), out_normalised_code_value=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog_v1(samples, in_reflection=False), - log_encoding_CanonLog_v1_2(samples, in_reflection=False), + xp_assert_close( + log_encoding_CanonLog_v1(xp_as_array(samples, xp=xp), in_reflection=False), + as_ndarray( + log_encoding_CanonLog_v1_2( + xp_as_array(samples, xp=xp), in_reflection=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_CanonLog_v1_2(self) -> None: + def test_n_dimensional_log_encoding_CanonLog_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog_v1_2` definition n-dimensional arrays support. """ x = 0.18 - clog = log_encoding_CanonLog_v1_2(x) + clog = as_ndarray(log_encoding_CanonLog_v1_2(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - clog = np.tile(clog, 6) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(x), clog, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + clog = xp.tile(xp_as_array(clog, xp=xp), (6,)) + xp_assert_close( + log_encoding_CanonLog_v1_2(x), + clog, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3)) - clog = np.reshape(clog, (2, 3)) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(x), clog, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_encoding_CanonLog_v1_2(x), + clog, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3, 1)) - clog = np.reshape(clog, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(x), clog, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_encoding_CanonLog_v1_2(x), + clog, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_CanonLog_v1_2(self) -> None: + def test_domain_range_scale_log_encoding_CanonLog_v1_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog_v1_2` definition domain and range scale support. """ x = 0.18 - clog = log_encoding_CanonLog_v1_2(x) + clog = as_ndarray(log_encoding_CanonLog_v1_2(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_CanonLog_v1_2(x * factor), + xp_assert_close( + log_encoding_CanonLog_v1_2(xp_as_array(x * factor, xp=xp)), clog * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -403,120 +437,142 @@ class TestLogDecoding_CanonLog_v1_2: log_decoding_CanonLog_v1_2` definition unit tests methods. """ - def test_log_decoding_CanonLog_v1_2(self) -> None: + def test_log_decoding_CanonLog_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog_v1_2` definition. """ - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(-0.023560121389098), + xp_assert_close( + log_decoding_CanonLog_v1_2(xp_as_array(-0.023560121389098, xp=xp)), -0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(0.125122480000000), + xp_assert_close( + log_decoding_CanonLog_v1_2(xp_as_array(0.125122480000000, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(0.343389649295280), + xp_assert_close( + log_decoding_CanonLog_v1_2(xp_as_array(0.343389649295280, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(0.343389649295281, 12), + xp_assert_close( + log_decoding_CanonLog_v1_2(xp_as_array(0.343389649295281, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(0.327953894097114, 10, False), + xp_assert_close( + log_decoding_CanonLog_v1_2( + xp_as_array(0.327953894097114, xp=xp), 10, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(0.312012852877809, 10, False, False), + xp_assert_close( + log_decoding_CanonLog_v1_2( + xp_as_array(0.312012852877809, xp=xp), 10, False, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(0.618775480298287), + xp_assert_close( + log_decoding_CanonLog_v1_2(xp_as_array(0.618775480298287, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) samples = np.linspace(0, 1, 10000) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples), - log_decoding_CanonLog_v1_2(samples), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_decoding_CanonLog_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples), - log_decoding_CanonLog_v1_2(samples), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_decoding_CanonLog_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples, in_normalised_code_value=False), - log_decoding_CanonLog_v1_2(samples, in_normalised_code_value=False), + xp_assert_close( + log_decoding_CanonLog_v1( + xp_as_array(samples, xp=xp), in_normalised_code_value=False + ), + as_ndarray( + log_decoding_CanonLog_v1_2( + xp_as_array(samples, xp=xp), in_normalised_code_value=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples, out_reflection=False), - log_decoding_CanonLog_v1_2(samples, out_reflection=False), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(samples, xp=xp), out_reflection=False), + as_ndarray( + log_decoding_CanonLog_v1_2( + xp_as_array(samples, xp=xp), out_reflection=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_CanonLog_v1_2(self) -> None: + def test_n_dimensional_log_decoding_CanonLog_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog_v1_2` definition n-dimensional arrays support. """ clog = 0.343389649295280 - x = log_decoding_CanonLog_v1_2(clog) + x = as_ndarray(log_decoding_CanonLog_v1_2(xp_as_array(clog, xp=xp))) - clog = np.tile(clog, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(clog), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog = xp.tile(xp_as_array(clog, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close( + log_decoding_CanonLog_v1_2(clog), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog = np.reshape(clog, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(clog), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_decoding_CanonLog_v1_2(clog), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog = np.reshape(clog, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(clog), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog = xp_reshape(xp_as_array(clog, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_decoding_CanonLog_v1_2(clog), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_CanonLog_v1_2(self) -> None: + def test_domain_range_scale_log_decoding_CanonLog_v1_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog_v1_2` definition domain and range scale support. """ clog = 0.343389649295280 - x = log_decoding_CanonLog_v1_2(clog) + x = as_ndarray(log_decoding_CanonLog_v1_2(xp_as_array(clog, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_CanonLog_v1_2(clog * factor), + xp_assert_close( + log_decoding_CanonLog_v1_2(xp_as_array(clog * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -537,95 +593,101 @@ class TestLogEncoding_CanonLog2_v1: log_encoding_CanonLog2_v1` definition unit tests methods. """ - def test_log_encoding_CanonLog2_v1(self) -> None: + def test_log_encoding_CanonLog2_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog2_v1` definition. """ - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(-0.1), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(-0.1, xp=xp)), -0.155370131996824, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(0.0), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(0.0, xp=xp)), 0.092864125247312, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(0.18), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(0.18, xp=xp)), 0.398254694983167, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(0.18, 12), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(0.18, xp=xp), 12), 0.397962933301861, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(0.18, 10, False), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(0.18, xp=xp), 10, False), 0.392025745397009, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(0.18, 10, False, False), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(0.18, xp=xp), 10, False, False), 0.379864582222983, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(1.0), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(1.0, xp=xp)), 0.573229282897641, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_CanonLog2_v1(self) -> None: + def test_n_dimensional_log_encoding_CanonLog2_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog2_v1` definition n-dimensional arrays support. """ x = 0.18 - clog2 = log_encoding_CanonLog2_v1(x) + clog2 = as_ndarray(log_encoding_CanonLog2_v1(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - clog2 = np.tile(clog2, 6) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(x), clog2, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + clog2 = xp.tile(xp_as_array(clog2, xp=xp), (6,)) + xp_assert_close( + log_encoding_CanonLog2_v1(x), + clog2, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3)) - clog2 = np.reshape(clog2, (2, 3)) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(x), clog2, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_encoding_CanonLog2_v1(x), + clog2, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3, 1)) - clog2 = np.reshape(clog2, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(x), clog2, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_encoding_CanonLog2_v1(x), + clog2, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_CanonLog2_v1(self) -> None: + def test_domain_range_scale_log_encoding_CanonLog2_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog2_v1` definition domain and range scale support. """ x = 0.18 - clog2 = log_encoding_CanonLog2_v1(x) + clog2 = as_ndarray(log_encoding_CanonLog2_v1(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(x * factor), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(x * factor, xp=xp)), clog2 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -646,95 +708,103 @@ class TestLogDecoding_CanonLog2_v1: log_decoding_CanonLog2_v1` definition unit tests methods. """ - def test_log_decoding_CanonLog2_v1(self) -> None: + def test_log_decoding_CanonLog2_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog2_v1` definition. """ - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(-0.155370131996824), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(-0.155370131996824, xp=xp)), -0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(0.092864125247312), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(0.092864125247312, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(0.398254694983167), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(0.398254694983167, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(0.397962933301861, 12), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(0.397962933301861, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(0.392025745397009, 10, False), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(0.392025745397009, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(0.379864582222983, 10, False, False), + xp_assert_close( + log_decoding_CanonLog2_v1( + xp_as_array(0.379864582222983, xp=xp), 10, False, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(0.573229282897641), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(0.573229282897641, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_CanonLog2_v1(self) -> None: + def test_n_dimensional_log_decoding_CanonLog2_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog2_v1` definition n-dimensional arrays support. """ clog2 = 0.398254694983167 - x = log_decoding_CanonLog2_v1(clog2) + x = as_ndarray(log_decoding_CanonLog2_v1(xp_as_array(clog2, xp=xp))) - clog2 = np.tile(clog2, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(clog2), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog2 = xp.tile(xp_as_array(clog2, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close( + log_decoding_CanonLog2_v1(clog2), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog2 = np.reshape(clog2, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(clog2), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_decoding_CanonLog2_v1(clog2), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog2 = np.reshape(clog2, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(clog2), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_decoding_CanonLog2_v1(clog2), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_CanonLog2_v1(self) -> None: + def test_domain_range_scale_log_decoding_CanonLog2_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog2_v1` definition domain and range scale support. """ clog = 0.398254694983167 - x = log_decoding_CanonLog2_v1(clog) + x = as_ndarray(log_decoding_CanonLog2_v1(xp_as_array(clog, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_CanonLog2_v1(clog * factor), + xp_assert_close( + log_decoding_CanonLog2_v1(xp_as_array(clog * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -755,126 +825,138 @@ class TestLogEncoding_CanonLog2_v1_2: log_encoding_CanonLog2_v1_2` definition unit tests methods. """ - def test_log_encoding_CanonLog2_v1_2(self) -> None: + def test_log_encoding_CanonLog2_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog2_v1_2` definition. """ - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(-0.1), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(-0.1, xp=xp)), -0.155370130476722, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(0.0), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(0.0, xp=xp)), 0.092864125000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(0.18), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(0.18, xp=xp)), 0.398254692561492, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(0.18, 12), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(0.18, xp=xp), 12), 0.398254692561492, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(0.18, 10, False), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(0.18, xp=xp), 10, False), 0.392025742568957, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(0.18, 10, False, False), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(0.18, xp=xp), 10, False, False), 0.379864579481518, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(1.0), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(1.0, xp=xp)), 0.573229279230156, atol=TOLERANCE_ABSOLUTE_TESTS, ) samples = np.linspace(0, 1, 10000) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(samples), - log_encoding_CanonLog2_v1_2(samples), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_encoding_CanonLog2_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(samples), - log_encoding_CanonLog2_v1_2(samples), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_encoding_CanonLog2_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(samples, out_normalised_code_value=False), - log_encoding_CanonLog2_v1_2(samples, out_normalised_code_value=False), + xp_assert_close( + log_encoding_CanonLog2_v1( + xp_as_array(samples, xp=xp), out_normalised_code_value=False + ), + as_ndarray( + log_encoding_CanonLog2_v1_2( + xp_as_array(samples, xp=xp), out_normalised_code_value=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog2_v1(samples, in_reflection=False), - log_encoding_CanonLog2_v1_2(samples, in_reflection=False), + xp_assert_close( + log_encoding_CanonLog2_v1(xp_as_array(samples, xp=xp), in_reflection=False), + as_ndarray( + log_encoding_CanonLog2_v1_2( + xp_as_array(samples, xp=xp), in_reflection=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_CanonLog2_v1_2(self) -> None: + def test_n_dimensional_log_encoding_CanonLog2_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog2_v1_2` definition n-dimensional arrays support. """ x = 0.18 - clog2 = log_encoding_CanonLog2_v1_2(x) + clog2 = as_ndarray(log_encoding_CanonLog2_v1_2(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - clog2 = np.tile(clog2, 6) - np.testing.assert_allclose( + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + clog2 = xp.tile(xp_as_array(clog2, xp=xp), (6,)) + xp_assert_close( log_encoding_CanonLog2_v1_2(x), clog2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3)) - clog2 = np.reshape(clog2, (2, 3)) - np.testing.assert_allclose( + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_encoding_CanonLog2_v1_2(x), clog2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3, 1)) - clog2 = np.reshape(clog2, (2, 3, 1)) - np.testing.assert_allclose( + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_encoding_CanonLog2_v1_2(x), clog2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_CanonLog2_v1_2(self) -> None: + def test_domain_range_scale_log_encoding_CanonLog2_v1_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog2_v1_2` definition domain and range scale support. """ x = 0.18 - clog2 = log_encoding_CanonLog2_v1_2(x) + clog2 = as_ndarray(log_encoding_CanonLog2_v1_2(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_CanonLog2_v1_2(x * factor), + xp_assert_close( + log_encoding_CanonLog2_v1_2(xp_as_array(x * factor, xp=xp)), clog2 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -895,126 +977,142 @@ class TestLogDecoding_CanonLog2_v1_2: log_decoding_CanonLog2_v1_2` definition unit tests methods. """ - def test_log_decoding_CanonLog2_v1_2(self) -> None: + def test_log_decoding_CanonLog2_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog2_v1_2` definition. """ - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(-0.155370130476722), + xp_assert_close( + log_decoding_CanonLog2_v1_2(xp_as_array(-0.155370130476722, xp=xp)), -0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(0.092864125000000), + xp_assert_close( + log_decoding_CanonLog2_v1_2(xp_as_array(0.092864125000000, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(0.398254692561492), + xp_assert_close( + log_decoding_CanonLog2_v1_2(xp_as_array(0.398254692561492, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(0.398254692561492, 12), + xp_assert_close( + log_decoding_CanonLog2_v1_2(xp_as_array(0.398254692561492, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(0.392025742568957, 10, False), + xp_assert_close( + log_decoding_CanonLog2_v1_2( + xp_as_array(0.392025742568957, xp=xp), 10, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(0.379864579481518, 10, False, False), + xp_assert_close( + log_decoding_CanonLog2_v1_2( + xp_as_array(0.379864579481518, xp=xp), 10, False, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(0.573229279230156), + xp_assert_close( + log_decoding_CanonLog2_v1_2(xp_as_array(0.573229279230156, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) samples = np.linspace(0, 1, 10000) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples), - log_decoding_CanonLog_v1_2(samples), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_decoding_CanonLog_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples), - log_decoding_CanonLog_v1_2(samples), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_decoding_CanonLog_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples, in_normalised_code_value=False), - log_decoding_CanonLog_v1_2(samples, in_normalised_code_value=False), + xp_assert_close( + log_decoding_CanonLog_v1( + xp_as_array(samples, xp=xp), in_normalised_code_value=False + ), + as_ndarray( + log_decoding_CanonLog_v1_2( + xp_as_array(samples, xp=xp), in_normalised_code_value=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog_v1(samples, out_reflection=False), - log_decoding_CanonLog_v1_2(samples, out_reflection=False), + xp_assert_close( + log_decoding_CanonLog_v1(xp_as_array(samples, xp=xp), out_reflection=False), + as_ndarray( + log_decoding_CanonLog_v1_2( + xp_as_array(samples, xp=xp), out_reflection=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_CanonLog2_v1_2(self) -> None: + def test_n_dimensional_log_decoding_CanonLog2_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog2_v1_2` definition n-dimensional arrays support. """ clog2 = 0.398254692561492 - x = log_decoding_CanonLog2_v1_2(clog2) + x = as_ndarray(log_decoding_CanonLog2_v1_2(xp_as_array(clog2, xp=xp))) - clog2 = np.tile(clog2, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( + clog2 = xp.tile(xp_as_array(clog2, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close( log_decoding_CanonLog2_v1_2(clog2), x, atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog2 = np.reshape(clog2, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_decoding_CanonLog2_v1_2(clog2), x, atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog2 = np.reshape(clog2, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( + clog2 = xp_reshape(xp_as_array(clog2, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_decoding_CanonLog2_v1_2(clog2), x, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_CanonLog2_v1_2(self) -> None: + def test_domain_range_scale_log_decoding_CanonLog2_v1_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog2_v1_2` definition domain and range scale support. """ clog = 0.398254692561492 - x = log_decoding_CanonLog2_v1_2(clog) + x = as_ndarray(log_decoding_CanonLog2_v1_2(xp_as_array(clog, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_CanonLog2_v1_2(clog * factor), + xp_assert_close( + log_decoding_CanonLog2_v1_2(xp_as_array(clog * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1035,95 +1133,101 @@ class TestLogEncoding_CanonLog3_v1: log_encoding_CanonLog3_v1` definition unit tests methods. """ - def test_log_encoding_CanonLog3_v1(self) -> None: + def test_log_encoding_CanonLog3_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog3_v1` definition. """ - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(-0.1), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(-0.1, xp=xp)), -0.028494506076432, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(0.0), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(0.0, xp=xp)), 0.125122189869013, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(0.18), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(0.18, xp=xp)), 0.343389369388687, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(0.18, 12), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(0.18, xp=xp), 12), 0.343137802085105, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(0.18, 10, False), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(0.18, xp=xp), 10, False), 0.327953567219893, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(0.18, 10, False, False), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(0.18, xp=xp), 10, False, False), 0.313436005886328, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(1.0), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(1.0, xp=xp)), 0.580277796238604, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_CanonLog3_v1(self) -> None: + def test_n_dimensional_log_encoding_CanonLog3_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog3_v1` definition n-dimensional arrays support. """ x = 0.18 - clog3 = log_encoding_CanonLog3_v1(x) + clog3 = as_ndarray(log_encoding_CanonLog3_v1(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - clog3 = np.tile(clog3, 6) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(x), clog3, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + clog3 = xp.tile(xp_as_array(clog3, xp=xp), (6,)) + xp_assert_close( + log_encoding_CanonLog3_v1(x), + clog3, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3)) - clog3 = np.reshape(clog3, (2, 3)) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(x), clog3, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_encoding_CanonLog3_v1(x), + clog3, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3, 1)) - clog3 = np.reshape(clog3, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(x), clog3, atol=TOLERANCE_ABSOLUTE_TESTS + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_encoding_CanonLog3_v1(x), + clog3, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_CanonLog3_v1(self) -> None: + def test_domain_range_scale_log_encoding_CanonLog3_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog3_v1` definition domain and range scale support. """ x = 0.18 - clog3 = log_encoding_CanonLog3_v1(x) + clog3 = as_ndarray(log_encoding_CanonLog3_v1(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(x * factor), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(x * factor, xp=xp)), clog3 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1144,95 +1248,103 @@ class TestLogDecoding_CanonLog3_v1: log_decoding_CanonLog3_v1` definition unit tests methods. """ - def test_log_decoding_CanonLog3_v1(self) -> None: + def test_log_decoding_CanonLog3_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog3_v1` definition. """ - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(-0.028494506076432), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(-0.028494506076432, xp=xp)), -0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(0.125122189869013), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(0.125122189869013, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(0.343389369388687), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(0.343389369388687, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(0.343137802085105, 12), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(0.343137802085105, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(0.327953567219893, 10, False), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(0.327953567219893, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(0.313436005886328, 10, False, False), + xp_assert_close( + log_decoding_CanonLog3_v1( + xp_as_array(0.313436005886328, xp=xp), 10, False, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(0.580277796238604), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(0.580277796238604, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_CanonLog3_v1(self) -> None: + def test_n_dimensional_log_decoding_CanonLog3_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog3_v1` definition n-dimensional arrays support. """ clog3 = 0.343389369388687 - x = log_decoding_CanonLog3_v1(clog3) + x = as_ndarray(log_decoding_CanonLog3_v1(xp_as_array(clog3, xp=xp))) - clog3 = np.tile(clog3, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(clog3), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog3 = xp.tile(xp_as_array(clog3, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close( + log_decoding_CanonLog3_v1(clog3), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog3 = np.reshape(clog3, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(clog3), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close( + log_decoding_CanonLog3_v1(clog3), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog3 = np.reshape(clog3, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(clog3), x, atol=TOLERANCE_ABSOLUTE_TESTS + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + log_decoding_CanonLog3_v1(clog3), + x, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_CanonLog3_v1(self) -> None: + def test_domain_range_scale_log_decoding_CanonLog3_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog3_v1` definition domain and range scale support. """ clog = 0.343389369388687 - x = log_decoding_CanonLog3_v1(clog) + x = as_ndarray(log_decoding_CanonLog3_v1(xp_as_array(clog, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(clog * factor), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(clog * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1253,126 +1365,138 @@ class TestLogEncoding_CanonLog3_v1_2: log_encoding_CanonLog3_v1_2` definition unit tests methods. """ - def test_log_encoding_CanonLog3_v1_2(self) -> None: + def test_log_encoding_CanonLog3_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog3_v1_2` definition. """ - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(-0.1), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(-0.1, xp=xp)), -0.028494507620494, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(0.0), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(0.0, xp=xp)), 0.125122189999999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(0.18), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(0.18, xp=xp)), 0.343389370373936, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(0.18, 12), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(0.18, xp=xp), 12), 0.343389370373936, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(0.18, 10, False), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(0.18, xp=xp), 10, False), 0.327953568370475, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(0.18, 10, False, False), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(0.18, xp=xp), 10, False, False), 0.313436007221221, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(1.0), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(1.0, xp=xp)), 0.580277794216371, atol=TOLERANCE_ABSOLUTE_TESTS, ) samples = np.linspace(0, 1, 10000) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(samples), - log_encoding_CanonLog3_v1_2(samples), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_encoding_CanonLog3_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(samples), - log_encoding_CanonLog3_v1_2(samples), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_encoding_CanonLog3_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(samples, out_normalised_code_value=False), - log_encoding_CanonLog3_v1_2(samples, out_normalised_code_value=False), + xp_assert_close( + log_encoding_CanonLog3_v1( + xp_as_array(samples, xp=xp), out_normalised_code_value=False + ), + as_ndarray( + log_encoding_CanonLog3_v1_2( + xp_as_array(samples, xp=xp), out_normalised_code_value=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_CanonLog3_v1(samples, in_reflection=False), - log_encoding_CanonLog3_v1_2(samples, in_reflection=False), + xp_assert_close( + log_encoding_CanonLog3_v1(xp_as_array(samples, xp=xp), in_reflection=False), + as_ndarray( + log_encoding_CanonLog3_v1_2( + xp_as_array(samples, xp=xp), in_reflection=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_CanonLog3_v1_2(self) -> None: + def test_n_dimensional_log_encoding_CanonLog3_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog3_v1_2` definition n-dimensional arrays support. """ x = 0.18 - clog3 = log_encoding_CanonLog3_v1_2(x) + clog3 = as_ndarray(log_encoding_CanonLog3_v1_2(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - clog3 = np.tile(clog3, 6) - np.testing.assert_allclose( + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + clog3 = xp.tile(xp_as_array(clog3, xp=xp), (6,)) + xp_assert_close( log_encoding_CanonLog3_v1_2(x), clog3, atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3)) - clog3 = np.reshape(clog3, (2, 3)) - np.testing.assert_allclose( + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_encoding_CanonLog3_v1_2(x), clog3, atol=TOLERANCE_ABSOLUTE_TESTS, ) - x = np.reshape(x, (2, 3, 1)) - clog3 = np.reshape(clog3, (2, 3, 1)) - np.testing.assert_allclose( + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_encoding_CanonLog3_v1_2(x), clog3, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_encoding_CanonLog3_v1_2(self) -> None: + def test_domain_range_scale_log_encoding_CanonLog3_v1_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_encoding_CanonLog3_v1_2` definition domain and range scale support. """ x = 0.18 - clog3 = log_encoding_CanonLog3_v1_2(x) + clog3 = as_ndarray(log_encoding_CanonLog3_v1_2(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_CanonLog3_v1_2(x * factor), + xp_assert_close( + log_encoding_CanonLog3_v1_2(xp_as_array(x * factor, xp=xp)), clog3 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1393,126 +1517,144 @@ class TestLogDecoding_CanonLog3_v1_2: log_decoding_CanonLog3_v1_2` definition unit tests methods. """ - def test_log_decoding_CanonLog3_v1_2(self) -> None: + def test_log_decoding_CanonLog3_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog3_v1_2` definition. """ - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(-0.028494507620494), + xp_assert_close( + log_decoding_CanonLog3_v1_2(xp_as_array(-0.028494507620494, xp=xp)), -0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(0.125122189999999), + xp_assert_close( + log_decoding_CanonLog3_v1_2(xp_as_array(0.125122189999999, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(0.343389370373936), + xp_assert_close( + log_decoding_CanonLog3_v1_2(xp_as_array(0.343389370373936, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(0.343389370373936, 12), + xp_assert_close( + log_decoding_CanonLog3_v1_2(xp_as_array(0.343389370373936, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(0.327953568370475, 10, False), + xp_assert_close( + log_decoding_CanonLog3_v1_2( + xp_as_array(0.327953568370475, xp=xp), 10, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(0.313436007221221, 10, False, False), + xp_assert_close( + log_decoding_CanonLog3_v1_2( + xp_as_array(0.313436007221221, xp=xp), 10, False, False + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(0.580277794216371), + xp_assert_close( + log_decoding_CanonLog3_v1_2(xp_as_array(0.580277794216371, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) samples = np.linspace(0, 1, 10000) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(samples), - log_decoding_CanonLog3_v1_2(samples), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_decoding_CanonLog3_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(samples), - log_decoding_CanonLog3_v1_2(samples), + xp_assert_close( + log_decoding_CanonLog3_v1(xp_as_array(samples, xp=xp)), + as_ndarray(log_decoding_CanonLog3_v1_2(xp_as_array(samples, xp=xp))), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(samples, in_normalised_code_value=False), - log_decoding_CanonLog3_v1_2(samples, in_normalised_code_value=False), + xp_assert_close( + log_decoding_CanonLog3_v1( + xp_as_array(samples, xp=xp), in_normalised_code_value=False + ), + as_ndarray( + log_decoding_CanonLog3_v1_2( + xp_as_array(samples, xp=xp), in_normalised_code_value=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_CanonLog3_v1(samples, out_reflection=False), - log_decoding_CanonLog3_v1_2(samples, out_reflection=False), + xp_assert_close( + log_decoding_CanonLog3_v1( + xp_as_array(samples, xp=xp), out_reflection=False + ), + as_ndarray( + log_decoding_CanonLog3_v1_2( + xp_as_array(samples, xp=xp), out_reflection=False + ) + ), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_CanonLog3_v1_2(self) -> None: + def test_n_dimensional_log_decoding_CanonLog3_v1_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog3_v1_2` definition n-dimensional arrays support. """ clog3 = 0.343389370373936 - x = log_decoding_CanonLog3_v1_2(clog3) + x = as_ndarray(log_decoding_CanonLog3_v1_2(xp_as_array(clog3, xp=xp))) - clog3 = np.tile(clog3, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( + clog3 = xp.tile(xp_as_array(clog3, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close( log_decoding_CanonLog3_v1_2(clog3), x, atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog3 = np.reshape(clog3, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close( log_decoding_CanonLog3_v1_2(clog3), x, atol=TOLERANCE_ABSOLUTE_TESTS, ) - clog3 = np.reshape(clog3, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( + clog3 = xp_reshape(xp_as_array(clog3, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( log_decoding_CanonLog3_v1_2(clog3), x, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_log_decoding_CanonLog3_v1_2(self) -> None: + def test_domain_range_scale_log_decoding_CanonLog3_v1_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.canon.\ log_decoding_CanonLog3_v1_2` definition domain and range scale support. """ clog = 0.343389370373936 - x = log_decoding_CanonLog3_v1_2(clog) + x = as_ndarray(log_decoding_CanonLog3_v1_2(xp_as_array(clog, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_CanonLog3_v1_2(clog * factor), + xp_assert_close( + log_decoding_CanonLog3_v1_2(xp_as_array(clog * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_cineon.py b/colour/models/rgb/transfer_functions/tests/test_cineon.py index 7354dc916b..12d4cb1a79 100644 --- a/colour/models/rgb/transfer_functions/tests/test_cineon.py +++ b/colour/models/rgb/transfer_functions/tests/test_cineon.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.cineon` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_Cineon, log_encoding_Cineon, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +45,65 @@ class TestLogEncoding_Cineon: log_encoding_Cineon` definition unit tests methods. """ - def test_log_encoding_Cineon(self) -> None: + def test_log_encoding_Cineon(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.cineon.\ log_encoding_Cineon` definition. """ - np.testing.assert_allclose( - log_encoding_Cineon(0.0), + xp_assert_close( + log_encoding_Cineon(xp_as_array(0.0, xp=xp)), 0.092864125122190, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Cineon(0.18), + xp_assert_close( + log_encoding_Cineon(xp_as_array(0.18, xp=xp)), 0.457319613085418, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Cineon(1.0), + xp_assert_close( + log_encoding_Cineon(xp_as_array(1.0, xp=xp)), 0.669599217986315, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Cineon(self) -> None: + def test_n_dimensional_log_encoding_Cineon(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.cineon.\ log_encoding_Cineon` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Cineon(x) + y = as_ndarray(log_encoding_Cineon(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Cineon(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Cineon(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Cineon(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Cineon(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Cineon(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Cineon(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Cineon(self) -> None: + def test_domain_range_scale_log_encoding_Cineon(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.cineon.\ log_encoding_Cineon` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Cineon(x) + y = as_ndarray(log_encoding_Cineon(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Cineon(x * factor), + xp_assert_close( + log_encoding_Cineon(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -116,71 +124,65 @@ class TestLogDecoding_Cineon: log_decoding_Cineon` definition unit tests methods. """ - def test_log_decoding_Cineon(self) -> None: + def test_log_decoding_Cineon(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.cineon.\ log_decoding_Cineon` definition. """ - np.testing.assert_allclose( - log_decoding_Cineon(0.092864125122190), + xp_assert_close( + log_decoding_Cineon(xp_as_array(0.092864125122190, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Cineon(0.457319613085418), + xp_assert_close( + log_decoding_Cineon(xp_as_array(0.457319613085418, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Cineon(0.669599217986315), + xp_assert_close( + log_decoding_Cineon(xp_as_array(0.669599217986315, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Cineon(self) -> None: + def test_n_dimensional_log_decoding_Cineon(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.cineon.\ log_decoding_Cineon` definition n-dimensional arrays support. """ y = 0.457319613085418 - x = log_decoding_Cineon(y) + x = as_ndarray(log_decoding_Cineon(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Cineon(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Cineon(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Cineon(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Cineon(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Cineon(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Cineon(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Cineon(self) -> None: + def test_domain_range_scale_log_decoding_Cineon(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.cineon.\ log_decoding_Cineon` definition domain and range scale support. """ y = 0.457319613085418 - x = log_decoding_Cineon(y) + x = as_ndarray(log_decoding_Cineon(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Cineon(y * factor), + xp_assert_close( + log_decoding_Cineon(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_common.py b/colour/models/rgb/transfer_functions/tests/test_common.py index 18f68848b1..06b9e61553 100644 --- a/colour/models/rgb/transfer_functions/tests/test_common.py +++ b/colour/models/rgb/transfer_functions/tests/test_common.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.common` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import CV_range, full_to_legal, legal_to_full -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -35,29 +49,29 @@ def test_CV_range(self) -> None: definition. """ - np.testing.assert_array_equal(CV_range(8, True, True), np.array([16, 235])) + xp_assert_equal(CV_range(8, True, True), [16, 235]) - np.testing.assert_array_equal(CV_range(8, False, True), np.array([0, 255])) + xp_assert_equal(CV_range(8, False, True), [0, 255]) - np.testing.assert_allclose( + xp_assert_close( CV_range(8, True, False), - np.array([0.06274510, 0.92156863]), + [0.06274510, 0.92156863], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal(CV_range(8, False, False), np.array([0, 1])) + xp_assert_equal(CV_range(8, False, False), [0, 1]) - np.testing.assert_array_equal(CV_range(10, True, True), np.array([64, 940])) + xp_assert_equal(CV_range(10, True, True), [64, 940]) - np.testing.assert_array_equal(CV_range(10, False, True), np.array([0, 1023])) + xp_assert_equal(CV_range(10, False, True), [0, 1023]) - np.testing.assert_allclose( + xp_assert_close( CV_range(10, True, False), - np.array([0.06256109, 0.91886608]), + [0.06256109, 0.91886608], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal(CV_range(10, False, False), np.array([0, 1])) + xp_assert_equal(CV_range(10, False, False), [0, 1]) class TestLegalToFull: @@ -66,54 +80,80 @@ class TestLegalToFull: definition unit tests methods. """ - def test_legal_to_full(self) -> None: + def test_legal_to_full(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.common.legal_to_full` definition. """ - np.testing.assert_allclose(legal_to_full(64 / 1023), 0.0) + xp_assert_close( + legal_to_full(xp_as_array(64 / 1023, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(940 / 1023), 1.0) + xp_assert_close( + legal_to_full(xp_as_array(940 / 1023, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(64 / 1023, out_int=True), 0) + xp_assert_close( + legal_to_full(xp_as_array(64 / 1023, xp=xp), out_int=True), + 0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(940 / 1023, out_int=True), 1023) + xp_assert_close( + legal_to_full(xp_as_array(940 / 1023, xp=xp), out_int=True), + 1023, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(64, in_int=True), 0.0) + xp_assert_close( + legal_to_full(xp_as_array(64, xp=xp), in_int=True), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(940, in_int=True), 1.0) + xp_assert_close( + legal_to_full(xp_as_array(940, xp=xp), in_int=True), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(64, in_int=True, out_int=True), 0) + xp_assert_close( + legal_to_full(xp_as_array(64, xp=xp), in_int=True, out_int=True), + 0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(legal_to_full(940, in_int=True, out_int=True), 1023) + xp_assert_close( + legal_to_full(xp_as_array(940, xp=xp), in_int=True, out_int=True), + 1023, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_n_dimensional_legal_to_full(self) -> None: + def test_n_dimensional_legal_to_full(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.common.legal_to_full` definition n-dimensional arrays support. """ CV_l = 0.918866080156403 - CV_f = legal_to_full(CV_l, 10) + CV_f = as_ndarray(legal_to_full(xp_as_array(CV_l, xp=xp), 10)) - CV_l = np.tile(CV_l, 6) - CV_f = np.tile(CV_f, 6) - np.testing.assert_allclose( - legal_to_full(CV_l, 10), CV_f, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CV_l = xp.tile(xp_as_array(CV_l, xp=xp), (6,)) + CV_f = xp.tile(xp_as_array(CV_f, xp=xp), (6,)) + xp_assert_close(legal_to_full(CV_l, 10), CV_f, atol=TOLERANCE_ABSOLUTE_TESTS) - CV_l = np.reshape(CV_l, (2, 3)) - CV_f = np.reshape(CV_f, (2, 3)) - np.testing.assert_allclose( - legal_to_full(CV_l, 10), CV_f, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CV_l = xp_reshape(xp_as_array(CV_l, xp=xp), (2, 3), xp=xp) + CV_f = xp_reshape(xp_as_array(CV_f, xp=xp), (2, 3), xp=xp) + xp_assert_close(legal_to_full(CV_l, 10), CV_f, atol=TOLERANCE_ABSOLUTE_TESTS) - CV_l = np.reshape(CV_l, (2, 3, 1)) - CV_f = np.reshape(CV_f, (2, 3, 1)) - np.testing.assert_allclose( - legal_to_full(CV_l, 10), CV_f, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CV_l = xp_reshape(xp_as_array(CV_l, xp=xp), (2, 3, 1), xp=xp) + CV_f = xp_reshape(xp_as_array(CV_f, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(legal_to_full(CV_l, 10), CV_f, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_legal_to_full(self) -> None: @@ -131,54 +171,80 @@ class TestFullToLegal: definition unit tests methods. """ - def test_full_to_legal(self) -> None: + def test_full_to_legal(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.common.full_to_legal` definition. """ - np.testing.assert_allclose(full_to_legal(0.0), 0.062561094819159) + xp_assert_close( + full_to_legal(xp_as_array(0.0, xp=xp)), + 0.062561094819159, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(1.0), 0.918866080156403) + xp_assert_close( + full_to_legal(xp_as_array(1.0, xp=xp)), + 0.918866080156403, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(0.0, out_int=True), 64) + xp_assert_close( + full_to_legal(xp_as_array(0.0, xp=xp), out_int=True), + 64, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(1.0, out_int=True), 940) + xp_assert_close( + full_to_legal(xp_as_array(1.0, xp=xp), out_int=True), + 940, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(0, in_int=True), 0.062561094819159) + xp_assert_close( + full_to_legal(xp_as_array(0, xp=xp), in_int=True), + 0.062561094819159, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(1023, in_int=True), 0.918866080156403) + xp_assert_close( + full_to_legal(xp_as_array(1023, xp=xp), in_int=True), + 0.918866080156403, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(0, in_int=True, out_int=True), 64) + xp_assert_close( + full_to_legal(xp_as_array(0, xp=xp), in_int=True, out_int=True), + 64, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - np.testing.assert_allclose(full_to_legal(1023, in_int=True, out_int=True), 940) + xp_assert_close( + full_to_legal(xp_as_array(1023, xp=xp), in_int=True, out_int=True), + 940, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_n_dimensional_full_to_legal(self) -> None: + def test_n_dimensional_full_to_legal(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.common.full_to_legal` definition n-dimensional arrays support. """ CF_f = 1.0 - CV_l = full_to_legal(CF_f, 10) + CV_l = as_ndarray(full_to_legal(xp_as_array(CF_f, xp=xp), 10)) - CF_f = np.tile(CF_f, 6) - CV_l = np.tile(CV_l, 6) - np.testing.assert_allclose( - full_to_legal(CF_f, 10), CV_l, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CF_f = xp.tile(xp_as_array(CF_f, xp=xp), (6,)) + CV_l = xp.tile(xp_as_array(CV_l, xp=xp), (6,)) + xp_assert_close(full_to_legal(CF_f, 10), CV_l, atol=TOLERANCE_ABSOLUTE_TESTS) - CF_f = np.reshape(CF_f, (2, 3)) - CV_l = np.reshape(CV_l, (2, 3)) - np.testing.assert_allclose( - full_to_legal(CF_f, 10), CV_l, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CF_f = xp_reshape(xp_as_array(CF_f, xp=xp), (2, 3), xp=xp) + CV_l = xp_reshape(xp_as_array(CV_l, xp=xp), (2, 3), xp=xp) + xp_assert_close(full_to_legal(CF_f, 10), CV_l, atol=TOLERANCE_ABSOLUTE_TESTS) - CF_f = np.reshape(CF_f, (2, 3, 1)) - CV_l = np.reshape(CV_l, (2, 3, 1)) - np.testing.assert_allclose( - full_to_legal(CF_f, 10), CV_l, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CF_f = xp_reshape(xp_as_array(CF_f, xp=xp), (2, 3, 1), xp=xp) + CV_l = xp_reshape(xp_as_array(CV_l, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(full_to_legal(CF_f, 10), CV_l, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_full_to_legal(self) -> None: diff --git a/colour/models/rgb/transfer_functions/tests/test_davinci_intermediate.py b/colour/models/rgb/transfer_functions/tests/test_davinci_intermediate.py index 863f7c87ec..e15e6539db 100644 --- a/colour/models/rgb/transfer_functions/tests/test_davinci_intermediate.py +++ b/colour/models/rgb/transfer_functions/tests/test_davinci_intermediate.py @@ -3,6 +3,10 @@ davinci_intermediate` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ oetf_DaVinciIntermediate, oetf_inverse_DaVinciIntermediate, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,41 +45,43 @@ class TestOetf_DaVinciIntermediate: oetf_DaVinciIntermediate` definition unit tests methods. """ - def test_oetf_DaVinciIntermediate(self) -> None: + def test_oetf_DaVinciIntermediate(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ davinci_intermediate.oetf_DaVinciIntermediate` definition. """ - np.testing.assert_allclose( - oetf_DaVinciIntermediate(-0.01), + xp_assert_close( + oetf_DaVinciIntermediate(xp_as_array(-0.01, xp=xp)), -0.104442685500000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_DaVinciIntermediate(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(0.18), + xp_assert_close( + oetf_DaVinciIntermediate(xp_as_array(0.18, xp=xp)), 0.336043272384855, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(1.0), + xp_assert_close( + oetf_DaVinciIntermediate(xp_as_array(1.0, xp=xp)), 0.513837441116225, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(100.0), + xp_assert_close( + oetf_DaVinciIntermediate(xp_as_array(100.0, xp=xp)), 0.999999987016872, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_DaVinciIntermediate(self) -> None: + def test_n_dimensional_oetf_DaVinciIntermediate(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ davinci_intermediate.oetf_DaVinciIntermediate` definition n-dimensional arrays @@ -73,27 +89,21 @@ def test_n_dimensional_oetf_DaVinciIntermediate(self) -> None: """ L = 0.18 - V = oetf_DaVinciIntermediate(L) + V = as_ndarray(oetf_DaVinciIntermediate(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - V = np.tile(V, 6) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + xp_assert_close(oetf_DaVinciIntermediate(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - V = np.reshape(V, (2, 3)) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_DaVinciIntermediate(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - V = np.reshape(V, (2, 3, 1)) - np.testing.assert_allclose( - oetf_DaVinciIntermediate(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_DaVinciIntermediate(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_DaVinciIntermediate(self) -> None: + def test_domain_range_scale_oetf_DaVinciIntermediate(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ davinci_intermediate.oetf_DaVinciIntermediate` definition domain and range @@ -101,13 +111,13 @@ def test_domain_range_scale_oetf_DaVinciIntermediate(self) -> None: """ L = 0.18 - V = oetf_DaVinciIntermediate(L) + V = as_ndarray(oetf_DaVinciIntermediate(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_DaVinciIntermediate(L * factor), + xp_assert_close( + oetf_DaVinciIntermediate(xp_as_array(L * factor, xp=xp)), V * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -129,43 +139,45 @@ class TestOetf_inverse_DaVinciIntermediate: methods. """ - def test_oetf_inverse_DaVinciIntermediate(self) -> None: + def test_oetf_inverse_DaVinciIntermediate(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ davinci_intermediate.oetf_inverse_DaVinciIntermediate` definition. """ - np.testing.assert_allclose( - oetf_inverse_DaVinciIntermediate(-0.104442685500000), + xp_assert_close( + oetf_inverse_DaVinciIntermediate(xp_as_array(-0.104442685500000, xp=xp)), -0.01, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_DaVinciIntermediate(0.0), + xp_assert_close( + oetf_inverse_DaVinciIntermediate(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_DaVinciIntermediate(0.336043272384855), + xp_assert_close( + oetf_inverse_DaVinciIntermediate(xp_as_array(0.336043272384855, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_DaVinciIntermediate(0.513837441116225), + xp_assert_close( + oetf_inverse_DaVinciIntermediate(xp_as_array(0.513837441116225, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_DaVinciIntermediate(0.999999987016872), + xp_assert_close( + oetf_inverse_DaVinciIntermediate(xp_as_array(0.999999987016872, xp=xp)), 100.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_DaVinciIntermediate(self) -> None: + def test_n_dimensional_oetf_inverse_DaVinciIntermediate( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ davinci_intermediate.oetf_inverse_DaVinciIntermediate` definition n-dimensional @@ -173,33 +185,35 @@ def test_n_dimensional_oetf_inverse_DaVinciIntermediate(self) -> None: """ V = 0.336043272384855 - L = oetf_inverse_DaVinciIntermediate(V) + L = as_ndarray(oetf_inverse_DaVinciIntermediate(xp_as_array(V, xp=xp))) - V = np.tile(V, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close( oetf_inverse_DaVinciIntermediate(V), L, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close( oetf_inverse_DaVinciIntermediate(V), L, atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( oetf_inverse_DaVinciIntermediate(V), L, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_oetf_inverse_DaVinciIntermediate(self) -> None: + def test_domain_range_scale_oetf_inverse_DaVinciIntermediate( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ davinci_intermediate.oetf_inverse_DaVinciIntermediate` definition domain and @@ -207,13 +221,13 @@ def test_domain_range_scale_oetf_inverse_DaVinciIntermediate(self) -> None: """ V = 0.336043272384855 - L = oetf_inverse_DaVinciIntermediate(V) + L = as_ndarray(oetf_inverse_DaVinciIntermediate(xp_as_array(V, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_DaVinciIntermediate(V * factor), + xp_assert_close( + oetf_inverse_DaVinciIntermediate(xp_as_array(V * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_dcdm.py b/colour/models/rgb/transfer_functions/tests/test_dcdm.py index 122504d1c4..c23c1231ea 100644 --- a/colour/models/rgb/transfer_functions/tests/test_dcdm.py +++ b/colour/models/rgb/transfer_functions/tests/test_dcdm.py @@ -3,11 +3,25 @@ module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import eotf_DCDM, eotf_inverse_DCDM -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,67 +42,69 @@ class TestEotf_inverse_DCDM: definition unit tests methods. """ - def test_eotf_inverse_DCDM(self) -> None: + def test_eotf_inverse_DCDM(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ dcdm.eotf_inverse_DCDM` definition. """ - np.testing.assert_allclose( - eotf_inverse_DCDM(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_DCDM(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_DCDM(0.18), 0.11281861, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_DCDM(xp_as_array(0.18, xp=xp)), + 0.11281861, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_DCDM(1.0), 0.21817973, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_DCDM(xp_as_array(1.0, xp=xp)), + 0.21817973, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert eotf_inverse_DCDM(0.18, out_int=True) == 462 + assert ( + as_ndarray(eotf_inverse_DCDM(xp_as_array(0.18, xp=xp), out_int=True)) == 462 + ) - def test_n_dimensional_eotf_inverse_DCDM(self) -> None: + def test_n_dimensional_eotf_inverse_DCDM(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dcdm.\ eotf_inverse_DCDM` definition n-dimensional arrays support. """ XYZ = 0.18 - XYZ_p = eotf_inverse_DCDM(XYZ) + XYZ_p = as_ndarray(eotf_inverse_DCDM(xp_as_array(XYZ, xp=xp))) - XYZ = np.tile(XYZ, 6) - XYZ_p = np.tile(XYZ_p, 6) - np.testing.assert_allclose( - eotf_inverse_DCDM(XYZ), XYZ_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6,)) + XYZ_p = xp.tile(xp_as_array(XYZ_p, xp=xp), (6,)) + xp_assert_close(eotf_inverse_DCDM(XYZ), XYZ_p, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3)) - XYZ_p = np.reshape(XYZ_p, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_DCDM(XYZ), XYZ_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3), xp=xp) + XYZ_p = xp_reshape(xp_as_array(XYZ_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_inverse_DCDM(XYZ), XYZ_p, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 1)) - XYZ_p = np.reshape(XYZ_p, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_DCDM(XYZ), XYZ_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1), xp=xp) + XYZ_p = xp_reshape(xp_as_array(XYZ_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_inverse_DCDM(XYZ), XYZ_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_inverse_DCDM(self) -> None: + def test_domain_range_scale_eotf_inverse_DCDM(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.\ dcdm.eotf_inverse_DCDM` definition domain and range scale support. """ XYZ = 0.18 - XYZ_p = eotf_inverse_DCDM(XYZ) + XYZ_p = as_ndarray(eotf_inverse_DCDM(xp_as_array(XYZ, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_DCDM(XYZ * factor), + xp_assert_close( + eotf_inverse_DCDM(xp_as_array(XYZ * factor, xp=xp)), XYZ_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -109,59 +125,69 @@ class TestEotf_DCDM: definition unit tests methods. """ - def test_eotf_DCDM(self) -> None: + def test_eotf_DCDM(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dcdm.eotf_DCDM` definition. """ - np.testing.assert_allclose(eotf_DCDM(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + eotf_DCDM(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - eotf_DCDM(0.11281861), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_DCDM(xp_as_array(0.11281861, xp=xp)), + 0.18, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_DCDM(0.21817973), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_DCDM(xp_as_array(0.21817973, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(eotf_DCDM(462, in_int=True), 0.18, atol=1e-5) + xp_assert_close( + eotf_DCDM(xp_as_array(462, xp=xp), in_int=True), + 0.18, + atol=TOLERANCE_ABSOLUTE_TESTS * 100, + ) - def test_n_dimensional_eotf_DCDM(self) -> None: + def test_n_dimensional_eotf_DCDM(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dcdm.eotf_DCDM` definition n-dimensional arrays support. """ XYZ_p = 0.11281861 - XYZ = eotf_DCDM(XYZ_p) + XYZ = as_ndarray(eotf_DCDM(xp_as_array(XYZ_p, xp=xp))) - XYZ_p = np.tile(XYZ_p, 6) - XYZ = np.tile(XYZ, 6) - np.testing.assert_allclose(eotf_DCDM(XYZ_p), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ_p = xp.tile(xp_as_array(XYZ_p, xp=xp), (6,)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6,)) + xp_assert_close(eotf_DCDM(XYZ_p), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ_p = np.reshape(XYZ_p, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3)) - np.testing.assert_allclose(eotf_DCDM(XYZ_p), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ_p = xp_reshape(xp_as_array(XYZ_p, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_DCDM(XYZ_p), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ_p = np.reshape(XYZ_p, (2, 3, 1)) - XYZ = np.reshape(XYZ, (2, 3, 1)) - np.testing.assert_allclose(eotf_DCDM(XYZ_p), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ_p = xp_reshape(xp_as_array(XYZ_p, xp=xp), (2, 3, 1), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_DCDM(XYZ_p), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_DCDM(self) -> None: + def test_domain_range_scale_eotf_DCDM(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dcdm.eotf_DCDM` definition domain and range scale support. """ XYZ_p = 0.11281861 - XYZ = eotf_DCDM(XYZ_p) + XYZ = as_ndarray(eotf_DCDM(xp_as_array(XYZ_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_DCDM(XYZ_p * factor), + xp_assert_close( + eotf_DCDM(xp_as_array(XYZ_p * factor, xp=xp)), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_dicom_gsdf.py b/colour/models/rgb/transfer_functions/tests/test_dicom_gsdf.py index 7f97dd2068..72cac19df0 100644 --- a/colour/models/rgb/transfer_functions/tests/test_dicom_gsdf.py +++ b/colour/models/rgb/transfer_functions/tests/test_dicom_gsdf.py @@ -3,11 +3,26 @@ :mod:`colour.models.rgb.transfer_functions.dicom_gsdf` module. """ +from __future__ import annotations + +import typing + import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import eotf_DICOMGSDF, eotf_inverse_DICOMGSDF -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,77 +43,71 @@ class TestEotf_inverse_DICOMGSDF: eotf_inverse_DICOMGSDF` definition unit tests methods. """ - def test_eotf_inverse_DICOMGSDF(self) -> None: + def test_eotf_inverse_DICOMGSDF(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dicom_gsdf.\ eotf_inverse_DICOMGSDF` definition. """ - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(0.05), + xp_assert_close( + eotf_inverse_DICOMGSDF(xp_as_array(0.05, xp=xp)), 0.001007281350787, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(130.0662), + xp_assert_close( + eotf_inverse_DICOMGSDF(xp_as_array(130.0662, xp=xp)), 0.500486263438448, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(4000), + xp_assert_close( + eotf_inverse_DICOMGSDF(xp_as_array(4000, xp=xp)), 1.000160314715578, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(130.0662, out_int=True), + xp_assert_close( + eotf_inverse_DICOMGSDF(xp_as_array(130.0662, xp=xp), out_int=True), 512, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_DICOMGSDF(self) -> None: + def test_n_dimensional_eotf_inverse_DICOMGSDF(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dicom_gsdf.\ eotf_inverse_DICOMGSDF` definition n-dimensional arrays support. """ L = 130.0662 - J = eotf_inverse_DICOMGSDF(L) + J = as_ndarray(eotf_inverse_DICOMGSDF(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - J = np.tile(J, 6) - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(L), J, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + J = xp.tile(xp_as_array(J, xp=xp), (6,)) + xp_assert_close(eotf_inverse_DICOMGSDF(L), J, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - J = np.reshape(J, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(L), J, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + J = xp_reshape(xp_as_array(J, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_inverse_DICOMGSDF(L), J, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - J = np.reshape(J, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(L), J, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + J = xp_reshape(xp_as_array(J, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_inverse_DICOMGSDF(L), J, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_inverse_DICOMGSDF(self) -> None: + def test_domain_range_scale_eotf_inverse_DICOMGSDF(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dicom_gsdf.\ eotf_inverse_DICOMGSDF` definition domain and range scale support. """ L = 130.0662 - J = eotf_inverse_DICOMGSDF(L) + J = as_ndarray(eotf_inverse_DICOMGSDF(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_DICOMGSDF(L * factor), + xp_assert_close( + eotf_inverse_DICOMGSDF(xp_as_array(L * factor, xp=xp)), J * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -119,71 +128,72 @@ class TestEotf_DICOMGSDF: eotf_DICOMGSDF` definition unit tests methods. """ - def test_eotf_DICOMGSDF(self) -> None: + @pytest.mark.mps_tolerance_absolute(2e-1) + def test_eotf_DICOMGSDF(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dicom_gsdf.\ eotf_DICOMGSDF` definition. """ - np.testing.assert_allclose( - eotf_DICOMGSDF(0.001007281350787), + xp_assert_close( + eotf_DICOMGSDF(xp_as_array(0.001007281350787, xp=xp)), 0.050143440671692, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_DICOMGSDF(0.500486263438448), + xp_assert_close( + eotf_DICOMGSDF(xp_as_array(0.500486263438448, xp=xp)), 130.062864706476550, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_DICOMGSDF(1.000160314715578), + xp_assert_close( + eotf_DICOMGSDF(xp_as_array(1.000160314715578, xp=xp)), 3997.586161113322300, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_DICOMGSDF(512, in_int=True), + xp_assert_close( + eotf_DICOMGSDF(xp_as_array(512, xp=xp), in_int=True), 130.065284012159790, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_DICOMGSDF(self) -> None: + def test_n_dimensional_eotf_DICOMGSDF(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dicom_gsdf.\ eotf_DICOMGSDF` definition n-dimensional arrays support. """ J = 0.500486263438448 - L = eotf_DICOMGSDF(J) + L = as_ndarray(eotf_DICOMGSDF(xp_as_array(J, xp=xp))) - J = np.tile(J, 6) - L = np.tile(L, 6) - np.testing.assert_allclose(eotf_DICOMGSDF(J), L, atol=TOLERANCE_ABSOLUTE_TESTS) + J = xp.tile(xp_as_array(J, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(eotf_DICOMGSDF(J), L, atol=TOLERANCE_ABSOLUTE_TESTS) - J = np.reshape(J, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose(eotf_DICOMGSDF(J), L, atol=TOLERANCE_ABSOLUTE_TESTS) + J = xp_reshape(xp_as_array(J, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_DICOMGSDF(J), L, atol=TOLERANCE_ABSOLUTE_TESTS) - J = np.reshape(J, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose(eotf_DICOMGSDF(J), L, atol=TOLERANCE_ABSOLUTE_TESTS) + J = xp_reshape(xp_as_array(J, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_DICOMGSDF(J), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_DICOMGSDF(self) -> None: + def test_domain_range_scale_eotf_DICOMGSDF(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dicom_gsdf.\ eotf_DICOMGSDF` definition domain and range scale support. """ J = 0.500486263438448 - L = eotf_DICOMGSDF(J) + L = as_ndarray(eotf_DICOMGSDF(xp_as_array(J, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_DICOMGSDF(J * factor), + xp_assert_close( + eotf_DICOMGSDF(xp_as_array(J * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_dji_d_log.py b/colour/models/rgb/transfer_functions/tests/test_dji_d_log.py index dae2a47f49..c5eb3c9148 100644 --- a/colour/models/rgb/transfer_functions/tests/test_dji_d_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_dji_d_log.py @@ -3,6 +3,10 @@ dji_d_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_DJIDLog, log_encoding_DJIDLog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,67 +45,65 @@ class TestLogEncoding_DJIDLog: log_encoding_DJIDLog` definition unit tests methods. """ - def test_log_encoding_DJIDLog(self) -> None: + def test_log_encoding_DJIDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dji_d_log.\ log_encoding_DJIDLog` definition. """ - np.testing.assert_allclose( - log_encoding_DJIDLog(0.0), 0.0929, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_DJIDLog(xp_as_array(0.0, xp=xp)), + 0.0929, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_DJIDLog(0.18), + xp_assert_close( + log_encoding_DJIDLog(xp_as_array(0.18, xp=xp)), 0.398764556189331, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_DJIDLog(1.0), 0.584555, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_DJIDLog(xp_as_array(1.0, xp=xp)), + 0.584555, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_DLog(self) -> None: + def test_n_dimensional_log_encoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dji_d_log.\ log_encoding_DJIDLog` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_DJIDLog(x) + y = as_ndarray(log_encoding_DJIDLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_DJIDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_DJIDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_DJIDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_DJIDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_DJIDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_DJIDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_DLog(self) -> None: + def test_domain_range_scale_log_encoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dji_d_log.\ log_encoding_DJIDLog` definition domain and range scale support. """ x = 0.18 - y = log_encoding_DJIDLog(x) + y = as_ndarray(log_encoding_DJIDLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_DJIDLog(x * factor), + xp_assert_close( + log_encoding_DJIDLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -112,63 +124,65 @@ class TestLogDecoding_DJIDLog: log_decoding_DJIDLog` definition unit tests methods. """ - def test_log_decoding_DJIDLog(self) -> None: + def test_log_decoding_DJIDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dji_d_log.\ log_decoding_DJIDLog` definition. """ - np.testing.assert_allclose( - log_decoding_DJIDLog(0.0929), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_DJIDLog(xp_as_array(0.0929, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_DJIDLog(0.398764556189331), 0.18, atol=1e-6 + xp_assert_close( + log_decoding_DJIDLog(xp_as_array(0.398764556189331, xp=xp)), + 0.18, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - np.testing.assert_allclose(log_decoding_DJIDLog(0.584555), 1.0, atol=1e-6) + xp_assert_close( + log_decoding_DJIDLog(xp_as_array(0.584555, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, + ) - def test_n_dimensional_log_decoding_DLog(self) -> None: + def test_n_dimensional_log_decoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dji_d_log.\ log_decoding_DJIDLog` definition n-dimensional arrays support. """ y = 0.398764556189331 - x = log_decoding_DJIDLog(y) + x = as_ndarray(log_decoding_DJIDLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_DJIDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_DJIDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_DJIDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_DJIDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_DJIDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_DJIDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_DLog(self) -> None: + def test_domain_range_scale_log_decoding_DLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.dji_d_log.\ log_decoding_DJIDLog` definition domain and range scale support. """ y = 0.398764556189331 - x = log_decoding_DJIDLog(y) + x = as_ndarray(log_decoding_DJIDLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_DJIDLog(y * factor), + xp_assert_close( + log_decoding_DJIDLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_exponent.py b/colour/models/rgb/transfer_functions/tests/test_exponent.py index 476f4c8c46..bbc8865225 100644 --- a/colour/models/rgb/transfer_functions/tests/test_exponent.py +++ b/colour/models/rgb/transfer_functions/tests/test_exponent.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.exponent` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,15 @@ exponent_function_basic, exponent_function_monitor_curve, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,7 +43,7 @@ class TestExponentFunctionBasic: exponent_function_basic` definition unit tests methods. """ - def test_exponent_function_basic(self) -> None: + def test_exponent_function_basic(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.exponent.\ exponent_function_basic` definition. @@ -39,79 +51,83 @@ def test_exponent_function_basic(self) -> None: a = 0.18 a_p = 0.0229932049927 - np.testing.assert_allclose( - exponent_function_basic(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicMirrorFwd"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicPassThruFwd"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicPassThruFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) a = 0.0229932049927 a_p = 0.18 - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicRev"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicMirrorRev"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicPassThruRev"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicPassThruRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) a = -0.18 - np.testing.assert_allclose( - exponent_function_basic(a, 2.2), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicMirrorFwd"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicMirrorFwd"), -0.0229932049927, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicPassThruFwd"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicPassThruFwd"), -0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) a = -0.0229932049927 - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicRev"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicRev"), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicMirrorRev"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicMirrorRev"), -0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2, "basicPassThruRev"), + xp_assert_close( + exponent_function_basic(xp_as_array(a, xp=xp), 2.2, "basicPassThruRev"), -0.0229932049927, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_exponent_function_basic(self) -> None: + def test_n_dimensional_exponent_function_basic(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.exponent.\ exponent_function_basic` definition n-dimensional arrays support. @@ -120,49 +136,55 @@ def test_n_dimensional_exponent_function_basic(self) -> None: a = 0.18 a_p = 0.0229932049927 - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( + exponent_function_basic(a, 2.2), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicPassThruFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( + exponent_function_basic(a, 2.2), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicPassThruFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( - exponent_function_basic(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + exponent_function_basic(a, 2.2), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicPassThruFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -171,55 +193,55 @@ def test_n_dimensional_exponent_function_basic(self) -> None: a = 0.0229932049927 a_p = 0.18 - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( exponent_function_basic(a, 2.2, "basicRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicPassThruRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( exponent_function_basic(a, 2.2, "basicRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicPassThruRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( exponent_function_basic(a, 2.2, "basicRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_basic(a, 2.2, "basicPassThruRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -243,7 +265,7 @@ class TestExponentFunctionMonitorCurve: exponent_function_monitor_curve` definition unit tests methods. """ - def test_exponent_function_monitor_curve(self) -> None: + def test_exponent_function_monitor_curve(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.exponent.\ exponent_function_monitor_curve` definition. @@ -251,59 +273,73 @@ def test_exponent_function_monitor_curve(self) -> None: a = 0.18 a_p = 0.0232240466001 - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001), + xp_assert_close( + exponent_function_monitor_curve(xp_as_array(a, xp=xp), 2.2, 0.001), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorFwd"), + xp_assert_close( + exponent_function_monitor_curve( + xp_as_array(a, xp=xp), 2.2, 0.001, "monCurveMirrorFwd" + ), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) a = 0.0232240466001 a_p = 0.18 - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveRev"), + xp_assert_close( + exponent_function_monitor_curve( + xp_as_array(a, xp=xp), 2.2, 0.001, "monCurveRev" + ), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorRev"), + xp_assert_close( + exponent_function_monitor_curve( + xp_as_array(a, xp=xp), 2.2, 0.001, "monCurveMirrorRev" + ), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) a = -0.18 - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001), + xp_assert_close( + exponent_function_monitor_curve(xp_as_array(a, xp=xp), 2.2, 0.001), -0.000205413951, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorFwd"), + xp_assert_close( + exponent_function_monitor_curve( + xp_as_array(a, xp=xp), 2.2, 0.001, "monCurveMirrorFwd" + ), -0.0232240466001, atol=TOLERANCE_ABSOLUTE_TESTS, ) a = -0.000205413951 - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveRev"), + xp_assert_close( + exponent_function_monitor_curve( + xp_as_array(a, xp=xp), 2.2, 0.001, "monCurveRev" + ), -0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorRev"), + xp_assert_close( + exponent_function_monitor_curve( + xp_as_array(a, xp=xp), 2.2, 0.001, "monCurveMirrorRev" + ), -0.0201036111565, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_exponent_function_monitor_curve(self) -> None: + def test_n_dimensional_exponent_function_monitor_curve( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.exponent.\ exponent_function_monitor_curve` definition n-dimensional arrays support. @@ -312,40 +348,40 @@ def test_n_dimensional_exponent_function_monitor_curve(self) -> None: a = 0.18 a_p = 0.0232240466001 - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorFwd"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -354,40 +390,40 @@ def test_n_dimensional_exponent_function_monitor_curve(self) -> None: a = 0.0232240466001 a_p = 0.18 - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( exponent_function_monitor_curve(a, 2.2, 0.001, "monCurveMirrorRev"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/rgb/transfer_functions/tests/test_filmic_pro.py b/colour/models/rgb/transfer_functions/tests/test_filmic_pro.py index 0fff4fa156..21e84a3e20 100644 --- a/colour/models/rgb/transfer_functions/tests/test_filmic_pro.py +++ b/colour/models/rgb/transfer_functions/tests/test_filmic_pro.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.filmic_pro` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,18 @@ log_decoding_FilmicPro6, log_encoding_FilmicPro6, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +46,65 @@ class TestLogEncoding_FilmicPro6: log_encoding_FilmicPro6` definition unit tests methods. """ - def test_log_encoding_FilmicPro6(self) -> None: + def test_log_encoding_FilmicPro6(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmic_pro.\ log_encoding_FilmicPro6` definition. """ - np.testing.assert_allclose( - log_encoding_FilmicPro6(0.0), + xp_assert_close( + log_encoding_FilmicPro6(xp_as_array(0.0, xp=xp)), -np.inf, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FilmicPro6(0.18), + xp_assert_close( + log_encoding_FilmicPro6(xp_as_array(0.18, xp=xp)), 0.606634519924703, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FilmicPro6(1.0), + xp_assert_close( + log_encoding_FilmicPro6(xp_as_array(1.0, xp=xp)), 1.000000819999999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_FilmicPro6(self) -> None: + def test_n_dimensional_log_encoding_FilmicPro6(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmic_pro.\ log_encoding_FilmicPro6` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_FilmicPro6(x) + y = as_ndarray(log_encoding_FilmicPro6(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_FilmicPro6(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_FilmicPro6(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_FilmicPro6(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_FilmicPro6(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_FilmicPro6(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_FilmicPro6(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_FilmicPro6(self) -> None: + def test_domain_range_scale_log_encoding_FilmicPro6(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmic_pro.\ log_encoding_FilmicPro6` definition domain and range scale support. """ x = 0.18 - y = log_encoding_FilmicPro6(x) + y = as_ndarray(log_encoding_FilmicPro6(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_FilmicPro6(x * factor), + xp_assert_close( + log_encoding_FilmicPro6(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -116,67 +125,61 @@ class TestLogDecoding_FilmicPro6: log_decoding_FilmicPro6` definition unit tests methods. """ - def test_log_decoding_FilmicPro6(self) -> None: + def test_log_decoding_FilmicPro6(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmic_pro.\ log_decoding_FilmicPro6` definition. """ - np.testing.assert_array_equal(log_decoding_FilmicPro6(-np.inf), 0.0) + xp_assert_equal(log_decoding_FilmicPro6(xp_as_array(-np.inf, xp=xp)), 0.0) - np.testing.assert_allclose( - log_decoding_FilmicPro6(0.606634519924703), + xp_assert_close( + log_decoding_FilmicPro6(xp_as_array(0.606634519924703, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FilmicPro6(1.000000819999999), + xp_assert_close( + log_decoding_FilmicPro6(xp_as_array(1.000000819999999, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_FilmicPro6(self) -> None: + def test_n_dimensional_log_decoding_FilmicPro6(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmic_pro.\ log_decoding_FilmicPro6` definition n-dimensional arrays support. """ y = 0.606634519924703 - x = log_decoding_FilmicPro6(y) + x = as_ndarray(log_decoding_FilmicPro6(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_FilmicPro6(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_FilmicPro6(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_FilmicPro6(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_FilmicPro6(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_FilmicPro6(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_FilmicPro6(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_FilmicPro6(self) -> None: + def test_domain_range_scale_log_decoding_FilmicPro6(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmic_pro.\ log_decoding_FilmicPro6` definition domain and range scale support. """ y = 0.606634519924703 - x = log_decoding_FilmicPro6(y) + x = as_ndarray(log_decoding_FilmicPro6(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_FilmicPro6(y * factor), + xp_assert_close( + log_decoding_FilmicPro6(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_filmlight_t_log.py b/colour/models/rgb/transfer_functions/tests/test_filmlight_t_log.py index 4344e9fd45..4d2aaac5d3 100644 --- a/colour/models/rgb/transfer_functions/tests/test_filmlight_t_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_filmlight_t_log.py @@ -3,6 +3,10 @@ filmlight_t_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_FilmLightTLog, log_encoding_FilmLightTLog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +45,65 @@ class TestLogEncoding_FilmLightTLog: log_encoding_FilmLightTLog` definition unit tests methods. """ - def test_log_encoding_FilmLightTLog(self) -> None: + def test_log_encoding_FilmLightTLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmlight_t_log.\ log_encoding_FilmLightTLog` definition. """ - np.testing.assert_allclose( - log_encoding_FilmLightTLog(0.0), + xp_assert_close( + log_encoding_FilmLightTLog(xp_as_array(0.0, xp=xp)), 0.075, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FilmLightTLog(0.18), + xp_assert_close( + log_encoding_FilmLightTLog(xp_as_array(0.18, xp=xp)), 0.396567801298332, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FilmLightTLog(1.0), + xp_assert_close( + log_encoding_FilmLightTLog(xp_as_array(1.0, xp=xp)), 0.552537881005859, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_TLog(self) -> None: + def test_n_dimensional_log_encoding_TLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmlight_t_log.\ log_encoding_FilmLightTLog` definition n-dimensional arrays support. """ x = 0.18 - t = log_encoding_FilmLightTLog(x) + t = as_ndarray(log_encoding_FilmLightTLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - t = np.tile(t, 6) - np.testing.assert_allclose( - log_encoding_FilmLightTLog(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + t = xp.tile(xp_as_array(t, xp=xp), (6,)) + xp_assert_close(log_encoding_FilmLightTLog(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - t = np.reshape(t, (2, 3)) - np.testing.assert_allclose( - log_encoding_FilmLightTLog(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_FilmLightTLog(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - t = np.reshape(t, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_FilmLightTLog(x), t, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_FilmLightTLog(x), t, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_TLog(self) -> None: + def test_domain_range_scale_log_encoding_TLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmlight_t_log.\ log_encoding_FilmLightTLog` definition domain and range scale support. """ x = 0.18 - t = log_encoding_FilmLightTLog(x) + t = as_ndarray(log_encoding_FilmLightTLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_FilmLightTLog(x * factor), + xp_assert_close( + log_encoding_FilmLightTLog(xp_as_array(x * factor, xp=xp)), t * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -116,71 +124,65 @@ class TestLogDecoding_FilmLightTLog: log_decoding_FilmLightTLog` definition unit tests methods. """ - def test_log_decoding_FilmLightTLog(self) -> None: + def test_log_decoding_FilmLightTLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmlight_t_log.\ log_decoding_FilmLightTLog` definition. """ - np.testing.assert_allclose( - log_decoding_FilmLightTLog(0.075), + xp_assert_close( + log_decoding_FilmLightTLog(xp_as_array(0.075, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FilmLightTLog(0.396567801298332), + xp_assert_close( + log_decoding_FilmLightTLog(xp_as_array(0.396567801298332, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FilmLightTLog(0.552537881005859), + xp_assert_close( + log_decoding_FilmLightTLog(xp_as_array(0.552537881005859, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_TLog(self) -> None: + def test_n_dimensional_log_decoding_TLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmlight_t_log.\ log_decoding_FilmLightTLog` definition n-dimensional arrays support. """ t = 0.396567801298332 - x = log_decoding_FilmLightTLog(t) + x = as_ndarray(log_decoding_FilmLightTLog(xp_as_array(t, xp=xp))) - t = np.tile(t, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_FilmLightTLog(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp.tile(xp_as_array(t, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_FilmLightTLog(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - t = np.reshape(t, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_FilmLightTLog(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_FilmLightTLog(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - t = np.reshape(t, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_FilmLightTLog(t), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + t = xp_reshape(xp_as_array(t, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_FilmLightTLog(t), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_TLog(self) -> None: + def test_domain_range_scale_log_decoding_TLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.filmlight_t_log.\ log_decoding_FilmLightTLog` definition domain and range scale support. """ t = 0.396567801298332 - x = log_decoding_FilmLightTLog(t) + x = as_ndarray(log_decoding_FilmLightTLog(xp_as_array(t, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_FilmLightTLog(t * factor), + xp_assert_close( + log_decoding_FilmLightTLog(xp_as_array(t * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_fujifilm_f_log.py b/colour/models/rgb/transfer_functions/tests/test_fujifilm_f_log.py index dccbbd35f7..bfcf168532 100644 --- a/colour/models/rgb/transfer_functions/tests/test_fujifilm_f_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_fujifilm_f_log.py @@ -3,6 +3,10 @@ fujifilm_f_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -12,7 +16,18 @@ log_encoding_FLog, log_encoding_FLog2, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -35,89 +50,83 @@ class TestLogEncoding_FLog: log_encoding_FLog` definition unit tests methods. """ - def test_log_encoding_FLog(self) -> None: + def test_log_encoding_FLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_f_log.\ log_encoding_FLog` definition. """ - np.testing.assert_allclose( - log_encoding_FLog(0.0), + xp_assert_close( + log_encoding_FLog(xp_as_array(0.0, xp=xp)), 0.092864000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog(0.18), + xp_assert_close( + log_encoding_FLog(xp_as_array(0.18, xp=xp)), 0.459318458661621, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog(0.18, 12), + xp_assert_close( + log_encoding_FLog(xp_as_array(0.18, xp=xp), 12), 0.459318458661621, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog(0.18, 10, False), + xp_assert_close( + log_encoding_FLog(xp_as_array(0.18, xp=xp), 10, False), 0.463336510514656, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog(0.18, 10, False, False), + xp_assert_close( + log_encoding_FLog(xp_as_array(0.18, xp=xp), 10, False, False), 0.446590337236003, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog(1.0), + xp_assert_close( + log_encoding_FLog(xp_as_array(1.0, xp=xp)), 0.704996409216428, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_FLog(self) -> None: + def test_n_dimensional_log_encoding_FLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_f_log.\ log_encoding_FLog` definition n-dimensional arrays support. """ L_in = 0.18 - V_out = log_encoding_FLog(L_in) + V_out = as_ndarray(log_encoding_FLog(xp_as_array(L_in, xp=xp))) - L_in = np.tile(L_in, 6) - V_out = np.tile(V_out, 6) - np.testing.assert_allclose( - log_encoding_FLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp.tile(xp_as_array(L_in, xp=xp), (6,)) + V_out = xp.tile(xp_as_array(V_out, xp=xp), (6,)) + xp_assert_close(log_encoding_FLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - L_in = np.reshape(L_in, (2, 3)) - V_out = np.reshape(V_out, (2, 3)) - np.testing.assert_allclose( - log_encoding_FLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3), xp=xp) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_FLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - L_in = np.reshape(L_in, (2, 3, 1)) - V_out = np.reshape(V_out, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_FLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3, 1), xp=xp) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_FLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_FLog(self) -> None: + def test_domain_range_scale_log_encoding_FLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_f_log.\ log_encoding_FLog` definition domain and range scale support. """ L_in = 0.18 - V_out = log_encoding_FLog(L_in) + V_out = as_ndarray(log_encoding_FLog(xp_as_array(L_in, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_FLog(L_in * factor), + xp_assert_close( + log_encoding_FLog(xp_as_array(L_in * factor, xp=xp)), V_out * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -138,89 +147,83 @@ class TestLogDecoding_FLog: log_decoding_FLog` definition unit tests methods. """ - def test_log_decoding_FLog(self) -> None: + def test_log_decoding_FLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_f_log.\ log_decoding_FLog` definition. """ - np.testing.assert_allclose( - log_decoding_FLog(0.092864000000000), + xp_assert_close( + log_decoding_FLog(xp_as_array(0.092864000000000, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog(0.459318458661621), + xp_assert_close( + log_decoding_FLog(xp_as_array(0.459318458661621, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog(0.459318458661621, 12), + xp_assert_close( + log_decoding_FLog(xp_as_array(0.459318458661621, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog(0.463336510514656, 10, False), + xp_assert_close( + log_decoding_FLog(xp_as_array(0.463336510514656, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog(0.446590337236003, 10, False, False), + xp_assert_close( + log_decoding_FLog(xp_as_array(0.446590337236003, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog(0.704996409216428), + xp_assert_close( + log_decoding_FLog(xp_as_array(0.704996409216428, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_FLog(self) -> None: + def test_n_dimensional_log_decoding_FLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_f_log.\ log_decoding_FLog` definition n-dimensional arrays support. """ V_out = 0.459318458661621 - L_in = log_decoding_FLog(V_out) + L_in = as_ndarray(log_decoding_FLog(xp_as_array(V_out, xp=xp))) - V_out = np.tile(V_out, 6) - L_in = np.tile(L_in, 6) - np.testing.assert_allclose( - log_decoding_FLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp.tile(xp_as_array(V_out, xp=xp), (6,)) + L_in = xp.tile(xp_as_array(L_in, xp=xp), (6,)) + xp_assert_close(log_decoding_FLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - V_out = np.reshape(V_out, (2, 3)) - L_in = np.reshape(L_in, (2, 3)) - np.testing.assert_allclose( - log_decoding_FLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3), xp=xp) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_FLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - V_out = np.reshape(V_out, (2, 3, 1)) - L_in = np.reshape(L_in, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_FLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3, 1), xp=xp) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_FLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_FLog(self) -> None: + def test_domain_range_scale_log_decoding_FLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_f_log.\ log_decoding_FLog` definition domain and range scale support. """ V_out = 0.459318458661621 - L_in = log_decoding_FLog(V_out) + L_in = as_ndarray(log_decoding_FLog(xp_as_array(V_out, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_FLog(V_out * factor), + xp_assert_close( + log_decoding_FLog(xp_as_array(V_out * factor, xp=xp)), L_in * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -241,89 +244,84 @@ class TestLogEncoding_FLog2: log_encoding_FLog2` definition unit tests methods. """ - def test_log_encoding_FLog2(self) -> None: + def test_log_encoding_FLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_flog.\ log_encoding_FLog2` definition. """ - np.testing.assert_allclose( - log_encoding_FLog2(0.0), + xp_assert_close( + log_encoding_FLog2(xp_as_array(0.0, xp=xp)), 0.092864000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog2(0.18), + xp_assert_close( + log_encoding_FLog2(xp_as_array(0.18, xp=xp)), 0.39100724189123, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog2(0.18, 12), + xp_assert_close( + log_encoding_FLog2(xp_as_array(0.18, xp=xp), 12), 0.39100724189123, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog2(0.18, 10, False), + xp_assert_close( + log_encoding_FLog2(xp_as_array(0.18, xp=xp), 10, False), 0.383562110108137, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog2(0.18, 10, False, False), + xp_assert_close( + log_encoding_FLog2(xp_as_array(0.18, xp=xp), 10, False, False), 0.371293971820387, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_FLog2(1.0), + xp_assert_close( + log_encoding_FLog2(xp_as_array(1.0, xp=xp)), 0.568219370444443, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_FLog2(self) -> None: + def test_n_dimensional_log_encoding_FLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_flog.\ log_encoding_FLog2` definition n-dimensional arrays support. """ L_in = 0.18 - V_out = log_encoding_FLog2(L_in) + V_out = as_ndarray(log_encoding_FLog2(xp_as_array(L_in, xp=xp))) - L_in = np.tile(L_in, 6) - V_out = np.tile(V_out, 6) - np.testing.assert_allclose( - log_encoding_FLog2(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp.tile(xp_as_array(L_in, xp=xp), (6,)) + V_out = xp.tile(xp_as_array(V_out, xp=xp), (6,)) + xp_assert_close(log_encoding_FLog2(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - L_in = np.reshape(L_in, (2, 3)) - V_out = np.reshape(V_out, (2, 3)) - np.testing.assert_allclose( - log_encoding_FLog2(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3), xp=xp) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_FLog2(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - L_in = np.reshape(L_in, (2, 3, 1)) - V_out = np.reshape(V_out, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_FLog2(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3, 1), xp=xp) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_FLog2(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_FLog2(self) -> None: + def test_domain_range_scale_log_encoding_FLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_flog.\ log_encoding_FLog2` definition domain and range scale support. """ L_in = 0.18 - V_out = log_encoding_FLog2(L_in) + V_out = as_ndarray(log_encoding_FLog2(xp_as_array(L_in, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_equal( - log_encoding_FLog2(L_in * factor), V_out * factor + xp_assert_equal( + log_encoding_FLog2(xp_as_array(L_in * factor, xp=xp)), + V_out * factor, ) @ignore_numpy_errors @@ -342,89 +340,83 @@ class TestLogDecoding_FLog2: log_decoding_FLog2` definition unit tests methods. """ - def test_log_decoding_FLog2(self) -> None: + def test_log_decoding_FLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_flog.\ log_decoding_FLog2` definition. """ - np.testing.assert_allclose( - log_decoding_FLog2(0.092864000000000), + xp_assert_close( + log_decoding_FLog2(xp_as_array(0.092864000000000, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog2(0.391007241891230), + xp_assert_close( + log_decoding_FLog2(xp_as_array(0.391007241891230, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog2(0.391007241891230, 12), + xp_assert_close( + log_decoding_FLog2(xp_as_array(0.391007241891230, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog2(0.383562110108137, 10, False), + xp_assert_close( + log_decoding_FLog2(xp_as_array(0.383562110108137, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog2(0.371293971820387, 10, False, False), + xp_assert_close( + log_decoding_FLog2(xp_as_array(0.371293971820387, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_FLog2(0.568219370444443), + xp_assert_close( + log_decoding_FLog2(xp_as_array(0.568219370444443, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_FLog2(self) -> None: + def test_n_dimensional_log_decoding_FLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_flog.\ log_decoding_FLog2` definition n-dimensional arrays support. """ V_out = 0.39100724189123 - L_in = log_decoding_FLog2(V_out) + L_in = as_ndarray(log_decoding_FLog2(xp_as_array(V_out, xp=xp))) - V_out = np.tile(V_out, 6) - L_in = np.tile(L_in, 6) - np.testing.assert_allclose( - log_decoding_FLog2(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp.tile(xp_as_array(V_out, xp=xp), (6,)) + L_in = xp.tile(xp_as_array(L_in, xp=xp), (6,)) + xp_assert_close(log_decoding_FLog2(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - V_out = np.reshape(V_out, (2, 3)) - L_in = np.reshape(L_in, (2, 3)) - np.testing.assert_allclose( - log_decoding_FLog2(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3), xp=xp) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_FLog2(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - V_out = np.reshape(V_out, (2, 3, 1)) - L_in = np.reshape(L_in, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_FLog2(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3, 1), xp=xp) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_FLog2(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_FLog2(self) -> None: + def test_domain_range_scale_log_decoding_FLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.fujifilm_flog.\ log_decoding_FLog2` definition domain and range scale support. """ V_out = 0.39100724189123 - L_in = log_decoding_FLog2(V_out) + L_in = as_ndarray(log_decoding_FLog2(xp_as_array(V_out, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_equal( - log_decoding_FLog2(V_out * factor), + xp_assert_equal( + log_decoding_FLog2(xp_as_array(V_out * factor, xp=xp)), L_in * factor, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_gamma.py b/colour/models/rgb/transfer_functions/tests/test_gamma.py index 97faddae6d..23e688777d 100644 --- a/colour/models/rgb/transfer_functions/tests/test_gamma.py +++ b/colour/models/rgb/transfer_functions/tests/test_gamma.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.gamma` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import gamma_function -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -27,126 +41,124 @@ class TestGammaFunction: definition unit tests methods. """ - def test_gamma_function(self) -> None: + def test_gamma_function(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gamma.\ gamma_function` definition. """ - np.testing.assert_allclose( - gamma_function(0.0, 2.2), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + gamma_function(xp_as_array(0.0, xp=xp), 2.2), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(0.18, 2.2), + xp_assert_close( + gamma_function(xp_as_array(0.18, xp=xp), 2.2), 0.022993204992707, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(0.022993204992707, 1.0 / 2.2), + xp_assert_close( + gamma_function(xp_as_array(0.022993204992707, xp=xp), 1.0 / 2.2), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(-0.18, 2.0), + xp_assert_close( + gamma_function(xp_as_array(-0.18, xp=xp), 2.0), 0.0323999999999998, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal(gamma_function(-0.18, 2.2), np.nan) + xp_assert_equal(gamma_function(xp_as_array(-0.18, xp=xp), 2.2), np.nan) - np.testing.assert_allclose( - gamma_function(-0.18, 2.2, "Mirror"), + xp_assert_close( + gamma_function(xp_as_array(-0.18, xp=xp), 2.2, "Mirror"), -0.022993204992707, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(-0.18, 2.2, "Preserve"), + xp_assert_close( + gamma_function(xp_as_array(-0.18, xp=xp), 2.2, "Preserve"), -0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(-0.18, 2.2, "Clamp"), + xp_assert_close( + gamma_function(xp_as_array(-0.18, xp=xp), 2.2, "Clamp"), 0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal(gamma_function(-0.18, -2.2), np.nan) + xp_assert_equal(gamma_function(xp_as_array(-0.18, xp=xp), -2.2), np.nan) - np.testing.assert_allclose( - gamma_function(0.0, -2.2, "Mirror"), + xp_assert_close( + gamma_function(xp_as_array(0.0, xp=xp), -2.2, "Mirror"), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(0.0, 2.2, "Preserve"), + xp_assert_close( + gamma_function(xp_as_array(0.0, xp=xp), 2.2, "Preserve"), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gamma_function(0.0, 2.2, "Clamp"), 0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + gamma_function(xp_as_array(0.0, xp=xp), 2.2, "Clamp"), + 0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_gamma_function(self) -> None: + def test_n_dimensional_gamma_function(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gamma.\ gamma_function` definition n-dimensional arrays support. """ a = 0.18 - a_p = gamma_function(a, 2.2) + a_p = as_ndarray(gamma_function(xp_as_array(a, xp=xp), 2.2)) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( - gamma_function(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close(gamma_function(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( - gamma_function(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(gamma_function(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( - gamma_function(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(gamma_function(a, 2.2), a_p, atol=TOLERANCE_ABSOLUTE_TESTS) a = -0.18 a_p = -0.022993204992707 - np.testing.assert_allclose( + xp_assert_close( gamma_function(a, 2.2, "Mirror"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( gamma_function(a, 2.2, "Mirror"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( gamma_function(a, 2.2, "Mirror"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( gamma_function(a, 2.2, "Mirror"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -154,31 +166,31 @@ def test_n_dimensional_gamma_function(self) -> None: a = -0.18 a_p = -0.18 - np.testing.assert_allclose( + xp_assert_close( gamma_function(a, 2.2, "Preserve"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( gamma_function(a, 2.2, "Preserve"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( gamma_function(a, 2.2, "Preserve"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( gamma_function(a, 2.2, "Preserve"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -186,26 +198,34 @@ def test_n_dimensional_gamma_function(self) -> None: a = -0.18 a_p = 0.0 - np.testing.assert_allclose( - gamma_function(a, 2.2, "Clamp"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + gamma_function(a, 2.2, "Clamp"), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( - gamma_function(a, 2.2, "Clamp"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( + gamma_function(a, 2.2, "Clamp"), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( - gamma_function(a, 2.2, "Clamp"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( + gamma_function(a, 2.2, "Clamp"), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( - gamma_function(a, 2.2, "Clamp"), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + gamma_function(a, 2.2, "Clamp"), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors diff --git a/colour/models/rgb/transfer_functions/tests/test_gopro.py b/colour/models/rgb/transfer_functions/tests/test_gopro.py index 6ea2eda7d6..bad15306e8 100644 --- a/colour/models/rgb/transfer_functions/tests/test_gopro.py +++ b/colour/models/rgb/transfer_functions/tests/test_gopro.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.gopro` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_Protune, log_encoding_Protune, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,67 +45,65 @@ class TestLogEncoding_Protune: log_encoding_Protune` definition unit tests methods. """ - def test_log_encoding_Protune(self) -> None: + def test_log_encoding_Protune(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gopro.\ log_encoding_Protune` definition. """ - np.testing.assert_allclose( - log_encoding_Protune(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_Protune(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Protune(0.18), + xp_assert_close( + log_encoding_Protune(xp_as_array(0.18, xp=xp)), 0.645623486803636, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Protune(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_Protune(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Protune(self) -> None: + def test_n_dimensional_log_encoding_Protune(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gopro.\ log_encoding_Protune` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Protune(x) + y = as_ndarray(log_encoding_Protune(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Protune(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Protune(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Protune(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Protune(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Protune(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Protune(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Protune(self) -> None: + def test_domain_range_scale_log_encoding_Protune(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gopro.\ log_encoding_Protune` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Protune(x) + y = as_ndarray(log_encoding_Protune(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Protune(x * factor), + xp_assert_close( + log_encoding_Protune(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -112,67 +124,65 @@ class TestLogDecoding_Protune: log_decoding_Protune` definition unit tests methods. """ - def test_log_decoding_Protune(self) -> None: + def test_log_decoding_Protune(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gopro.\ log_decoding_Protune` definition. """ - np.testing.assert_allclose( - log_decoding_Protune(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_Protune(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Protune(0.645623486803636), + xp_assert_close( + log_decoding_Protune(xp_as_array(0.645623486803636, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Protune(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_Protune(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Protune(self) -> None: + def test_n_dimensional_log_decoding_Protune(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gopro.\ log_decoding_Protune` definition n-dimensional arrays support. """ y = 0.645623486803636 - x = log_decoding_Protune(y) + x = as_ndarray(log_decoding_Protune(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Protune(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Protune(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Protune(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Protune(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Protune(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Protune(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Protune(self) -> None: + def test_domain_range_scale_log_decoding_Protune(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.gopro.\ log_decoding_Protune` definition domain and range scale support. """ y = 0.645623486803636 - x = log_decoding_Protune(y) + x = as_ndarray(log_decoding_Protune(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Protune(y * factor), + xp_assert_close( + log_decoding_Protune(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itur_bt_1361.py b/colour/models/rgb/transfer_functions/tests/test_itur_bt_1361.py index 99362056ed..c128ee1592 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itur_bt_1361.py +++ b/colour/models/rgb/transfer_functions/tests/test_itur_bt_1361.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.itur_bt_1361` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import oetf_BT1361, oetf_inverse_BT1361 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,67 +42,73 @@ class TestOetf_BT1361: definition unit tests methods. """ - def test_oetf_BT1361(self) -> None: + def test_oetf_BT1361(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1361.\ oetf_BT1361` definition. """ - np.testing.assert_allclose( - oetf_BT1361(-0.18), + xp_assert_close( + oetf_BT1361(xp_as_array(-0.18, xp=xp)), -0.212243985492969, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(oetf_BT1361(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT1361(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - oetf_BT1361(0.015), + xp_assert_close( + oetf_BT1361(xp_as_array(0.015, xp=xp)), 0.067500000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT1361(0.18), 0.409007728864150, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT1361(xp_as_array(0.18, xp=xp)), + 0.409007728864150, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(oetf_BT1361(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT1361(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - def test_n_dimensional_oetf_BT1361(self) -> None: + def test_n_dimensional_oetf_BT1361(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1361.\ oetf_BT1361` definition n-dimensional arrays support. """ L = 0.18 - V = oetf_BT1361(L) + V = as_ndarray(oetf_BT1361(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - V = np.tile(V, 6) - np.testing.assert_allclose(oetf_BT1361(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + xp_assert_close(oetf_BT1361(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - V = np.reshape(V, (2, 3)) - np.testing.assert_allclose(oetf_BT1361(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_BT1361(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - V = np.reshape(V, (2, 3, 1)) - np.testing.assert_allclose(oetf_BT1361(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_BT1361(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_BT1361(self) -> None: + def test_domain_range_scale_oetf_BT1361(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1361.\ oetf_BT1361` definition domain and range scale support. """ L = 0.18 - V = oetf_BT1361(L) + V = as_ndarray(oetf_BT1361(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BT1361(L * factor), + xp_assert_close( + oetf_BT1361(xp_as_array(L * factor, xp=xp)), V * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -109,79 +129,77 @@ class TestOetf_inverse_BT1361: oetf_inverse_BT1361` definition unit tests methods. """ - def test_oetf_inverse_BT1361(self) -> None: + def test_oetf_inverse_BT1361(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1361.\ oetf_inverse_BT1361` definition. """ - np.testing.assert_allclose( - oetf_inverse_BT1361(-0.212243985492969), + xp_assert_close( + oetf_inverse_BT1361(xp_as_array(-0.212243985492969, xp=xp)), -0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT1361(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT1361(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT1361(0.067500000000000), + xp_assert_close( + oetf_inverse_BT1361(xp_as_array(0.067500000000000, xp=xp)), 0.015, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT1361(0.409007728864150), + xp_assert_close( + oetf_inverse_BT1361(xp_as_array(0.409007728864150, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT1361(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT1361(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BT1361(self) -> None: + def test_n_dimensional_oetf_inverse_BT1361(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1361.\ oetf_inverse_BT1361` definition n-dimensional arrays support. """ V = 0.409007728864150 - L = oetf_inverse_BT1361(V) + L = as_ndarray(oetf_inverse_BT1361(xp_as_array(V, xp=xp))) - V = np.tile(V, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( - oetf_inverse_BT1361(V), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(oetf_inverse_BT1361(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_BT1361(V), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_BT1361(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_BT1361(V), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_BT1361(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_BT1361(self) -> None: + def test_domain_range_scale_oetf_inverse_BT1361(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1361.\ oetf_inverse_BT1361` definition domain and range scale support. """ V = 0.409007728864150 - L = oetf_inverse_BT1361(V) + L = as_ndarray(oetf_inverse_BT1361(xp_as_array(V, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BT1361(V * factor), + xp_assert_close( + oetf_inverse_BT1361(xp_as_array(V * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itur_bt_1886.py b/colour/models/rgb/transfer_functions/tests/test_itur_bt_1886.py index 28b1d971cb..c93061aa23 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itur_bt_1886.py +++ b/colour/models/rgb/transfer_functions/tests/test_itur_bt_1886.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.itur_bt_1886` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import eotf_BT1886, eotf_inverse_BT1886 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,67 +42,65 @@ class TestEotf_inverse_BT1886: eotf_inverse_BT1886` definition unit tests methods. """ - def test_eotf_inverse_BT1886(self) -> None: + def test_eotf_inverse_BT1886(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1886.\ eotf_inverse_BT1886` definition. """ - np.testing.assert_allclose( - eotf_inverse_BT1886(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT1886(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT1886(0.016317514686316), + xp_assert_close( + eotf_inverse_BT1886(xp_as_array(0.016317514686316, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT1886(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT1886(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_BT1886(self) -> None: + def test_n_dimensional_eotf_inverse_BT1886(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1886.\ eotf_inverse_BT1886` definition n-dimensional arrays support. """ L = 0.016317514686316 - V = eotf_inverse_BT1886(L) + V = as_ndarray(eotf_inverse_BT1886(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - V = np.tile(V, 6) - np.testing.assert_allclose( - eotf_inverse_BT1886(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + xp_assert_close(eotf_inverse_BT1886(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - V = np.reshape(V, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_BT1886(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_inverse_BT1886(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - V = np.reshape(V, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_BT1886(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_inverse_BT1886(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_inverse_BT1886(self) -> None: + def test_domain_range_scale_eotf_inverse_BT1886(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1886.\ eotf_inverse_BT1886` definition domain and range scale support. """ L = 0.18 - V = eotf_inverse_BT1886(L) + V = as_ndarray(eotf_inverse_BT1886(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_BT1886(L * factor), + xp_assert_close( + eotf_inverse_BT1886(xp_as_array(L * factor, xp=xp)), V * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -109,55 +121,61 @@ class TestEotf_BT1886: eotf_BT1886` definition unit tests methods. """ - def test_eotf_BT1886(self) -> None: + def test_eotf_BT1886(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1886.\ eotf_BT1886` definition. """ - np.testing.assert_allclose(eotf_BT1886(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + eotf_BT1886(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - eotf_BT1886(0.18), 0.016317514686316, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_BT1886(xp_as_array(0.18, xp=xp)), + 0.016317514686316, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(eotf_BT1886(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + eotf_BT1886(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - def test_n_dimensional_eotf_BT1886(self) -> None: + def test_n_dimensional_eotf_BT1886(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1886.\ eotf_BT1886` definition n-dimensional arrays support. """ V = 0.18 - L = eotf_BT1886(V) + L = as_ndarray(eotf_BT1886(xp_as_array(V, xp=xp))) - V = np.tile(V, 6) - L = np.tile(L, 6) - np.testing.assert_allclose(eotf_BT1886(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(eotf_BT1886(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose(eotf_BT1886(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_BT1886(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose(eotf_BT1886(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_BT1886(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_BT1886(self) -> None: + def test_domain_range_scale_eotf_BT1886(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_1886.\ eotf_BT1886` definition domain and range scale support. """ V = 0.016317514686316 - L = eotf_BT1886(V) + L = as_ndarray(eotf_BT1886(xp_as_array(V, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_BT1886(V * factor), + xp_assert_close( + eotf_BT1886(xp_as_array(V * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itur_bt_2020.py b/colour/models/rgb/transfer_functions/tests/test_itur_bt_2020.py index e9f72d755b..ccdb432fd6 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itur_bt_2020.py +++ b/colour/models/rgb/transfer_functions/tests/test_itur_bt_2020.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.itur_bt_2020` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import oetf_BT2020, oetf_inverse_BT2020 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,55 +42,61 @@ class TestOetf_BT2020: oetf_BT2020` definition unit tests methods. """ - def test_oetf_BT2020(self) -> None: + def test_oetf_BT2020(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2020.\ oetf_BT2020` definition. """ - np.testing.assert_allclose(oetf_BT2020(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT2020(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - oetf_BT2020(0.18), 0.409007728864150, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT2020(xp_as_array(0.18, xp=xp)), + 0.409007728864150, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(oetf_BT2020(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT2020(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - def test_n_dimensional_oetf_BT2020(self) -> None: + def test_n_dimensional_oetf_BT2020(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2020.\ oetf_BT2020` definition n-dimensional arrays support. """ E = 0.18 - E_p = oetf_BT2020(E) + E_p = as_ndarray(oetf_BT2020(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose(oetf_BT2020(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_BT2020(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose(oetf_BT2020(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_BT2020(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose(oetf_BT2020(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_BT2020(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_BT2020(self) -> None: + def test_domain_range_scale_oetf_BT2020(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2020.\ oetf_BT2020` definition domain and range scale support. """ E = 0.18 - E_p = oetf_BT2020(E) + E_p = as_ndarray(oetf_BT2020(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BT2020(E * factor), + xp_assert_close( + oetf_BT2020(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -97,67 +117,65 @@ class TestOetf_inverse_BT2020: oetf_inverse_BT2020` definition unit tests methods. """ - def test_oetf_inverse_BT2020(self) -> None: + def test_oetf_inverse_BT2020(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2020.\ oetf_inverse_BT2020` definition. """ - np.testing.assert_allclose( - oetf_inverse_BT2020(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT2020(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT2020(0.409007728864150), + xp_assert_close( + oetf_inverse_BT2020(xp_as_array(0.409007728864150, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT2020(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT2020(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BT2020(self) -> None: + def test_n_dimensional_oetf_inverse_BT2020(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2020.\ oetf_inverse_BT2020` definition n-dimensional arrays support. """ E_p = 0.409007728864150 - E = oetf_inverse_BT2020(E_p) + E = as_ndarray(oetf_inverse_BT2020(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - oetf_inverse_BT2020(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(oetf_inverse_BT2020(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_BT2020(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_BT2020(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_BT2020(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_BT2020(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_BT2020(self) -> None: + def test_domain_range_scale_oetf_inverse_BT2020(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2020.\ oetf_inverse_BT2020` definition domain and range scale support. """ E_p = 0.409007728864150 - E = oetf_inverse_BT2020(E_p) + E = as_ndarray(oetf_inverse_BT2020(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BT2020(E_p * factor), + xp_assert_close( + oetf_inverse_BT2020(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itur_bt_2100.py b/colour/models/rgb/transfer_functions/tests/test_itur_bt_2100.py index 38d601163a..3531cf7857 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itur_bt_2100.py +++ b/colour/models/rgb/transfer_functions/tests/test_itur_bt_2100.py @@ -3,7 +3,12 @@ :mod:`colour.models.rgb.transfer_functions.itur_bt_2100` module. """ +from __future__ import annotations + +import typing + import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import ( @@ -29,7 +34,17 @@ ootf_inverse_BT2100_HLG_1, ootf_inverse_BT2100_HLG_2, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -67,71 +82,65 @@ class TestOetf_BT2100_PQ: oetf_BT2100_PQ` definition unit tests methods. """ - def test_oetf_BT2100_PQ(self) -> None: + def test_oetf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_BT2100_PQ` definition. """ - np.testing.assert_allclose( - oetf_BT2100_PQ(0.0), + xp_assert_close( + oetf_BT2100_PQ(xp_as_array(0.0, xp=xp)), 0.000000730955903, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT2100_PQ(0.1), + xp_assert_close( + oetf_BT2100_PQ(xp_as_array(0.1, xp=xp)), 0.724769816665726, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT2100_PQ(1.0), + xp_assert_close( + oetf_BT2100_PQ(xp_as_array(1.0, xp=xp)), 0.999999934308041, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_BT2100_PQ(self) -> None: + def test_n_dimensional_oetf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_BT2100_PQ` definition n-dimensional arrays support. """ E = 0.1 - E_p = oetf_BT2100_PQ(E) + E_p = as_ndarray(oetf_BT2100_PQ(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - oetf_BT2100_PQ(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_BT2100_PQ(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - oetf_BT2100_PQ(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_BT2100_PQ(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - oetf_BT2100_PQ(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_BT2100_PQ(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_BT2100_PQ(self) -> None: + def test_domain_range_scale_oetf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_BT2100_PQ` definition domain and range scale support. """ E = 0.1 - E_p = oetf_BT2100_PQ(E) + E_p = as_ndarray(oetf_BT2100_PQ(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BT2100_PQ(E * factor), + xp_assert_close( + oetf_BT2100_PQ(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -152,71 +161,65 @@ class TestOetf_inverse_BT2100_PQ: oetf_inverse_BT2100_PQ` definition unit tests methods. """ - def test_oetf_inverse_BT2100_PQ(self) -> None: + def test_oetf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_inverse_BT2100_PQ` definition. """ - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(0.000000730955903), + xp_assert_close( + oetf_inverse_BT2100_PQ(xp_as_array(0.000000730955903, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(0.724769816665726), + xp_assert_close( + oetf_inverse_BT2100_PQ(xp_as_array(0.724769816665726, xp=xp)), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(0.999999934308041), + xp_assert_close( + oetf_inverse_BT2100_PQ(xp_as_array(0.999999934308041, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BT2100_PQ(self) -> None: + def test_n_dimensional_oetf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_inverse_BT2100_PQ` definition n-dimensional arrays support. """ E_p = 0.724769816665726 - E = oetf_inverse_BT2100_PQ(E_p) + E = as_ndarray(oetf_inverse_BT2100_PQ(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(oetf_inverse_BT2100_PQ(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_BT2100_PQ(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_BT2100_PQ(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_BT2100_PQ(self) -> None: + def test_domain_range_scale_oetf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_inverse_BT2100_PQ` definition domain and range scale support. """ E_p = 0.724769816665726 - E = oetf_inverse_BT2100_PQ(E_p) + E = as_ndarray(oetf_inverse_BT2100_PQ(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BT2100_PQ(E_p * factor), + xp_assert_close( + oetf_inverse_BT2100_PQ(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -237,67 +240,66 @@ class TestEotf_BT2100_PQ: eotf_BT2100_PQ` definition unit tests methods. """ - def test_eotf_BT2100_PQ(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_eotf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_PQ` definition. """ - np.testing.assert_allclose( - eotf_BT2100_PQ(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_BT2100_PQ(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_PQ(0.724769816665726), + xp_assert_close( + eotf_BT2100_PQ(xp_as_array(0.724769816665726, xp=xp)), 779.98836083408537, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_PQ(1.0), 10000.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_BT2100_PQ(xp_as_array(1.0, xp=xp)), + 10000.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_BT2100_PQ(self) -> None: + def test_n_dimensional_eotf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_PQ` definition n-dimensional arrays support. """ E_p = 0.724769816665726 - F_D = eotf_BT2100_PQ(E_p) + F_D = as_ndarray(eotf_BT2100_PQ(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - F_D = np.tile(F_D, 6) - np.testing.assert_allclose( - eotf_BT2100_PQ(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + xp_assert_close(eotf_BT2100_PQ(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - F_D = np.reshape(F_D, (2, 3)) - np.testing.assert_allclose( - eotf_BT2100_PQ(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_BT2100_PQ(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - F_D = np.reshape(F_D, (2, 3, 1)) - np.testing.assert_allclose( - eotf_BT2100_PQ(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_BT2100_PQ(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_BT2100_PQ(self) -> None: + def test_domain_range_scale_eotf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_PQ` definition domain and range scale support. """ E_p = 0.724769816665726 - F_D = eotf_BT2100_PQ(E_p) + F_D = as_ndarray(eotf_BT2100_PQ(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_BT2100_PQ(E_p * factor), + xp_assert_close( + eotf_BT2100_PQ(xp_as_array(E_p * factor, xp=xp)), F_D * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -318,69 +320,65 @@ class TestEotf_inverse_BT2100_PQ: eotf_inverse_BT2100_PQ` definition unit tests methods. """ - def test_eotf_inverse_BT2100_PQ(self) -> None: + def test_eotf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_PQ` definition. """ - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(0.0), + xp_assert_close( + eotf_inverse_BT2100_PQ(xp_as_array(0.0, xp=xp)), 0.000000730955903, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(779.98836083408537), + xp_assert_close( + eotf_inverse_BT2100_PQ(xp_as_array(779.98836083408537, xp=xp)), 0.724769816665726, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(10000.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT2100_PQ(xp_as_array(10000.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_BT2100_PQ(self) -> None: + def test_n_dimensional_eotf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_PQ` definition n-dimensional arrays support. """ F_D = 779.98836083408537 - E_p = eotf_inverse_BT2100_PQ(F_D) + E_p = as_ndarray(eotf_inverse_BT2100_PQ(xp_as_array(F_D, xp=xp))) - F_D = np.tile(F_D, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(eotf_inverse_BT2100_PQ(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - F_D = np.reshape(F_D, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_inverse_BT2100_PQ(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - F_D = np.reshape(F_D, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_inverse_BT2100_PQ(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_inverse_BT2100_PQ(self) -> None: + def test_domain_range_scale_eotf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_PQ` definition domain and range scale support. """ F_D = 779.98836083408537 - E_p = eotf_inverse_BT2100_PQ(F_D) + E_p = as_ndarray(eotf_inverse_BT2100_PQ(xp_as_array(F_D, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_BT2100_PQ(F_D * factor), + xp_assert_close( + eotf_inverse_BT2100_PQ(xp_as_array(F_D * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -401,69 +399,65 @@ class TestOotf_BT2100_PQ: ootf_BT2100_PQ` definition unit tests methods. """ - def test_ootf_BT2100_PQ(self) -> None: + def test_ootf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_PQ` definition. """ - np.testing.assert_allclose( - ootf_BT2100_PQ(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_BT2100_PQ(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_PQ(0.1), + xp_assert_close( + ootf_BT2100_PQ(xp_as_array(0.1, xp=xp)), 779.98836083411584, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_PQ(1.0), + xp_assert_close( + ootf_BT2100_PQ(xp_as_array(1.0, xp=xp)), 9999.993723673924300, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ootf_BT2100_PQ(self) -> None: + def test_n_dimensional_ootf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_PQ` definition n-dimensional arrays support. """ E = 0.1 - F_D = ootf_BT2100_PQ(E) + F_D = as_ndarray(ootf_BT2100_PQ(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - F_D = np.tile(F_D, 6) - np.testing.assert_allclose( - ootf_BT2100_PQ(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + xp_assert_close(ootf_BT2100_PQ(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - F_D = np.reshape(F_D, (2, 3)) - np.testing.assert_allclose( - ootf_BT2100_PQ(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + xp_assert_close(ootf_BT2100_PQ(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - F_D = np.reshape(F_D, (2, 3, 1)) - np.testing.assert_allclose( - ootf_BT2100_PQ(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(ootf_BT2100_PQ(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ootf_BT2100_PQ(self) -> None: + def test_domain_range_scale_ootf_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_PQ` definition domain and range scale support. """ E = 0.1 - F_D = ootf_BT2100_PQ(E) + F_D = as_ndarray(ootf_BT2100_PQ(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - ootf_BT2100_PQ(E * factor), + xp_assert_close( + ootf_BT2100_PQ(xp_as_array(E * factor, xp=xp)), F_D * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -484,69 +478,65 @@ class TestOotf_inverse_BT2100_PQ: ootf_inverse_BT2100_PQ` definition unit tests methods. """ - def test_ootf_inverse_BT2100_PQ(self) -> None: + def test_ootf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_PQ` definition. """ - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_inverse_BT2100_PQ(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(779.98836083411584), + xp_assert_close( + ootf_inverse_BT2100_PQ(xp_as_array(779.98836083411584, xp=xp)), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(9999.993723673924300), + xp_assert_close( + ootf_inverse_BT2100_PQ(xp_as_array(9999.993723673924300, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ootf_inverse_BT2100_PQ(self) -> None: + def test_n_dimensional_ootf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_PQ` definition n-dimensional arrays support. """ F_D = 779.98836083411584 - E = ootf_inverse_BT2100_PQ(F_D) + E = as_ndarray(ootf_inverse_BT2100_PQ(xp_as_array(F_D, xp=xp))) - F_D = np.tile(F_D, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(ootf_inverse_BT2100_PQ(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS) - F_D = np.reshape(F_D, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(ootf_inverse_BT2100_PQ(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS) - F_D = np.reshape(F_D, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(ootf_inverse_BT2100_PQ(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ootf_inverse_BT2100_PQ(self) -> None: + def test_domain_range_scale_ootf_inverse_BT2100_PQ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_PQ` definition domain and range scale support. """ F_D = 779.98836083411584 - E = ootf_inverse_BT2100_PQ(F_D) + E = as_ndarray(ootf_inverse_BT2100_PQ(xp_as_array(F_D, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - ootf_inverse_BT2100_PQ(F_D * factor), + xp_assert_close( + ootf_inverse_BT2100_PQ(xp_as_array(F_D * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -567,31 +557,31 @@ class TestGamma_function_BT2100_HLG: gamma_function_BT2100_HLG` definition unit tests methods. """ - def test_gamma_function_BT2100_HLG(self) -> None: + def test_gamma_function_BT2100_HLG(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ gamma_function_BT2100_HLG` definition. """ - np.testing.assert_allclose( + xp_assert_close( gamma_function_BT2100_HLG(1000.0), 1.2, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( gamma_function_BT2100_HLG(2000.0), 1.326432598178872, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( gamma_function_BT2100_HLG(4000.0), 1.452865196357744, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( gamma_function_BT2100_HLG(10000.0), 1.619999999999999, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -604,69 +594,65 @@ class TestOetf_BT2100_HLG: oetf_BT2100_HLG` definition unit tests methods. """ - def test_oetf_BT2100_HLG(self) -> None: + def test_oetf_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_BT2100_HLG` definition. """ - np.testing.assert_allclose( - oetf_BT2100_HLG(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT2100_HLG(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT2100_HLG(0.18 / 12), + xp_assert_close( + oetf_BT2100_HLG(xp_as_array(0.18 / 12, xp=xp)), 0.212132034355964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT2100_HLG(1.0), + xp_assert_close( + oetf_BT2100_HLG(xp_as_array(1.0, xp=xp)), 0.999999995536569, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_BT2100_HLG(self) -> None: + def test_n_dimensional_oetf_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_BT2100_HLG` definition n-dimensional arrays support. """ E = 0.18 / 12 - E_p = oetf_BT2100_HLG(E) + E_p = as_ndarray(oetf_BT2100_HLG(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - oetf_BT2100_HLG(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_BT2100_HLG(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - oetf_BT2100_HLG(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_BT2100_HLG(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - oetf_BT2100_HLG(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_BT2100_HLG(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_BT2100_HLG(self) -> None: + def test_domain_range_scale_oetf_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_BT2100_HLG` definition domain and range scale support. """ E = 0.18 / 12 - E_p = oetf_BT2100_HLG(E) + E_p = as_ndarray(oetf_BT2100_HLG(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BT2100_HLG(E * factor), + xp_assert_close( + oetf_BT2100_HLG(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -687,69 +673,65 @@ class TestOetf_inverse_BT2100_HLG: oetf_inverse_BT2100_HLG` definition unit tests methods. """ - def test_oetf_inverse_BT2100_HLG(self) -> None: + def test_oetf_inverse_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_inverse_BT2100_HLG` definition. """ - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT2100_HLG(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(0.212132034355964), + xp_assert_close( + oetf_inverse_BT2100_HLG(xp_as_array(0.212132034355964, xp=xp)), 0.18 / 12, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(0.999999995536569), + xp_assert_close( + oetf_inverse_BT2100_HLG(xp_as_array(0.999999995536569, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BT2100_HLG(self) -> None: + def test_n_dimensional_oetf_inverse_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_inverse_BT2100_HLG` definition n-dimensional arrays support. """ E_p = 0.212132034355964 - E = oetf_inverse_BT2100_HLG(E_p) + E = as_ndarray(oetf_inverse_BT2100_HLG(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(oetf_inverse_BT2100_HLG(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_BT2100_HLG(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_BT2100_HLG(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_BT2100_HLG(self) -> None: + def test_domain_range_scale_oetf_inverse_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ oetf_inverse_BT2100_HLG` definition domain and range scale support. """ E_p = 0.212132034355964 - E = oetf_inverse_BT2100_HLG(E_p) + E = as_ndarray(oetf_inverse_BT2100_HLG(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BT2100_HLG(E_p * factor), + xp_assert_close( + oetf_inverse_BT2100_HLG(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -770,99 +752,87 @@ class TestEotf_BT2100_HLG_1: eotf_BT2100_HLG_1` definition unit tests methods. """ - def test_eotf_BT2100_HLG_1(self) -> None: + def test_eotf_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_HLG_1` definition. """ - np.testing.assert_allclose( - eotf_BT2100_HLG_1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_BT2100_HLG_1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(0.212132034355964), + xp_assert_close( + eotf_BT2100_HLG_1(xp_as_array(0.212132034355964, xp=xp)), 6.476039825649814, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(1.0), + xp_assert_close( + eotf_BT2100_HLG_1(xp_as_array(1.0, xp=xp)), 1000.000032321769100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(0.212132034355964, 0.001, 10000, 1.4), + xp_assert_close( + eotf_BT2100_HLG_1(xp_as_array(0.212132034355964, xp=xp), 0.001, 10000, 1.4), 27.96039175299561, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_BT2100_HLG_1(self) -> None: + def test_n_dimensional_eotf_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_HLG_1` definition n-dimensional arrays support. """ E_p = 0.212132034355964 - F_D = eotf_BT2100_HLG_1(E_p) + F_D = as_ndarray(eotf_BT2100_HLG_1(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - F_D = np.tile(F_D, 6) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - F_D = np.reshape(F_D, (2, 3)) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - F_D = np.reshape(F_D, (2, 3, 1)) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (6, 1)) - F_D = np.reshape(F_D, (6, 1)) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (6, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.array([0.25, 0.50, 0.75]) + E_p = xp_as_array([0.25, 0.50, 0.75], xp=xp) F_D = np.array([12.49759413, 49.99037650, 158.94693786]) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.tile(E_p, (6, 1)) - F_D = np.tile(F_D, (6, 1)) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6, 1)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 3)) - F_D = np.reshape(F_D, (2, 3, 3)) - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(eotf_BT2100_HLG_1(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_BT2100_HLG_1(self) -> None: + def test_domain_range_scale_eotf_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_HLG_1` definition domain and range scale support. """ E_p = 0.212132034355964 - F_D = eotf_BT2100_HLG_1(E_p) + F_D = as_ndarray(eotf_BT2100_HLG_1(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_BT2100_HLG_1(E_p * factor), + xp_assert_close( + eotf_BT2100_HLG_1(xp_as_array(E_p * factor, xp=xp)), F_D * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -883,99 +853,87 @@ class TestEotf_BT2100_HLG_2: eotf_BT2100_HLG_2` definition unit tests methods. """ - def test_eotf_BT2100_HLG_2(self) -> None: + def test_eotf_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_HLG_2` definition. """ - np.testing.assert_allclose( - eotf_BT2100_HLG_2(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_BT2100_HLG_2(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(0.212132034355964), + xp_assert_close( + eotf_BT2100_HLG_2(xp_as_array(0.212132034355964, xp=xp)), 6.476039825649814, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(1.0), + xp_assert_close( + eotf_BT2100_HLG_2(xp_as_array(1.0, xp=xp)), 1000.000032321769100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(0.212132034355964, 0.001, 10000, 1.4), + xp_assert_close( + eotf_BT2100_HLG_2(xp_as_array(0.212132034355964, xp=xp), 0.001, 10000, 1.4), 29.581261576946076, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_BT2100_HLG_2(self) -> None: + def test_n_dimensional_eotf_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_HLG_2` definition n-dimensional arrays support. """ E_p = 0.212132034355964 - F_D = eotf_BT2100_HLG_2(E_p) + F_D = as_ndarray(eotf_BT2100_HLG_2(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - F_D = np.tile(F_D, 6) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - F_D = np.reshape(F_D, (2, 3)) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - F_D = np.reshape(F_D, (2, 3, 1)) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (6, 1)) - F_D = np.reshape(F_D, (6, 1)) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (6, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.array([0.25, 0.50, 0.75]) + E_p = xp_as_array([0.25, 0.50, 0.75], xp=xp) F_D = np.array([12.49759413, 49.99037650, 158.94693786]) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.tile(E_p, (6, 1)) - F_D = np.tile(F_D, (6, 1)) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6, 1)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 3)) - F_D = np.reshape(F_D, (2, 3, 3)) - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(eotf_BT2100_HLG_2(E_p), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_BT2100_HLG_2(self) -> None: + def test_domain_range_scale_eotf_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_BT2100_HLG_2` definition domain and range scale support. """ E_p = 0.212132034355964 - F_D = eotf_BT2100_HLG_2(E_p) + F_D = as_ndarray(eotf_BT2100_HLG_2(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_BT2100_HLG_2(E_p * factor), + xp_assert_close( + eotf_BT2100_HLG_2(xp_as_array(E_p * factor, xp=xp)), F_D * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -996,99 +954,117 @@ class TestEotf_inverse_BT2100_HLG_1: eotf_inverse_BT2100_HLG_1` definition unit tests methods. """ - def test_eotf_inverse_BT2100_HLG_1(self) -> None: + def test_eotf_inverse_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_HLG_1` definition. """ - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT2100_HLG_1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(6.476039825649814), + xp_assert_close( + eotf_inverse_BT2100_HLG_1(xp_as_array(6.476039825649814, xp=xp)), 0.212132034355964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(1000.000032321769100), + xp_assert_close( + eotf_inverse_BT2100_HLG_1(xp_as_array(1000.000032321769100, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(27.96039175299561, 0.001, 10000, 1.4), + xp_assert_close( + eotf_inverse_BT2100_HLG_1( + xp_as_array(27.96039175299561, xp=xp), 0.001, 10000, 1.4 + ), 0.212132034355964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_BT2100_HLG_1(self) -> None: + def test_n_dimensional_eotf_inverse_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_HLG_1` definition n-dimensional arrays support. """ F_D = 6.476039825649814 - E_p = eotf_inverse_BT2100_HLG_1(F_D) + E_p = as_ndarray(eotf_inverse_BT2100_HLG_1(xp_as_array(F_D, xp=xp))) - F_D = np.tile(F_D, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (6, 1)) - E_p = np.reshape(E_p, (6, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (6, 1), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.array([12.49759413, 49.99037650, 158.94693786]) + F_D = xp_as_array([12.49759413, 49.99037650, 158.94693786], xp=xp) E_p = np.array([0.25, 0.50, 0.75]) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.tile(F_D, (6, 1)) - E_p = np.tile(E_p, (6, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6, 1)) + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (2, 3, 3)) - E_p = np.reshape(E_p, (2, 3, 3)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_1(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_eotf_inverse_BT2100_HLG_1(self) -> None: + def test_domain_range_scale_eotf_inverse_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_HLG_1` definition domain and range scale support. """ F_D = 6.476039825649814 - E_p = eotf_inverse_BT2100_HLG_1(F_D) + E_p = as_ndarray(eotf_inverse_BT2100_HLG_1(xp_as_array(F_D, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_1(F_D * factor), + xp_assert_close( + eotf_inverse_BT2100_HLG_1(xp_as_array(F_D * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1109,99 +1085,117 @@ class TestEotf_inverse_BT2100_HLG_2: eotf_inverse_BT2100_HLG_2` definition unit tests methods. """ - def test_eotf_inverse_BT2100_HLG_2(self) -> None: + def test_eotf_inverse_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_HLG_2` definition. """ - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT2100_HLG_2(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(6.476039825649814), + xp_assert_close( + eotf_inverse_BT2100_HLG_2(xp_as_array(6.476039825649814, xp=xp)), 0.212132034355964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(1000.000032321769100), + xp_assert_close( + eotf_inverse_BT2100_HLG_2(xp_as_array(1000.000032321769100, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(29.581261576946076, 0.001, 10000, 1.4), + xp_assert_close( + eotf_inverse_BT2100_HLG_2( + xp_as_array(29.581261576946076, xp=xp), 0.001, 10000, 1.4 + ), 0.212132034355964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_BT2100_HLG_2(self) -> None: + def test_n_dimensional_eotf_inverse_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_HLG_2` definition n-dimensional arrays support. """ F_D = 6.476039825649814 - E_p = eotf_inverse_BT2100_HLG_2(F_D) + E_p = as_ndarray(eotf_inverse_BT2100_HLG_2(xp_as_array(F_D, xp=xp))) - F_D = np.tile(F_D, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (6, 1)) - E_p = np.reshape(E_p, (6, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (6, 1), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.array([12.49759413, 49.99037650, 158.94693786]) + F_D = xp_as_array([12.49759413, 49.99037650, 158.94693786], xp=xp) E_p = np.array([0.25, 0.50, 0.75]) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.tile(F_D, (6, 1)) - E_p = np.tile(E_p, (6, 1)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6, 1)) + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - F_D = np.reshape(F_D, (2, 3, 3)) - E_p = np.reshape(E_p, (2, 3, 3)) - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D), E_p, atol=TOLERANCE_ABSOLUTE_TESTS + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + eotf_inverse_BT2100_HLG_2(F_D), + E_p, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_eotf_inverse_BT2100_HLG_2(self) -> None: + def test_domain_range_scale_eotf_inverse_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ eotf_inverse_BT2100_HLG_2` definition domain and range scale support. """ F_D = 6.476039825649814 - E_p = eotf_inverse_BT2100_HLG_2(F_D) + E_p = as_ndarray(eotf_inverse_BT2100_HLG_2(xp_as_array(F_D, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_BT2100_HLG_2(F_D * factor), + xp_assert_close( + eotf_inverse_BT2100_HLG_2(xp_as_array(F_D * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1222,28 +1216,32 @@ class TestOotf_BT2100_HLG_1: ootf_BT2100_HLG_1` definition unit tests methods. """ - def test_ootf_BT2100_HLG_1(self) -> None: + def test_ootf_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG_1` definition. """ - np.testing.assert_allclose( - ootf_BT2100_HLG_1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_BT2100_HLG_1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(0.1), + xp_assert_close( + ootf_BT2100_HLG_1(xp_as_array(0.1, xp=xp)), 63.095734448019336, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(1.0), 1000.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_BT2100_HLG_1(xp_as_array(1.0, xp=xp)), + 1000.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(0.1, 0.001, 10000, 1.4), + xp_assert_close( + ootf_BT2100_HLG_1(xp_as_array(0.1, xp=xp), 0.001, 10000, 1.4), 398.108130742780300, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1260,86 +1258,73 @@ def test_ootf_BT2100_HLG_1(self) -> None: [51.320396090100672, -51.320396090100672, 51.320396090100672], ], ) - np.testing.assert_allclose( + xp_assert_close( ootf_BT2100_HLG_1( - np.array( + xp_as_array( [ [0.1, 0.0, -0.1], [-0.1, -0.1, -0.1], [0.1, 0.1, 0.1], [0.1, -0.1, 0.1], - ] + ], + xp=xp, ) ), a, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ootf_BT2100_HLG_1(self) -> None: + def test_n_dimensional_ootf_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG_1` definition n-dimensional arrays support. """ E = 0.1 - F_D = ootf_BT2100_HLG_1(E) + F_D = as_ndarray(ootf_BT2100_HLG_1(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - F_D = np.tile(F_D, 6) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - F_D = np.reshape(F_D, (2, 3)) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - F_D = np.reshape(F_D, (2, 3, 1)) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (6, 1)) - F_D = np.reshape(F_D, (6, 1)) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (6, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.array([0.25, 0.50, 0.75]) + E = xp_as_array([0.25, 0.50, 0.75], xp=xp) F_D = np.array([213.01897444, 426.03794887, 639.05692331]) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.tile(E, (6, 1)) - F_D = np.tile(F_D, (6, 1)) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6, 1)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 3)) - F_D = np.reshape(F_D, (2, 3, 3)) - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(ootf_BT2100_HLG_1(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ootf_BT2100_HLG_1(self) -> None: + def test_domain_range_scale_ootf_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG_1` definition domain and range scale support. """ E = 0.1 - F_D = ootf_BT2100_HLG_1(E) + F_D = as_ndarray(ootf_BT2100_HLG_1(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E * factor), + xp_assert_close( + ootf_BT2100_HLG_1(xp_as_array(E * factor, xp=xp)), F_D * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1360,28 +1345,32 @@ class TestOotf_BT2100_HLG_2: ootf_BT2100_HLG_2` definition unit tests methods. """ - def test_ootf_BT2100_HLG_2(self) -> None: + def test_ootf_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG_2` definition. """ - np.testing.assert_allclose( - ootf_BT2100_HLG_2(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_BT2100_HLG_2(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(0.1), + xp_assert_close( + ootf_BT2100_HLG_2(xp_as_array(0.1, xp=xp)), 63.095734448019336, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(1.0), 1000.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_BT2100_HLG_2(xp_as_array(1.0, xp=xp)), + 1000.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(0.1, 10000, 1.4), + xp_assert_close( + ootf_BT2100_HLG_2(xp_as_array(0.1, xp=xp), 10000, 1.4), 398.107170553497380, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1398,86 +1387,73 @@ def test_ootf_BT2100_HLG_2(self) -> None: [51.320396090100672, -51.320396090100672, 51.320396090100672], ], ) - np.testing.assert_allclose( + xp_assert_close( ootf_BT2100_HLG_2( - np.array( + xp_as_array( [ [0.1, 0.0, -0.1], [-0.1, -0.1, -0.1], [0.1, 0.1, 0.1], [0.1, -0.1, 0.1], - ] + ], + xp=xp, ) ), a, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ootf_BT2100_HLG_2(self) -> None: + def test_n_dimensional_ootf_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG_2` definition n-dimensional arrays support. """ E = 0.1 - F_D = ootf_BT2100_HLG_2(E) + F_D = as_ndarray(ootf_BT2100_HLG_2(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - F_D = np.tile(F_D, 6) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - F_D = np.reshape(F_D, (2, 3)) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - F_D = np.reshape(F_D, (2, 3, 1)) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (6, 1)) - F_D = np.reshape(F_D, (6, 1)) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (6, 1), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.array([0.25, 0.50, 0.75]) + E = xp_as_array([0.25, 0.50, 0.75], xp=xp) F_D = np.array([213.01897444, 426.03794887, 639.05692331]) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.tile(E, (6, 1)) - F_D = np.tile(F_D, (6, 1)) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6, 1)) + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 3)) - F_D = np.reshape(F_D, (2, 3, 3)) - np.testing.assert_allclose( - ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 3), xp=xp) + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(ootf_BT2100_HLG_2(E), F_D, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ootf_BT2100_HLG_2(self) -> None: + def test_domain_range_scale_ootf_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG_2` definition domain and range scale support. """ E = 0.1 - F_D = ootf_BT2100_HLG_1(E) + F_D = as_ndarray(ootf_BT2100_HLG_1(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - ootf_BT2100_HLG_1(E * factor), + xp_assert_close( + ootf_BT2100_HLG_1(xp_as_array(E * factor, xp=xp)), F_D * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1498,30 +1474,34 @@ class TestOotf_inverse_BT2100_HLG_1: ootf_inverse_BT2100_HLG_1` definition unit tests methods. """ - def test_ootf_inverse_BT2100_HLG_1(self) -> None: + def test_ootf_inverse_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG_1` definition. """ - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_inverse_BT2100_HLG_1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_1(63.095734448019336), + xp_assert_close( + ootf_inverse_BT2100_HLG_1(xp_as_array(63.095734448019336, xp=xp)), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_1(1000.0), + xp_assert_close( + ootf_inverse_BT2100_HLG_1(xp_as_array(1000.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_1(398.108130742780300, 0.001, 10000, 1.4), + xp_assert_close( + ootf_inverse_BT2100_HLG_1( + xp_as_array(398.108130742780300, xp=xp), 0.001, 10000, 1.4 + ), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1538,84 +1518,82 @@ def test_ootf_inverse_BT2100_HLG_1(self) -> None: [51.320396090100672, -51.320396090100672, 51.320396090100672], ] ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_1(a), - np.array( - [ - [0.1, 0.0, -0.1], - [-0.1, -0.1, -0.1], - [0.1, 0.1, 0.1], - [0.1, -0.1, 0.1], - ] - ), + xp_assert_close( + ootf_inverse_BT2100_HLG_1(xp_as_array(a, xp=xp)), + [ + [0.1, 0.0, -0.1], + [-0.1, -0.1, -0.1], + [0.1, 0.1, 0.1], + [0.1, -0.1, 0.1], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ootf_inverse_BT2100_HLG_1(self) -> None: + def test_n_dimensional_ootf_inverse_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG_1` definition n-dimensional arrays support. """ F_D = 63.095734448019336 - E = ootf_inverse_BT2100_HLG_1(F_D) + E = as_ndarray(ootf_inverse_BT2100_HLG_1(xp_as_array(F_D, xp=xp))) - F_D = np.tile(F_D, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (6, 1)) - E = np.reshape(E, (6, 1)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (6, 1), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.array([213.01897444, 426.03794887, 639.05692331]) + F_D = xp_as_array([213.01897444, 426.03794887, 639.05692331], xp=xp) E = np.array([0.25, 0.50, 0.75]) - np.testing.assert_allclose( + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.tile(F_D, (6, 1)) - E = np.tile(E, (6, 1)) - np.testing.assert_allclose( + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + E = xp.tile(xp_as_array(E, xp=xp), (6, 1)) + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (2, 3, 3)) - E = np.reshape(E, (2, 3, 3)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_1(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_ootf_inverse_BT2100_HLG_1(self) -> None: + def test_domain_range_scale_ootf_inverse_BT2100_HLG_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG_1` definition domain and range scale support. """ F_D = 63.095734448019336 - E = ootf_inverse_BT2100_HLG_1(F_D) + E = as_ndarray(ootf_inverse_BT2100_HLG_1(xp_as_array(F_D, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_1(F_D * factor), + xp_assert_close( + ootf_inverse_BT2100_HLG_1(xp_as_array(F_D * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1636,30 +1614,34 @@ class TestOotf_inverse_BT2100_HLG_2: ootf_inverse_BT2100_HLG_2` definition unit tests methods. """ - def test_ootf_inverse_BT2100_HLG_2(self) -> None: + def test_ootf_inverse_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG_2` definition. """ - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_2(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + ootf_inverse_BT2100_HLG_2(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_2(63.095734448019336), + xp_assert_close( + ootf_inverse_BT2100_HLG_2(xp_as_array(63.095734448019336, xp=xp)), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_2(1000.0), + xp_assert_close( + ootf_inverse_BT2100_HLG_2(xp_as_array(1000.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_2(398.107170553497380, 10000, 1.4), + xp_assert_close( + ootf_inverse_BT2100_HLG_2( + xp_as_array(398.107170553497380, xp=xp), 10000, 1.4 + ), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1676,84 +1658,82 @@ def test_ootf_inverse_BT2100_HLG_2(self) -> None: [51.320396090100672, -51.320396090100672, 51.320396090100672], ] ) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_2(a), - np.array( - [ - [0.1, 0.0, -0.1], - [-0.1, -0.1, -0.1], - [0.1, 0.1, 0.1], - [0.1, -0.1, 0.1], - ] - ), + xp_assert_close( + ootf_inverse_BT2100_HLG_2(xp_as_array(a, xp=xp)), + [ + [0.1, 0.0, -0.1], + [-0.1, -0.1, -0.1], + [0.1, 0.1, 0.1], + [0.1, -0.1, 0.1], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ootf_inverse_BT2100_HLG_2(self) -> None: + def test_n_dimensional_ootf_inverse_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG_2` definition n-dimensional arrays support. """ F_D = 63.095734448019336 - E = ootf_inverse_BT2100_HLG_2(F_D) + E = as_ndarray(ootf_inverse_BT2100_HLG_2(xp_as_array(F_D, xp=xp))) - F_D = np.tile(F_D, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (6, 1)) - E = np.reshape(E, (6, 1)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (6, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (6, 1), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.array([213.01897444, 426.03794887, 639.05692331]) + F_D = xp_as_array([213.01897444, 426.03794887, 639.05692331], xp=xp) E = np.array([0.25, 0.50, 0.75]) - np.testing.assert_allclose( + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.tile(F_D, (6, 1)) - E = np.tile(E, (6, 1)) - np.testing.assert_allclose( + F_D = xp.tile(xp_as_array(F_D, xp=xp), (6, 1)) + E = xp.tile(xp_as_array(E, xp=xp), (6, 1)) + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - F_D = np.reshape(F_D, (2, 3, 3)) - E = np.reshape(E, (2, 3, 3)) - np.testing.assert_allclose( + F_D = xp_reshape(xp_as_array(F_D, xp=xp), (2, 3, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( ootf_inverse_BT2100_HLG_2(F_D), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_ootf_inverse_BT2100_HLG_2(self) -> None: + def test_domain_range_scale_ootf_inverse_BT2100_HLG_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG_2` definition domain and range scale support. """ F_D = 63.095734448019336 - E = ootf_inverse_BT2100_HLG_2(F_D) + E = as_ndarray(ootf_inverse_BT2100_HLG_2(xp_as_array(F_D, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG_2(F_D * factor), + xp_assert_close( + ootf_inverse_BT2100_HLG_2(xp_as_array(F_D * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1774,29 +1754,29 @@ class TestOotfBT2100HLG: ootf_BT2100_HLG` definition unit tests methods. """ - def test_ootf_BT2100_HLG(self) -> None: + def test_ootf_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_BT2100_HLG` definition. """ # Test default method (ITU-R BT.2100-2) - np.testing.assert_allclose( - ootf_BT2100_HLG(0.1), + xp_assert_close( + ootf_BT2100_HLG(xp_as_array(0.1, xp=xp)), 63.095734448019336, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test ITU-R BT.2100-1 method - np.testing.assert_allclose( - ootf_BT2100_HLG(0.1, 0.01, method="ITU-R BT.2100-1"), + xp_assert_close( + ootf_BT2100_HLG(xp_as_array(0.1, xp=xp), 0.01, method="ITU-R BT.2100-1"), 63.105103490674857, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test with different L_W value - np.testing.assert_allclose( - ootf_BT2100_HLG(0.1, L_W=2000), + xp_assert_close( + ootf_BT2100_HLG(xp_as_array(0.1, xp=xp), L_W=2000), 94.3186112317, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1808,29 +1788,31 @@ class TestOotfInverseBT2100HLG: ootf_inverse_BT2100_HLG` definition unit tests methods. """ - def test_ootf_inverse_BT2100_HLG(self) -> None: + def test_ootf_inverse_BT2100_HLG(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_2100.\ ootf_inverse_BT2100_HLG` definition. """ # Test default method (ITU-R BT.2100-2) - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG(63.095734448019336), + xp_assert_close( + ootf_inverse_BT2100_HLG(xp_as_array(63.095734448019336, xp=xp)), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test ITU-R BT.2100-1 method - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG(63.105103490674857, 0.01, method="ITU-R BT.2100-1"), + xp_assert_close( + ootf_inverse_BT2100_HLG( + xp_as_array(63.105103490674857, xp=xp), 0.01, method="ITU-R BT.2100-1" + ), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test with different L_W value - np.testing.assert_allclose( - ootf_inverse_BT2100_HLG(94.3186112317, L_W=2000), + xp_assert_close( + ootf_inverse_BT2100_HLG(xp_as_array(94.3186112317, xp=xp), L_W=2000), 0.1, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itur_bt_601.py b/colour/models/rgb/transfer_functions/tests/test_itur_bt_601.py index cff4c5961b..d1a58b6aed 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itur_bt_601.py +++ b/colour/models/rgb/transfer_functions/tests/test_itur_bt_601.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.itur_bt_601` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import oetf_BT601, oetf_inverse_BT601 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,59 +42,67 @@ class TestOetf_BT601: definition unit tests methods. """ - def test_oetf_BT601(self) -> None: + def test_oetf_BT601(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_601.\ oetf_BT601` definition. """ - np.testing.assert_allclose(oetf_BT601(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT601(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - oetf_BT601(0.015), 0.067500000000000, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT601(xp_as_array(0.015, xp=xp)), + 0.067500000000000, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT601(0.18), 0.409007728864150, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT601(xp_as_array(0.18, xp=xp)), + 0.409007728864150, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(oetf_BT601(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT601(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - def test_n_dimensional_oetf_BT601(self) -> None: + def test_n_dimensional_oetf_BT601(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_601.\ oetf_BT601` definition n-dimensional arrays support. """ L = 0.18 - E = oetf_BT601(L) + E = as_ndarray(oetf_BT601(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - E = np.tile(E, 6) - np.testing.assert_allclose(oetf_BT601(L), E, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(oetf_BT601(L), E, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose(oetf_BT601(L), E, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_BT601(L), E, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose(oetf_BT601(L), E, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_BT601(L), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_BT601(self) -> None: + def test_domain_range_scale_oetf_BT601(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_601.\ oetf_BT601` definition domain and range scale support. """ L = 0.18 - E = oetf_BT601(L) + E = as_ndarray(oetf_BT601(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BT601(L * factor), + xp_assert_close( + oetf_BT601(xp_as_array(L * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -101,73 +123,71 @@ class TestOetf_inverse_BT601: oetf_inverse_BT601` definition unit tests methods. """ - def test_oetf_inverse_BT601(self) -> None: + def test_oetf_inverse_BT601(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_601.\ oetf_inverse_BT601` definition. """ - np.testing.assert_allclose( - oetf_inverse_BT601(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT601(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT601(0.067500000000000), + xp_assert_close( + oetf_inverse_BT601(xp_as_array(0.067500000000000, xp=xp)), 0.015, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT601(0.409007728864150), + xp_assert_close( + oetf_inverse_BT601(xp_as_array(0.409007728864150, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT601(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT601(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BT601(self) -> None: + def test_n_dimensional_oetf_inverse_BT601(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_601.\ oetf_inverse_BT601` definition n-dimensional arrays support. """ E = 0.409007728864150 - L = oetf_inverse_BT601(E) + L = as_ndarray(oetf_inverse_BT601(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( - oetf_inverse_BT601(E), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(oetf_inverse_BT601(E), L, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_BT601(E), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_BT601(E), L, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_BT601(E), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_BT601(E), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_BT601(self) -> None: + def test_domain_range_scale_oetf_inverse_BT601(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_601.\ oetf_inverse_BT601` definition domain and range scale support. """ E = 0.409007728864150 - L = oetf_inverse_BT601(E) + L = as_ndarray(oetf_inverse_BT601(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BT601(E * factor), + xp_assert_close( + oetf_inverse_BT601(xp_as_array(E * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itur_bt_709.py b/colour/models/rgb/transfer_functions/tests/test_itur_bt_709.py index 12b628682e..56ad376fc3 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itur_bt_709.py +++ b/colour/models/rgb/transfer_functions/tests/test_itur_bt_709.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.itur_bt_709` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import oetf_BT709, oetf_inverse_BT709 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,59 +42,67 @@ class TestOetf_BT709: definition unit tests methods. """ - def test_oetf_BT709(self) -> None: + def test_oetf_BT709(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_709.\ oetf_BT709` definition. """ - np.testing.assert_allclose(oetf_BT709(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT709(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - oetf_BT709(0.015), 0.067500000000000, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT709(xp_as_array(0.015, xp=xp)), + 0.067500000000000, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_BT709(0.18), 0.409007728864150, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_BT709(xp_as_array(0.18, xp=xp)), + 0.409007728864150, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(oetf_BT709(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + oetf_BT709(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - def test_n_dimensional_oetf_BT709(self) -> None: + def test_n_dimensional_oetf_BT709(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_709.\ oetf_BT709` definition n-dimensional arrays support. """ L = 0.18 - V = oetf_BT709(L) + V = as_ndarray(oetf_BT709(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - V = np.tile(V, 6) - np.testing.assert_allclose(oetf_BT709(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + xp_assert_close(oetf_BT709(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - V = np.reshape(V, (2, 3)) - np.testing.assert_allclose(oetf_BT709(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_BT709(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - V = np.reshape(V, (2, 3, 1)) - np.testing.assert_allclose(oetf_BT709(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_BT709(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_BT709(self) -> None: + def test_domain_range_scale_oetf_BT709(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_709.\ oetf_BT709` definition domain and range scale support. """ L = 0.18 - V = oetf_BT709(L) + V = as_ndarray(oetf_BT709(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_BT709(L * factor), + xp_assert_close( + oetf_BT709(xp_as_array(L * factor, xp=xp)), V * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -101,73 +123,71 @@ class TestOetf_inverse_BT709: oetf_inverse_BT709` definition unit tests methods. """ - def test_oetf_inverse_BT709(self) -> None: + def test_oetf_inverse_BT709(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_709.\ oetf_inverse_BT709` definition. """ - np.testing.assert_allclose( - oetf_inverse_BT709(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT709(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT709(0.067500000000000), + xp_assert_close( + oetf_inverse_BT709(xp_as_array(0.067500000000000, xp=xp)), 0.015, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT709(0.409007728864150), + xp_assert_close( + oetf_inverse_BT709(xp_as_array(0.409007728864150, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_BT709(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_BT709(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_BT709(self) -> None: + def test_n_dimensional_oetf_inverse_BT709(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_709.\ oetf_inverse_BT709` definition n-dimensional arrays support. """ V = 0.409007728864150 - L = oetf_inverse_BT709(V) + L = as_ndarray(oetf_inverse_BT709(xp_as_array(V, xp=xp))) - V = np.tile(V, 6) - L = np.tile(L, 6) - np.testing.assert_allclose( - oetf_inverse_BT709(V), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(oetf_inverse_BT709(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_BT709(V), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_BT709(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_BT709(V), L, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_BT709(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_BT709(self) -> None: + def test_domain_range_scale_oetf_inverse_BT709(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itur_bt_709.\ oetf_inverse_BT709` definition domain and range scale support. """ V = 0.409007728864150 - L = oetf_inverse_BT709(V) + L = as_ndarray(oetf_inverse_BT709(xp_as_array(V, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_BT709(V * factor), + xp_assert_close( + oetf_inverse_BT709(xp_as_array(V * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_itut_h_273.py b/colour/models/rgb/transfer_functions/tests/test_itut_h_273.py index 1d825e96c3..f13b2b49bb 100644 --- a/colour/models/rgb/transfer_functions/tests/test_itut_h_273.py +++ b/colour/models/rgb/transfer_functions/tests/test_itut_h_273.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.itut_h_273` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -16,7 +20,17 @@ oetf_inverse_H273_Log, oetf_inverse_H273_LogSqrt, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -43,61 +57,65 @@ class TestOetf_H273_Log: oetf_H273_Log` definition unit tests methods. """ - def test_oetf_H273_Log(self) -> None: + def test_oetf_H273_Log(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_Log` definition. """ - np.testing.assert_allclose( - oetf_H273_Log(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_H273_Log(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_Log(0.18), + xp_assert_close( + oetf_H273_Log(xp_as_array(0.18, xp=xp)), 0.627636252551653, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_Log(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_H273_Log(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_H273_Log(self) -> None: + def test_n_dimensional_oetf_H273_Log(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_Log` definition n-dimensional arrays support. """ E = 0.18 - E_p = oetf_H273_Log(E) + E_p = as_ndarray(oetf_H273_Log(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose(oetf_H273_Log(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_H273_Log(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose(oetf_H273_Log(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_H273_Log(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose(oetf_H273_Log(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_H273_Log(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_H273_Log(self) -> None: + def test_domain_range_scale_oetf_H273_Log(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_Log` definition domain and range scale support. """ E = 0.18 - E_p = oetf_H273_Log(E) + E_p = as_ndarray(oetf_H273_Log(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_H273_Log(E * factor), + xp_assert_close( + oetf_H273_Log(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -118,7 +136,7 @@ class TestOetf_inverse_H273_Log: oetf_inverse_H273_Log` definition unit tests methods. """ - def test_oetf_inverse_H273_Log(self) -> None: + def test_oetf_inverse_H273_Log(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_Log` definition. @@ -126,61 +144,59 @@ def test_oetf_inverse_H273_Log(self) -> None: # NOTE: The function is unfortunately clamped and cannot roundtrip # properly. - np.testing.assert_allclose( - oetf_inverse_H273_Log(0.0), 0.01, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_H273_Log(xp_as_array(0.0, xp=xp)), + 0.01, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_Log(0.627636252551653), + xp_assert_close( + oetf_inverse_H273_Log(xp_as_array(0.627636252551653, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_Log(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_H273_Log(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_H273_Log(self) -> None: + def test_n_dimensional_oetf_inverse_H273_Log(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_Log` definition n-dimensional arrays support. """ E_p = 0.627636252551653 - E = oetf_inverse_H273_Log(E_p) + E = as_ndarray(oetf_inverse_H273_Log(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - oetf_inverse_H273_Log(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(oetf_inverse_H273_Log(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_H273_Log(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_inverse_H273_Log(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_H273_Log(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_inverse_H273_Log(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_inverse_H273_Log(self) -> None: + def test_domain_range_scale_oetf_inverse_H273_Log(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_Log` definition domain and range scale support. """ E_p = 0.627636252551653 - E = oetf_inverse_H273_Log(E_p) + E = as_ndarray(oetf_inverse_H273_Log(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_H273_Log(E_p * factor), + xp_assert_close( + oetf_inverse_H273_Log(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -201,67 +217,65 @@ class TestOetf_H273_LogSqrt: oetf_H273_LogSqrt` definition unit tests methods. """ - def test_oetf_H273_LogSqrt(self) -> None: + def test_oetf_H273_LogSqrt(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_LogSqrt` definition. """ - np.testing.assert_allclose( - oetf_H273_LogSqrt(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_H273_LogSqrt(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_LogSqrt(0.18), + xp_assert_close( + oetf_H273_LogSqrt(xp_as_array(0.18, xp=xp)), 0.702109002041322, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_LogSqrt(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_H273_LogSqrt(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_H273_LogSqrt(self) -> None: + def test_n_dimensional_oetf_H273_LogSqrt(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_LogSqrt` definition n-dimensional arrays support. """ E = 0.18 - E_p = oetf_H273_LogSqrt(E) + E_p = as_ndarray(oetf_H273_LogSqrt(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - oetf_H273_LogSqrt(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_H273_LogSqrt(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - oetf_H273_LogSqrt(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_H273_LogSqrt(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - oetf_H273_LogSqrt(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_H273_LogSqrt(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_H273_LogSqrt(self) -> None: + def test_domain_range_scale_oetf_H273_LogSqrt(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_LogSqrt` definition domain and range scale support. """ E = 0.18 - E_p = oetf_H273_LogSqrt(E) + E_p = as_ndarray(oetf_H273_LogSqrt(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_H273_LogSqrt(E * factor), + xp_assert_close( + oetf_H273_LogSqrt(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -282,7 +296,7 @@ class TestOetf_inverse_H273_LogSqrt: oetf_inverse_H273_LogSqrt` definition unit tests methods. """ - def test_oetf_inverse_H273_LogSqrt(self) -> None: + def test_oetf_inverse_H273_LogSqrt(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_LogSqrt` definition. @@ -290,63 +304,65 @@ def test_oetf_inverse_H273_LogSqrt(self) -> None: # NOTE: The function is unfortunately clamped and cannot roundtrip # properly. - np.testing.assert_allclose( - oetf_inverse_H273_LogSqrt(0.0), + xp_assert_close( + oetf_inverse_H273_LogSqrt(xp_as_array(0.0, xp=xp)), 0.003162277660168, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_LogSqrt(0.702109002041322), + xp_assert_close( + oetf_inverse_H273_LogSqrt(xp_as_array(0.702109002041322, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_LogSqrt(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_inverse_H273_LogSqrt(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_H273_LogSqrt(self) -> None: + def test_n_dimensional_oetf_inverse_H273_LogSqrt(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_LogSqrt` definition n-dimensional arrays support. """ E_p = 0.702109002041322 - E = oetf_inverse_H273_LogSqrt(E_p) + E = as_ndarray(oetf_inverse_H273_LogSqrt(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close( oetf_inverse_H273_LogSqrt(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close( oetf_inverse_H273_LogSqrt(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( oetf_inverse_H273_LogSqrt(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_oetf_inverse_H273_LogSqrt(self) -> None: + def test_domain_range_scale_oetf_inverse_H273_LogSqrt(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_LogSqrt` definition domain and range scale support. """ E_p = 0.702109002041322 - E = oetf_inverse_H273_LogSqrt(E_p) + E = as_ndarray(oetf_inverse_H273_LogSqrt(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_H273_LogSqrt(E_p * factor), + xp_assert_close( + oetf_inverse_H273_LogSqrt(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -367,73 +383,71 @@ class TestOetf_H273_IEC61966_2: oetf_H273_IEC61966_2` definition unit tests methods. """ - def test_oetf_H273_IEC61966_2(self) -> None: + def test_oetf_H273_IEC61966_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_IEC61966_2` definition. """ - np.testing.assert_allclose( - oetf_H273_IEC61966_2(-0.18), + xp_assert_close( + oetf_H273_IEC61966_2(xp_as_array(-0.18, xp=xp)), -0.461356129500442, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_IEC61966_2(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_H273_IEC61966_2(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_IEC61966_2(0.18), + xp_assert_close( + oetf_H273_IEC61966_2(xp_as_array(0.18, xp=xp)), 0.461356129500442, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_H273_IEC61966_2(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_H273_IEC61966_2(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_H273_IEC61966_2(self) -> None: + def test_n_dimensional_oetf_H273_IEC61966_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_IEC61966_2` definition n-dimensional arrays support. """ E = 0.18 - E_p = oetf_H273_IEC61966_2(E) + E_p = as_ndarray(oetf_H273_IEC61966_2(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( - oetf_H273_IEC61966_2(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close(oetf_H273_IEC61966_2(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( - oetf_H273_IEC61966_2(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_H273_IEC61966_2(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( - oetf_H273_IEC61966_2(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_H273_IEC61966_2(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_H273_IEC61966_2(self) -> None: + def test_domain_range_scale_oetf_H273_IEC61966_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_H273_IEC61966_2` definition domain and range scale support. """ E = 0.18 - E_p = oetf_H273_IEC61966_2(E) + E_p = as_ndarray(oetf_H273_IEC61966_2(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_H273_IEC61966_2(E * factor), + xp_assert_close( + oetf_H273_IEC61966_2(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -454,77 +468,85 @@ class TestOetf_inverse_H273_IEC61966_2: oetf_inverse_H273_IEC61966_2` definition unit tests methods. """ - def test_oetf_inverse_H273_IEC61966_2(self) -> None: + def test_oetf_inverse_H273_IEC61966_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_IEC61966_2` definition. """ - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(-0.461356129500442), + xp_assert_close( + oetf_inverse_H273_IEC61966_2(xp_as_array(-0.461356129500442, xp=xp)), -0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(0.0), + xp_assert_close( + oetf_inverse_H273_IEC61966_2(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(0.461356129500442), + xp_assert_close( + oetf_inverse_H273_IEC61966_2(xp_as_array(0.461356129500442, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(1.0), + xp_assert_close( + oetf_inverse_H273_IEC61966_2(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_inverse_H273_IEC61966_2(self) -> None: + def test_n_dimensional_oetf_inverse_H273_IEC61966_2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_IEC61966_2` definition n-dimensional arrays support. """ E_p = 0.627636252551653 - E = oetf_inverse_H273_IEC61966_2(E_p) + E = as_ndarray(oetf_inverse_H273_IEC61966_2(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close( + oetf_inverse_H273_IEC61966_2(E_p), + E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close( + oetf_inverse_H273_IEC61966_2(E_p), + E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + oetf_inverse_H273_IEC61966_2(E_p), + E, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_oetf_inverse_H273_IEC61966_2(self) -> None: + def test_domain_range_scale_oetf_inverse_H273_IEC61966_2( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ oetf_inverse_H273_IEC61966_2` definition domain and range scale support. """ E_p = 0.627636252551653 - E = oetf_inverse_H273_IEC61966_2(E_p) + E = as_ndarray(oetf_inverse_H273_IEC61966_2(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_inverse_H273_IEC61966_2(E_p * factor), + xp_assert_close( + oetf_inverse_H273_IEC61966_2(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -547,69 +569,71 @@ class TestEotf_inverse_H273_ST428_1: eotf_inverse_H273_ST428_1` definition unit tests methods. """ - def test_eotf_inverse_H273_ST428_1(self) -> None: + def test_eotf_inverse_H273_ST428_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ eotf_inverse_H273_ST428_1` definition. """ - np.testing.assert_allclose( - eotf_inverse_H273_ST428_1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_H273_ST428_1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_H273_ST428_1(0.18), + xp_assert_close( + eotf_inverse_H273_ST428_1(xp_as_array(0.18, xp=xp)), 0.500048337717236, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_H273_ST428_1(1.0), + xp_assert_close( + eotf_inverse_H273_ST428_1(xp_as_array(1.0, xp=xp)), 0.967042675317934, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_H273_ST428_1(self) -> None: + def test_n_dimensional_eotf_inverse_H273_ST428_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ eotf_inverse_H273_ST428_1` definition n-dimensional arrays support. """ E = 0.18 - E_p = eotf_inverse_H273_ST428_1(E) + E_p = as_ndarray(eotf_inverse_H273_ST428_1(xp_as_array(E, xp=xp))) - E = np.tile(E, 6) - E_p = np.tile(E_p, 6) - np.testing.assert_allclose( + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + xp_assert_close( eotf_inverse_H273_ST428_1(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS ) - E = np.reshape(E, (2, 3)) - E_p = np.reshape(E_p, (2, 3)) - np.testing.assert_allclose( + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( eotf_inverse_H273_ST428_1(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS ) - E = np.reshape(E, (2, 3, 1)) - E_p = np.reshape(E_p, (2, 3, 1)) - np.testing.assert_allclose( + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( eotf_inverse_H273_ST428_1(E), E_p, atol=TOLERANCE_ABSOLUTE_TESTS ) - def test_domain_range_scale_eotf_inverse_H273_ST428_1(self) -> None: + def test_domain_range_scale_eotf_inverse_H273_ST428_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ eotf_inverse_H273_ST428_1` definition domain and range scale support. """ E = 0.18 - E_p = eotf_inverse_H273_ST428_1(E) + E_p = as_ndarray(eotf_inverse_H273_ST428_1(xp_as_array(E, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_H273_ST428_1(E * factor), + xp_assert_close( + eotf_inverse_H273_ST428_1(xp_as_array(E * factor, xp=xp)), E_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -630,69 +654,65 @@ class TestEotf_H273_ST428_1: eotf_H273_ST428_1` definition unit tests methods. """ - def test_eotf_H273_ST428_1(self) -> None: + def test_eotf_H273_ST428_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ eotf_H273_ST428_1` definition. """ - np.testing.assert_allclose( - eotf_H273_ST428_1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_H273_ST428_1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_H273_ST428_1(0.500048337717236), + xp_assert_close( + eotf_H273_ST428_1(xp_as_array(0.500048337717236, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_H273_ST428_1(0.967042675317934), + xp_assert_close( + eotf_H273_ST428_1(xp_as_array(0.967042675317934, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_H273_ST428_1(self) -> None: + def test_n_dimensional_eotf_H273_ST428_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ eotf_H273_ST428_1` definition n-dimensional arrays support. """ E_p = 0.500048337717236 - E = eotf_H273_ST428_1(E_p) + E = as_ndarray(eotf_H273_ST428_1(xp_as_array(E_p, xp=xp))) - E_p = np.tile(E_p, 6) - E = np.tile(E, 6) - np.testing.assert_allclose( - eotf_H273_ST428_1(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp.tile(xp_as_array(E_p, xp=xp), (6,)) + E = xp.tile(xp_as_array(E, xp=xp), (6,)) + xp_assert_close(eotf_H273_ST428_1(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3)) - E = np.reshape(E, (2, 3)) - np.testing.assert_allclose( - eotf_H273_ST428_1(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_H273_ST428_1(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - E_p = np.reshape(E_p, (2, 3, 1)) - E = np.reshape(E, (2, 3, 1)) - np.testing.assert_allclose( - eotf_H273_ST428_1(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS - ) + E_p = xp_reshape(xp_as_array(E_p, xp=xp), (2, 3, 1), xp=xp) + E = xp_reshape(xp_as_array(E, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_H273_ST428_1(E_p), E, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_H273_ST428_1(self) -> None: + def test_domain_range_scale_eotf_H273_ST428_1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.itut_h_273.\ eotf_H273_ST428_1` definition domain and range scale support. """ E_p = 0.500048337717236 - E = eotf_H273_ST428_1(E_p) + E = as_ndarray(eotf_H273_ST428_1(xp_as_array(E_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_H273_ST428_1(E_p * factor), + xp_assert_close( + eotf_H273_ST428_1(xp_as_array(E_p * factor, xp=xp)), E * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_leica_l_log.py b/colour/models/rgb/transfer_functions/tests/test_leica_l_log.py index d74ffece3d..a00215453a 100644 --- a/colour/models/rgb/transfer_functions/tests/test_leica_l_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_leica_l_log.py @@ -3,11 +3,25 @@ leica_l_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import log_decoding_LLog, log_encoding_LLog -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,89 +42,83 @@ class TestLogEncoding_LLog: log_encoding_LLog` definition unit tests methods. """ - def test_log_encoding_LLog(self) -> None: + def test_log_encoding_LLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.leica_l_log.\ log_encoding_LLog` definition. """ - np.testing.assert_allclose( - log_encoding_LLog(0.0), + xp_assert_close( + log_encoding_LLog(xp_as_array(0.0, xp=xp)), 0.089999999999999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_LLog(0.18), + xp_assert_close( + log_encoding_LLog(xp_as_array(0.18, xp=xp)), 0.435313904043927, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_LLog(0.18, 12), + xp_assert_close( + log_encoding_LLog(xp_as_array(0.18, xp=xp), 12), 0.435313904043927, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_LLog(0.18, 10, False), + xp_assert_close( + log_encoding_LLog(xp_as_array(0.18, xp=xp), 10, False), 0.4353037943344028, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_LLog(0.18, 10, False, False), + xp_assert_close( + log_encoding_LLog(xp_as_array(0.18, xp=xp), 10, False, False), 0.421586960452824, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_LLog(1.0), + xp_assert_close( + log_encoding_LLog(xp_as_array(1.0, xp=xp)), 0.631797439630121, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_LLog(self) -> None: + def test_n_dimensional_log_encoding_LLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.leica_l_log.\ log_encoding_LLog` definition n-dimensional arrays support. """ y = 0.18 - x = log_encoding_LLog(y) + x = as_ndarray(log_encoding_LLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_encoding_LLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_encoding_LLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_encoding_LLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_LLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_LLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_LLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_LLog(self) -> None: + def test_domain_range_scale_log_encoding_LLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.leica_l_log.\ log_encoding_LLog` definition domain and range scale support. """ y = 0.18 - x = log_encoding_LLog(y) + x = as_ndarray(log_encoding_LLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_LLog(y * factor), + xp_assert_close( + log_encoding_LLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -131,89 +139,83 @@ class TestLogDecoding_LLog: log_decoding_LLog` definition unit tests methods. """ - def test_log_decoding_LLog(self) -> None: + def test_log_decoding_LLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.leica_l_log.\ log_decoding_LLog` definition. """ - np.testing.assert_allclose( - log_decoding_LLog(0.089999999999999), + xp_assert_close( + log_decoding_LLog(xp_as_array(0.089999999999999, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_LLog(0.435313904043927), + xp_assert_close( + log_decoding_LLog(xp_as_array(0.435313904043927, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_LLog(0.435313904043927, 12), + xp_assert_close( + log_decoding_LLog(xp_as_array(0.435313904043927, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_LLog(0.4353037943344028, 10, False), + xp_assert_close( + log_decoding_LLog(xp_as_array(0.4353037943344028, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_LLog(0.421586960452824, 10, False, False), + xp_assert_close( + log_decoding_LLog(xp_as_array(0.421586960452824, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_LLog(0.631797439630121), + xp_assert_close( + log_decoding_LLog(xp_as_array(0.631797439630121, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_LLog(self) -> None: + def test_n_dimensional_log_decoding_LLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.leica_l_log.\ log_decoding_LLog` definition n-dimensional arrays support. """ x = 0.435313904043927 - y = log_decoding_LLog(x) + y = as_ndarray(log_decoding_LLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_decoding_LLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_decoding_LLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_decoding_LLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_LLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_LLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_LLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_LLog(self) -> None: + def test_domain_range_scale_log_decoding_LLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.leica_l_log.\ log_decoding_LLog` definition domain and range scale support. """ x = 0.435313904043927 - y = log_decoding_LLog(x) + y = as_ndarray(log_decoding_LLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_LLog(x * factor), + xp_assert_close( + log_decoding_LLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_linear.py b/colour/models/rgb/transfer_functions/tests/test_linear.py index 8c828ebe8e..4a93244eed 100644 --- a/colour/models/rgb/transfer_functions/tests/test_linear.py +++ b/colour/models/rgb/transfer_functions/tests/test_linear.py @@ -3,11 +3,24 @@ :mod:`colour.models.rgb.transfer_functions.linear` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import linear_function -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -27,44 +40,38 @@ class TestLinearFunction: linear_function` definition unit tests methods. """ - def test_linear_function(self) -> None: + def test_linear_function(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.linear.\ linear_function` definition. """ - assert linear_function(0.0) == 0.0 + assert as_ndarray(linear_function(xp_as_array(0.0, xp=xp))) == 0.0 - assert linear_function(0.18) == 0.18 + assert as_ndarray(linear_function(xp_as_array(0.18, xp=xp))) == 0.18 - assert linear_function(1.0) == 1.0 + assert as_ndarray(linear_function(xp_as_array(1.0, xp=xp))) == 1.0 - def test_n_dimensional_linear_function(self) -> None: + def test_n_dimensional_linear_function(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.linear.\ linear_function` definition n-dimensional arrays support. """ a = 0.18 - a_p = linear_function(a) - - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( - linear_function(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) - - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( - linear_function(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) - - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( - linear_function(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + a_p = as_ndarray(linear_function(xp_as_array(a, xp=xp))) + + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close(linear_function(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS) + + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(linear_function(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS) + + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(linear_function(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_linear_function(self) -> None: diff --git a/colour/models/rgb/transfer_functions/tests/test_log.py b/colour/models/rgb/transfer_functions/tests/test_log.py index ceab43a171..88307bb8dc 100644 --- a/colour/models/rgb/transfer_functions/tests/test_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_log.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -13,7 +17,16 @@ logarithmic_function_camera, logarithmic_function_quasilog, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -37,49 +50,55 @@ class TestLogarithmFunction_Basic: logarithmic_function_basic` definition unit tests methods. """ - def test_logarithmic_function_basic(self) -> None: + def test_logarithmic_function_basic(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ logarithmic_function_basic` definition. """ - np.testing.assert_allclose( - logarithmic_function_basic(0.18), + xp_assert_close( + logarithmic_function_basic(xp_as_array(0.18, xp=xp)), -2.473931188332412, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_basic(-2.473931188332412, "antiLog2"), + xp_assert_close( + logarithmic_function_basic( + xp_as_array(-2.473931188332412, xp=xp), "antiLog2" + ), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_basic(0.18, "log10"), + xp_assert_close( + logarithmic_function_basic(xp_as_array(0.18, xp=xp), "log10"), -0.744727494896694, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_basic(-0.744727494896694, "antiLog10"), + xp_assert_close( + logarithmic_function_basic( + xp_as_array(-0.744727494896694, xp=xp), "antiLog10" + ), 0.179999999999999, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_basic(0.18, "logB", 3), + xp_assert_close( + logarithmic_function_basic(xp_as_array(0.18, xp=xp), "logB", 3), -1.560876795007312, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_basic(-1.560876795007312, "antiLogB", 3), + xp_assert_close( + logarithmic_function_basic( + xp_as_array(-1.560876795007312, xp=xp), "antiLogB", 3 + ), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_logarithmic_function_basic(self) -> None: + def test_n_dimensional_logarithmic_function_basic(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ logarithmic_function_basic` definition n-dimensional arrays support. @@ -89,27 +108,27 @@ def test_n_dimensional_logarithmic_function_basic(self) -> None: for style in styles: a = 0.18 - a_p = logarithmic_function_basic(a, style) + a_p = as_ndarray(logarithmic_function_basic(xp_as_array(a, xp=xp), style)) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( logarithmic_function_basic(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( logarithmic_function_basic(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( logarithmic_function_basic(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -134,55 +153,65 @@ class TestLogarithmFunction_Quasilog: logarithmic_function_quasilog` definition unit tests methods. """ - def test_logarithmic_function_quasilog(self) -> None: + def test_logarithmic_function_quasilog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ logarithmic_function_quasilog` definition. """ - np.testing.assert_allclose( - logarithmic_function_quasilog(0.18), + xp_assert_close( + logarithmic_function_quasilog(xp_as_array(0.18, xp=xp)), -2.473931188332412, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(-2.473931188332412, "logToLin"), + xp_assert_close( + logarithmic_function_quasilog( + xp_as_array(-2.473931188332412, xp=xp), "logToLin" + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(0.18, "linToLog", 10), + xp_assert_close( + logarithmic_function_quasilog(xp_as_array(0.18, xp=xp), "linToLog", 10), -0.744727494896694, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(-0.744727494896694, "logToLin", 10), + xp_assert_close( + logarithmic_function_quasilog( + xp_as_array(-0.744727494896694, xp=xp), "logToLin", 10 + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(0.18, "linToLog", 10, 0.75), + xp_assert_close( + logarithmic_function_quasilog( + xp_as_array(0.18, xp=xp), "linToLog", 10, 0.75 + ), -0.558545621172520, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(-0.558545621172520, "logToLin", 10, 0.75), + xp_assert_close( + logarithmic_function_quasilog( + xp_as_array(-0.558545621172520, xp=xp), "logToLin", 10, 0.75 + ), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(0.18, "linToLog", 10, 0.75, 0.75), + xp_assert_close( + logarithmic_function_quasilog( + xp_as_array(0.18, xp=xp), "linToLog", 10, 0.75, 0.75 + ), -0.652249673628745, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_quasilog( -0.652249673628745, "logToLin", 10, 0.75, 0.75 ), @@ -190,13 +219,15 @@ def test_logarithmic_function_quasilog(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_quasilog(0.18, "linToLog", 10, 0.75, 0.75, 0.001), + xp_assert_close( + logarithmic_function_quasilog( + xp_as_array(0.18, xp=xp), "linToLog", 10, 0.75, 0.75, 0.001 + ), -0.651249673628745, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_quasilog( -0.651249673628745, "logToLin", 10, 0.75, 0.75, 0.001 ), @@ -204,7 +235,7 @@ def test_logarithmic_function_quasilog(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_quasilog( 0.18, "linToLog", 10, 0.75, 0.75, 0.001, 0.01 ), @@ -212,7 +243,7 @@ def test_logarithmic_function_quasilog(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_quasilog( -0.627973998323769, "logToLin", 10, 0.75, 0.75, 0.001, 0.01 ), @@ -220,7 +251,7 @@ def test_logarithmic_function_quasilog(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_logarithmic_function_quasilog(self) -> None: + def test_n_dimensional_logarithmic_function_quasilog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ logarithmic_function_quasilog` definition n-dimensional arrays support. @@ -230,27 +261,29 @@ def test_n_dimensional_logarithmic_function_quasilog(self) -> None: for style in styles: a = 0.18 - a_p = logarithmic_function_quasilog(a, style) + a_p = as_ndarray( + logarithmic_function_quasilog(xp_as_array(a, xp=xp), style) + ) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( logarithmic_function_quasilog(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( logarithmic_function_quasilog(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( logarithmic_function_quasilog(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -275,79 +308,91 @@ class TestLogarithmFunction_Camera: logarithmic_function_camera` definition unit tests methods. """ - def test_logarithmic_function_camera(self) -> None: + def test_logarithmic_function_camera(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ logarithmic_function_camera` definition. """ - np.testing.assert_allclose( - logarithmic_function_camera(0, "cameraLinToLog"), + xp_assert_close( + logarithmic_function_camera(xp_as_array(0, xp=xp), "cameraLinToLog"), -9.08655123066369, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(-9.08655123066369, "cameraLogToLin"), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(-9.08655123066369, xp=xp), "cameraLogToLin" + ), 0.000000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(0.18, "cameraLinToLog"), + xp_assert_close( + logarithmic_function_camera(xp_as_array(0.18, xp=xp), "cameraLinToLog"), -2.473931188332412, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(-2.473931188332412, "cameraLogToLin"), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(-2.473931188332412, xp=xp), "cameraLogToLin" + ), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(1, "cameraLinToLog"), + xp_assert_close( + logarithmic_function_camera(xp_as_array(1, xp=xp), "cameraLinToLog"), 0.000000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(0, "cameraLogToLin"), + xp_assert_close( + logarithmic_function_camera(xp_as_array(0, xp=xp), "cameraLogToLin"), 1.000000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(0.18, "cameraLinToLog", 10), + xp_assert_close( + logarithmic_function_camera(xp_as_array(0.18, xp=xp), "cameraLinToLog", 10), -0.744727494896693, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(-0.744727494896693, "cameraLogToLin", 10), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(-0.744727494896693, xp=xp), "cameraLogToLin", 10 + ), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(0.18, "cameraLinToLog", 10, 0.25), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(0.18, xp=xp), "cameraLinToLog", 10, 0.25 + ), -0.186181873724173, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(-0.186181873724173, "cameraLogToLin", 10, 0.25), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(-0.186181873724173, xp=xp), "cameraLogToLin", 10, 0.25 + ), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(0.18, "cameraLinToLog", 10, 0.25, 0.95), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(0.18, xp=xp), "cameraLinToLog", 10, 0.25, 0.95 + ), -0.191750972401961, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( -0.191750972401961, "cameraLogToLin", 10, 0.25, 0.95 ), @@ -355,13 +400,15 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - logarithmic_function_camera(0.18, "cameraLinToLog", 10, 0.25, 0.95, 0.6), + xp_assert_close( + logarithmic_function_camera( + xp_as_array(0.18, xp=xp), "cameraLinToLog", 10, 0.25, 0.95, 0.6 + ), 0.408249027598038, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.408249027598038, "cameraLogToLin", 10, 0.25, 0.95, 0.6 ), @@ -369,7 +416,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.18, "cameraLinToLog", 10, 0.25, 0.95, 0.6, 0.01 ), @@ -377,7 +424,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.414419643717296, "cameraLogToLin", 10, 0.25, 0.95, 0.6, 0.01 ), @@ -385,7 +432,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.005, "cameraLinToLog", 10, 0.25, 0.95, 0.6, 0.01, 0.01 ), @@ -393,7 +440,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.146061232468316, "cameraLogToLin", @@ -408,7 +455,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.005, "cameraLinToLog", 10, 0.25, 0.95, 0.6, 0.01, 0.01, 6 ), @@ -416,7 +463,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( logarithmic_function_camera( 0.142508652840630, "cameraLogToLin", @@ -432,7 +479,7 @@ def test_logarithmic_function_camera(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_logarithmic_function_camera(self) -> None: + def test_n_dimensional_logarithmic_function_camera(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ logarithmic_function_camera` definition n-dimensional arrays support. @@ -442,27 +489,27 @@ def test_n_dimensional_logarithmic_function_camera(self) -> None: for style in styles: a = 0.18 - a_p = logarithmic_function_camera(a, style) + a_p = as_ndarray(logarithmic_function_camera(xp_as_array(a, xp=xp), style)) - a = np.tile(a, 6) - a_p = np.tile(a_p, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6,)) + a_p = xp.tile(xp_as_array(a_p, xp=xp), (6,)) + xp_assert_close( logarithmic_function_camera(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3)) - a_p = np.reshape(a_p, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3), xp=xp) + xp_assert_close( logarithmic_function_camera(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 1)) - a_p = np.reshape(a_p, (2, 3, 1)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 1), xp=xp) + a_p = xp_reshape(xp_as_array(a_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( logarithmic_function_camera(a, style), a_p, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -487,70 +534,68 @@ class TestLogEncoding_Log2: log_encoding_Log2` definition unit tests methods. """ - def test_log_encoding_Log2(self) -> None: + def test_log_encoding_Log2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ log_encoding_Log2` definition. """ - np.testing.assert_allclose( - log_encoding_Log2(0.0), -np.inf, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_Log2(xp_as_array(0.0, xp=xp)), + -np.inf, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log2(0.18), 0.5, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_Log2(xp_as_array(0.18, xp=xp)), + 0.5, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log2(1.0), + xp_assert_close( + log_encoding_Log2(xp_as_array(1.0, xp=xp)), 0.690302399102493, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log2(0.18, 0.12), + xp_assert_close( + log_encoding_Log2(xp_as_array(0.18, xp=xp), 0.12), 0.544997115440089, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log2(0.18, 0.12, 2**-10), + xp_assert_close( + log_encoding_Log2(xp_as_array(0.18, xp=xp), 0.12, 2**-10), 0.089857490719529, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log2(0.18, 0.12, 2**-10, 2**10), + xp_assert_close( + log_encoding_Log2(xp_as_array(0.18, xp=xp), 0.12, 2**-10, 2**10), 0.000570299311674, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Log2(self) -> None: + def test_n_dimensional_log_encoding_Log2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ log_encoding_Log2` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Log2(x) + y = as_ndarray(log_encoding_Log2(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Log2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Log2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Log2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Log2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Log2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Log2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_log_encoding_Log2(self) -> None: @@ -568,72 +613,70 @@ class TestLogDecoding_Log2: log_decoding_Log2` definition unit tests methods. """ - def test_log_decoding_Log2(self) -> None: + def test_log_decoding_Log2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ log_decoding_Log2` definition. """ - np.testing.assert_allclose( - log_decoding_Log2(0.0), + xp_assert_close( + log_decoding_Log2(xp_as_array(0.0, xp=xp)), 0.001988737822087, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log2(0.5), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_Log2(xp_as_array(0.5, xp=xp)), + 0.18, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log2(0.690302399102493), + xp_assert_close( + log_decoding_Log2(xp_as_array(0.690302399102493, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log2(0.544997115440089, 0.12), + xp_assert_close( + log_decoding_Log2(xp_as_array(0.544997115440089, xp=xp), 0.12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log2(0.089857490719529, 0.12, 2**-10), + xp_assert_close( + log_decoding_Log2(xp_as_array(0.089857490719529, xp=xp), 0.12, 2**-10), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log2(0.000570299311674, 0.12, 2**-10, 2**10), + xp_assert_close( + log_decoding_Log2( + xp_as_array(0.000570299311674, xp=xp), 0.12, 2**-10, 2**10 + ), 0.180000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Log2(self) -> None: + def test_n_dimensional_log_decoding_Log2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.log.\ log_decoding_Log2` definition n-dimensional arrays support. """ y = 0.5 - x = log_decoding_Log2(y) + x = as_ndarray(log_decoding_Log2(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Log2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Log2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Log2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Log2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Log2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Log2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_log_decoding_Log2(self) -> None: diff --git a/colour/models/rgb/transfer_functions/tests/test_nikon_n_log.py b/colour/models/rgb/transfer_functions/tests/test_nikon_n_log.py index 797aa9da00..046ec552a8 100644 --- a/colour/models/rgb/transfer_functions/tests/test_nikon_n_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_nikon_n_log.py @@ -3,11 +3,25 @@ nikon_n_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import log_decoding_NLog, log_encoding_NLog -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,89 +42,83 @@ class TestLogEncoding_NLog: log_encoding_NLog` definition unit tests methods. """ - def test_log_encoding_NLog(self) -> None: + def test_log_encoding_NLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.nikon_n_log.\ log_encoding_NLog` definition. """ - np.testing.assert_allclose( - log_encoding_NLog(0.0), + xp_assert_close( + log_encoding_NLog(xp_as_array(0.0, xp=xp)), 0.124372627896372, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_NLog(0.18), + xp_assert_close( + log_encoding_NLog(xp_as_array(0.18, xp=xp)), 0.363667770117139, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_NLog(0.18, 12), + xp_assert_close( + log_encoding_NLog(xp_as_array(0.18, xp=xp), 12), 0.363667770117139, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_NLog(0.18, 10, False), + xp_assert_close( + log_encoding_NLog(xp_as_array(0.18, xp=xp), 10, False), 0.351634850262366, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_NLog(0.18, 10, False, False), + xp_assert_close( + log_encoding_NLog(xp_as_array(0.18, xp=xp), 10, False, False), 0.337584957293328, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_NLog(1.0), + xp_assert_close( + log_encoding_NLog(xp_as_array(1.0, xp=xp)), 0.605083088954056, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_NLog(self) -> None: + def test_n_dimensional_log_encoding_NLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.nikon_n_log.\ log_encoding_NLog` definition n-dimensional arrays support. """ y = 0.18 - x = log_encoding_NLog(y) + x = as_ndarray(log_encoding_NLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_encoding_NLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_encoding_NLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_encoding_NLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_NLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_NLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_NLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_NLog(self) -> None: + def test_domain_range_scale_log_encoding_NLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.nikon_n_log.\ log_encoding_NLog` definition domain and range scale support. """ y = 0.18 - x = log_encoding_NLog(y) + x = as_ndarray(log_encoding_NLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_NLog(y * factor), + xp_assert_close( + log_encoding_NLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -131,89 +139,83 @@ class TestLogDecoding_NLog: log_decoding_NLog` definition unit tests methods. """ - def test_log_decoding_NLog(self) -> None: + def test_log_decoding_NLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.nikon_n_log.\ log_decoding_NLog` definition. """ - np.testing.assert_allclose( - log_decoding_NLog(0.124372627896372), + xp_assert_close( + log_decoding_NLog(xp_as_array(0.124372627896372, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_NLog(0.363667770117139), + xp_assert_close( + log_decoding_NLog(xp_as_array(0.363667770117139, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_NLog(0.363667770117139, 12), + xp_assert_close( + log_decoding_NLog(xp_as_array(0.363667770117139, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_NLog(0.351634850262366, 10, False), + xp_assert_close( + log_decoding_NLog(xp_as_array(0.351634850262366, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_NLog(0.337584957293328, 10, False, False), + xp_assert_close( + log_decoding_NLog(xp_as_array(0.337584957293328, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_NLog(0.605083088954056), + xp_assert_close( + log_decoding_NLog(xp_as_array(0.605083088954056, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_NLog(self) -> None: + def test_n_dimensional_log_decoding_NLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.nikon_n_log.\ log_decoding_NLog` definition n-dimensional arrays support. """ x = 0.363667770117139 - y = log_decoding_NLog(x) + y = as_ndarray(log_decoding_NLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_decoding_NLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_decoding_NLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_decoding_NLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_NLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_NLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_NLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_NLog(self) -> None: + def test_domain_range_scale_log_decoding_NLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.nikon_n_log.\ log_decoding_NLog` definition domain and range scale support. """ x = 0.363667770117139 - y = log_decoding_NLog(x) + y = as_ndarray(log_decoding_NLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_NLog(x * factor), + xp_assert_close( + log_decoding_NLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_oppo_o_log.py b/colour/models/rgb/transfer_functions/tests/test_oppo_o_log.py index 521f9dacfe..6d371571db 100644 --- a/colour/models/rgb/transfer_functions/tests/test_oppo_o_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_oppo_o_log.py @@ -3,6 +3,10 @@ oppo_o_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_OPPOOLog, log_encoding_OPPOOLog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +45,65 @@ class TestLogEncoding_OPPOOLog: log_encoding_OPPOOLog` definition unit tests methods. """ - def test_log_encoding_OPPOOLog(self) -> None: + def test_log_encoding_OPPOOLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.oppo_o_log.\ log_encoding_OPPOOLog` definition. """ - np.testing.assert_allclose( - log_encoding_OPPOOLog(0.0), + xp_assert_close( + log_encoding_OPPOOLog(xp_as_array(0.0, xp=xp)), 0.06309903, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_OPPOOLog(0.18), + xp_assert_close( + log_encoding_OPPOOLog(xp_as_array(0.18, xp=xp)), 0.38959139, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_OPPOOLog(0.90), + xp_assert_close( + log_encoding_OPPOOLog(xp_as_array(0.90, xp=xp)), 0.60225879, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_OPPOOLog(self) -> None: + def test_n_dimensional_log_encoding_OPPOOLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.oppo_o_log.\ log_encoding_OPPOOLog` definition n-dimensional arrays support. """ R = 0.18 - P = log_encoding_OPPOOLog(R) + P = as_ndarray(log_encoding_OPPOOLog(xp_as_array(R, xp=xp))) - R = np.tile(R, 6) - P = np.tile(P, 6) - np.testing.assert_allclose( - log_encoding_OPPOOLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS - ) + R = xp.tile(xp_as_array(R, xp=xp), (6,)) + P = xp.tile(xp_as_array(P, xp=xp), (6,)) + xp_assert_close(log_encoding_OPPOOLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS) - R = np.reshape(R, (2, 3)) - P = np.reshape(P, (2, 3)) - np.testing.assert_allclose( - log_encoding_OPPOOLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS - ) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3), xp=xp) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_OPPOOLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS) - R = np.reshape(R, (2, 3, 1)) - P = np.reshape(P, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_OPPOOLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS - ) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3, 1), xp=xp) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_OPPOOLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_OPPOOLog(self) -> None: + def test_domain_range_scale_log_encoding_OPPOOLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.oppo_o_log.\ log_encoding_OPPOOLog` definition domain and range scale support. """ R = 0.18 - P = log_encoding_OPPOOLog(R) + P = as_ndarray(log_encoding_OPPOOLog(xp_as_array(R, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_OPPOOLog(R * factor), + xp_assert_close( + log_encoding_OPPOOLog(xp_as_array(R * factor, xp=xp)), P * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -116,71 +124,65 @@ class TestLogDecoding_OPPOOLog: log_decoding_OPPOOLog` definition unit tests methods. """ - def test_log_decoding_OPPOOLog(self) -> None: + def test_log_decoding_OPPOOLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.oppo_o_log.\ log_decoding_OPPOOLog` definition. """ - np.testing.assert_allclose( - log_decoding_OPPOOLog(0.06309903), + xp_assert_close( + log_decoding_OPPOOLog(xp_as_array(0.06309903, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_OPPOOLog(0.38959139), + xp_assert_close( + log_decoding_OPPOOLog(xp_as_array(0.38959139, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_OPPOOLog(0.60225879), + xp_assert_close( + log_decoding_OPPOOLog(xp_as_array(0.60225879, xp=xp)), 0.90, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_OPPOOLog(self) -> None: + def test_n_dimensional_log_decoding_OPPOOLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.oppo_o_log.\ log_decoding_OPPOOLog` definition n-dimensional arrays support. """ P = 0.38959139 - R = log_decoding_OPPOOLog(P) + R = as_ndarray(log_decoding_OPPOOLog(xp_as_array(P, xp=xp))) - P = np.tile(P, 6) - R = np.tile(R, 6) - np.testing.assert_allclose( - log_decoding_OPPOOLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + P = xp.tile(xp_as_array(P, xp=xp), (6,)) + R = xp.tile(xp_as_array(R, xp=xp), (6,)) + xp_assert_close(log_decoding_OPPOOLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS) - P = np.reshape(P, (2, 3)) - R = np.reshape(R, (2, 3)) - np.testing.assert_allclose( - log_decoding_OPPOOLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3), xp=xp) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_OPPOOLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS) - P = np.reshape(P, (2, 3, 1)) - R = np.reshape(R, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_OPPOOLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3, 1), xp=xp) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_OPPOOLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_OPPOOLog(self) -> None: + def test_domain_range_scale_log_decoding_OPPOOLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.oppo_o_log.\ log_decoding_OPPOOLog` definition domain and range scale support. """ P = 0.38959139 - R = log_decoding_OPPOOLog(P) + R = as_ndarray(log_decoding_OPPOOLog(xp_as_array(P, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_OPPOOLog(P * factor), + xp_assert_close( + log_decoding_OPPOOLog(xp_as_array(P * factor, xp=xp)), R * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_panalog.py b/colour/models/rgb/transfer_functions/tests/test_panalog.py index 837e53c148..8b4b503468 100644 --- a/colour/models/rgb/transfer_functions/tests/test_panalog.py +++ b/colour/models/rgb/transfer_functions/tests/test_panalog.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.panalog` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_Panalog, log_encoding_Panalog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +45,65 @@ class TestLogEncoding_Panalog: log_encoding_Panalog` definition unit tests methods. """ - def test_log_encoding_Panalog(self) -> None: + def test_log_encoding_Panalog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panalog.\ log_encoding_Panalog` definition. """ - np.testing.assert_allclose( - log_encoding_Panalog(0.0), + xp_assert_close( + log_encoding_Panalog(xp_as_array(0.0, xp=xp)), 0.062561094819159, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Panalog(0.18), + xp_assert_close( + log_encoding_Panalog(xp_as_array(0.18, xp=xp)), 0.374576791382298, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Panalog(1.0), + xp_assert_close( + log_encoding_Panalog(xp_as_array(1.0, xp=xp)), 0.665689149560117, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Panalog(self) -> None: + def test_n_dimensional_log_encoding_Panalog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panalog.\ log_encoding_Panalog` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Panalog(x) + y = as_ndarray(log_encoding_Panalog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Panalog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Panalog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Panalog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Panalog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Panalog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Panalog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Panalog(self) -> None: + def test_domain_range_scale_log_encoding_Panalog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panalog.\ log_encoding_Panalog` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Panalog(x) + y = as_ndarray(log_encoding_Panalog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Panalog(x * factor), + xp_assert_close( + log_encoding_Panalog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -116,71 +124,65 @@ class TestLogDecoding_Panalog: log_decoding_Panalog` definition unit tests methods. """ - def test_log_decoding_Panalog(self) -> None: + def test_log_decoding_Panalog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panalog.\ log_decoding_Panalog` definition. """ - np.testing.assert_allclose( - log_decoding_Panalog(0.062561094819159), + xp_assert_close( + log_decoding_Panalog(xp_as_array(0.062561094819159, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Panalog(0.374576791382298), + xp_assert_close( + log_decoding_Panalog(xp_as_array(0.374576791382298, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Panalog(0.665689149560117), + xp_assert_close( + log_decoding_Panalog(xp_as_array(0.665689149560117, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Panalog(self) -> None: + def test_n_dimensional_log_decoding_Panalog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panalog.\ log_decoding_Panalog` definition n-dimensional arrays support. """ y = 0.374576791382298 - x = log_decoding_Panalog(y) + x = as_ndarray(log_decoding_Panalog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Panalog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Panalog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Panalog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Panalog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Panalog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Panalog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Panalog(self) -> None: + def test_domain_range_scale_log_decoding_Panalog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panalog.\ log_decoding_Panalog` definition domain and range scale support. """ y = 0.374576791382298 - x = log_decoding_Panalog(y) + x = as_ndarray(log_decoding_Panalog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Panalog(y * factor), + xp_assert_close( + log_decoding_Panalog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_panasonic_vlog.py b/colour/models/rgb/transfer_functions/tests/test_panasonic_vlog.py index 73d8a88e10..235fc85bf6 100644 --- a/colour/models/rgb/transfer_functions/tests/test_panasonic_vlog.py +++ b/colour/models/rgb/transfer_functions/tests/test_panasonic_vlog.py @@ -3,11 +3,25 @@ panasonic_v_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import log_decoding_VLog, log_encoding_VLog -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,87 +42,83 @@ class TestLogEncoding_VLog: log_encoding_VLog` definition unit tests methods. """ - def test_log_encoding_VLog(self) -> None: + def test_log_encoding_VLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panasonic_v_log.\ log_encoding_VLog` definition. """ - np.testing.assert_allclose( - log_encoding_VLog(0.0), 0.125, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_VLog(xp_as_array(0.0, xp=xp)), + 0.125, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_VLog(0.18), + xp_assert_close( + log_encoding_VLog(xp_as_array(0.18, xp=xp)), 0.423311448760136, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_VLog(0.18, 12), + xp_assert_close( + log_encoding_VLog(xp_as_array(0.18, xp=xp), 12), 0.423311448760136, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_VLog(0.18, 10, False), + xp_assert_close( + log_encoding_VLog(xp_as_array(0.18, xp=xp), 10, False), 0.421287228403675, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_VLog(0.18, 10, False, False), + xp_assert_close( + log_encoding_VLog(xp_as_array(0.18, xp=xp), 10, False, False), 0.409009628526078, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_VLog(1.0), + xp_assert_close( + log_encoding_VLog(xp_as_array(1.0, xp=xp)), 0.599117700158146, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_VLog(self) -> None: + def test_n_dimensional_log_encoding_VLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panasonic_v_log.\ log_encoding_VLog` definition n-dimensional arrays support. """ L_in = 0.18 - V_out = log_encoding_VLog(L_in) + V_out = as_ndarray(log_encoding_VLog(xp_as_array(L_in, xp=xp))) - L_in = np.tile(L_in, 6) - V_out = np.tile(V_out, 6) - np.testing.assert_allclose( - log_encoding_VLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp.tile(xp_as_array(L_in, xp=xp), (6,)) + V_out = xp.tile(xp_as_array(V_out, xp=xp), (6,)) + xp_assert_close(log_encoding_VLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - L_in = np.reshape(L_in, (2, 3)) - V_out = np.reshape(V_out, (2, 3)) - np.testing.assert_allclose( - log_encoding_VLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3), xp=xp) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_VLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - L_in = np.reshape(L_in, (2, 3, 1)) - V_out = np.reshape(V_out, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_VLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3, 1), xp=xp) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_VLog(L_in), V_out, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_VLog(self) -> None: + def test_domain_range_scale_log_encoding_VLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panasonic_v_log.\ log_encoding_VLog` definition domain and range scale support. """ L_in = 0.18 - V_out = log_encoding_VLog(L_in) + V_out = as_ndarray(log_encoding_VLog(xp_as_array(L_in, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_VLog(L_in * factor), + xp_assert_close( + log_encoding_VLog(xp_as_array(L_in * factor, xp=xp)), V_out * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -129,87 +139,83 @@ class TestLogDecoding_VLog: log_decoding_VLog` definition unit tests methods. """ - def test_log_decoding_VLog(self) -> None: + def test_log_decoding_VLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panasonic_v_log.\ log_decoding_VLog` definition. """ - np.testing.assert_allclose( - log_decoding_VLog(0.125), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_VLog(xp_as_array(0.125, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_VLog(0.423311448760136), + xp_assert_close( + log_decoding_VLog(xp_as_array(0.423311448760136, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_VLog(0.423311448760136, 12), + xp_assert_close( + log_decoding_VLog(xp_as_array(0.423311448760136, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_VLog(0.421287228403675, 10, False), + xp_assert_close( + log_decoding_VLog(xp_as_array(0.421287228403675, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_VLog(0.409009628526078, 10, False, False), + xp_assert_close( + log_decoding_VLog(xp_as_array(0.409009628526078, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_VLog(0.599117700158146), + xp_assert_close( + log_decoding_VLog(xp_as_array(0.599117700158146, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_VLog(self) -> None: + def test_n_dimensional_log_decoding_VLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panasonic_v_log.\ log_decoding_VLog` definition n-dimensional arrays support. """ V_out = 0.423311448760136 - L_in = log_decoding_VLog(V_out) + L_in = as_ndarray(log_decoding_VLog(xp_as_array(V_out, xp=xp))) - V_out = np.tile(V_out, 6) - L_in = np.tile(L_in, 6) - np.testing.assert_allclose( - log_decoding_VLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp.tile(xp_as_array(V_out, xp=xp), (6,)) + L_in = xp.tile(xp_as_array(L_in, xp=xp), (6,)) + xp_assert_close(log_decoding_VLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - V_out = np.reshape(V_out, (2, 3)) - L_in = np.reshape(L_in, (2, 3)) - np.testing.assert_allclose( - log_decoding_VLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3), xp=xp) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_VLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - V_out = np.reshape(V_out, (2, 3, 1)) - L_in = np.reshape(L_in, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_VLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_out = xp_reshape(xp_as_array(V_out, xp=xp), (2, 3, 1), xp=xp) + L_in = xp_reshape(xp_as_array(L_in, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_VLog(V_out), L_in, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_VLog(self) -> None: + def test_domain_range_scale_log_decoding_VLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.panasonic_v_log.\ log_decoding_VLog` definition domain and range scale support. """ V_out = 0.423311448760136 - L_in = log_decoding_VLog(V_out) + L_in = as_ndarray(log_decoding_VLog(xp_as_array(V_out, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_VLog(V_out * factor), + xp_assert_close( + log_decoding_VLog(xp_as_array(V_out * factor, xp=xp)), L_in * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_pivoted_log.py b/colour/models/rgb/transfer_functions/tests/test_pivoted_log.py index 65b605416f..126b2053c7 100644 --- a/colour/models/rgb/transfer_functions/tests/test_pivoted_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_pivoted_log.py @@ -3,6 +3,10 @@ pivoted_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_PivotedLog, log_encoding_PivotedLog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,71 +45,65 @@ class TestLogEncoding_PivotedLog: log_encoding_PivotedLog` definition unit tests methods. """ - def test_log_encoding_PivotedLog(self) -> None: + def test_log_encoding_PivotedLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.pivoted_log.\ log_encoding_PivotedLog` definition. """ - np.testing.assert_allclose( - log_encoding_PivotedLog(0.0), + xp_assert_close( + log_encoding_PivotedLog(xp_as_array(0.0, xp=xp)), -np.inf, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_PivotedLog(0.18), + xp_assert_close( + log_encoding_PivotedLog(xp_as_array(0.18, xp=xp)), 0.434995112414467, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_PivotedLog(1.0), + xp_assert_close( + log_encoding_PivotedLog(xp_as_array(1.0, xp=xp)), 0.653390272208219, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_PivotedLog(self) -> None: + def test_n_dimensional_log_encoding_PivotedLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.pivoted_log.\ log_encoding_PivotedLog` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_PivotedLog(x) + y = as_ndarray(log_encoding_PivotedLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_PivotedLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_PivotedLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_PivotedLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_PivotedLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_PivotedLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_PivotedLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_PivotedLog(self) -> None: + def test_domain_range_scale_log_encoding_PivotedLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.pivoted_log.\ log_encoding_PivotedLog` definition domain and range scale support. """ x = 0.18 - y = log_encoding_PivotedLog(x) + y = as_ndarray(log_encoding_PivotedLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_PivotedLog(x * factor), + xp_assert_close( + log_encoding_PivotedLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -116,71 +124,65 @@ class TestLogDecoding_PivotedLog: log_decoding_PivotedLog` definition unit tests methods. """ - def test_log_decoding_PivotedLog(self) -> None: + def test_log_decoding_PivotedLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.pivoted_log.\ log_decoding_PivotedLog` definition. """ - np.testing.assert_allclose( - log_decoding_PivotedLog(-np.inf), + xp_assert_close( + log_decoding_PivotedLog(xp_as_array(-np.inf, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_PivotedLog(0.434995112414467), + xp_assert_close( + log_decoding_PivotedLog(xp_as_array(0.434995112414467, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_PivotedLog(0.653390272208219), + xp_assert_close( + log_decoding_PivotedLog(xp_as_array(0.653390272208219, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_PivotedLog(self) -> None: + def test_n_dimensional_log_decoding_PivotedLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.pivoted_log.\ log_decoding_PivotedLog` definition n-dimensional arrays support. """ y = 0.434995112414467 - x = log_decoding_PivotedLog(y) + x = as_ndarray(log_decoding_PivotedLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_PivotedLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_PivotedLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_PivotedLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_PivotedLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_PivotedLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_PivotedLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_PivotedLog(self) -> None: + def test_domain_range_scale_log_decoding_PivotedLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.pivoted_log.\ log_decoding_PivotedLog` definition domain and range scale support. """ y = 0.434995112414467 - x = log_decoding_PivotedLog(y) + x = as_ndarray(log_decoding_PivotedLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_PivotedLog(y * factor), + xp_assert_close( + log_decoding_PivotedLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_red.py b/colour/models/rgb/transfer_functions/tests/test_red.py index e4f3a39ae2..2f7843362f 100644 --- a/colour/models/rgb/transfer_functions/tests/test_red.py +++ b/colour/models/rgb/transfer_functions/tests/test_red.py @@ -3,7 +3,12 @@ :mod:`colour.models.rgb.transfer_functions.red` module. """ +from __future__ import annotations + +import typing + import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import ( @@ -22,7 +27,17 @@ log_encoding_Log3G10_v2, log_encoding_Log3G10_v3, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -53,67 +68,65 @@ class TestLogEncoding_REDLog: log_encoding_REDLog` definition unit tests methods. """ - def test_log_encoding_REDLog(self) -> None: + def test_log_encoding_REDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_REDLog` definition. """ - np.testing.assert_allclose( - log_encoding_REDLog(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_REDLog(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_REDLog(0.18), + xp_assert_close( + log_encoding_REDLog(xp_as_array(0.18, xp=xp)), 0.637621845988175, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_REDLog(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_REDLog(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_REDLog(self) -> None: + def test_n_dimensional_log_encoding_REDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_REDLog` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_REDLog(x) + y = as_ndarray(log_encoding_REDLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_REDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_REDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_REDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_REDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_REDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_REDLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_REDLog(self) -> None: + def test_domain_range_scale_log_encoding_REDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_REDLog` definition domain and range scale support. """ x = 0.18 - y = log_encoding_REDLog(x) + y = as_ndarray(log_encoding_REDLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_REDLog(x * factor), + xp_assert_close( + log_encoding_REDLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -134,67 +147,65 @@ class TestLogDecoding_REDLog: log_decoding_REDLog` definition unit tests methods. """ - def test_log_decoding_REDLog(self) -> None: + def test_log_decoding_REDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_REDLog` definition. """ - np.testing.assert_allclose( - log_decoding_REDLog(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_REDLog(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_REDLog(0.637621845988175), + xp_assert_close( + log_decoding_REDLog(xp_as_array(0.637621845988175, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_REDLog(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_REDLog(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_REDLog(self) -> None: + def test_n_dimensional_log_decoding_REDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_REDLog` definition n-dimensional arrays support. """ y = 0.637621845988175 - x = log_decoding_REDLog(y) + x = as_ndarray(log_decoding_REDLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_REDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_REDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_REDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_REDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_REDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_REDLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_REDLog(self) -> None: + def test_domain_range_scale_log_decoding_REDLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_REDLog` definition domain and range scale support. """ y = 0.637621845988175 - x = log_decoding_REDLog(y) + x = as_ndarray(log_decoding_REDLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_REDLog(y * factor), + xp_assert_close( + log_decoding_REDLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -215,71 +226,65 @@ class TestLogEncoding_REDLogFilm: log_encoding_REDLogFilm` definition unit tests methods. """ - def test_log_encoding_REDLogFilm(self) -> None: + def test_log_encoding_REDLogFilm(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_REDLogFilm` definition. """ - np.testing.assert_allclose( - log_encoding_REDLogFilm(0.0), + xp_assert_close( + log_encoding_REDLogFilm(xp_as_array(0.0, xp=xp)), 0.092864125122190, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_REDLogFilm(0.18), + xp_assert_close( + log_encoding_REDLogFilm(xp_as_array(0.18, xp=xp)), 0.457319613085418, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_REDLogFilm(1.0), + xp_assert_close( + log_encoding_REDLogFilm(xp_as_array(1.0, xp=xp)), 0.669599217986315, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_REDLogFilm(self) -> None: + def test_n_dimensional_log_encoding_REDLogFilm(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_REDLogFilm` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_REDLogFilm(x) + y = as_ndarray(log_encoding_REDLogFilm(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_REDLogFilm(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_REDLogFilm(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_REDLogFilm(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_REDLogFilm(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_REDLogFilm(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_REDLogFilm(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_REDLogFilm(self) -> None: + def test_domain_range_scale_log_encoding_REDLogFilm(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_REDLogFilm` definition domain and range scale support. """ x = 0.18 - y = log_encoding_REDLogFilm(x) + y = as_ndarray(log_encoding_REDLogFilm(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_REDLogFilm(x * factor), + xp_assert_close( + log_encoding_REDLogFilm(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -300,71 +305,65 @@ class TestLogDecoding_REDLogFilm: log_decoding_REDLogFilm` definition unit tests methods. """ - def test_log_decoding_REDLogFilm(self) -> None: + def test_log_decoding_REDLogFilm(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_REDLogFilm` definition. """ - np.testing.assert_allclose( - log_decoding_REDLogFilm(0.092864125122190), + xp_assert_close( + log_decoding_REDLogFilm(xp_as_array(0.092864125122190, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_REDLogFilm(0.457319613085418), + xp_assert_close( + log_decoding_REDLogFilm(xp_as_array(0.457319613085418, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_REDLogFilm(0.669599217986315), + xp_assert_close( + log_decoding_REDLogFilm(xp_as_array(0.669599217986315, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_REDLogFilm(self) -> None: + def test_n_dimensional_log_decoding_REDLogFilm(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_REDLogFilm` definition n-dimensional arrays support. """ y = 0.457319613085418 - x = log_decoding_REDLogFilm(y) + x = as_ndarray(log_decoding_REDLogFilm(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_REDLogFilm(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_REDLogFilm(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_REDLogFilm(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_REDLogFilm(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_REDLogFilm(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_REDLogFilm(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_REDLogFilm(self) -> None: + def test_domain_range_scale_log_decoding_REDLogFilm(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_REDLogFilm` definition domain and range scale support. """ y = 0.457319613085418 - x = log_decoding_REDLogFilm(y) + x = as_ndarray(log_decoding_REDLogFilm(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_REDLogFilm(y * factor), + xp_assert_close( + log_decoding_REDLogFilm(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -385,69 +384,65 @@ class TestLogEncoding_Log3G10_v1: log_encoding_Log3G10_v1` definition unit tests methods. """ - def test_log_encoding_Log3G10_v1(self) -> None: + def test_log_encoding_Log3G10_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v1` definition. """ - np.testing.assert_allclose( - log_encoding_Log3G10_v1(-1.0), + xp_assert_close( + log_encoding_Log3G10_v1(xp_as_array(-1.0, xp=xp)), -0.496483569056003, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G10_v1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_Log3G10_v1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G10_v1(0.18), + xp_assert_close( + log_encoding_Log3G10_v1(xp_as_array(0.18, xp=xp)), 0.333333644207707, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Log3G10_v1(self) -> None: + def test_n_dimensional_log_encoding_Log3G10_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v1` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Log3G10_v1(x) + y = as_ndarray(log_encoding_Log3G10_v1(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Log3G10_v1(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Log3G10_v1(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Log3G10_v1(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Log3G10_v1(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Log3G10_v1(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Log3G10_v1(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Log3G10_v1(self) -> None: + def test_domain_range_scale_log_encoding_Log3G10_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v1` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Log3G10_v1(x) + y = as_ndarray(log_encoding_Log3G10_v1(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Log3G10_v1(x * factor), + xp_assert_close( + log_encoding_Log3G10_v1(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -468,69 +463,65 @@ class TestLogDecoding_Log3G10_v1: log_decoding_Log3G10_v1` definition unit tests methods. """ - def test_log_decoding_Log3G10_v1(self) -> None: + def test_log_decoding_Log3G10_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v1` definition. """ - np.testing.assert_allclose( - log_decoding_Log3G10_v1(-0.496483569056003), + xp_assert_close( + log_decoding_Log3G10_v1(xp_as_array(-0.496483569056003, xp=xp)), -1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G10_v1(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_Log3G10_v1(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G10_v1(0.333333644207707), + xp_assert_close( + log_decoding_Log3G10_v1(xp_as_array(0.333333644207707, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Log3G10_v1(self) -> None: + def test_n_dimensional_log_decoding_Log3G10_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v1` definition n-dimensional arrays support. """ y = 0.333333644207707 - x = log_decoding_Log3G10_v1(y) + x = as_ndarray(log_decoding_Log3G10_v1(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Log3G10_v1(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Log3G10_v1(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Log3G10_v1(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Log3G10_v1(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Log3G10_v1(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Log3G10_v1(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Log3G10_v1(self) -> None: + def test_domain_range_scale_log_decoding_Log3G10_v1(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v1` definition domain and range scale support. """ y = 0.333333644207707 - x = log_decoding_Log3G10_v1(y) + x = as_ndarray(log_decoding_Log3G10_v1(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Log3G10_v1(y * factor), + xp_assert_close( + log_decoding_Log3G10_v1(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -551,71 +542,65 @@ class TestLogEncoding_Log3G10_v2: log_encoding_Log3G10_v2` definition unit tests methods. """ - def test_log_encoding_Log3G10_v2(self) -> None: + def test_log_encoding_Log3G10_v2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v2` definition. """ - np.testing.assert_allclose( - log_encoding_Log3G10_v2(-1.0), + xp_assert_close( + log_encoding_Log3G10_v2(xp_as_array(-1.0, xp=xp)), -0.491512777522511, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G10_v2(0.0), + xp_assert_close( + log_encoding_Log3G10_v2(xp_as_array(0.0, xp=xp)), 0.091551487714745, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G10_v2(0.18), + xp_assert_close( + log_encoding_Log3G10_v2(xp_as_array(0.18, xp=xp)), 0.333332912025992, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Log3G10_v2(self) -> None: + def test_n_dimensional_log_encoding_Log3G10_v2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v2` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Log3G10_v2(x) + y = as_ndarray(log_encoding_Log3G10_v2(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Log3G10_v2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Log3G10_v2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Log3G10_v2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Log3G10_v2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Log3G10_v2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Log3G10_v2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Log3G10_v2(self) -> None: + def test_domain_range_scale_log_encoding_Log3G10_v2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v2` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Log3G10_v2(x) + y = as_ndarray(log_encoding_Log3G10_v2(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Log3G10_v2(x * factor), + xp_assert_close( + log_encoding_Log3G10_v2(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -636,71 +621,65 @@ class TestLogDecoding_Log3G10_v2: log_decoding_Log3G10_v2` definition unit tests methods. """ - def test_log_decoding_Log3G10_v2(self) -> None: + def test_log_decoding_Log3G10_v2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v2` definition. """ - np.testing.assert_allclose( - log_decoding_Log3G10_v2(-0.491512777522511), + xp_assert_close( + log_decoding_Log3G10_v2(xp_as_array(-0.491512777522511, xp=xp)), -1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G10_v2(0.091551487714745), + xp_assert_close( + log_decoding_Log3G10_v2(xp_as_array(0.091551487714745, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G10_v2(0.333332912025992), + xp_assert_close( + log_decoding_Log3G10_v2(xp_as_array(0.333332912025992, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Log3G10_v2(self) -> None: + def test_n_dimensional_log_decoding_Log3G10_v2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v2` definition n-dimensional arrays support. """ y = 0.333332912025992 - x = log_decoding_Log3G10_v2(y) + x = as_ndarray(log_decoding_Log3G10_v2(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Log3G10_v2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Log3G10_v2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Log3G10_v2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Log3G10_v2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Log3G10_v2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Log3G10_v2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Log3G10_v2(self) -> None: + def test_domain_range_scale_log_decoding_Log3G10_v2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v2` definition domain and range scale support. """ y = 0.333333644207707 - x = log_decoding_Log3G10_v2(y) + x = as_ndarray(log_decoding_Log3G10_v2(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Log3G10_v2(y * factor), + xp_assert_close( + log_decoding_Log3G10_v2(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -721,71 +700,65 @@ class TestLogEncoding_Log3G10_v3: log_encoding_Log3G10_v3` definition unit tests methods. """ - def test_log_encoding_Log3G10_v3(self) -> None: + def test_log_encoding_Log3G10_v3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v3` definition. """ - np.testing.assert_allclose( - log_encoding_Log3G10_v3(-1.0), + xp_assert_close( + log_encoding_Log3G10_v3(xp_as_array(-1.0, xp=xp)), -15.040773, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G10_v3(0.0), + xp_assert_close( + log_encoding_Log3G10_v3(xp_as_array(0.0, xp=xp)), 0.091551487714745, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G10_v3(0.18), + xp_assert_close( + log_encoding_Log3G10_v3(xp_as_array(0.18, xp=xp)), 0.333332912025992, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Log3G10_v3(self) -> None: + def test_n_dimensional_log_encoding_Log3G10_v3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v3` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Log3G10_v3(x) + y = as_ndarray(log_encoding_Log3G10_v3(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Log3G10_v3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Log3G10_v3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Log3G10_v3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Log3G10_v3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Log3G10_v3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Log3G10_v3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Log3G10_v3(self) -> None: + def test_domain_range_scale_log_encoding_Log3G10_v3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G10_v3` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Log3G10_v3(x) + y = as_ndarray(log_encoding_Log3G10_v3(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Log3G10_v3(x * factor), + xp_assert_close( + log_encoding_Log3G10_v3(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -806,71 +779,65 @@ class TestLogDecoding_Log3G10_v3: log_decoding_Log3G10_v3` definition unit tests methods. """ - def test_log_decoding_Log3G10_v3(self) -> None: + def test_log_decoding_Log3G10_v3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v3` definition. """ - np.testing.assert_allclose( - log_decoding_Log3G10_v3(-15.040773), + xp_assert_close( + log_decoding_Log3G10_v3(xp_as_array(-15.040773, xp=xp)), -1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G10_v3(0.091551487714745), + xp_assert_close( + log_decoding_Log3G10_v3(xp_as_array(0.091551487714745, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G10_v3(0.333332912025992), + xp_assert_close( + log_decoding_Log3G10_v3(xp_as_array(0.333332912025992, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Log3G10_v3(self) -> None: + def test_n_dimensional_log_decoding_Log3G10_v3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v3` definition n-dimensional arrays support. """ y = 0.333332912025992 - x = log_decoding_Log3G10_v3(y) + x = as_ndarray(log_decoding_Log3G10_v3(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Log3G10_v3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Log3G10_v3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Log3G10_v3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Log3G10_v3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Log3G10_v3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Log3G10_v3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Log3G10_v3(self) -> None: + def test_domain_range_scale_log_decoding_Log3G10_v3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G10_v3` definition domain and range scale support. """ y = 0.333333644207707 - x = log_decoding_Log3G10_v3(y) + x = as_ndarray(log_decoding_Log3G10_v3(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Log3G10_v3(y * factor), + xp_assert_close( + log_decoding_Log3G10_v3(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -891,75 +858,71 @@ class TestLogEncoding_Log3G12: log_encoding_Log3G12` definition unit tests methods. """ - def test_log_encoding_Log3G12(self) -> None: + def test_log_encoding_Log3G12(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G12` definition. """ - np.testing.assert_allclose( - log_encoding_Log3G12(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_Log3G12(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G12(0.18), + xp_assert_close( + log_encoding_Log3G12(xp_as_array(0.18, xp=xp)), 0.333332662015923, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G12(1.0), + xp_assert_close( + log_encoding_Log3G12(xp_as_array(1.0, xp=xp)), 0.469991923234319, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_Log3G12(0.18 * 2**12), + xp_assert_close( + log_encoding_Log3G12(xp_as_array(0.18 * 2**12, xp=xp)), 0.999997986792394, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_Log3G12(self) -> None: + def test_n_dimensional_log_encoding_Log3G12(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G12` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_Log3G12(x) + y = as_ndarray(log_encoding_Log3G12(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_Log3G12(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_Log3G12(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_Log3G12(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_Log3G12(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_Log3G12(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_Log3G12(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_Log3G12(self) -> None: + def test_domain_range_scale_log_encoding_Log3G12(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_encoding_Log3G12` definition domain and range scale support. """ x = 0.18 - y = log_encoding_Log3G12(x) + y = as_ndarray(log_encoding_Log3G12(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_Log3G12(x * factor), + xp_assert_close( + log_encoding_Log3G12(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -980,75 +943,72 @@ class TestLogDecoding_Log3G12: log_decoding_Log3G12` definition unit tests methods. """ - def test_log_decoding_Log3G12(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_log_decoding_Log3G12(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G12` definition. """ - np.testing.assert_allclose( - log_decoding_Log3G12(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_Log3G12(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G12(0.333332662015923), + xp_assert_close( + log_decoding_Log3G12(xp_as_array(0.333332662015923, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G12(0.469991923234319), + xp_assert_close( + log_decoding_Log3G12(xp_as_array(0.469991923234319, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_Log3G12(1.0), + xp_assert_close( + log_decoding_Log3G12(xp_as_array(1.0, xp=xp)), 737.29848406719, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_Log3G12(self) -> None: + def test_n_dimensional_log_decoding_Log3G12(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G12` definition n-dimensional arrays support. """ y = 0.333332662015923 - x = log_decoding_Log3G12(y) + x = as_ndarray(log_decoding_Log3G12(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_Log3G12(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_Log3G12(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_Log3G12(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_Log3G12(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_Log3G12(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_Log3G12(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_Log3G12(self) -> None: + def test_domain_range_scale_log_decoding_Log3G12(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.red.\ log_decoding_Log3G12` definition domain and range scale support. """ y = 0.18 - x = log_decoding_Log3G12(y) + x = as_ndarray(log_decoding_Log3G12(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_Log3G12(y * factor), + xp_assert_close( + log_decoding_Log3G12(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_rimm_romm_rgb.py b/colour/models/rgb/transfer_functions/tests/test_rimm_romm_rgb.py index dc5ce6f8b8..024dfef019 100644 --- a/colour/models/rgb/transfer_functions/tests/test_rimm_romm_rgb.py +++ b/colour/models/rgb/transfer_functions/tests/test_rimm_romm_rgb.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.rimm_romm_rgb` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -14,7 +18,17 @@ log_decoding_ERIMMRGB, log_encoding_ERIMMRGB, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -39,71 +53,79 @@ class TestCctfEncoding_ROMMRGB: cctf_encoding_ROMMRGB` definition unit tests methods. """ - def test_cctf_encoding_ROMMRGB(self) -> None: + def test_cctf_encoding_ROMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_encoding_ROMMRGB` definition. """ - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + cctf_encoding_ROMMRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(0.18), + xp_assert_close( + cctf_encoding_ROMMRGB(xp_as_array(0.18, xp=xp)), 0.385711424751138, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + cctf_encoding_ROMMRGB(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert cctf_encoding_ROMMRGB(0.18, out_int=True) == 98 + assert ( + as_ndarray(cctf_encoding_ROMMRGB(xp_as_array(0.18, xp=xp), out_int=True)) + == 98 + ) - assert cctf_encoding_ROMMRGB(0.18, bit_depth=12, out_int=True) == 1579 + assert ( + as_ndarray( + cctf_encoding_ROMMRGB( + xp_as_array(0.18, xp=xp), bit_depth=12, out_int=True + ) + ) + == 1579 + ) - def test_n_dimensional_cctf_encoding_ROMMRGB(self) -> None: + def test_n_dimensional_cctf_encoding_ROMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_encoding_ROMMRGB` definition n-dimensional arrays support. """ X = 0.18 - X_ROMM = cctf_encoding_ROMMRGB(X) + X_ROMM = as_ndarray(cctf_encoding_ROMMRGB(xp_as_array(X, xp=xp))) - X = np.tile(X, 6) - X_ROMM = np.tile(X_ROMM, 6) - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(X), X_ROMM, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp.tile(xp_as_array(X, xp=xp), (6,)) + X_ROMM = xp.tile(xp_as_array(X_ROMM, xp=xp), (6,)) + xp_assert_close(cctf_encoding_ROMMRGB(X), X_ROMM, atol=TOLERANCE_ABSOLUTE_TESTS) - X = np.reshape(X, (2, 3)) - X_ROMM = np.reshape(X_ROMM, (2, 3)) - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(X), X_ROMM, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3), xp=xp) + X_ROMM = xp_reshape(xp_as_array(X_ROMM, xp=xp), (2, 3), xp=xp) + xp_assert_close(cctf_encoding_ROMMRGB(X), X_ROMM, atol=TOLERANCE_ABSOLUTE_TESTS) - X = np.reshape(X, (2, 3, 1)) - X_ROMM = np.reshape(X_ROMM, (2, 3, 1)) - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(X), X_ROMM, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 1), xp=xp) + X_ROMM = xp_reshape(xp_as_array(X_ROMM, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(cctf_encoding_ROMMRGB(X), X_ROMM, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_cctf_encoding_ROMMRGB(self) -> None: + def test_domain_range_scale_cctf_encoding_ROMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_encoding_ROMMRGB` definition domain and range scale support. """ X = 0.18 - X_p = cctf_encoding_ROMMRGB(X) + X_p = as_ndarray(cctf_encoding_ROMMRGB(xp_as_array(X, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - cctf_encoding_ROMMRGB(X * factor), + xp_assert_close( + cctf_encoding_ROMMRGB(xp_as_array(X * factor, xp=xp)), X_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -124,79 +146,77 @@ class TestCctfDecoding_ROMMRGB: cctf_decoding_ROMMRGB` definition unit tests methods. """ - def test_cctf_decoding_ROMMRGB(self) -> None: + def test_cctf_decoding_ROMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_decoding_ROMMRGB` definition. """ - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + cctf_decoding_ROMMRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(0.385711424751138), + xp_assert_close( + cctf_decoding_ROMMRGB(xp_as_array(0.385711424751138, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + cctf_decoding_ROMMRGB(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(98, in_int=True), + xp_assert_close( + cctf_decoding_ROMMRGB(xp_as_array(98, xp=xp), in_int=True), 0.18, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(1579, bit_depth=12, in_int=True), + xp_assert_close( + cctf_decoding_ROMMRGB(xp_as_array(1579, xp=xp), bit_depth=12, in_int=True), 0.18, - atol=0.001, + atol=TOLERANCE_ABSOLUTE_TESTS * 10000, ) - def test_n_dimensional_cctf_decoding_ROMMRGB(self) -> None: + def test_n_dimensional_cctf_decoding_ROMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_decoding_ROMMRGB` definition n-dimensional arrays support. """ X_p = 0.385711424751138 - X = cctf_decoding_ROMMRGB(X_p) + X = as_ndarray(cctf_decoding_ROMMRGB(xp_as_array(X_p, xp=xp))) - X_p = np.tile(X_p, 6) - X = np.tile(X, 6) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp.tile(xp_as_array(X_p, xp=xp), (6,)) + X = xp.tile(xp_as_array(X, xp=xp), (6,)) + xp_assert_close(cctf_decoding_ROMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - X_p = np.reshape(X_p, (2, 3)) - X = np.reshape(X, (2, 3)) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3), xp=xp) + xp_assert_close(cctf_decoding_ROMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - X_p = np.reshape(X_p, (2, 3, 1)) - X = np.reshape(X, (2, 3, 1)) - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3, 1), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(cctf_decoding_ROMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_cctf_decoding_ROMMRGB(self) -> None: + def test_domain_range_scale_cctf_decoding_ROMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_decoding_ROMMRGB` definition domain and range scale support. """ X_p = 0.385711424751138 - X = cctf_decoding_ROMMRGB(X_p) + X = as_ndarray(cctf_decoding_ROMMRGB(xp_as_array(X_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - cctf_decoding_ROMMRGB(X_p * factor), + xp_assert_close( + cctf_decoding_ROMMRGB(xp_as_array(X_p * factor, xp=xp)), X * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -217,73 +237,79 @@ class TestCctfEncoding_RIMMRGB: cctf_encoding_RIMMRGB` definition unit tests methods. """ - def test_cctf_encoding_RIMMRGB(self) -> None: + def test_cctf_encoding_RIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_encoding_RIMMRGB` definition. """ - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + cctf_encoding_RIMMRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(0.18), + xp_assert_close( + cctf_encoding_RIMMRGB(xp_as_array(0.18, xp=xp)), 0.291673732475746, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(1.0), + xp_assert_close( + cctf_encoding_RIMMRGB(xp_as_array(1.0, xp=xp)), 0.713125234297525, atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert cctf_encoding_RIMMRGB(0.18, out_int=True) == 74 + assert ( + as_ndarray(cctf_encoding_RIMMRGB(xp_as_array(0.18, xp=xp), out_int=True)) + == 74 + ) - assert cctf_encoding_RIMMRGB(0.18, bit_depth=12, out_int=True) == 1194 + assert ( + as_ndarray( + cctf_encoding_RIMMRGB( + xp_as_array(0.18, xp=xp), bit_depth=12, out_int=True + ) + ) + == 1194 + ) - def test_n_dimensional_cctf_encoding_RIMMRGB(self) -> None: + def test_n_dimensional_cctf_encoding_RIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_encoding_RIMMRGB` definition n-dimensional arrays support. """ X = 0.18 - X_p = cctf_encoding_RIMMRGB(X) + X_p = as_ndarray(cctf_encoding_RIMMRGB(xp_as_array(X, xp=xp))) - X = np.tile(X, 6) - X_p = np.tile(X_p, 6) - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp.tile(xp_as_array(X, xp=xp), (6,)) + X_p = xp.tile(xp_as_array(X_p, xp=xp), (6,)) + xp_assert_close(cctf_encoding_RIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS) - X = np.reshape(X, (2, 3)) - X_p = np.reshape(X_p, (2, 3)) - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3), xp=xp) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(cctf_encoding_RIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS) - X = np.reshape(X, (2, 3, 1)) - X_p = np.reshape(X_p, (2, 3, 1)) - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 1), xp=xp) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(cctf_encoding_RIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_cctf_encoding_RIMMRGB(self) -> None: + def test_domain_range_scale_cctf_encoding_RIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_encoding_RIMMRGB` definition domain and range scale support. """ X = 0.18 - X_p = cctf_encoding_RIMMRGB(X) + X_p = as_ndarray(cctf_encoding_RIMMRGB(xp_as_array(X, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - cctf_encoding_RIMMRGB(X * factor), + xp_assert_close( + cctf_encoding_RIMMRGB(xp_as_array(X * factor, xp=xp)), X_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -304,81 +330,77 @@ class TestCctfDecoding_RIMMRGB: cctf_decoding_RIMMRGB` definition unit tests methods. """ - def test_cctf_decoding_RIMMRGB(self) -> None: + def test_cctf_decoding_RIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_decoding_RIMMRGB` definition. """ - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + cctf_decoding_RIMMRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(0.291673732475746), + xp_assert_close( + cctf_decoding_RIMMRGB(xp_as_array(0.291673732475746, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(0.713125234297525), + xp_assert_close( + cctf_decoding_RIMMRGB(xp_as_array(0.713125234297525, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(74, in_int=True), + xp_assert_close( + cctf_decoding_RIMMRGB(xp_as_array(74, xp=xp), in_int=True), 0.18, - atol=0.005, + atol=TOLERANCE_ABSOLUTE_TESTS * 50000, ) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(1194, bit_depth=12, in_int=True), + xp_assert_close( + cctf_decoding_RIMMRGB(xp_as_array(1194, xp=xp), bit_depth=12, in_int=True), 0.18, - atol=0.005, + atol=TOLERANCE_ABSOLUTE_TESTS * 50000, ) - def test_n_dimensional_cctf_decoding_RIMMRGB(self) -> None: + def test_n_dimensional_cctf_decoding_RIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_decoding_RIMMRGB` definition n-dimensional arrays support. """ X_p = 0.291673732475746 - X = cctf_decoding_RIMMRGB(X_p) + X = as_ndarray(cctf_decoding_RIMMRGB(xp_as_array(X_p, xp=xp))) - X_p = np.tile(X_p, 6) - X = np.tile(X, 6) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp.tile(xp_as_array(X_p, xp=xp), (6,)) + X = xp.tile(xp_as_array(X, xp=xp), (6,)) + xp_assert_close(cctf_decoding_RIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - X_p = np.reshape(X_p, (2, 3)) - X = np.reshape(X, (2, 3)) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3), xp=xp) + xp_assert_close(cctf_decoding_RIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - X_p = np.reshape(X_p, (2, 3, 1)) - X = np.reshape(X, (2, 3, 1)) - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3, 1), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(cctf_decoding_RIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_cctf_decoding_RIMMRGB(self) -> None: + def test_domain_range_scale_cctf_decoding_RIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ cctf_decoding_RIMMRGB` definition domain and range scale support. """ X_p = 0.291673732475746 - X = cctf_decoding_RIMMRGB(X_p) + X = as_ndarray(cctf_decoding_RIMMRGB(xp_as_array(X_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - cctf_decoding_RIMMRGB(X_p * factor), + xp_assert_close( + cctf_decoding_RIMMRGB(xp_as_array(X_p * factor, xp=xp)), X * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -399,73 +421,79 @@ class TestLog_encoding_ERIMMRGB: log_encoding_ERIMMRGB` definition unit tests methods. """ - def test_log_encoding_ERIMMRGB(self) -> None: + def test_log_encoding_ERIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ log_encoding_ERIMMRGB` definition. """ - np.testing.assert_allclose( - log_encoding_ERIMMRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_ERIMMRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ERIMMRGB(0.18), + xp_assert_close( + log_encoding_ERIMMRGB(xp_as_array(0.18, xp=xp)), 0.410052389492129, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ERIMMRGB(1.0), + xp_assert_close( + log_encoding_ERIMMRGB(xp_as_array(1.0, xp=xp)), 0.545458327405113, atol=TOLERANCE_ABSOLUTE_TESTS, ) - assert log_encoding_ERIMMRGB(0.18, out_int=True) == 105 + assert ( + as_ndarray(log_encoding_ERIMMRGB(xp_as_array(0.18, xp=xp), out_int=True)) + == 105 + ) - assert log_encoding_ERIMMRGB(0.18, bit_depth=12, out_int=True) == 1679 + assert ( + as_ndarray( + log_encoding_ERIMMRGB( + xp_as_array(0.18, xp=xp), bit_depth=12, out_int=True + ) + ) + == 1679 + ) - def test_n_dimensional_log_encoding_ERIMMRGB(self) -> None: + def test_n_dimensional_log_encoding_ERIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ log_encoding_ERIMMRGB` definition n-dimensional arrays support. """ X = 0.18 - X_p = log_encoding_ERIMMRGB(X) + X_p = as_ndarray(log_encoding_ERIMMRGB(xp_as_array(X, xp=xp))) - X = np.tile(X, 6) - X_p = np.tile(X_p, 6) - np.testing.assert_allclose( - log_encoding_ERIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp.tile(xp_as_array(X, xp=xp), (6,)) + X_p = xp.tile(xp_as_array(X_p, xp=xp), (6,)) + xp_assert_close(log_encoding_ERIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS) - X = np.reshape(X, (2, 3)) - X_p = np.reshape(X_p, (2, 3)) - np.testing.assert_allclose( - log_encoding_ERIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3), xp=xp) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_ERIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS) - X = np.reshape(X, (2, 3, 1)) - X_p = np.reshape(X_p, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_ERIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 1), xp=xp) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_ERIMMRGB(X), X_p, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_ERIMMRGB(self) -> None: + def test_domain_range_scale_log_encoding_ERIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ log_encoding_ERIMMRGB` definition domain and range scale support. """ X = 0.18 - X_p = log_encoding_ERIMMRGB(X) + X_p = as_ndarray(log_encoding_ERIMMRGB(xp_as_array(X, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ERIMMRGB(X * factor), + xp_assert_close( + log_encoding_ERIMMRGB(xp_as_array(X * factor, xp=xp)), X_p * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -486,81 +514,77 @@ class TestLog_decoding_ERIMMRGB: log_decoding_ERIMMRGB` definition unit tests methods. """ - def test_log_decoding_ERIMMRGB(self) -> None: + def test_log_decoding_ERIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ log_decoding_ERIMMRGB` definition. """ - np.testing.assert_allclose( - log_decoding_ERIMMRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_ERIMMRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(0.410052389492129), + xp_assert_close( + log_decoding_ERIMMRGB(xp_as_array(0.410052389492129, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(0.545458327405113), + xp_assert_close( + log_decoding_ERIMMRGB(xp_as_array(0.545458327405113, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(105, in_int=True), + xp_assert_close( + log_decoding_ERIMMRGB(xp_as_array(105, xp=xp), in_int=True), 0.18, - atol=0.005, + atol=TOLERANCE_ABSOLUTE_TESTS * 50000, ) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(1679, bit_depth=12, in_int=True), + xp_assert_close( + log_decoding_ERIMMRGB(xp_as_array(1679, xp=xp), bit_depth=12, in_int=True), 0.18, - atol=0.005, + atol=TOLERANCE_ABSOLUTE_TESTS * 50000, ) - def test_n_dimensional_log_decoding_ERIMMRGB(self) -> None: + def test_n_dimensional_log_decoding_ERIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ log_decoding_ERIMMRGB` definition n-dimensional arrays support. """ X_p = 0.410052389492129 - X = log_decoding_ERIMMRGB(X_p) + X = as_ndarray(log_decoding_ERIMMRGB(xp_as_array(X_p, xp=xp))) - X_p = np.tile(X_p, 6) - X = np.tile(X, 6) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp.tile(xp_as_array(X_p, xp=xp), (6,)) + X = xp.tile(xp_as_array(X, xp=xp), (6,)) + xp_assert_close(log_decoding_ERIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - X_p = np.reshape(X_p, (2, 3)) - X = np.reshape(X, (2, 3)) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_ERIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - X_p = np.reshape(X_p, (2, 3, 1)) - X = np.reshape(X, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_ERIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS - ) + X_p = xp_reshape(xp_as_array(X_p, xp=xp), (2, 3, 1), xp=xp) + X = xp_reshape(xp_as_array(X, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_ERIMMRGB(X_p), X, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_ERIMMRGB(self) -> None: + def test_domain_range_scale_log_decoding_ERIMMRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.rimm_romm_rgb.\ log_decoding_ERIMMRGB` definition domain and range scale support. """ X_p = 0.410052389492129 - X = log_decoding_ERIMMRGB(X_p) + X = as_ndarray(log_decoding_ERIMMRGB(xp_as_array(X_p, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ERIMMRGB(X_p * factor), + xp_assert_close( + log_decoding_ERIMMRGB(xp_as_array(X_p * factor, xp=xp)), X * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_smpte_240m.py b/colour/models/rgb/transfer_functions/tests/test_smpte_240m.py index 0d49b5fe46..9cf6cd6f82 100644 --- a/colour/models/rgb/transfer_functions/tests/test_smpte_240m.py +++ b/colour/models/rgb/transfer_functions/tests/test_smpte_240m.py @@ -3,11 +3,25 @@ :mod:`colour.models.rgb.transfer_functions.smpte_240m` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import eotf_SMPTE240M, oetf_SMPTE240M -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,73 +42,71 @@ class TestOetf_SMPTE240M: oetf_SMPTE240M` definition unit tests methods. """ - def test_oetf_SMPTE240M(self) -> None: + def test_oetf_SMPTE240M(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.smpte_240m.\ oetf_SMPTE240M` definition. """ - np.testing.assert_allclose( - oetf_SMPTE240M(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_SMPTE240M(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_SMPTE240M(0.02), + xp_assert_close( + oetf_SMPTE240M(xp_as_array(0.02, xp=xp)), 0.080000000000000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_SMPTE240M(0.18), + xp_assert_close( + oetf_SMPTE240M(xp_as_array(0.18, xp=xp)), 0.402285796753870, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - oetf_SMPTE240M(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + oetf_SMPTE240M(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_oetf_SMPTE240M(self) -> None: + def test_n_dimensional_oetf_SMPTE240M(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.smpte_240m.\ oetf_SMPTE240M` definition n-dimensional arrays support. """ L_c = 0.18 - V_c = oetf_SMPTE240M(L_c) + V_c = as_ndarray(oetf_SMPTE240M(xp_as_array(L_c, xp=xp))) - L_c = np.tile(L_c, 6) - V_c = np.tile(V_c, 6) - np.testing.assert_allclose( - oetf_SMPTE240M(L_c), V_c, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_c = xp.tile(xp_as_array(L_c, xp=xp), (6,)) + V_c = xp.tile(xp_as_array(V_c, xp=xp), (6,)) + xp_assert_close(oetf_SMPTE240M(L_c), V_c, atol=TOLERANCE_ABSOLUTE_TESTS) - L_c = np.reshape(L_c, (2, 3)) - V_c = np.reshape(V_c, (2, 3)) - np.testing.assert_allclose( - oetf_SMPTE240M(L_c), V_c, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_c = xp_reshape(xp_as_array(L_c, xp=xp), (2, 3), xp=xp) + V_c = xp_reshape(xp_as_array(V_c, xp=xp), (2, 3), xp=xp) + xp_assert_close(oetf_SMPTE240M(L_c), V_c, atol=TOLERANCE_ABSOLUTE_TESTS) - L_c = np.reshape(L_c, (2, 3, 1)) - V_c = np.reshape(V_c, (2, 3, 1)) - np.testing.assert_allclose( - oetf_SMPTE240M(L_c), V_c, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L_c = xp_reshape(xp_as_array(L_c, xp=xp), (2, 3, 1), xp=xp) + V_c = xp_reshape(xp_as_array(V_c, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(oetf_SMPTE240M(L_c), V_c, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_oetf_SMPTE240M(self) -> None: + def test_domain_range_scale_oetf_SMPTE240M(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.smpte_240m.\ oetf_SMPTE240M` definition domain and range scale support. """ L_c = 0.18 - V_c = oetf_SMPTE240M(L_c) + V_c = as_ndarray(oetf_SMPTE240M(xp_as_array(L_c, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - oetf_SMPTE240M(L_c * factor), + xp_assert_close( + oetf_SMPTE240M(xp_as_array(L_c * factor, xp=xp)), V_c * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -115,73 +127,71 @@ class TestEotf_SMPTE240M: eotf_SMPTE240M` definition unit tests methods. """ - def test_eotf_SMPTE240M(self) -> None: + def test_eotf_SMPTE240M(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.smpte_240m.\ eotf_SMPTE240M` definition. """ - np.testing.assert_allclose( - eotf_SMPTE240M(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_SMPTE240M(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_SMPTE240M(0.080000000000000), + xp_assert_close( + eotf_SMPTE240M(xp_as_array(0.080000000000000, xp=xp)), 0.02, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_SMPTE240M(0.402285796753870), + xp_assert_close( + eotf_SMPTE240M(xp_as_array(0.402285796753870, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_SMPTE240M(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_SMPTE240M(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_SMPTE240M(self) -> None: + def test_n_dimensional_eotf_SMPTE240M(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.smpte_240m.\ eotf_SMPTE240M` definition n-dimensional arrays support. """ V_r = 0.402285796753870 - L_r = eotf_SMPTE240M(V_r) + L_r = as_ndarray(eotf_SMPTE240M(xp_as_array(V_r, xp=xp))) - V_r = np.tile(V_r, 6) - L_r = np.tile(L_r, 6) - np.testing.assert_allclose( - eotf_SMPTE240M(V_r), L_r, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_r = xp.tile(xp_as_array(V_r, xp=xp), (6,)) + L_r = xp.tile(xp_as_array(L_r, xp=xp), (6,)) + xp_assert_close(eotf_SMPTE240M(V_r), L_r, atol=TOLERANCE_ABSOLUTE_TESTS) - V_r = np.reshape(V_r, (2, 3)) - L_r = np.reshape(L_r, (2, 3)) - np.testing.assert_allclose( - eotf_SMPTE240M(V_r), L_r, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_r = xp_reshape(xp_as_array(V_r, xp=xp), (2, 3), xp=xp) + L_r = xp_reshape(xp_as_array(L_r, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_SMPTE240M(V_r), L_r, atol=TOLERANCE_ABSOLUTE_TESTS) - V_r = np.reshape(V_r, (2, 3, 1)) - L_r = np.reshape(L_r, (2, 3, 1)) - np.testing.assert_allclose( - eotf_SMPTE240M(V_r), L_r, atol=TOLERANCE_ABSOLUTE_TESTS - ) + V_r = xp_reshape(xp_as_array(V_r, xp=xp), (2, 3, 1), xp=xp) + L_r = xp_reshape(xp_as_array(L_r, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_SMPTE240M(V_r), L_r, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_SMPTE240M(self) -> None: + def test_domain_range_scale_eotf_SMPTE240M(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.smpte_240m.\ eotf_SMPTE240M` definition domain and range scale support. """ V_r = 0.402285796753870 - L_r = eotf_SMPTE240M(V_r) + L_r = as_ndarray(eotf_SMPTE240M(xp_as_array(V_r, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_SMPTE240M(V_r * factor), + xp_assert_close( + eotf_SMPTE240M(xp_as_array(V_r * factor, xp=xp)), L_r * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_sony.py b/colour/models/rgb/transfer_functions/tests/test_sony.py index f82e69ce94..e9e51f9d1b 100644 --- a/colour/models/rgb/transfer_functions/tests/test_sony.py +++ b/colour/models/rgb/transfer_functions/tests/test_sony.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.sony` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -14,7 +18,17 @@ log_encoding_SLog2, log_encoding_SLog3, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -39,89 +53,83 @@ class TestLogEncoding_SLog: log_encoding_SLog` definition unit tests methods. """ - def test_log_encoding_SLog(self) -> None: + def test_log_encoding_SLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog` definition. """ - np.testing.assert_allclose( - log_encoding_SLog(0.0), + xp_assert_close( + log_encoding_SLog(xp_as_array(0.0, xp=xp)), 0.088251291513446, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog(0.18), + xp_assert_close( + log_encoding_SLog(xp_as_array(0.18, xp=xp)), 0.384970815928670, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog(0.18, 12), + xp_assert_close( + log_encoding_SLog(xp_as_array(0.18, xp=xp), 12), 0.384688786026891, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog(0.18, 10, False), + xp_assert_close( + log_encoding_SLog(xp_as_array(0.18, xp=xp), 10, False), 0.376512722254600, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog(0.18, 10, False, False), + xp_assert_close( + log_encoding_SLog(xp_as_array(0.18, xp=xp), 10, False, False), 0.359987846422154, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog(1.0), + xp_assert_close( + log_encoding_SLog(xp_as_array(1.0, xp=xp)), 0.638551684622532, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_SLog(self) -> None: + def test_n_dimensional_log_encoding_SLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_SLog(x) + y = as_ndarray(log_encoding_SLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_SLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_SLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_SLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_SLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_SLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_SLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_SLog(self) -> None: + def test_domain_range_scale_log_encoding_SLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog` definition domain and range scale support. """ x = 0.18 - y = log_encoding_SLog(x) + y = as_ndarray(log_encoding_SLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_SLog(x * factor), + xp_assert_close( + log_encoding_SLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -142,89 +150,83 @@ class TestLogDecoding_SLog: log_decoding_SLog` definition unit tests methods. """ - def test_log_decoding_SLog(self) -> None: + def test_log_decoding_SLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog` definition. """ - np.testing.assert_allclose( - log_decoding_SLog(0.088251291513446), + xp_assert_close( + log_decoding_SLog(xp_as_array(0.088251291513446, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog(0.384970815928670), + xp_assert_close( + log_decoding_SLog(xp_as_array(0.384970815928670, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog(0.384688786026891, 12), + xp_assert_close( + log_decoding_SLog(xp_as_array(0.384688786026891, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog(0.376512722254600, 10, False), + xp_assert_close( + log_decoding_SLog(xp_as_array(0.376512722254600, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog(0.359987846422154, 10, False, False), + xp_assert_close( + log_decoding_SLog(xp_as_array(0.359987846422154, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog(0.638551684622532), + xp_assert_close( + log_decoding_SLog(xp_as_array(0.638551684622532, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_SLog(self) -> None: + def test_n_dimensional_log_decoding_SLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog` definition n-dimensional arrays support. """ y = 0.384970815928670 - x = log_decoding_SLog(y) + x = as_ndarray(log_decoding_SLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_SLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_SLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_SLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_SLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_SLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_SLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_SLog(self) -> None: + def test_domain_range_scale_log_decoding_SLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog` definition domain and range scale support. """ y = 0.384970815928670 - x = log_decoding_SLog(y) + x = as_ndarray(log_decoding_SLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_SLog(y * factor), + xp_assert_close( + log_decoding_SLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -245,89 +247,83 @@ class TestLogEncoding_SLog2: log_encoding_SLog2` definition unit tests methods. """ - def test_log_encoding_SLog2(self) -> None: + def test_log_encoding_SLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog2` definition. """ - np.testing.assert_allclose( - log_encoding_SLog2(0.0), + xp_assert_close( + log_encoding_SLog2(xp_as_array(0.0, xp=xp)), 0.088251291513446, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog2(0.18), + xp_assert_close( + log_encoding_SLog2(xp_as_array(0.18, xp=xp)), 0.339532524633774, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog2(0.18, 12), + xp_assert_close( + log_encoding_SLog2(xp_as_array(0.18, xp=xp), 12), 0.339283782857486, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog2(0.18, 10, False), + xp_assert_close( + log_encoding_SLog2(xp_as_array(0.18, xp=xp), 10, False), 0.323449512215013, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog2(0.18, 10, False, False), + xp_assert_close( + log_encoding_SLog2(xp_as_array(0.18, xp=xp), 10, False, False), 0.307980741258647, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog2(1.0), + xp_assert_close( + log_encoding_SLog2(xp_as_array(1.0, xp=xp)), 0.585091059564112, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_SLog2(self) -> None: + def test_n_dimensional_log_encoding_SLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog2` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_SLog2(x) + y = as_ndarray(log_encoding_SLog2(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_SLog2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_SLog2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_SLog2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_SLog2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_SLog2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_SLog2(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_SLog2(self) -> None: + def test_domain_range_scale_log_encoding_SLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog2` definition domain and range scale support. """ x = 0.18 - y = log_encoding_SLog2(x) + y = as_ndarray(log_encoding_SLog2(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_SLog2(x * factor), + xp_assert_close( + log_encoding_SLog2(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -348,89 +344,83 @@ class TestLogDecoding_SLog2: log_decoding_SLog2` definition unit tests methods. """ - def test_log_decoding_SLog2(self) -> None: + def test_log_decoding_SLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog2` definition. """ - np.testing.assert_allclose( - log_decoding_SLog2(0.088251291513446), + xp_assert_close( + log_decoding_SLog2(xp_as_array(0.088251291513446, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog2(0.339532524633774), + xp_assert_close( + log_decoding_SLog2(xp_as_array(0.339532524633774, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog2(0.339283782857486, 12), + xp_assert_close( + log_decoding_SLog2(xp_as_array(0.339283782857486, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog2(0.323449512215013, 10, False), + xp_assert_close( + log_decoding_SLog2(xp_as_array(0.323449512215013, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog2(0.307980741258647, 10, False, False), + xp_assert_close( + log_decoding_SLog2(xp_as_array(0.307980741258647, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog2(0.585091059564112), + xp_assert_close( + log_decoding_SLog2(xp_as_array(0.585091059564112, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_SLog2(self) -> None: + def test_n_dimensional_log_decoding_SLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog2` definition n-dimensional arrays support. """ y = 0.339532524633774 - x = log_decoding_SLog2(y) + x = as_ndarray(log_decoding_SLog2(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_SLog2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_SLog2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_SLog2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_SLog2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_SLog2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_SLog2(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_SLog2(self) -> None: + def test_domain_range_scale_log_decoding_SLog2(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog2` definition domain and range scale support. """ y = 0.339532524633774 - x = log_decoding_SLog2(y) + x = as_ndarray(log_decoding_SLog2(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_SLog2(y * factor), + xp_assert_close( + log_decoding_SLog2(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -451,89 +441,83 @@ class TestLogEncoding_SLog3: log_encoding_SLog3` definition unit tests methods. """ - def test_log_encoding_SLog3(self) -> None: + def test_log_encoding_SLog3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog3` definition. """ - np.testing.assert_allclose( - log_encoding_SLog3(0.0), + xp_assert_close( + log_encoding_SLog3(xp_as_array(0.0, xp=xp)), 0.092864125122190, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog3(0.18), + xp_assert_close( + log_encoding_SLog3(xp_as_array(0.18, xp=xp)), 0.41055718475073, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog3(0.18, 12), + xp_assert_close( + log_encoding_SLog3(xp_as_array(0.18, xp=xp), 12), 0.410557184750733, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog3(0.18, 10, False), + xp_assert_close( + log_encoding_SLog3(xp_as_array(0.18, xp=xp), 10, False), 0.406392694063927, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog3(0.18, 10, False, False), + xp_assert_close( + log_encoding_SLog3(xp_as_array(0.18, xp=xp), 10, False, False), 0.393489294768447, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_SLog3(1.0), + xp_assert_close( + log_encoding_SLog3(xp_as_array(1.0, xp=xp)), 0.596027343690123, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_SLog3(self) -> None: + def test_n_dimensional_log_encoding_SLog3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog3` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_SLog3(x) + y = as_ndarray(log_encoding_SLog3(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_SLog3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_SLog3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_SLog3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_SLog3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_SLog3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_SLog3(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_SLog3(self) -> None: + def test_domain_range_scale_log_encoding_SLog3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_encoding_SLog3` definition domain and range scale support. """ x = 0.18 - y = log_encoding_SLog3(x) + y = as_ndarray(log_encoding_SLog3(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_SLog3(x * factor), + xp_assert_close( + log_encoding_SLog3(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -554,89 +538,83 @@ class TestLogDecoding_SLog3: log_decoding_SLog3` definition unit tests methods. """ - def test_log_decoding_SLog3(self) -> None: + def test_log_decoding_SLog3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog3` definition. """ - np.testing.assert_allclose( - log_decoding_SLog3(0.092864125122190), + xp_assert_close( + log_decoding_SLog3(xp_as_array(0.092864125122190, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog3(0.41055718475073), + xp_assert_close( + log_decoding_SLog3(xp_as_array(0.41055718475073, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog3(0.410557184750733, 12), + xp_assert_close( + log_decoding_SLog3(xp_as_array(0.410557184750733, xp=xp), 12), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog3(0.406392694063927, 10, False), + xp_assert_close( + log_decoding_SLog3(xp_as_array(0.406392694063927, xp=xp), 10, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog3(0.393489294768447, 10, False, False), + xp_assert_close( + log_decoding_SLog3(xp_as_array(0.393489294768447, xp=xp), 10, False, False), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_SLog3(0.596027343690123), + xp_assert_close( + log_decoding_SLog3(xp_as_array(0.596027343690123, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_SLog3(self) -> None: + def test_n_dimensional_log_decoding_SLog3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog3` definition n-dimensional arrays support. """ y = 0.41055718475073 - x = log_decoding_SLog3(y) + x = as_ndarray(log_decoding_SLog3(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_SLog3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_SLog3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_SLog3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_SLog3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_SLog3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_SLog3(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_SLog3(self) -> None: + def test_domain_range_scale_log_decoding_SLog3(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sony.\ log_decoding_SLog3` definition domain and range scale support. """ y = 0.41055718475073 - x = log_decoding_SLog3(y) + x = as_ndarray(log_decoding_SLog3(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_SLog3(y * factor), + xp_assert_close( + log_decoding_SLog3(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_srgb.py b/colour/models/rgb/transfer_functions/tests/test_srgb.py index 99a38393d7..f182f37c5a 100644 --- a/colour/models/rgb/transfer_functions/tests/test_srgb.py +++ b/colour/models/rgb/transfer_functions/tests/test_srgb.py @@ -3,11 +3,25 @@ module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import eotf_inverse_sRGB, eotf_sRGB -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,67 +42,65 @@ class TestEotf_inverse_sRGB: definition unit tests methods. """ - def test_eotf_inverse_sRGB(self) -> None: + def test_eotf_inverse_sRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sRGB.\ eotf_inverse_sRGB` definition. """ - np.testing.assert_allclose( - eotf_inverse_sRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_sRGB(xp_as_array(0.0, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_sRGB(0.18), + xp_assert_close( + eotf_inverse_sRGB(xp_as_array(0.18, xp=xp)), 0.461356129500442, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_sRGB(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_sRGB(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_sRGB(self) -> None: + def test_n_dimensional_eotf_inverse_sRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sRGB.\ eotf_inverse_sRGB` definition n-dimensional arrays support. """ L = 0.18 - V = eotf_inverse_sRGB(L) + V = as_ndarray(eotf_inverse_sRGB(xp_as_array(L, xp=xp))) - L = np.tile(L, 6) - V = np.tile(V, 6) - np.testing.assert_allclose( - eotf_inverse_sRGB(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + xp_assert_close(eotf_inverse_sRGB(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3)) - V = np.reshape(V, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_sRGB(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_inverse_sRGB(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - L = np.reshape(L, (2, 3, 1)) - V = np.reshape(V, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_sRGB(L), V, atol=TOLERANCE_ABSOLUTE_TESTS - ) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_inverse_sRGB(L), V, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_inverse_sRGB(self) -> None: + def test_domain_range_scale_eotf_inverse_sRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sRGB.\ eotf_inverse_sRGB` definition domain and range scale support. """ L = 0.18 - V = eotf_inverse_sRGB(L) + V = as_ndarray(eotf_inverse_sRGB(xp_as_array(L, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_sRGB(L * factor), + xp_assert_close( + eotf_inverse_sRGB(xp_as_array(L * factor, xp=xp)), V * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -109,55 +121,61 @@ class TestEotf_sRGB: definition unit tests methods. """ - def test_eotf_sRGB(self) -> None: + def test_eotf_sRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sRGB.\ eotf_sRGB` definition. """ - np.testing.assert_allclose(eotf_sRGB(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + eotf_sRGB(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - eotf_sRGB(0.461356129500442), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_sRGB(xp_as_array(0.461356129500442, xp=xp)), + 0.18, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose(eotf_sRGB(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + eotf_sRGB(xp_as_array(1.0, xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - def test_n_dimensional_eotf_sRGB(self) -> None: + def test_n_dimensional_eotf_sRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sRGB.\ eotf_sRGB` definition n-dimensional arrays support. """ V = 0.461356129500442 - L = eotf_sRGB(V) + L = as_ndarray(eotf_sRGB(xp_as_array(V, xp=xp))) - V = np.tile(V, 6) - L = np.tile(L, 6) - np.testing.assert_allclose(eotf_sRGB(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + L = xp.tile(xp_as_array(L, xp=xp), (6,)) + xp_assert_close(eotf_sRGB(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3)) - L = np.reshape(L, (2, 3)) - np.testing.assert_allclose(eotf_sRGB(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_sRGB(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - V = np.reshape(V, (2, 3, 1)) - L = np.reshape(L, (2, 3, 1)) - np.testing.assert_allclose(eotf_sRGB(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + L = xp_reshape(xp_as_array(L, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_sRGB(V), L, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_sRGB(self) -> None: + def test_domain_range_scale_eotf_sRGB(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.sRGB.\ eotf_sRGB` definition domain and range scale support. """ V = 0.461356129500442 - L = eotf_sRGB(V) + L = as_ndarray(eotf_sRGB(xp_as_array(V, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_sRGB(V * factor), + xp_assert_close( + eotf_sRGB(xp_as_array(V * factor, xp=xp)), L * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_st_2084.py b/colour/models/rgb/transfer_functions/tests/test_st_2084.py index 2af02a7f88..5aeca0b0e5 100644 --- a/colour/models/rgb/transfer_functions/tests/test_st_2084.py +++ b/colour/models/rgb/transfer_functions/tests/test_st_2084.py @@ -3,11 +3,26 @@ :mod:`colour.models.rgb.transfer_functions.st_2084` module. """ +from __future__ import annotations + +import typing + import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import eotf_inverse_ST2084, eotf_ST2084 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,75 +43,71 @@ class TestEotf_inverse_ST2084: eotf_inverse_ST2084` definition unit tests methods. """ - def test_eotf_inverse_ST2084(self) -> None: + def test_eotf_inverse_ST2084(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.st_2084.\ eotf_inverse_ST2084` definition. """ - np.testing.assert_allclose( - eotf_inverse_ST2084(0.0), + xp_assert_close( + eotf_inverse_ST2084(xp_as_array(0.0, xp=xp)), 0.000000730955903, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_ST2084(100), + xp_assert_close( + eotf_inverse_ST2084(xp_as_array(100, xp=xp)), 0.508078421517399, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_ST2084(400), + xp_assert_close( + eotf_inverse_ST2084(xp_as_array(400, xp=xp)), 0.652578597563067, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_inverse_ST2084(5000, 5000), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_inverse_ST2084(xp_as_array(5000, xp=xp), 5000), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_inverse_ST2084(self) -> None: + def test_n_dimensional_eotf_inverse_ST2084(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.st_2084.\ eotf_inverse_ST2084` definition n-dimensional arrays support. """ C = 100 - N = eotf_inverse_ST2084(C) + N = as_ndarray(eotf_inverse_ST2084(xp_as_array(C, xp=xp))) - C = np.tile(C, 6) - N = np.tile(N, 6) - np.testing.assert_allclose( - eotf_inverse_ST2084(C), N, atol=TOLERANCE_ABSOLUTE_TESTS - ) + C = xp.tile(xp_as_array(C, xp=xp), (6,)) + N = xp.tile(xp_as_array(N, xp=xp), (6,)) + xp_assert_close(eotf_inverse_ST2084(C), N, atol=TOLERANCE_ABSOLUTE_TESTS) - C = np.reshape(C, (2, 3)) - N = np.reshape(N, (2, 3)) - np.testing.assert_allclose( - eotf_inverse_ST2084(C), N, atol=TOLERANCE_ABSOLUTE_TESTS - ) + C = xp_reshape(xp_as_array(C, xp=xp), (2, 3), xp=xp) + N = xp_reshape(xp_as_array(N, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_inverse_ST2084(C), N, atol=TOLERANCE_ABSOLUTE_TESTS) - C = np.reshape(C, (2, 3, 1)) - N = np.reshape(N, (2, 3, 1)) - np.testing.assert_allclose( - eotf_inverse_ST2084(C), N, atol=TOLERANCE_ABSOLUTE_TESTS - ) + C = xp_reshape(xp_as_array(C, xp=xp), (2, 3, 1), xp=xp) + N = xp_reshape(xp_as_array(N, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_inverse_ST2084(C), N, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_inverse_ST2084(self) -> None: + def test_domain_range_scale_eotf_inverse_ST2084(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.st_2084.\ eotf_inverse_ST2084` definition domain and range scale support. """ C = 100 - N = eotf_inverse_ST2084(C) + N = as_ndarray(eotf_inverse_ST2084(xp_as_array(C, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_inverse_ST2084(C * factor), + xp_assert_close( + eotf_inverse_ST2084(xp_as_array(C * factor, xp=xp)), N * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -117,61 +128,70 @@ class TestEotf_ST2084: definition unit tests methods. """ - def test_eotf_ST2084(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_eotf_ST2084(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.st_2084.\ eotf_ST2084` definition. """ - np.testing.assert_allclose(eotf_ST2084(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + eotf_ST2084(xp_as_array(0.0, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) - np.testing.assert_allclose( - eotf_ST2084(0.508078421517399), 100, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_ST2084(xp_as_array(0.508078421517399, xp=xp)), + 100, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_ST2084(0.652578597563067), 400, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_ST2084(xp_as_array(0.652578597563067, xp=xp)), + 400, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - eotf_ST2084(1.0, 5000), 5000.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + eotf_ST2084(xp_as_array(1.0, xp=xp), 5000), + 5000.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_eotf_ST2084(self) -> None: + def test_n_dimensional_eotf_ST2084(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.st_2084.\ eotf_ST2084` definition n-dimensional arrays support. """ N = 0.508078421517399 - C = eotf_ST2084(N) + C = as_ndarray(eotf_ST2084(xp_as_array(N, xp=xp))) - N = np.tile(N, 6) - C = np.tile(C, 6) - np.testing.assert_allclose(eotf_ST2084(N), C, atol=TOLERANCE_ABSOLUTE_TESTS) + N = xp.tile(xp_as_array(N, xp=xp), (6,)) + C = xp.tile(xp_as_array(C, xp=xp), (6,)) + xp_assert_close(eotf_ST2084(N), C, atol=TOLERANCE_ABSOLUTE_TESTS) - N = np.reshape(N, (2, 3)) - C = np.reshape(C, (2, 3)) - np.testing.assert_allclose(eotf_ST2084(N), C, atol=TOLERANCE_ABSOLUTE_TESTS) + N = xp_reshape(xp_as_array(N, xp=xp), (2, 3), xp=xp) + C = xp_reshape(xp_as_array(C, xp=xp), (2, 3), xp=xp) + xp_assert_close(eotf_ST2084(N), C, atol=TOLERANCE_ABSOLUTE_TESTS) - N = np.reshape(N, (2, 3, 1)) - C = np.reshape(C, (2, 3, 1)) - np.testing.assert_allclose(eotf_ST2084(N), C, atol=TOLERANCE_ABSOLUTE_TESTS) + N = xp_reshape(xp_as_array(N, xp=xp), (2, 3, 1), xp=xp) + C = xp_reshape(xp_as_array(C, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(eotf_ST2084(N), C, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_eotf_ST2084(self) -> None: + def test_domain_range_scale_eotf_ST2084(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.st_2084.\ eotf_ST2084` definition domain and range scale support. """ N = 0.508078421517399 - C = eotf_ST2084(N) + C = as_ndarray(eotf_ST2084(xp_as_array(N, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - eotf_ST2084(N * factor), + xp_assert_close( + eotf_ST2084(xp_as_array(N * factor, xp=xp)), C * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_viper_log.py b/colour/models/rgb/transfer_functions/tests/test_viper_log.py index 394a55d536..080d538af4 100644 --- a/colour/models/rgb/transfer_functions/tests/test_viper_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_viper_log.py @@ -3,6 +3,10 @@ :mod:`colour.models.rgb.transfer_functions.viper_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_ViperLog, log_encoding_ViperLog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,67 +45,65 @@ class TestLogEncoding_ViperLog: log_encoding_ViperLog` definition unit tests methods. """ - def test_log_encoding_ViperLog(self) -> None: + def test_log_encoding_ViperLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.viper_log.\ log_encoding_ViperLog` definition. """ - np.testing.assert_allclose( - log_encoding_ViperLog(0.0), -np.inf, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_ViperLog(xp_as_array(0.0, xp=xp)), + -np.inf, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ViperLog(0.18), + xp_assert_close( + log_encoding_ViperLog(xp_as_array(0.18, xp=xp)), 0.636008067010413, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_ViperLog(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_encoding_ViperLog(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_ViperLog(self) -> None: + def test_n_dimensional_log_encoding_ViperLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.viper_log.\ log_encoding_ViperLog` definition n-dimensional arrays support. """ x = 0.18 - y = log_encoding_ViperLog(x) + y = as_ndarray(log_encoding_ViperLog(xp_as_array(x, xp=xp))) - x = np.tile(x, 6) - y = np.tile(y, 6) - np.testing.assert_allclose( - log_encoding_ViperLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + xp_assert_close(log_encoding_ViperLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3)) - y = np.reshape(y, (2, 3)) - np.testing.assert_allclose( - log_encoding_ViperLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_ViperLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - x = np.reshape(x, (2, 3, 1)) - y = np.reshape(y, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_ViperLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS - ) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_ViperLog(x), y, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_ViperLog(self) -> None: + def test_domain_range_scale_log_encoding_ViperLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.viper_log.\ log_encoding_ViperLog` definition domain and range scale support. """ x = 0.18 - y = log_encoding_ViperLog(x) + y = as_ndarray(log_encoding_ViperLog(xp_as_array(x, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_ViperLog(x * factor), + xp_assert_close( + log_encoding_ViperLog(xp_as_array(x * factor, xp=xp)), y * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -112,67 +124,65 @@ class TestLogDecoding_ViperLog: log_decoding_ViperLog` definition unit tests methods. """ - def test_log_decoding_ViperLog(self) -> None: + def test_log_decoding_ViperLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.viper_log.\ log_decoding_ViperLog` definition. """ - np.testing.assert_allclose( - log_decoding_ViperLog(-np.inf), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_ViperLog(xp_as_array(-np.inf, xp=xp)), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ViperLog(0.636008067010413), + xp_assert_close( + log_decoding_ViperLog(xp_as_array(0.636008067010413, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_ViperLog(1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + log_decoding_ViperLog(xp_as_array(1.0, xp=xp)), + 1.0, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_ViperLog(self) -> None: + def test_n_dimensional_log_decoding_ViperLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.viper_log.\ log_decoding_ViperLog` definition n-dimensional arrays support. """ y = 0.636008067010413 - x = log_decoding_ViperLog(y) + x = as_ndarray(log_decoding_ViperLog(xp_as_array(y, xp=xp))) - y = np.tile(y, 6) - x = np.tile(x, 6) - np.testing.assert_allclose( - log_decoding_ViperLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp.tile(xp_as_array(y, xp=xp), (6,)) + x = xp.tile(xp_as_array(x, xp=xp), (6,)) + xp_assert_close(log_decoding_ViperLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3)) - x = np.reshape(x, (2, 3)) - np.testing.assert_allclose( - log_decoding_ViperLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_ViperLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - y = np.reshape(y, (2, 3, 1)) - x = np.reshape(x, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_ViperLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS - ) + y = xp_reshape(xp_as_array(y, xp=xp), (2, 3, 1), xp=xp) + x = xp_reshape(xp_as_array(x, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_ViperLog(y), x, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_ViperLog(self) -> None: + def test_domain_range_scale_log_decoding_ViperLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.viper_log.\ log_decoding_ViperLog` definition domain and range scale support. """ y = 0.636008067010413 - x = log_decoding_ViperLog(y) + x = as_ndarray(log_decoding_ViperLog(xp_as_array(y, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_ViperLog(y * factor), + xp_assert_close( + log_decoding_ViperLog(xp_as_array(y * factor, xp=xp)), x * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/tests/test_xiaomi_mi_log.py b/colour/models/rgb/transfer_functions/tests/test_xiaomi_mi_log.py index e3d0fef4b2..4a0861d5d2 100644 --- a/colour/models/rgb/transfer_functions/tests/test_xiaomi_mi_log.py +++ b/colour/models/rgb/transfer_functions/tests/test_xiaomi_mi_log.py @@ -3,6 +3,10 @@ xiaomi_mi_log` module. """ +from __future__ import annotations + +import typing + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -10,7 +14,17 @@ log_decoding_MiLog, log_encoding_MiLog, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,72 +45,66 @@ class TestLogEncoding_MiLog: log_encoding_MiLog` definition unit tests methods. """ - def test_log_encoding_MiLog(self) -> None: + def test_log_encoding_MiLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.xiaomi_mi_log.\ log_encoding_MiLog` definition. """ # Test values from the whitepaper - np.testing.assert_allclose( - log_encoding_MiLog(0.0), + xp_assert_close( + log_encoding_MiLog(xp_as_array(0.0, xp=xp)), 0.14742742, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_MiLog(0.18), + xp_assert_close( + log_encoding_MiLog(xp_as_array(0.18, xp=xp)), 0.45345968, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_encoding_MiLog(0.90), + xp_assert_close( + log_encoding_MiLog(xp_as_array(0.90, xp=xp)), 0.66086763, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_encoding_MiLog(self) -> None: + def test_n_dimensional_log_encoding_MiLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.xiaomi_mi_log.\ log_encoding_MiLog` definition n-dimensional arrays support. """ R = 0.18 - P = log_encoding_MiLog(R) + P = as_ndarray(log_encoding_MiLog(xp_as_array(R, xp=xp))) - R = np.tile(R, 6) - P = np.tile(P, 6) - np.testing.assert_allclose( - log_encoding_MiLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS - ) + R = xp.tile(xp_as_array(R, xp=xp), (6,)) + P = xp.tile(xp_as_array(P, xp=xp), (6,)) + xp_assert_close(log_encoding_MiLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS) - R = np.reshape(R, (2, 3)) - P = np.reshape(P, (2, 3)) - np.testing.assert_allclose( - log_encoding_MiLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS - ) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3), xp=xp) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_encoding_MiLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS) - R = np.reshape(R, (2, 3, 1)) - P = np.reshape(P, (2, 3, 1)) - np.testing.assert_allclose( - log_encoding_MiLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS - ) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3, 1), xp=xp) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_encoding_MiLog(R), P, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_encoding_MiLog(self) -> None: + def test_domain_range_scale_log_encoding_MiLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.xiaomi_mi_log.\ log_encoding_MiLog` definition domain and range scale support. """ R = 0.18 - P = log_encoding_MiLog(R) + P = as_ndarray(log_encoding_MiLog(xp_as_array(R, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_encoding_MiLog(R * factor), + xp_assert_close( + log_encoding_MiLog(xp_as_array(R * factor, xp=xp)), P * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -117,72 +125,66 @@ class TestLogDecoding_MiLog: log_decoding_MiLog` definition unit tests methods. """ - def test_log_decoding_MiLog(self) -> None: + def test_log_decoding_MiLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.xiaomi_mi_log.\ log_decoding_MiLog` definition. """ # Test inverse of values from the whitepaper - np.testing.assert_allclose( - log_decoding_MiLog(0.14742742), + xp_assert_close( + log_decoding_MiLog(xp_as_array(0.14742742, xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_MiLog(0.45345968), + xp_assert_close( + log_decoding_MiLog(xp_as_array(0.45345968, xp=xp)), 0.18, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - log_decoding_MiLog(0.66086763), + xp_assert_close( + log_decoding_MiLog(xp_as_array(0.66086763, xp=xp)), 0.90, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_log_decoding_MiLog(self) -> None: + def test_n_dimensional_log_decoding_MiLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.xiaomi_mi_log.\ log_decoding_MiLog` definition n-dimensional arrays support. """ P = 0.45345968 - R = log_decoding_MiLog(P) + R = as_ndarray(log_decoding_MiLog(xp_as_array(P, xp=xp))) - P = np.tile(P, 6) - R = np.tile(R, 6) - np.testing.assert_allclose( - log_decoding_MiLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + P = xp.tile(xp_as_array(P, xp=xp), (6,)) + R = xp.tile(xp_as_array(R, xp=xp), (6,)) + xp_assert_close(log_decoding_MiLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS) - P = np.reshape(P, (2, 3)) - R = np.reshape(R, (2, 3)) - np.testing.assert_allclose( - log_decoding_MiLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3), xp=xp) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3), xp=xp) + xp_assert_close(log_decoding_MiLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS) - P = np.reshape(P, (2, 3, 1)) - R = np.reshape(R, (2, 3, 1)) - np.testing.assert_allclose( - log_decoding_MiLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + P = xp_reshape(xp_as_array(P, xp=xp), (2, 3, 1), xp=xp) + R = xp_reshape(xp_as_array(R, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(log_decoding_MiLog(P), R, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_log_decoding_MiLog(self) -> None: + def test_domain_range_scale_log_decoding_MiLog(self, xp: ModuleType) -> None: """ Test :func:`colour.models.rgb.transfer_functions.xiaomi_mi_log.\ log_decoding_MiLog` definition domain and range scale support. """ P = 0.45345968 - R = log_decoding_MiLog(P) + R = as_ndarray(log_decoding_MiLog(xp_as_array(P, xp=xp))) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - log_decoding_MiLog(P * factor), + xp_assert_close( + log_decoding_MiLog(xp_as_array(P * factor, xp=xp)), R * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/rgb/transfer_functions/viper_log.py b/colour/models/rgb/transfer_functions/viper_log.py index 029cb47fb3..de1cd2d44d 100644 --- a/colour/models/rgb/transfer_functions/viper_log.py +++ b/colour/models/rgb/transfer_functions/viper_log.py @@ -17,13 +17,11 @@ from __future__ import annotations -import numpy as np - from colour.hints import ( # noqa: TC001 Domain1, Range1, ) -from colour.utilities import as_float, from_range_1, to_domain_1 +from colour.utilities import array_namespace, as_float, from_range_1, to_domain_1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -78,7 +76,9 @@ def log_encoding_ViperLog(x: Domain1) -> Range1: x = to_domain_1(x) - y = (1023 + 500 * np.log10(x)) / 1023 + xp = array_namespace(x) + + y = (1023 + 500 * xp.log10(x)) / 1023 return as_float(from_range_1(y)) diff --git a/colour/models/rgb/ycbcr.py b/colour/models/rgb/ycbcr.py index 242c69e671..09ff88433f 100644 --- a/colour/models/rgb/ycbcr.py +++ b/colour/models/rgb/ycbcr.py @@ -69,6 +69,7 @@ ) from colour.utilities import ( CanonicalMapping, + array_namespace, as_float_array, as_int_array, domain_range_scale, @@ -76,6 +77,7 @@ to_domain_1, tsplit, tstack, + xp_as_float_array, ) __author__ = "Colour Developers" @@ -162,7 +164,9 @@ def round_BT2100(a: ArrayLike) -> NDArrayFloat: array([0., 1., 1.]) """ - return cast("NDArrayFloat", np.sign(a) * np.floor(np.abs(a) + 0.5)) + xp = array_namespace(a) + + return cast("NDArrayFloat", xp.sign(a) * xp.floor(xp.abs(a) + 0.5)) def ranges_YCbCr(bits: int, is_legal: bool, is_int: bool) -> NDArrayFloat: @@ -308,18 +312,20 @@ def matrix_YCbCr( array([ 38, 140, 171]) """ + xp = array_namespace(K) + Kr, Kb = K Cb_scale, Cr_scale = S Y_min, Y_max, C_min, C_max = ranges_YCbCr(bits, is_legal, is_int) - Y = np.array([Kr, (1 - Kr - Kb), Kb]) - Cb = Cb_scale * (np.array([0, 0, 1]) - Y) / (1 - Kb) - Cr = Cr_scale * (np.array([1, 0, 0]) - Y) / (1 - Kr) + Y = xp_as_float_array([Kr, (1 - Kr - Kb), Kb], xp=xp, like=K) + Cb = Cb_scale * (xp_as_float_array([0, 0, 1], xp=xp, like=K) - Y) / (1 - Kb) + Cr = Cr_scale * (xp_as_float_array([1, 0, 0], xp=xp, like=K) - Y) / (1 - Kr) Y = Y * (Y_max - Y_min) Cb = Cb * (C_max - C_min) Cr = Cr * (C_max - C_min) - return np.linalg.inv(np.vstack([Y, Cb, Cr])) + return xp.linalg.inv(xp.stack([Y, Cb, Cr])) def offset_YCbCr( @@ -549,6 +555,8 @@ def RGB_to_YCbCr( RGB = as_float_array(RGB) if in_int else to_domain_1(RGB) + xp = array_namespace(RGB) + Kr, Kb = K Cb_scale, Cr_scale = S RGB_min, RGB_max = kwargs.get("in_range", CV_range(in_bits, in_legal, in_int)) @@ -574,7 +582,7 @@ def RGB_to_YCbCr( if out_int: return as_int_array( - round_BT2100(np.clip(YCbCr, 0, 2**out_bits - 1) if clamp_int else YCbCr) + round_BT2100(xp.clip(YCbCr, 0, 2**out_bits - 1) if clamp_int else YCbCr) ) return from_range_1(YCbCr) @@ -704,6 +712,8 @@ def YCbCr_to_RGB( YCbCr = as_float_array(YCbCr) if in_int else to_domain_1(YCbCr) + xp = array_namespace(YCbCr) + Y, Cb, Cr = tsplit(YCbCr) Kr, Kb = K Cb_scale, Cr_scale = S @@ -727,7 +737,7 @@ def YCbCr_to_RGB( return ( as_int_array( - round_BT2100(np.clip(RGB, 0, 2**out_bits - 1) if clamp_int else RGB) + round_BT2100(xp.clip(RGB, 0, 2**out_bits - 1) if clamp_int else RGB) ) if out_int else from_range_1(RGB) @@ -818,7 +828,12 @@ def RGB_to_YcCbcCrc( array([422, 512, 512]...) """ - R, G, B = tsplit(to_domain_1(RGB)) + RGB = to_domain_1(RGB) + + xp = array_namespace(RGB) + + R, G, B = tsplit(RGB) + Y_min, Y_max, C_min, C_max = kwargs.get( "out_range", ranges_YCbCr(out_bits, out_legal, out_int) ) @@ -830,8 +845,8 @@ def RGB_to_YcCbcCrc( R = oetf_BT2020(R, is_12_bits_system=is_12_bits_system) B = oetf_BT2020(B, is_12_bits_system=is_12_bits_system) - Cbc = np.where((B - Yc) <= 0, (B - Yc) / 1.9404, (B - Yc) / 1.5816) - Crc = np.where((R - Yc) <= 0, (R - Yc) / 1.7184, (R - Yc) / 0.9936) + Cbc = xp.where((B - Yc) <= 0, (B - Yc) / 1.9404, (B - Yc) / 1.5816) + Crc = xp.where((R - Yc) <= 0, (R - Yc) / 1.7184, (R - Yc) / 0.9936) Yc = Yc * (Y_max - Y_min) + Y_min Cbc = Cbc * (C_max - C_min) + (C_max + C_min) / 2 Crc = Crc * (C_max - C_min) + (C_max + C_min) / 2 @@ -839,7 +854,7 @@ def RGB_to_YcCbcCrc( YcCbcCrc = tstack([Yc, Cbc, Crc]) if out_int: - return as_int_array(np.round(YcCbcCrc)) + return as_int_array(xp.round(YcCbcCrc)) return from_range_1(YcCbcCrc) @@ -932,6 +947,8 @@ def YcCbcCrc_to_RGB( YcCbcCrc = as_float_array(YcCbcCrc) if in_int else to_domain_1(YcCbcCrc) + xp = array_namespace(YcCbcCrc) + Yc, Cbc, Crc = tsplit(YcCbcCrc) Y_min, Y_max, C_min, C_max = kwargs.get( "in_range", ranges_YCbCr(in_bits, in_legal, in_int) @@ -940,8 +957,8 @@ def YcCbcCrc_to_RGB( Yc = (Yc - Y_min) * (1 / (Y_max - Y_min)) Cbc = (Cbc - (C_max + C_min) / 2) * (1 / (C_max - C_min)) Crc = (Crc - (C_max + C_min) / 2) * (1 / (C_max - C_min)) - B = np.where(Cbc <= 0, Cbc * 1.9404 + Yc, Cbc * 1.5816 + Yc) - R = np.where(Crc <= 0, Crc * 1.7184 + Yc, Crc * 0.9936 + Yc) + B = xp.where(Cbc <= 0, Cbc * 1.9404 + Yc, Cbc * 1.5816 + Yc) + R = xp.where(Crc <= 0, Crc * 1.7184 + Yc, Crc * 0.9936 + Yc) with domain_range_scale("ignore"): Yc = oetf_inverse_BT2020(Yc, is_12_bits_system) diff --git a/colour/models/sucs.py b/colour/models/sucs.py index f393e3f024..6f9ae413e8 100644 --- a/colour/models/sucs.py +++ b/colour/models/sucs.py @@ -39,6 +39,7 @@ ) from colour.models import Iab_to_XYZ, XYZ_to_Iab from colour.utilities import ( + array_namespace, as_float, domain_range_scale, from_range_1, @@ -49,6 +50,8 @@ to_domain_degrees, tsplit, tstack, + xp_degrees, + xp_radians, ) __author__ = "UltraMo114(Molin Li), Colour Developers" @@ -258,9 +261,13 @@ def sUCS_chroma(Iab: Domain100) -> Range100: np.float64(40.4205110...) """ - _I, a, b = tsplit(to_domain_100(Iab)) + Iab = to_domain_100(Iab) + + xp = array_namespace(Iab) + + _I, a, b = tsplit(Iab) - C = 1 / 0.0252 * np.log(1 + 0.0447 * np.hypot(a, b)) + C = 1 / 0.0252 * xp.log(1 + 0.0447 * xp.hypot(a, b)) return as_float(from_range_100(C)) @@ -304,9 +311,13 @@ def sUCS_hue_angle(Iab: Domain100) -> Range360: np.float64(20.9041560...) """ - _I, a, b = tsplit(to_domain_100(Iab)) + Iab = to_domain_100(Iab) + + xp = array_namespace(Iab) - h = np.degrees(np.arctan2(b, a)) % 360 + _I, a, b = tsplit(Iab) + + h = xp_degrees(xp.atan2(b, a)) % 360 return as_float(from_range_degrees(h)) @@ -357,11 +368,15 @@ def sUCS_Iab_to_sUCS_ICh( array([42.6292365..., 40.4205110..., 20.9041560...]) """ - I, a, b = tsplit(to_domain_100(Iab)) # noqa: E741 + Iab = to_domain_100(Iab) + + xp = array_namespace(Iab) - C = 1 / 0.0252 * np.log(1 + 0.0447 * np.hypot(a, b)) + I, a, b = tsplit(Iab) # noqa: E741 - h = np.degrees(np.arctan2(b, a)) % 360 + C = 1 / 0.0252 * xp.log(1 + 0.0447 * xp.hypot(a, b)) + + h = xp_degrees(xp.atan2(b, a)) % 360 return tstack([from_range_100(I), from_range_100(C), from_range_degrees(h)]) @@ -412,14 +427,16 @@ def sUCS_ICh_to_sUCS_Iab( array([42.6292365..., 36.9764682..., 14.1230135...]) """ + xp = array_namespace(ICh) + I, C, h = tsplit(ICh) # noqa: E741 I = to_domain_100(I) # noqa: E741 C = to_domain_100(C) h = to_domain_degrees(h) - C = (np.exp(0.0252 * C) - 1) / 0.0447 + C = (xp.exp(0.0252 * C) - 1) / 0.0447 - a = C * np.cos(np.radians(h)) - b = C * np.sin(np.radians(h)) + a = C * xp.cos(xp_radians(h)) + b = C * xp.sin(xp_radians(h)) return from_range_100(tstack([I, a, b])) diff --git a/colour/models/tests/test_cam02_ucs.py b/colour/models/tests/test_cam02_ucs.py index 764d544249..173b98839d 100644 --- a/colour/models/tests/test_cam02_ucs.py +++ b/colour/models/tests/test_cam02_ucs.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -33,7 +38,15 @@ UCS_Luo2006_to_XYZ, XYZ_to_UCS_Luo2006, ) -from colour.utilities import attest, domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + attest, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -68,102 +81,98 @@ def setup_method(self) -> None: self._JMh = np.array([specification.J, specification.M, specification.h]) - def test_JMh_CIECAM02_to_UCS_Luo2006(self) -> None: + def test_JMh_CIECAM02_to_UCS_Luo2006(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.JMh_CIECAM02_to_UCS_Luo2006` definition. """ - np.testing.assert_allclose( - JMh_CIECAM02_to_UCS_Luo2006( - self._JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] - ), - np.array([54.90433134, -0.08450395, -0.06854831]), + JMh = xp_as_array(self._JMh, xp=xp) + + xp_assert_close( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), + [54.90433134, -0.08450395, -0.06854831], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JMh_CIECAM02_to_UCS_Luo2006( - self._JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] - ), - JMh_CIECAM02_to_CAM02LCD(self._JMh), + xp_assert_close( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), + as_ndarray(JMh_CIECAM02_to_CAM02LCD(JMh)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JMh_CIECAM02_to_UCS_Luo2006( - self._JMh, COEFFICIENTS_UCS_LUO2006["CAM02-SCD"] - ), - np.array([54.90433134, -0.08436178, -0.06843298]), + xp_assert_close( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-SCD"]), + [54.90433134, -0.08436178, -0.06843298], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JMh_CIECAM02_to_UCS_Luo2006( - self._JMh, COEFFICIENTS_UCS_LUO2006["CAM02-SCD"] - ), - JMh_CIECAM02_to_CAM02SCD(self._JMh), + xp_assert_close( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-SCD"]), + as_ndarray(JMh_CIECAM02_to_CAM02SCD(JMh)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JMh_CIECAM02_to_UCS_Luo2006( - self._JMh, COEFFICIENTS_UCS_LUO2006["CAM02-UCS"] - ), - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_assert_close( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-UCS"]), + [54.90433134, -0.08442362, -0.06848314], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JMh_CIECAM02_to_UCS_Luo2006( - self._JMh, COEFFICIENTS_UCS_LUO2006["CAM02-UCS"] - ), - JMh_CIECAM02_to_CAM02UCS(self._JMh), + xp_assert_close( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-UCS"]), + as_ndarray(JMh_CIECAM02_to_CAM02UCS(JMh)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_JMh_CIECAM02_to_UCS_Luo2006(self) -> None: + def test_n_dimensional_JMh_CIECAM02_to_UCS_Luo2006(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.JMh_CIECAM02_to_UCS_Luo2006` definition n-dimensional support. """ - JMh = self._JMh - Jpapbp = JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + JMh = xp_as_array(self._JMh, xp=xp) + Jpapbp = as_ndarray( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) - JMh = np.tile(JMh, (6, 1)) - Jpapbp = np.tile(Jpapbp, (6, 1)) - np.testing.assert_allclose( + JMh = xp.tile(xp_as_array(JMh, xp=xp), (6, 1)) + Jpapbp = xp.tile(xp_as_array(Jpapbp, xp=xp), (6, 1)) + xp_assert_close( JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), Jpapbp, atol=TOLERANCE_ABSOLUTE_TESTS, ) - JMh = np.reshape(JMh, (2, 3, 3)) - Jpapbp = np.reshape(Jpapbp, (2, 3, 3)) - np.testing.assert_allclose( + JMh = xp_reshape(xp_as_array(JMh, xp=xp), (2, 3, 3), xp=xp) + Jpapbp = xp_reshape(xp_as_array(Jpapbp, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), Jpapbp, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_JMh_CIECAM02_to_UCS_Luo2006(self) -> None: + def test_domain_range_scale_JMh_CIECAM02_to_UCS_Luo2006( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.cam02_ucs.JMh_CIECAM02_to_UCS_Luo2006` definition domain and range scale support. """ - JMh = self._JMh - Jpapbp = JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + JMh = xp_as_array(self._JMh, xp=xp) + Jpapbp = as_ndarray( + JMh_CIECAM02_to_UCS_Luo2006(JMh, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) d_r = ( ("reference", 1, 1), - ("1", np.array([0.01, 0.01, 1 / 360]), 0.01), - ("100", np.array([1, 1, 1 / 3.6]), 1), + ("1", xp_as_array([0.01, 0.01, 1 / 360], xp=xp), 0.01), + ("100", xp_as_array([1, 1, 1 / 3.6], xp=xp), 1), ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( JMh_CIECAM02_to_UCS_Luo2006( JMh * factor_a, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] ), @@ -189,99 +198,105 @@ class TestUCS_Luo2006_to_JMh_CIECAM02: definition unit tests methods. """ - def test_UCS_Luo2006_to_JMh_CIECAM02(self) -> None: + def test_UCS_Luo2006_to_JMh_CIECAM02(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.UCS_Luo2006_to_JMh_CIECAM02` definition. """ - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - np.array([41.73109113, 0.10873867, 219.04843202]), + [41.73109113, 0.10873867, 219.04843202], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - CAM02LCD_to_JMh_CIECAM02(np.array([54.90433134, -0.08442362, -0.06848314])), + CAM02LCD_to_JMh_CIECAM02([54.90433134, -0.08442362, -0.06848314]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - np.array([41.73109113, 0.10892212, 219.04843202]), + [41.73109113, 0.10892212, 219.04843202], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - CAM02SCD_to_JMh_CIECAM02(np.array([54.90433134, -0.08442362, -0.06848314])), + CAM02SCD_to_JMh_CIECAM02([54.90433134, -0.08442362, -0.06848314]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - np.array([41.73109113, 0.10884218, 219.04843202]), + [41.73109113, 0.10884218, 219.04843202], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( - np.array([54.90433134, -0.08442362, -0.06848314]), + xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - CAM02UCS_to_JMh_CIECAM02(np.array([54.90433134, -0.08442362, -0.06848314])), + CAM02UCS_to_JMh_CIECAM02([54.90433134, -0.08442362, -0.06848314]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_UCS_Luo2006_to_JMh_CIECAM02(self) -> None: + def test_n_dimensional_UCS_Luo2006_to_JMh_CIECAM02(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.UCS_Luo2006_to_JMh_CIECAM02` definition n-dimensional support. """ - Jpapbp = np.array([54.90433134, -0.08442362, -0.06848314]) - JMh = UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + Jpapbp = xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp) + JMh = as_ndarray( + UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) - Jpapbp = np.tile(Jpapbp, (6, 1)) - JMh = np.tile(JMh, (6, 1)) - np.testing.assert_allclose( + Jpapbp = xp.tile(xp_as_array(Jpapbp, xp=xp), (6, 1)) + JMh = xp.tile(xp_as_array(JMh, xp=xp), (6, 1)) + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), JMh, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Jpapbp = np.reshape(Jpapbp, (2, 3, 3)) - JMh = np.reshape(JMh, (2, 3, 3)) - np.testing.assert_allclose( + Jpapbp = xp_reshape(xp_as_array(Jpapbp, xp=xp), (2, 3, 3), xp=xp) + JMh = xp_reshape(xp_as_array(JMh, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), JMh, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_UCS_Luo2006_to_JMh_CIECAM02(self) -> None: + def test_domain_range_scale_UCS_Luo2006_to_JMh_CIECAM02( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.models.cam02_ucs.UCS_Luo2006_to_JMh_CIECAM02` definition domain and range scale support. """ - Jpapbp = np.array([54.90433134, -0.08442362, -0.06848314]) - JMh = UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + Jpapbp = xp_as_array([54.90433134, -0.08442362, -0.06848314], xp=xp) + JMh = as_ndarray( + UCS_Luo2006_to_JMh_CIECAM02(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) d_r = ( ("reference", 1, 1), @@ -290,7 +305,7 @@ def test_domain_range_scale_UCS_Luo2006_to_JMh_CIECAM02(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_JMh_CIECAM02( Jpapbp * factor_a, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], @@ -317,106 +332,110 @@ class TestXYZ_to_UCS_Luo2006: unit tests methods. """ - def test_XYZ_to_UCS_Luo2006(self) -> None: + def test_XYZ_to_UCS_Luo2006(self, xp: ModuleType) -> None: """Test :func:`colour.models.cam02_ucs.XYZ_to_UCS_Luo2006` definition.""" - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - np.array([46.61386154, 39.35760236, 15.96730435]), + [46.61386154, 39.35760236, 15.96730435], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - XYZ_to_CAM02LCD(np.array([0.20654008, 0.12197225, 0.05136952])), + XYZ_to_CAM02LCD([0.20654008, 0.12197225, 0.05136952]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - np.array([46.61386154, 25.62879882, 10.39755489]), + [46.61386154, 25.62879882, 10.39755489], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - XYZ_to_CAM02SCD(np.array([0.20654008, 0.12197225, 0.05136952])), + XYZ_to_CAM02SCD([0.20654008, 0.12197225, 0.05136952]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - np.array([46.61386154, 29.88310013, 12.12351683]), + [46.61386154, 29.88310013, 12.12351683], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - XYZ_to_CAM02UCS(np.array([0.20654008, 0.12197225, 0.05136952])), + XYZ_to_CAM02UCS([0.20654008, 0.12197225, 0.05136952]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_UCS_Luo2006(self) -> None: + def test_n_dimensional_XYZ_to_UCS_Luo2006(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.XYZ_to_UCS_Luo2006` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Jpapbp = XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Jpapbp = as_ndarray( + XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) - XYZ = np.tile(XYZ, (6, 1)) - Jpapbp = np.tile(Jpapbp, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Jpapbp = xp.tile(xp_as_array(Jpapbp, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), Jpapbp, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Jpapbp = np.reshape(Jpapbp, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Jpapbp = xp_reshape(xp_as_array(Jpapbp, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), Jpapbp, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_UCS_Luo2006(self) -> None: + def test_domain_range_scale_XYZ_to_UCS_Luo2006(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.XYZ_to_UCS_Luo2006` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - XYZ_w = CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] / 100 - Jpapbp = XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + XYZ_w = xp_as_array(CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] / 100, xp=xp) + Jpapbp = as_ndarray( + XYZ_to_UCS_Luo2006(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Luo2006( - XYZ * factor_a, + XYZ * xp_as_array(factor_a, xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], - XYZ_w=XYZ_w * factor_a, + XYZ_w=XYZ_w * xp_as_array(factor_a, xp=xp), ), Jpapbp * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -440,104 +459,108 @@ class TestUCS_Luo2006_to_XYZ: unit tests methods. """ - def test_UCS_Luo2006_to_XYZ(self) -> None: + def test_UCS_Luo2006_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cam02_ucs.UCS_Luo2006_to_XYZ` definition.""" - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - np.array([46.61386154, 39.35760236, 15.96730435]), + xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - np.array([46.61386154, 39.35760236, 15.96730435]), + xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - CAM02LCD_to_XYZ(np.array([46.61386154, 39.35760236, 15.96730435])), + CAM02LCD_to_XYZ([46.61386154, 39.35760236, 15.96730435]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - np.array([46.61386154, 39.35760236, 15.96730435]), + xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - np.array([0.28264475, 0.11036927, 0.00824593]), + [0.28264475, 0.11036927, 0.00824593], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - np.array([46.61386154, 39.35760236, 15.96730435]), + xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-SCD"], ), - CAM02SCD_to_XYZ(np.array([46.61386154, 39.35760236, 15.96730435])), + CAM02SCD_to_XYZ([46.61386154, 39.35760236, 15.96730435]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - np.array([46.61386154, 39.35760236, 15.96730435]), + xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - np.array([0.24229809, 0.11573005, 0.02517649]), + [0.24229809, 0.11573005, 0.02517649], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - np.array([46.61386154, 39.35760236, 15.96730435]), + xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-UCS"], ), - CAM02UCS_to_XYZ(np.array([46.61386154, 39.35760236, 15.96730435])), + CAM02UCS_to_XYZ([46.61386154, 39.35760236, 15.96730435]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_UCS_Luo2006_to_XYZ(self) -> None: + def test_n_dimensional_UCS_Luo2006_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.UCS_Luo2006_to_XYZ` definition n-dimensional support. """ - Jpapbp = np.array([46.61386154, 39.35760236, 15.96730435]) - XYZ = UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + Jpapbp = xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp) + XYZ = as_ndarray( + UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) - Jpapbp = np.tile(Jpapbp, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + Jpapbp = xp.tile(xp_as_array(Jpapbp, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Jpapbp = np.reshape(Jpapbp, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + Jpapbp = xp_reshape(xp_as_array(Jpapbp, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_UCS_Luo2006_to_XYZ(self) -> None: + def test_domain_range_scale_UCS_Luo2006_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cam02_ucs.UCS_Luo2006_to_XYZ` definition domain and range scale support. """ - Jpapbp = np.array([46.61386154, 39.35760236, 15.96730435]) - XYZ = UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) - XYZ_w = CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] / 100 + Jpapbp = xp_as_array([46.61386154, 39.35760236, 15.96730435], xp=xp) + XYZ = as_ndarray( + UCS_Luo2006_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) + ) + XYZ_w = xp_as_array(CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] / 100, xp=xp) d_r = (("reference", 1, 1, 1), ("1", 0.01, 1, 1), ("100", 1, 100, 100)) for scale, factor_a, factor_b, factor_c in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( UCS_Luo2006_to_XYZ( - Jpapbp * factor_a, + Jpapbp * xp_as_array(factor_a, xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], XYZ_w=XYZ_w * factor_c, ), diff --git a/colour/models/tests/test_cam16_ucs.py b/colour/models/tests/test_cam16_ucs.py index bfb23de341..689b821d8d 100644 --- a/colour/models/tests/test_cam16_ucs.py +++ b/colour/models/tests/test_cam16_ucs.py @@ -2,7 +2,11 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.cam02_ucs import COEFFICIENTS_UCS_LUO2006 @@ -16,6 +20,7 @@ TestUCS_Luo2006_to_XYZ, TestXYZ_to_UCS_Luo2006, ) +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -64,25 +69,25 @@ class TestXYZ_to_UCS_Li2017(TestXYZ_to_UCS_Luo2006): definition unit tests methods. """ - def test_XYZ_to_UCS_Li2017(self) -> None: + def test_XYZ_to_UCS_Li2017(self, xp: ModuleType) -> None: """Test :func:`colour.models.cam16_ucs.XYZ_to_UCS_Li2017` definition.""" - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Li2017( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - np.array([46.06586033, 41.07586491, 14.51025826]), + [46.06586033, 41.07586491, 14.51025826], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS_Li2017( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], - XYZ_w=np.array([0.95047, 1.0, 1.08883]), + XYZ_w=xp_as_array([0.95047, 1.0, 1.08883], xp=xp), ), - np.array([46.06573617, 41.07444159, 14.50807598]), + [46.06573617, 41.07444159, 14.50807598], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -93,24 +98,24 @@ class TestUCS_Li2017_to_XYZ(TestUCS_Luo2006_to_XYZ): definition unit tests methods. """ - def test_UCS_Li2017_to_XYZ(self) -> None: + def test_UCS_Li2017_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cam16_ucs.UCS_Li2017_to_XYZ` definition.""" - np.testing.assert_allclose( + xp_assert_close( UCS_Li2017_to_XYZ( - np.array([46.06586033, 41.07586491, 14.51025826]), + xp_as_array([46.06586033, 41.07586491, 14.51025826], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UCS_Li2017_to_XYZ( - np.array([46.06586033, 41.07586491, 14.51025826]), + xp_as_array([46.06586033, 41.07586491, 14.51025826], xp=xp), COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], - XYZ_w=np.array([0.95047, 1.0, 1.08883]), + XYZ_w=xp_as_array([0.95047, 1.0, 1.08883], xp=xp), ), - np.array([0.2065444, 0.12197263, 0.05136016]), + [0.2065444, 0.12197263, 0.05136016], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_cie_lab.py b/colour/models/tests/test_cie_lab.py index 2a3e056a77..a6228717c0 100644 --- a/colour/models/tests/test_cie_lab.py +++ b/colour/models/tests/test_cie_lab.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import Lab_to_XYZ, XYZ_to_Lab -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,97 +41,91 @@ class TestXYZ_to_Lab: methods. """ - def test_XYZ_to_Lab(self) -> None: + def test_XYZ_to_Lab(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_lab.XYZ_to_Lab` definition.""" - np.testing.assert_allclose( - XYZ_to_Lab(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([41.52787529, 52.63858304, 26.92317922]), + xp_assert_close( + XYZ_to_Lab(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Lab(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([55.11636304, -41.08791787, 30.91825778]), + xp_assert_close( + XYZ_to_Lab(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [55.11636304, -41.08791787, 30.91825778], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Lab(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([29.80565520, 20.01830466, -48.34913874]), + xp_assert_close( + XYZ_to_Lab(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [29.80565520, 20.01830466, -48.34913874], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Lab( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([41.52787529, 38.48089305, -5.73295122]), + [41.52787529, 38.48089305, -5.73295122], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Lab( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([41.52787529, 51.19354174, 19.91843098]), + [41.52787529, 51.19354174, 19.91843098], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Lab( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([41.52787529, 51.19354174, 19.91843098]), + [41.52787529, 51.19354174, 19.91843098], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Lab(self) -> None: + def test_n_dimensional_XYZ_to_Lab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_lab.XYZ_to_Lab` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) - Lab = XYZ_to_Lab(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + Lab = as_ndarray(XYZ_to_Lab(XYZ, illuminant)) - XYZ = np.tile(XYZ, (6, 1)) - Lab = np.tile(Lab, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Lab(XYZ, illuminant), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Lab(XYZ, illuminant), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Lab(XYZ, illuminant), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Lab(XYZ, illuminant), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - Lab = np.reshape(Lab, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_Lab(XYZ, illuminant), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_Lab(XYZ, illuminant), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_Lab(self) -> None: + def test_domain_range_scale_XYZ_to_Lab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_lab.XYZ_to_Lab` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) - Lab = XYZ_to_Lab(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + Lab = as_ndarray(XYZ_to_Lab(XYZ, illuminant)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_Lab(XYZ * factor_a, illuminant), + xp_assert_close( + XYZ_to_Lab(XYZ * xp_as_array(factor_a, xp=xp), illuminant), Lab * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -139,97 +145,91 @@ class TestLab_to_XYZ: methods. """ - def test_Lab_to_XYZ(self) -> None: + def test_Lab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_lab.Lab_to_XYZ` definition.""" - np.testing.assert_allclose( - Lab_to_XYZ(np.array([41.52787529, 52.63858304, 26.92317922])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + Lab_to_XYZ(xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Lab_to_XYZ(np.array([55.11636304, -41.08791787, 30.91825778])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + Lab_to_XYZ(xp_as_array([55.11636304, -41.08791787, 30.91825778], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Lab_to_XYZ(np.array([29.80565520, 20.01830466, -48.34913874])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + Lab_to_XYZ(xp_as_array([29.80565520, 20.01830466, -48.34913874], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_XYZ( - np.array([41.52787529, 38.48089305, -5.73295122]), - np.array([0.44757, 0.40745]), + xp_as_array([41.52787529, 38.48089305, -5.73295122], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_XYZ( - np.array([41.52787529, 51.19354174, 19.91843098]), - np.array([0.34570, 0.35850]), + xp_as_array([41.52787529, 51.19354174, 19.91843098], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_XYZ( - np.array([41.52787529, 51.19354174, 19.91843098]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([41.52787529, 51.19354174, 19.91843098], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Lab_to_XYZ(self) -> None: + def test_n_dimensional_Lab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_lab.Lab_to_XYZ` definition n-dimensional support. """ - Lab = np.array([41.52787529, 52.63858304, 26.92317922]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = Lab_to_XYZ(Lab, illuminant) + Lab = xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(Lab_to_XYZ(Lab, illuminant)) - Lab = np.tile(Lab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - Lab_to_XYZ(Lab, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(Lab_to_XYZ(Lab, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - Lab_to_XYZ(Lab, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(Lab_to_XYZ(Lab, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Lab = np.reshape(Lab, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - Lab_to_XYZ(Lab, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Lab_to_XYZ(Lab, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Lab_to_XYZ(self) -> None: + def test_domain_range_scale_Lab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_lab.Lab_to_XYZ` definition domain and range scale support. """ - Lab = np.array([41.52787529, 52.63858304, 26.92317922]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = Lab_to_XYZ(Lab, illuminant) + Lab = xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(Lab_to_XYZ(Lab, illuminant)) d_r = (("reference", 1, 1), ("1", 0.01, 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - Lab_to_XYZ(Lab * factor_a, illuminant), + xp_assert_close( + Lab_to_XYZ(Lab * xp_as_array(factor_a, xp=xp), illuminant), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_cie_luv.py b/colour/models/tests/test_cie_luv.py index 7d6c6ada5a..4a16132818 100644 --- a/colour/models/tests/test_cie_luv.py +++ b/colour/models/tests/test_cie_luv.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -17,7 +22,14 @@ uv_to_Luv, xy_to_Luv_uv, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -44,97 +56,91 @@ class TestXYZ_to_Luv: methods. """ - def test_XYZ_to_Luv(self) -> None: + def test_XYZ_to_Luv(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.XYZ_to_Luv` definition.""" - np.testing.assert_allclose( - XYZ_to_Luv(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([41.52787529, 96.83626054, 17.75210149]), + xp_assert_close( + XYZ_to_Luv(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [41.52787529, 96.83626054, 17.75210149], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Luv(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([55.11636304, -37.59308176, 44.13768458]), + xp_assert_close( + XYZ_to_Luv(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [55.11636304, -37.59308176, 44.13768458], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Luv(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([29.80565520, -10.96316802, -65.06751860]), + xp_assert_close( + XYZ_to_Luv(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [29.80565520, -10.96316802, -65.06751860], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Luv( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([41.52787529, 65.45180940, -12.46626977]), + [41.52787529, 65.45180940, -12.46626977], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Luv( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([41.52787529, 90.70925962, 7.08455273]), + [41.52787529, 90.70925962, 7.08455273], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Luv( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([41.52787529, 90.70925962, 7.08455273]), + [41.52787529, 90.70925962, 7.08455273], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Luv(self) -> None: + def test_n_dimensional_XYZ_to_Luv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.XYZ_to_Luv` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) - Luv = XYZ_to_Luv(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + Luv = as_ndarray(XYZ_to_Luv(XYZ, illuminant)) - XYZ = np.tile(XYZ, (6, 1)) - Luv = np.tile(Luv, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Luv(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Luv = xp.tile(xp_as_array(Luv, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Luv(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Luv(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Luv(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - Luv = np.reshape(Luv, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_Luv(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + Luv = xp_reshape(xp_as_array(Luv, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_Luv(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_Luv(self) -> None: + def test_domain_range_scale_XYZ_to_Luv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.XYZ_to_Luv` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) - Luv = XYZ_to_Luv(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + Luv = as_ndarray(XYZ_to_Luv(XYZ, illuminant)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_Luv(XYZ * factor_a, illuminant), + xp_assert_close( + XYZ_to_Luv(XYZ * xp_as_array(factor_a, xp=xp), illuminant), Luv * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -154,97 +160,91 @@ class TestLuv_to_XYZ: methods. """ - def test_Luv_to_XYZ(self) -> None: + def test_Luv_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.Luv_to_XYZ` definition.""" - np.testing.assert_allclose( - Luv_to_XYZ(np.array([41.52787529, 96.83626054, 17.75210149])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + Luv_to_XYZ(xp_as_array([41.52787529, 96.83626054, 17.75210149], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Luv_to_XYZ(np.array([55.11636304, -37.59308176, 44.13768458])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + Luv_to_XYZ(xp_as_array([55.11636304, -37.59308176, 44.13768458], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Luv_to_XYZ(np.array([29.80565520, -10.96316802, -65.06751860])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + Luv_to_XYZ(xp_as_array([29.80565520, -10.96316802, -65.06751860], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Luv_to_XYZ( - np.array([41.52787529, 65.45180940, -12.46626977]), - np.array([0.44757, 0.40745]), + xp_as_array([41.52787529, 65.45180940, -12.46626977], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Luv_to_XYZ( - np.array([41.52787529, 90.70925962, 7.08455273]), - np.array([0.34570, 0.35850]), + xp_as_array([41.52787529, 90.70925962, 7.08455273], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Luv_to_XYZ( - np.array([41.52787529, 90.70925962, 7.08455273]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([41.52787529, 90.70925962, 7.08455273], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Luv_to_XYZ(self) -> None: + def test_n_dimensional_Luv_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.Luv_to_XYZ` definition n-dimensional support. """ - Luv = np.array([41.52787529, 96.83626054, 17.75210149]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = Luv_to_XYZ(Luv, illuminant) + Luv = xp_as_array([41.52787529, 96.83626054, 17.75210149], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(Luv_to_XYZ(Luv, illuminant)) - Luv = np.tile(Luv, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - Luv_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Luv = xp.tile(xp_as_array(Luv, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(Luv_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - Luv_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(Luv_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Luv = np.reshape(Luv, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - Luv_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Luv = xp_reshape(xp_as_array(Luv, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Luv_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Luv_to_XYZ(self) -> None: + def test_domain_range_scale_Luv_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.Luv_to_XYZ` definition domain and range scale support. """ - Luv = np.array([41.52787529, 96.83626054, 17.75210149]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = Luv_to_XYZ(Luv, illuminant) + Luv = xp_as_array([41.52787529, 96.83626054, 17.75210149], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(Luv_to_XYZ(Luv, illuminant)) d_r = (("reference", 1, 1), ("1", 0.01, 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - Luv_to_XYZ(Luv * factor_a, illuminant), + xp_assert_close( + Luv_to_XYZ(Luv * xp_as_array(factor_a, xp=xp), illuminant), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -264,96 +264,90 @@ class TestLuv_to_uv: methods. """ - def test_Luv_to_uv(self) -> None: + def test_Luv_to_uv(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.Luv_to_uv` definition.""" - np.testing.assert_allclose( - Luv_to_uv(np.array([41.52787529, 96.83626054, 17.75210149])), - np.array([0.37720213, 0.50120264]), + xp_assert_close( + Luv_to_uv(xp_as_array([41.52787529, 96.83626054, 17.75210149], xp=xp)), + [0.37720213, 0.50120264], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Luv_to_uv(np.array([55.11636304, -37.59308176, 44.13768458])), - np.array([0.14536327, 0.52992069]), + xp_assert_close( + Luv_to_uv(xp_as_array([55.11636304, -37.59308176, 44.13768458], xp=xp)), + [0.14536327, 0.52992069], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Luv_to_uv(np.array([29.80565520, -10.96316802, -65.06751860])), - np.array([0.16953603, 0.30039234]), + xp_assert_close( + Luv_to_uv(xp_as_array([29.80565520, -10.96316802, -65.06751860], xp=xp)), + [0.16953603, 0.30039234], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Luv_to_uv( - np.array([41.52787529, 65.45180940, -12.46626977]), - np.array([0.44757, 0.40745]), + xp_as_array([41.52787529, 65.45180940, -12.46626977], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.37720213, 0.50120264]), + [0.37720213, 0.50120264], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Luv_to_uv( - np.array([41.52787529, 90.70925962, 7.08455273]), - np.array([0.34570, 0.35850]), + xp_as_array([41.52787529, 90.70925962, 7.08455273], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([0.37720213, 0.50120264]), + [0.37720213, 0.50120264], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Luv_to_uv( - np.array([41.52787529, 90.70925962, 7.08455273]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([41.52787529, 90.70925962, 7.08455273], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([0.37720213, 0.50120264]), + [0.37720213, 0.50120264], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Luv_to_uv(self) -> None: + def test_n_dimensional_Luv_to_uv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.Luv_to_uv` definition n-dimensional support. """ - Luv = np.array([41.52787529, 96.83626054, 17.75210149]) - illuminant = np.array([0.31270, 0.32900]) - uv = Luv_to_uv(Luv, illuminant) + Luv = xp_as_array([41.52787529, 96.83626054, 17.75210149], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + uv = as_ndarray(Luv_to_uv(Luv, illuminant)) - Luv = np.tile(Luv, (6, 1)) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose( - Luv_to_uv(Luv, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Luv = xp.tile(xp_as_array(Luv, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(Luv_to_uv(Luv, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - Luv_to_uv(Luv, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(Luv_to_uv(Luv, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - Luv = np.reshape(Luv, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose( - Luv_to_uv(Luv, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Luv = xp_reshape(xp_as_array(Luv, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(Luv_to_uv(Luv, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Luv_to_uv(self) -> None: + def test_domain_range_scale_Luv_to_uv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.Luv_to_uv` definition domain and range scale support. """ - Luv = np.array([41.52787529, 96.83626054, 17.75210149]) - illuminant = np.array([0.31270, 0.32900]) - uv = Luv_to_uv(Luv, illuminant) + Luv = xp_as_array([41.52787529, 96.83626054, 17.75210149], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + uv = as_ndarray(Luv_to_uv(Luv, illuminant)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Luv_to_uv(Luv * factor, illuminant), uv, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -374,103 +368,97 @@ class Testuv_to_Luv: methods. """ - def test_uv_to_Luv(self) -> None: + def test_uv_to_Luv(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.uv_to_Luv` definition.""" - np.testing.assert_allclose( - uv_to_Luv(np.array([0.37720213, 0.50120264])), - np.array([100.00000000, 233.18376036, 42.74743858]), + xp_assert_close( + uv_to_Luv(xp_as_array([0.37720213, 0.50120264], xp=xp)), + [100.00000000, 233.18376036, 42.74743858], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_Luv(np.array([0.14536327, 0.52992069])), - np.array([100.00000000, -68.20675764, 80.08090358]), + xp_assert_close( + uv_to_Luv(xp_as_array([0.14536327, 0.52992069], xp=xp)), + [100.00000000, -68.20675764, 80.08090358], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_Luv(np.array([0.16953603, 0.30039234])), - np.array([100.00000000, -36.78216964, -218.3059514]), + xp_assert_close( + uv_to_Luv(xp_as_array([0.16953603, 0.30039234], xp=xp)), + [100.00000000, -36.78216964, -218.3059514], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_Luv( - np.array([0.37720213, 0.50120264]), - np.array([0.44757, 0.40745]), + xp_as_array([0.37720213, 0.50120264], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([100.00000000, 157.60933976, -30.01903705]), + [100.00000000, 157.60933976, -30.01903705], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_Luv( - np.array([0.37720213, 0.50120264]), - np.array([0.34570, 0.35850]), + xp_as_array([0.37720213, 0.50120264], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([100.00000000, 218.42981284, 17.05975609]), + [100.00000000, 218.42981284, 17.05975609], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_Luv( - np.array([0.37720213, 0.50120264]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([0.37720213, 0.50120264], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([100.00000000, 218.42981284, 17.05975609]), + [100.00000000, 218.42981284, 17.05975609], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_Luv(np.array([0.37720213, 0.50120264]), L=41.5278752), - np.array([41.52787529, 96.83626054, 17.75210149]), + xp_assert_close( + uv_to_Luv(xp_as_array([0.37720213, 0.50120264], xp=xp), L=41.5278752), + [41.52787529, 96.83626054, 17.75210149], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_uv_to_Luv(self) -> None: + def test_n_dimensional_uv_to_Luv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.uv_to_Luv` definition n-dimensional support. """ - uv = np.array([0.37720213, 0.50120264]) - illuminant = np.array([0.31270, 0.32900]) - Luv = uv_to_Luv(uv, illuminant) + uv = xp_as_array([0.37720213, 0.50120264], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + Luv = as_ndarray(uv_to_Luv(uv, illuminant)) - uv = np.tile(uv, (6, 1)) - Luv = np.tile(Luv, (6, 1)) - np.testing.assert_allclose( - uv_to_Luv(uv, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + Luv = xp.tile(xp_as_array(Luv, xp=xp), (6, 1)) + xp_assert_close(uv_to_Luv(uv, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - uv_to_Luv(uv, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(uv_to_Luv(uv, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - Luv = np.reshape(Luv, (2, 3, 3)) - np.testing.assert_allclose( - uv_to_Luv(uv, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + Luv = xp_reshape(xp_as_array(Luv, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(uv_to_Luv(uv, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_uv_to_Luv(self) -> None: + def test_domain_range_scale_uv_to_Luv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.uv_to_Luv` definition domain and range scale support. """ - uv = np.array([0.37720213, 0.50120264]) - illuminant = np.array([0.31270, 0.32900]) + uv = xp_as_array([0.37720213, 0.50120264], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) L = 100 - Luv = uv_to_Luv(uv, illuminant, L) + Luv = as_ndarray(uv_to_Luv(uv, illuminant, L)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( uv_to_Luv(uv, illuminant, L * factor), Luv * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -491,43 +479,43 @@ class TestLuv_uv_to_xy: methods. """ - def test_Luv_uv_to_xy(self) -> None: + def test_Luv_uv_to_xy(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.Luv_uv_to_xy` definition.""" - np.testing.assert_allclose( - Luv_uv_to_xy(np.array([0.37720213, 0.50120264])), - np.array([0.54369558, 0.32107944]), + xp_assert_close( + Luv_uv_to_xy(xp_as_array([0.37720213, 0.50120264], xp=xp)), + [0.54369558, 0.32107944], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Luv_uv_to_xy(np.array([0.14536327, 0.52992069])), - np.array([0.29777734, 0.48246445]), + xp_assert_close( + Luv_uv_to_xy(xp_as_array([0.14536327, 0.52992069], xp=xp)), + [0.29777734, 0.48246445], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Luv_uv_to_xy(np.array([0.16953603, 0.30039234])), - np.array([0.18582824, 0.14633764]), + xp_assert_close( + Luv_uv_to_xy(xp_as_array([0.16953603, 0.30039234], xp=xp)), + [0.18582824, 0.14633764], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Luv_uv_to_xy(self) -> None: + def test_n_dimensional_Luv_uv_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.Luv_uv_to_xy` definition n-dimensional arrays support. """ - uv = np.array([0.37720213, 0.50120264]) - xy = Luv_uv_to_xy(uv) + uv = xp_as_array([0.37720213, 0.50120264], xp=xp) + xy = as_ndarray(Luv_uv_to_xy(uv)) - uv = np.tile(uv, (6, 1)) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose(Luv_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(Luv_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose(Luv_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(Luv_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_Luv_uv_to_xy(self) -> None: @@ -547,43 +535,43 @@ class TestXy_to_Luv_uv: methods. """ - def test_xy_to_Luv_uv(self) -> None: + def test_xy_to_Luv_uv(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.xy_to_Luv_uv` definition.""" - np.testing.assert_allclose( - xy_to_Luv_uv(np.array([0.54369558, 0.32107944])), - np.array([0.37720213, 0.50120264]), + xp_assert_close( + xy_to_Luv_uv(xp_as_array([0.54369558, 0.32107944], xp=xp)), + [0.37720213, 0.50120264], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_Luv_uv(np.array([0.29777734, 0.48246445])), - np.array([0.14536327, 0.52992069]), + xp_assert_close( + xy_to_Luv_uv(xp_as_array([0.29777734, 0.48246445], xp=xp)), + [0.14536327, 0.52992069], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_Luv_uv(np.array([0.18582824, 0.14633764])), - np.array([0.16953603, 0.30039234]), + xp_assert_close( + xy_to_Luv_uv(xp_as_array([0.18582824, 0.14633764], xp=xp)), + [0.16953603, 0.30039234], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_Luv_uv(self) -> None: + def test_n_dimensional_xy_to_Luv_uv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.xy_to_Luv_uv` definition n-dimensional arrays support. """ - xy = np.array([0.54369558, 0.32107944]) - uv = xy_to_Luv_uv(xy) + xy = xp_as_array([0.54369558, 0.32107944], xp=xp) + uv = as_ndarray(xy_to_Luv_uv(xy)) - xy = np.tile(xy, (6, 1)) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose(xy_to_Luv_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(xy_to_Luv_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose(xy_to_Luv_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(xy_to_Luv_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_Luv_uv(self) -> None: @@ -603,97 +591,103 @@ class TestXYZ_to_CIE1976UCS: methods. """ - def test_XYZ_to_CIE1976UCS(self) -> None: + def test_XYZ_to_CIE1976UCS(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.XYZ_to_CIE1976UCS` definition.""" - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.37720213, 0.50120264, 41.52787529]), + xp_assert_close( + XYZ_to_CIE1976UCS(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.37720213, 0.50120264, 41.52787529], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.14536327, 0.52992069, 55.11636304]), + xp_assert_close( + XYZ_to_CIE1976UCS(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.14536327, 0.52992069, 55.11636304], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.16953603, 0.30039234, 29.80565520]), + xp_assert_close( + XYZ_to_CIE1976UCS(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.16953603, 0.30039234, 29.80565520], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_CIE1976UCS( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.37720213, 0.50120264, 41.52787529]), + [0.37720213, 0.50120264, 41.52787529], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_CIE1976UCS( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([0.37720213, 0.50120264, 41.52787529]), + [0.37720213, 0.50120264, 41.52787529], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_CIE1976UCS( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([0.37720213, 0.50120264, 41.52787529]), + [0.37720213, 0.50120264, 41.52787529], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_CIE1976UCS(self) -> None: + def test_n_dimensional_XYZ_to_CIE1976UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.XYZ_to_CIE1976UCS` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) - Luv = XYZ_to_CIE1976UCS(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + Luv = as_ndarray(XYZ_to_CIE1976UCS(XYZ, illuminant)) - XYZ = np.tile(XYZ, (6, 1)) - Luv = np.tile(Luv, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Luv = xp.tile(xp_as_array(Luv, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_CIE1976UCS(XYZ, illuminant), + Luv, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_CIE1976UCS(XYZ, illuminant), + Luv, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - Luv = np.reshape(Luv, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(XYZ, illuminant), Luv, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + Luv = xp_reshape(xp_as_array(Luv, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + XYZ_to_CIE1976UCS(XYZ, illuminant), + Luv, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_CIE1976UCS(self) -> None: + def test_domain_range_scale_XYZ_to_CIE1976UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.XYZ_to_CIE1976UCS` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) - uvL = XYZ_to_CIE1976UCS(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + uvL = as_ndarray(XYZ_to_CIE1976UCS(XYZ, illuminant)) d_r = (("reference", 1, 1), ("1", 1, np.array([1, 1, 0.01])), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_CIE1976UCS(XYZ * factor_a, illuminant), + xp_assert_close( + XYZ_to_CIE1976UCS(XYZ * xp_as_array(factor_a, xp=xp), illuminant), uvL * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -716,97 +710,109 @@ class TestCIE1976UCS_to_XYZ: methods. """ - def test_CIE1976UCS_to_XYZ(self) -> None: + def test_CIE1976UCS_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_luv.CIE1976UCS_to_XYZ` definition.""" - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(np.array([0.37720213, 0.50120264, 41.52787529])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + CIE1976UCS_to_XYZ( + xp_as_array([0.37720213, 0.50120264, 41.52787529], xp=xp) + ), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(np.array([0.14536327, 0.52992069, 55.11636304])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + CIE1976UCS_to_XYZ( + xp_as_array([0.14536327, 0.52992069, 55.11636304], xp=xp) + ), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(np.array([0.16953603, 0.30039234, 29.80565520])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + CIE1976UCS_to_XYZ( + xp_as_array([0.16953603, 0.30039234, 29.80565520], xp=xp) + ), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( CIE1976UCS_to_XYZ( - np.array([0.37720213, 0.50120264, 41.52787529]), - np.array([0.44757, 0.40745]), + xp_as_array([0.37720213, 0.50120264, 41.52787529], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( CIE1976UCS_to_XYZ( - np.array([0.37720213, 0.50120264, 41.52787529]), - np.array([0.34570, 0.35850]), + xp_as_array([0.37720213, 0.50120264, 41.52787529], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( CIE1976UCS_to_XYZ( - np.array([0.37720213, 0.50120264, 41.52787529]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([0.37720213, 0.50120264, 41.52787529], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CIE1976UCS_to_XYZ(self) -> None: + def test_n_dimensional_CIE1976UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.CIE1976UCS_to_XYZ` definition n-dimensional support. """ - Luv = np.array([0.37720213, 0.50120264, 41.52787529]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = CIE1976UCS_to_XYZ(Luv, illuminant) + Luv = xp_as_array([0.37720213, 0.50120264, 41.52787529], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(CIE1976UCS_to_XYZ(Luv, illuminant)) - Luv = np.tile(Luv, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS + Luv = xp.tile(xp_as_array(Luv, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( + CIE1976UCS_to_XYZ(Luv, illuminant), + XYZ, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close( + CIE1976UCS_to_XYZ(Luv, illuminant), + XYZ, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - Luv = np.reshape(Luv, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(Luv, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS + Luv = xp_reshape(xp_as_array(Luv, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + CIE1976UCS_to_XYZ(Luv, illuminant), + XYZ, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_CIE1976UCS_to_XYZ(self) -> None: + def test_domain_range_scale_CIE1976UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_luv.CIE1976UCS_to_XYZ` definition domain and range scale support. """ - uvL = np.array([0.37720213, 0.50120264, 41.52787529]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = CIE1976UCS_to_XYZ(uvL, illuminant) + uvL = xp_as_array([0.37720213, 0.50120264, 41.52787529], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(CIE1976UCS_to_XYZ(uvL, illuminant)) d_r = (("reference", 1, 1), ("1", np.array([1, 1, 0.01]), 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - CIE1976UCS_to_XYZ(uvL * factor_a, illuminant), + xp_assert_close( + CIE1976UCS_to_XYZ(uvL * xp_as_array(factor_a, xp=xp), illuminant), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_cie_ucs.py b/colour/models/tests/test_cie_ucs.py index 463b55a5f5..c03e416b36 100644 --- a/colour/models/tests/test_cie_ucs.py +++ b/colour/models/tests/test_cie_ucs.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -17,7 +22,14 @@ uv_to_UCS, xy_to_UCS_uv, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -44,57 +56,57 @@ class TestXYZ_to_UCS: methods. """ - def test_XYZ_to_UCS(self) -> None: + def test_XYZ_to_UCS(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.XYZ_to_UCS` definition.""" - np.testing.assert_allclose( - XYZ_to_UCS(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.13769339, 0.12197225, 0.10537310]), + xp_assert_close( + XYZ_to_UCS(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.13769339, 0.12197225, 0.10537310], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_UCS(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.09481340, 0.23042768, 0.32701033]), + xp_assert_close( + XYZ_to_UCS(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.09481340, 0.23042768, 0.32701033], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_UCS(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.05212520, 0.06157201, 0.19376075]), + xp_assert_close( + XYZ_to_UCS(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.05212520, 0.06157201, 0.19376075], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_UCS(self) -> None: + def test_n_dimensional_XYZ_to_UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.XYZ_to_UCS` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - UCS = XYZ_to_UCS(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + UCS = as_ndarray(XYZ_to_UCS(XYZ)) - UCS = np.tile(UCS, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(XYZ_to_UCS(XYZ), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) + UCS = xp.tile(xp_as_array(UCS, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_UCS(XYZ), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) - UCS = np.reshape(UCS, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(XYZ_to_UCS(XYZ), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) + UCS = xp_reshape(xp_as_array(UCS, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_UCS(XYZ), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_UCS(self) -> None: + def test_domain_range_scale_XYZ_to_UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.XYZ_to_UCS` definition domain and range scale support. """ - XYZ = np.array([0.0704953400, 0.1008000000, 0.0955831300]) - UCS = XYZ_to_UCS(XYZ) + XYZ = xp_as_array([0.0704953400, 0.1008000000, 0.0955831300], xp=xp) + UCS = as_ndarray(XYZ_to_UCS(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UCS(XYZ * factor), UCS * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -115,57 +127,57 @@ class TestUCS_to_XYZ: methods. """ - def test_UCS_to_XYZ(self) -> None: + def test_UCS_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.UCS_to_XYZ` definition.""" - np.testing.assert_allclose( - UCS_to_XYZ(np.array([0.13769339, 0.12197225, 0.10537310])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + UCS_to_XYZ(xp_as_array([0.13769339, 0.12197225, 0.10537310], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UCS_to_XYZ(np.array([0.09481340, 0.23042768, 0.32701033])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + UCS_to_XYZ(xp_as_array([0.09481340, 0.23042768, 0.32701033], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UCS_to_XYZ(np.array([0.05212520, 0.06157201, 0.19376075])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + UCS_to_XYZ(xp_as_array([0.05212520, 0.06157201, 0.19376075], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_UCS_to_XYZ(self) -> None: + def test_n_dimensional_UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.UCS_to_XYZ` definition n-dimensional support. """ - UCS = np.array([0.13769339, 0.12197225, 0.10537310]) - XYZ = UCS_to_XYZ(UCS) + UCS = xp_as_array([0.13769339, 0.12197225, 0.10537310], xp=xp) + XYZ = as_ndarray(UCS_to_XYZ(UCS)) - UCS = np.tile(UCS, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(UCS_to_XYZ(UCS), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + UCS = xp.tile(xp_as_array(UCS, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(UCS_to_XYZ(UCS), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - UCS = np.reshape(UCS, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(UCS_to_XYZ(UCS), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + UCS = xp_reshape(xp_as_array(UCS, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(UCS_to_XYZ(UCS), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_UCS_to_XYZ(self) -> None: + def test_domain_range_scale_UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.UCS_to_XYZ` definition domain and range scale support. """ - UCS = np.array([0.0469968933, 0.1008000000, 0.1637438950]) - XYZ = UCS_to_XYZ(UCS) + UCS = xp_as_array([0.0469968933, 0.1008000000, 0.1637438950], xp=xp) + XYZ = as_ndarray(UCS_to_XYZ(UCS)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( UCS_to_XYZ(UCS * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -186,58 +198,60 @@ class TestUCS_to_uv: methods. """ - def test_UCS_to_uv(self) -> None: + def test_UCS_to_uv(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.UCS_to_uv` definition.""" - np.testing.assert_allclose( - UCS_to_uv(np.array([0.13769339, 0.12197225, 0.10537310])), - np.array([0.37720213, 0.33413508]), + xp_assert_close( + UCS_to_uv(xp_as_array([0.13769339, 0.12197225, 0.10537310], xp=xp)), + [0.37720213, 0.33413508], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UCS_to_uv(np.array([0.09481340, 0.23042768, 0.32701033])), - np.array([0.14536327, 0.35328046]), + xp_assert_close( + UCS_to_uv(xp_as_array([0.09481340, 0.23042768, 0.32701033], xp=xp)), + [0.14536327, 0.35328046], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UCS_to_uv(np.array([0.05212520, 0.06157201, 0.19376075])), - np.array([0.16953602, 0.20026156]), + xp_assert_close( + UCS_to_uv(xp_as_array([0.05212520, 0.06157201, 0.19376075], xp=xp)), + [0.16953602, 0.20026156], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_UCS_to_uv(self) -> None: + def test_n_dimensional_UCS_to_uv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.UCS_to_uv` definition n-dimensional support. """ - UCS = np.array([0.13769339, 0.12197225, 0.10537310]) - uv = UCS_to_uv(UCS) + UCS = xp_as_array([0.13769339, 0.12197225, 0.10537310], xp=xp) + uv = as_ndarray(UCS_to_uv(UCS)) - UCS = np.tile(UCS, (6, 1)) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose(UCS_to_uv(UCS), uv, atol=TOLERANCE_ABSOLUTE_TESTS) + UCS = xp.tile(xp_as_array(UCS, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(UCS_to_uv(UCS), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - UCS = np.reshape(UCS, (2, 3, 3)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose(UCS_to_uv(UCS), uv, atol=TOLERANCE_ABSOLUTE_TESTS) + UCS = xp_reshape(xp_as_array(UCS, xp=xp), (2, 3, 3), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(UCS_to_uv(UCS), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_UCS_to_uv(self) -> None: + def test_domain_range_scale_UCS_to_uv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.UCS_to_uv` definition domain and range scale support. """ - UCS = np.array([0.0469968933, 0.1008000000, 0.1637438950]) - uv = UCS_to_uv(UCS) + UCS = xp_as_array([0.0469968933, 0.1008000000, 0.1637438950], xp=xp) + uv = as_ndarray(UCS_to_uv(UCS)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - UCS_to_uv(UCS * factor), uv, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + UCS_to_uv(UCS * factor), + uv, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -255,64 +269,64 @@ class Testuv_to_UCS: methods. """ - def test_uv_to_UCS(self) -> None: + def test_uv_to_UCS(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.uv_to_UCS` definition.""" - np.testing.assert_allclose( - uv_to_UCS(np.array([0.37720213, 0.33413508])), - np.array([1.12889114, 1.00000000, 0.86391046]), + xp_assert_close( + uv_to_UCS(xp_as_array([0.37720213, 0.33413508], xp=xp)), + [1.12889114, 1.00000000, 0.86391046], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_UCS(np.array([0.14536327, 0.35328046])), - np.array([0.41146705, 1.00000000, 1.41914520]), + xp_assert_close( + uv_to_UCS(xp_as_array([0.14536327, 0.35328046], xp=xp)), + [0.41146705, 1.00000000, 1.41914520], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_UCS(np.array([0.16953602, 0.20026156])), - np.array([0.84657295, 1.00000000, 3.14689659]), + xp_assert_close( + uv_to_UCS(xp_as_array([0.16953602, 0.20026156], xp=xp)), + [0.84657295, 1.00000000, 3.14689659], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_UCS(np.array([0.37720213, 0.33413508]), V=0.18), - np.array([0.20320040, 0.18000000, 0.15550388]), + xp_assert_close( + uv_to_UCS(xp_as_array([0.37720213, 0.33413508], xp=xp), V=0.18), + [0.20320040, 0.18000000, 0.15550388], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_uv_to_UCS(self) -> None: + def test_n_dimensional_uv_to_UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.uv_to_UCS` definition n-dimensional support. """ - uv = np.array([0.37720213, 0.33413508]) - UCS = uv_to_UCS(uv) + uv = xp_as_array([0.37720213, 0.33413508], xp=xp) + UCS = as_ndarray(uv_to_UCS(uv)) - uv = np.tile(uv, (6, 1)) - UCS = np.tile(UCS, (6, 1)) - np.testing.assert_allclose(uv_to_UCS(uv), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + UCS = xp.tile(xp_as_array(UCS, xp=xp), (6, 1)) + xp_assert_close(uv_to_UCS(uv), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - UCS = np.reshape(UCS, (2, 3, 3)) - np.testing.assert_allclose(uv_to_UCS(uv), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + UCS = xp_reshape(xp_as_array(UCS, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(uv_to_UCS(uv), UCS, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_uv_to_UCS(self) -> None: + def test_domain_range_scale_uv_to_UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.uv_to_UCS` definition domain and range scale support. """ - uv = np.array([0.37720213, 0.33413508]) + uv = xp_as_array([0.37720213, 0.33413508], xp=xp) V = 1 - UCS = uv_to_UCS(uv, V) + UCS = as_ndarray(uv_to_UCS(uv, V)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( uv_to_UCS(uv, V * factor), UCS * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -333,43 +347,43 @@ class TestUCS_uv_to_xy: methods. """ - def test_UCS_uv_to_xy(self) -> None: + def test_UCS_uv_to_xy(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.UCS_uv_to_xy` definition.""" - np.testing.assert_allclose( - UCS_uv_to_xy(np.array([0.37720213, 0.33413508])), - np.array([0.54369555, 0.32107941]), + xp_assert_close( + UCS_uv_to_xy(xp_as_array([0.37720213, 0.33413508], xp=xp)), + [0.54369555, 0.32107941], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UCS_uv_to_xy(np.array([0.14536327, 0.35328046])), - np.array([0.29777734, 0.48246445]), + xp_assert_close( + UCS_uv_to_xy(xp_as_array([0.14536327, 0.35328046], xp=xp)), + [0.29777734, 0.48246445], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UCS_uv_to_xy(np.array([0.16953602, 0.20026156])), - np.array([0.18582823, 0.14633764]), + xp_assert_close( + UCS_uv_to_xy(xp_as_array([0.16953602, 0.20026156], xp=xp)), + [0.18582823, 0.14633764], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_UCS_uv_to_xy(self) -> None: + def test_n_dimensional_UCS_uv_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.UCS_uv_to_xy` definition n-dimensional arrays support. """ - uv = np.array([0.37720213, 0.33413508]) - xy = UCS_uv_to_xy(uv) + uv = xp_as_array([0.37720213, 0.33413508], xp=xp) + xy = as_ndarray(UCS_uv_to_xy(uv)) - uv = np.tile(uv, (6, 1)) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose(UCS_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(UCS_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose(UCS_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(UCS_uv_to_xy(uv), xy, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_UCS_uv_to_xy(self) -> None: @@ -389,43 +403,43 @@ class TestXy_to_UCS_uv: methods. """ - def test_xy_to_UCS_uv(self) -> None: + def test_xy_to_UCS_uv(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.xy_to_UCS_uv` definition.""" - np.testing.assert_allclose( - xy_to_UCS_uv(np.array([0.54369555, 0.32107941])), - np.array([0.37720213, 0.33413508]), + xp_assert_close( + xy_to_UCS_uv(xp_as_array([0.54369555, 0.32107941], xp=xp)), + [0.37720213, 0.33413508], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_UCS_uv(np.array([0.29777734, 0.48246445])), - np.array([0.14536327, 0.35328046]), + xp_assert_close( + xy_to_UCS_uv(xp_as_array([0.29777734, 0.48246445], xp=xp)), + [0.14536327, 0.35328046], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_UCS_uv(np.array([0.18582823, 0.14633764])), - np.array([0.16953602, 0.20026156]), + xp_assert_close( + xy_to_UCS_uv(xp_as_array([0.18582823, 0.14633764], xp=xp)), + [0.16953602, 0.20026156], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_UCS_uv(self) -> None: + def test_n_dimensional_xy_to_UCS_uv(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.xy_to_UCS_uv` definition n-dimensional arrays support. """ - xy = np.array([0.54369555, 0.32107941]) - uv = xy_to_UCS_uv(xy) + xy = xp_as_array([0.54369555, 0.32107941], xp=xp) + uv = as_ndarray(xy_to_UCS_uv(xy)) - xy = np.tile(xy, (6, 1)) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose(xy_to_UCS_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(xy_to_UCS_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose(xy_to_UCS_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(xy_to_UCS_uv(xy), uv, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_UCS_uv(self) -> None: @@ -445,62 +459,58 @@ class TestXYZ_to_CIE1960UCS: methods. """ - def test_XYZ_to_CIE1960UCS(self) -> None: + def test_XYZ_to_CIE1960UCS(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.XYZ_to_CIE1960UCS` definition.""" - np.testing.assert_allclose( - XYZ_to_CIE1960UCS(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.37720213, 0.33413509, 0.12197225]), + xp_assert_close( + XYZ_to_CIE1960UCS(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.37720213, 0.33413509, 0.12197225], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_CIE1960UCS(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.14536327, 0.35328046, 0.23042768]), + xp_assert_close( + XYZ_to_CIE1960UCS(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.14536327, 0.35328046, 0.23042768], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_CIE1960UCS(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.16953603, 0.20026156, 0.06157201]), + xp_assert_close( + XYZ_to_CIE1960UCS(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.16953603, 0.20026156, 0.06157201], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_CIE1960UCS(self) -> None: + def test_n_dimensional_XYZ_to_CIE1960UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.XYZ_to_CIE1960UCS` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - uvV = XYZ_to_CIE1960UCS(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + uvV = as_ndarray(XYZ_to_CIE1960UCS(XYZ)) - uvV = np.tile(uvV, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CIE1960UCS(XYZ), uvV, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uvV = xp.tile(xp_as_array(uvV, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_CIE1960UCS(XYZ), uvV, atol=TOLERANCE_ABSOLUTE_TESTS) - uvV = np.reshape(uvV, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_CIE1960UCS(XYZ), uvV, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uvV = xp_reshape(xp_as_array(uvV, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_CIE1960UCS(XYZ), uvV, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_CIE1960UCS(self) -> None: + def test_domain_range_scale_XYZ_to_CIE1960UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.XYZ_to_CIE1960UCS` definition domain and range scale support. """ - XYZ = np.array([0.0704953400, 0.1008000000, 0.0955831300]) - uvV = XYZ_to_CIE1960UCS(XYZ) + XYZ = xp_as_array([0.0704953400, 0.1008000000, 0.0955831300], xp=xp) + uvV = as_ndarray(XYZ_to_CIE1960UCS(XYZ)) d_r = (("reference", 1, 1), ("1", 1, 1), ("100", 100, np.array([1, 1, 100]))) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_CIE1960UCS(XYZ * factor_a), + xp_assert_close( + XYZ_to_CIE1960UCS(XYZ * xp_as_array(factor_a, xp=xp)), uvV * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -523,62 +533,58 @@ class TestCIE1960UCS_to_XYZ: methods. """ - def test_CIE1960UCS_to_XYZ(self) -> None: + def test_CIE1960UCS_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_ucs.CIE1960UCS_to_XYZ` definition.""" - np.testing.assert_allclose( - CIE1960UCS_to_XYZ(np.array([0.37720213, 0.33413509, 0.12197225])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + CIE1960UCS_to_XYZ(xp_as_array([0.37720213, 0.33413509, 0.12197225], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CIE1960UCS_to_XYZ(np.array([0.14536327, 0.35328046, 0.23042768])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + CIE1960UCS_to_XYZ(xp_as_array([0.14536327, 0.35328046, 0.23042768], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CIE1960UCS_to_XYZ(np.array([0.16953603, 0.20026156, 0.06157201])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + CIE1960UCS_to_XYZ(xp_as_array([0.16953603, 0.20026156, 0.06157201], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CIE1960UCS_to_XYZ(self) -> None: + def test_n_dimensional_CIE1960UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.CIE1960UCS_to_XYZ` definition n-dimensional support. """ - uvV = np.array([0.37720213, 0.33413509, 0.12197225]) - XYZ = CIE1960UCS_to_XYZ(uvV) + uvV = xp_as_array([0.37720213, 0.33413509, 0.12197225], xp=xp) + XYZ = as_ndarray(CIE1960UCS_to_XYZ(uvV)) - uvV = np.tile(uvV, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - CIE1960UCS_to_XYZ(uvV), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uvV = xp.tile(xp_as_array(uvV, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(CIE1960UCS_to_XYZ(uvV), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - uvV = np.reshape(uvV, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - CIE1960UCS_to_XYZ(uvV), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uvV = xp_reshape(xp_as_array(uvV, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(CIE1960UCS_to_XYZ(uvV), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_CIE1960UCS_to_XYZ(self) -> None: + def test_domain_range_scale_CIE1960UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_ucs.CIE1960UCS_to_XYZ` definition domain and range scale support. """ - uvV = np.array([0.0469968933, 0.1008000000, 0.1637438950]) - XYZ = CIE1960UCS_to_XYZ(uvV) + uvV = xp_as_array([0.0469968933, 0.1008000000, 0.1637438950], xp=xp) + XYZ = as_ndarray(CIE1960UCS_to_XYZ(uvV)) d_r = (("reference", 1, 1), ("1", 1, 1), ("100", np.array([1, 1, 100]), 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - CIE1960UCS_to_XYZ(uvV * factor_a), + xp_assert_close( + CIE1960UCS_to_XYZ(uvV * xp_as_array(factor_a, xp=xp)), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_cie_uvw.py b/colour/models/tests/test_cie_uvw.py index 49048f6353..10233b4ce2 100644 --- a/colour/models/tests/test_cie_uvw.py +++ b/colour/models/tests/test_cie_uvw.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import UVW_to_XYZ, XYZ_to_UVW -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,96 +41,90 @@ class TestXYZ_to_UVW: methods. """ - def test_XYZ_to_UVW(self) -> None: + def test_XYZ_to_UVW(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_uvw.XYZ_to_UVW` definition.""" - np.testing.assert_allclose( - XYZ_to_UVW(np.array([0.20654008, 0.12197225, 0.05136952]) * 100), - np.array([94.55035725, 11.55536523, 40.54757405]), + xp_assert_close( + XYZ_to_UVW(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100), + [94.55035725, 11.55536523, 40.54757405], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_UVW(np.array([0.14222010, 0.23042768, 0.10495772]) * 100), - np.array([-36.92762376, 28.90425105, 54.14071478]), + xp_assert_close( + XYZ_to_UVW(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100), + [-36.92762376, 28.90425105, 54.14071478], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_UVW(np.array([0.07818780, 0.06157201, 0.28099326]) * 100), - np.array([-10.60111550, -41.94580000, 28.82134002]), + xp_assert_close( + XYZ_to_UVW(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100), + [-10.60111550, -41.94580000, 28.82134002], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UVW( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([63.90676310, -8.11466183, 40.54757405]), + [63.90676310, -8.11466183, 40.54757405], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UVW( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, - np.array([0.34570, 0.35850]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([88.56798946, 4.61154385, 40.54757405]), + [88.56798946, 4.61154385, 40.54757405], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UVW( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([88.56798946, 4.61154385, 40.54757405]), + [88.56798946, 4.61154385, 40.54757405], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_UVW(self) -> None: + def test_n_dimensional_XYZ_to_UVW(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_uvw.XYZ_to_UVW` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 - illuminant = np.array([0.31270, 0.32900]) - UVW = XYZ_to_UVW(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + UVW = as_ndarray(XYZ_to_UVW(XYZ, illuminant)) - XYZ = np.tile(XYZ, (6, 1)) - UVW = np.tile(UVW, (6, 1)) - np.testing.assert_allclose( - XYZ_to_UVW(XYZ, illuminant), UVW, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + UVW = xp.tile(xp_as_array(UVW, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_UVW(XYZ, illuminant), UVW, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - XYZ_to_UVW(XYZ, illuminant), UVW, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_UVW(XYZ, illuminant), UVW, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - UVW = np.reshape(UVW, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_UVW(XYZ, illuminant), UVW, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + UVW = xp_reshape(xp_as_array(UVW, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_UVW(XYZ, illuminant), UVW, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_UVW(self) -> None: + def test_domain_range_scale_XYZ_to_UVW(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_uvw.XYZ_to_UVW` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 - illuminant = np.array([0.31270, 0.32900]) - UVW = XYZ_to_UVW(XYZ, illuminant) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + UVW = as_ndarray(XYZ_to_UVW(XYZ, illuminant)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_UVW(XYZ * factor, illuminant), UVW * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -139,96 +145,90 @@ class TestUVW_to_XYZ: methods. """ - def test_UVW_to_XYZ(self) -> None: + def test_UVW_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_uvw.UVW_to_XYZ` definition.""" - np.testing.assert_allclose( - UVW_to_XYZ(np.array([94.55035725, 11.55536523, 40.54757405])), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_assert_close( + UVW_to_XYZ(xp_as_array([94.55035725, 11.55536523, 40.54757405], xp=xp)), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UVW_to_XYZ(np.array([-36.92762376, 28.90425105, 54.14071478])), - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, + xp_assert_close( + UVW_to_XYZ(xp_as_array([-36.92762376, 28.90425105, 54.14071478], xp=xp)), + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - UVW_to_XYZ(np.array([-10.60111550, -41.94580000, 28.82134002])), - np.array([0.07818780, 0.06157201, 0.28099326]) * 100, + xp_assert_close( + UVW_to_XYZ(xp_as_array([-10.60111550, -41.94580000, 28.82134002], xp=xp)), + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UVW_to_XYZ( - np.array([63.90676310, -8.11466183, 40.54757405]), - np.array([0.44757, 0.40745]), + xp_as_array([63.90676310, -8.11466183, 40.54757405], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UVW_to_XYZ( - np.array([88.56798946, 4.61154385, 40.54757405]), - np.array([0.34570, 0.35850]), + xp_as_array([88.56798946, 4.61154385, 40.54757405], xp=xp), + xp_as_array([0.34570, 0.35850], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( UVW_to_XYZ( - np.array([88.56798946, 4.61154385, 40.54757405]), - np.array([0.34570, 0.35850, 1.00000]), + xp_as_array([88.56798946, 4.61154385, 40.54757405], xp=xp), + xp_as_array([0.34570, 0.35850, 1.00000], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_UVW_to_XYZ(self) -> None: + def test_n_dimensional_UVW_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_uvw.UVW_to_XYZ` definition n-dimensional support. """ - UVW = np.array([94.55035725, 11.55536523, 40.54757405]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = UVW_to_XYZ(UVW, illuminant) + UVW = xp_as_array([94.55035725, 11.55536523, 40.54757405], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(UVW_to_XYZ(UVW, illuminant)) - XYZ = np.tile(XYZ, (6, 1)) - UVW = np.tile(UVW, (6, 1)) - np.testing.assert_allclose( - UVW_to_XYZ(UVW, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + UVW = xp.tile(xp_as_array(UVW, xp=xp), (6, 1)) + xp_assert_close(UVW_to_XYZ(UVW, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - illuminant = np.tile(illuminant, (6, 1)) - np.testing.assert_allclose( - UVW_to_XYZ(UVW, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + xp_assert_close(UVW_to_XYZ(UVW, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - UVW = np.reshape(UVW, (2, 3, 3)) - np.testing.assert_allclose( - UVW_to_XYZ(UVW, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + UVW = xp_reshape(xp_as_array(UVW, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(UVW_to_XYZ(UVW, illuminant), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_UVW_to_XYZ(self) -> None: + def test_domain_range_scale_UVW_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_uvw.UVW_to_XYZ` definition domain and range scale support. """ - UVW = np.array([94.55035725, 11.55536523, 40.54757405]) - illuminant = np.array([0.31270, 0.32900]) - XYZ = UVW_to_XYZ(UVW, illuminant) + UVW = xp_as_array([94.55035725, 11.55536523, 40.54757405], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) + XYZ = as_ndarray(UVW_to_XYZ(UVW, illuminant)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( UVW_to_XYZ(UVW * factor, illuminant), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_cie_xyy.py b/colour/models/tests/test_cie_xyy.py index 809d610aea..42f9216550 100644 --- a/colour/models/tests/test_cie_xyy.py +++ b/colour/models/tests/test_cie_xyy.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -15,7 +20,14 @@ xyY_to_xy, xyY_to_XYZ, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -40,80 +52,79 @@ class TestXYZ_to_xyY: methods. """ - def test_XYZ_to_xyY(self) -> None: + def test_XYZ_to_xyY(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_xyy.XYZ_to_xyY` definition.""" - np.testing.assert_allclose( - XYZ_to_xyY(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.54369557, 0.32107944, 0.12197225]), + xp_assert_close( + XYZ_to_xyY(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.54369557, 0.32107944, 0.12197225], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_xyY(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.29777735, 0.48246446, 0.23042768]), + xp_assert_close( + XYZ_to_xyY(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.29777735, 0.48246446, 0.23042768], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_xyY(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.18582823, 0.14633764, 0.06157201]), + xp_assert_close( + XYZ_to_xyY(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.18582823, 0.14633764, 0.06157201], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_xyY(np.array([0.00000000, 0.00000000, 1.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + XYZ_to_xyY(xp_as_array([0.00000000, 0.00000000, 1.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_xyY( - np.array( + xp_as_array( [ [0.20654008, 0.12197225, 0.05136952], [0.00000000, 0.00000000, 0.00000000], [0.00000000, 1.00000000, 0.00000000], - ] + ], + xp=xp, ) ), - np.array( - [ - [0.54369557, 0.32107944, 0.12197225], - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 1.00000000, 1.00000000], - ] - ), + [ + [0.54369557, 0.32107944, 0.12197225], + [0.00000000, 0.00000000, 0.00000000], + [0.00000000, 1.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_xyY(self) -> None: + def test_n_dimensional_XYZ_to_xyY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.XYZ_to_xyY` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - xyY = XYZ_to_xyY(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + xyY = as_ndarray(XYZ_to_xyY(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose(XYZ_to_xyY(XYZ), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_xyY(XYZ), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose(XYZ_to_xyY(XYZ), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_xyY(XYZ), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_xyY(self) -> None: + def test_domain_range_scale_XYZ_to_xyY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.XYZ_to_xyY` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - xyY = XYZ_to_xyY(XYZ) - XYZ = np.reshape(np.tile(XYZ, (6, 1)), (2, 3, 3)) - xyY = np.reshape(np.tile(xyY, (6, 1)), (2, 3, 3)) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + xyY = as_ndarray(XYZ_to_xyY(XYZ)) + XYZ = xp_reshape(xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + xyY = xp_reshape(xp.tile(xp_as_array(xyY, xp=xp), (6, 1)), (2, 3, 3), xp=xp) d_r = ( ("reference", 1, 1), @@ -122,9 +133,9 @@ def test_domain_range_scale_XYZ_to_xyY(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_xyY(XYZ * factor_a), - xyY * factor_b, + xp_assert_close( + XYZ_to_xyY(XYZ * xp_as_array(factor_a, xp=xp)), + xyY * xp_as_array(factor_b, xp=xp), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -143,80 +154,79 @@ class TestxyY_to_XYZ: methods. """ - def test_xyY_to_XYZ(self) -> None: + def test_xyY_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_xyy.xyY_to_XYZ` definition.""" - np.testing.assert_allclose( - xyY_to_XYZ(np.array([0.54369557, 0.32107944, 0.12197225])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + xyY_to_XYZ(xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xyY_to_XYZ(np.array([0.29777735, 0.48246446, 0.23042768])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + xyY_to_XYZ(xp_as_array([0.29777735, 0.48246446, 0.23042768], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xyY_to_XYZ(np.array([0.18582823, 0.14633764, 0.06157201])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + xyY_to_XYZ(xp_as_array([0.18582823, 0.14633764, 0.06157201], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xyY_to_XYZ(np.array([0.34567, 0.3585, 0.00000000])), - np.array([0.00000000, 0.00000000, 0.00000000]), + xp_assert_close( + xyY_to_XYZ(xp_as_array([0.34567, 0.3585, 0.00000000], xp=xp)), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xyY_to_XYZ( - np.array( + xp_as_array( [ [0.54369557, 0.32107944, 0.12197225], [0.31270000, 0.32900000, 0.00000000], [0.00000000, 1.00000000, 1.00000000], - ] + ], + xp=xp, ) ), - np.array( - [ - [0.20654008, 0.12197225, 0.05136952], - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - ] - ), + [ + [0.20654008, 0.12197225, 0.05136952], + [0.00000000, 0.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xyY_to_XYZ(self) -> None: + def test_n_dimensional_xyY_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xyY_to_XYZ` definition n-dimensional support. """ - xyY = np.array([0.54369557, 0.32107944, 0.12197225]) - XYZ = xyY_to_XYZ(xyY) + xyY = xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp) + XYZ = as_ndarray(xyY_to_XYZ(xyY)) - xyY = np.tile(xyY, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(xyY_to_XYZ(xyY), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(xyY_to_XYZ(xyY), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - xyY = np.reshape(xyY, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(xyY_to_XYZ(xyY), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(xyY_to_XYZ(xyY), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_xyY_to_XYZ(self) -> None: + def test_domain_range_scale_xyY_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xyY_to_XYZ` definition domain and range scale support. """ - xyY = np.array([0.54369557, 0.32107944, 0.12197225]) - XYZ = xyY_to_XYZ(xyY) - xyY = np.reshape(np.tile(xyY, (6, 1)), (2, 3, 3)) - XYZ = np.reshape(np.tile(XYZ, (6, 1)), (2, 3, 3)) + xyY = xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp) + XYZ = as_ndarray(xyY_to_XYZ(xyY)) + xyY = xp_reshape(xp.tile(xp_as_array(xyY, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)), (2, 3, 3), xp=xp) d_r = ( ("reference", 1, 1), @@ -225,8 +235,8 @@ def test_domain_range_scale_xyY_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - xyY_to_XYZ(xyY * factor_a), + xp_assert_close( + xyY_to_XYZ(xyY * xp_as_array(factor_a, xp=xp)), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -246,60 +256,60 @@ class TestxyY_to_xy: methods. """ - def test_xyY_to_xy(self) -> None: + def test_xyY_to_xy(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_xyy.xyY_to_xy` definition.""" - np.testing.assert_allclose( - xyY_to_xy(np.array([0.54369557, 0.32107944, 0.12197225])), - np.array([0.54369557, 0.32107944]), + xp_assert_close( + xyY_to_xy(xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp)), + [0.54369557, 0.32107944], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xyY_to_xy(np.array([0.29777735, 0.48246446, 0.23042768])), - np.array([0.29777735, 0.48246446]), + xp_assert_close( + xyY_to_xy(xp_as_array([0.29777735, 0.48246446, 0.23042768], xp=xp)), + [0.29777735, 0.48246446], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xyY_to_xy(np.array([0.18582823, 0.14633764, 0.06157201])), - np.array([0.18582823, 0.14633764]), + xp_assert_close( + xyY_to_xy(xp_as_array([0.18582823, 0.14633764, 0.06157201], xp=xp)), + [0.18582823, 0.14633764], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xyY_to_xy(np.array([0.31270, 0.32900])), - np.array([0.31270000, 0.32900000]), + xp_assert_close( + xyY_to_xy(xp_as_array([0.31270, 0.32900], xp=xp)), + [0.31270000, 0.32900000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xyY_to_xy(self) -> None: + def test_n_dimensional_xyY_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xyY_to_xy` definition n-dimensional support. """ - xyY = np.array([0.54369557, 0.32107944, 0.12197225]) - xy = xyY_to_xy(xyY) + xyY = xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp) + xy = as_ndarray(xyY_to_xy(xyY)) - xyY = np.tile(xyY, (6, 1)) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose(xyY_to_xy(xyY), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(xyY_to_xy(xyY), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - xyY = np.reshape(xyY, (2, 3, 3)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose(xyY_to_xy(xyY), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(xyY_to_xy(xyY), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_xyY_to_xy(self) -> None: + def test_domain_range_scale_xyY_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xyY_to_xy` definition domain and range scale support. """ - xyY = np.array([0.54369557, 0.32107944, 0.12197225]) - xy = xyY_to_xy(xyY) - xyY = np.reshape(np.tile(xyY, (6, 1)), (2, 3, 3)) - xy = np.reshape(np.tile(xy, (6, 1)), (2, 3, 2)) + xyY = xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp) + xy = as_ndarray(xyY_to_xy(xyY)) + xyY = xp_reshape(xp.tile(xp_as_array(xyY, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + xy = xp_reshape(xp.tile(xp_as_array(xy, xp=xp), (6, 1)), (2, 3, 2), xp=xp) d_r = ( ("reference", 1, 1), @@ -308,8 +318,8 @@ def test_domain_range_scale_xyY_to_xy(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - xyY_to_xy(xyY * factor_a), + xp_assert_close( + xyY_to_xy(xyY * xp_as_array(factor_a, xp=xp)), xy * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -329,66 +339,66 @@ class Testxy_to_xyY: methods. """ - def test_xy_to_xyY(self) -> None: + def test_xy_to_xyY(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_xyy.xy_to_xyY` definition.""" - np.testing.assert_allclose( - xy_to_xyY(np.array([0.54369557, 0.32107944])), - np.array([0.54369557, 0.32107944, 1.00000000]), + xp_assert_close( + xy_to_xyY(xp_as_array([0.54369557, 0.32107944], xp=xp)), + [0.54369557, 0.32107944, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_xyY(np.array([0.29777735, 0.48246446])), - np.array([0.29777735, 0.48246446, 1.00000000]), + xp_assert_close( + xy_to_xyY(xp_as_array([0.29777735, 0.48246446], xp=xp)), + [0.29777735, 0.48246446, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_xyY(np.array([0.18582823, 0.14633764])), - np.array([0.18582823, 0.14633764, 1.00000000]), + xp_assert_close( + xy_to_xyY(xp_as_array([0.18582823, 0.14633764], xp=xp)), + [0.18582823, 0.14633764, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_xyY(np.array([0.31270000, 0.32900000, 1.00000000])), - np.array([0.31270000, 0.32900000, 1.00000000]), + xp_assert_close( + xy_to_xyY(xp_as_array([0.31270000, 0.32900000, 1.00000000], xp=xp)), + [0.31270000, 0.32900000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_xyY(np.array([0.31270000, 0.32900000]), 100), - np.array([0.31270000, 0.32900000, 100.00000000]), + xp_assert_close( + xy_to_xyY(xp_as_array([0.31270000, 0.32900000], xp=xp), 100), + [0.31270000, 0.32900000, 100.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_xyY(self) -> None: + def test_n_dimensional_xy_to_xyY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xy_to_xyY` definition n-dimensional support. """ - xy = np.array([0.54369557, 0.32107944]) - xyY = xy_to_xyY(xy) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) + xyY = as_ndarray(xy_to_xyY(xy)) - xy = np.tile(xy, (6, 1)) - xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose(xy_to_xyY(xy), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + xp_assert_close(xy_to_xyY(xy), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose(xy_to_xyY(xy), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(xy_to_xyY(xy), xyY, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_xy_to_xyY(self) -> None: + def test_domain_range_scale_xy_to_xyY(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xy_to_xyY` definition domain and range scale support. """ - xy = np.array([0.54369557, 0.32107944, 0.12197225]) - xyY = xy_to_xyY(xy) - xy = np.reshape(np.tile(xy, (6, 1)), (2, 3, 3)) - xyY = np.reshape(np.tile(xyY, (6, 1)), (2, 3, 3)) + xy = xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp) + xyY = as_ndarray(xy_to_xyY(xy)) + xy = xp_reshape(xp.tile(xp_as_array(xy, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + xyY = xp_reshape(xp.tile(xp_as_array(xyY, xp=xp), (6, 1)), (2, 3, 3), xp=xp) d_r = ( ("reference", 1, 1), @@ -401,9 +411,9 @@ def test_domain_range_scale_xy_to_xyY(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - xy_to_xyY(xy * factor_a), - xyY * factor_b, + xp_assert_close( + xy_to_xyY(xy * xp_as_array(factor_a, xp=xp)), + xyY * xp_as_array(factor_b, xp=xp), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -422,66 +432,68 @@ class TestXYZ_to_xy: methods. """ - def test_XYZ_to_xy(self) -> None: + def test_XYZ_to_xy(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_xyy.XYZ_to_xy` definition.""" - np.testing.assert_allclose( - XYZ_to_xy(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.54369557, 0.32107944]), + xp_assert_close( + XYZ_to_xy(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.54369557, 0.32107944], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_xy(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.29777735, 0.48246446]), + xp_assert_close( + XYZ_to_xy(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.29777735, 0.48246446], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_xy(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.18582823, 0.14633764]), + xp_assert_close( + XYZ_to_xy(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.18582823, 0.14633764], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_xy(np.array([0.00000000, 0.00000000, 0.00000000])), - np.array([0.00000000, 0.00000000]), + xp_assert_close( + XYZ_to_xy(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)), + [0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_xy(self) -> None: + def test_n_dimensional_XYZ_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.XYZ_to_xy` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - xy = XYZ_to_xy(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + xy = as_ndarray(XYZ_to_xy(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose(XYZ_to_xy(XYZ), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_xy(XYZ), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose(XYZ_to_xy(XYZ), xy, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(XYZ_to_xy(XYZ), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_xy(self) -> None: + def test_domain_range_scale_XYZ_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.XYZ_to_xy` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - xy = XYZ_to_xy(XYZ) - XYZ = np.reshape(np.tile(XYZ, (6, 1)), (2, 3, 3)) - xy = np.reshape(np.tile(xy, (6, 1)), (2, 3, 2)) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + xy = as_ndarray(XYZ_to_xy(XYZ)) + XYZ = xp_reshape(xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + xy = xp_reshape(xp.tile(xp_as_array(xy, xp=xp), (6, 1)), (2, 3, 2), xp=xp) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_xy(XYZ * factor), xy, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + XYZ_to_xy(XYZ * factor), + xy, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -499,60 +511,60 @@ class Testxy_to_XYZ: methods. """ - def test_xy_to_XYZ(self) -> None: + def test_xy_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.cie_xyy.xy_to_XYZ` definition.""" - np.testing.assert_allclose( - xy_to_XYZ(np.array([0.54369557, 0.32107944])), - np.array([1.69333661, 1.00000000, 0.42115742]), + xp_assert_close( + xy_to_XYZ(xp_as_array([0.54369557, 0.32107944], xp=xp)), + [1.69333661, 1.00000000, 0.42115742], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_XYZ(np.array([0.29777735, 0.48246446])), - np.array([0.61720059, 1.00000000, 0.45549094]), + xp_assert_close( + xy_to_XYZ(xp_as_array([0.29777735, 0.48246446], xp=xp)), + [0.61720059, 1.00000000, 0.45549094], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_XYZ(np.array([0.18582823, 0.14633764])), - np.array([1.26985942, 1.00000000, 4.56365245]), + xp_assert_close( + xy_to_XYZ(xp_as_array([0.18582823, 0.14633764], xp=xp)), + [1.26985942, 1.00000000, 4.56365245], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_XYZ(np.array([0.31270000, 0.32900000])), - np.array([0.95045593, 1.00000000, 1.08905775]), + xp_assert_close( + xy_to_XYZ(xp_as_array([0.31270000, 0.32900000], xp=xp)), + [0.95045593, 1.00000000, 1.08905775], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_XYZ(self) -> None: + def test_n_dimensional_xy_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xy_to_XYZ` definition n-dimensional support. """ - xy = np.array([0.54369557, 0.32107944]) - XYZ = xy_to_XYZ(xy) + xy = xp_as_array([0.54369557, 0.32107944], xp=xp) + XYZ = as_ndarray(xy_to_XYZ(xy)) - xy = np.tile(xy, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(xy_to_XYZ(xy), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(xy_to_XYZ(xy), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(xy_to_XYZ(xy), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(xy_to_XYZ(xy), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_xy_to_XYZ(self) -> None: + def test_domain_range_scale_xy_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.cie_xyy.xy_to_XYZ` definition domain and range scale support. """ - xy = np.array([0.54369557, 0.32107944, 0.12197225]) - XYZ = xy_to_XYZ(xy) - xy = np.reshape(np.tile(xy, (6, 1)), (2, 3, 3)) - XYZ = np.reshape(np.tile(XYZ, (6, 1)), (2, 3, 3)) + xy = xp_as_array([0.54369557, 0.32107944, 0.12197225], xp=xp) + XYZ = as_ndarray(xy_to_XYZ(xy)) + xy = xp_reshape(xp.tile(xp_as_array(xy, xp=xp), (6, 1)), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)), (2, 3, 3), xp=xp) d_r = ( ("reference", 1, 1), @@ -561,8 +573,8 @@ def test_domain_range_scale_xy_to_XYZ(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - xy_to_XYZ(xy * factor_a), + xp_assert_close( + xy_to_XYZ(xy * xp_as_array(factor_a, xp=xp)), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_common.py b/colour/models/tests/test_common.py index c874bf85ec..5278ec1148 100644 --- a/colour/models/tests/test_common.py +++ b/colour/models/tests/test_common.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import Iab_to_XYZ, Jab_to_JCh, JCh_to_Jab, XYZ_to_Iab -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,56 +43,52 @@ class TestJab_to_JCh: methods. """ - def test_Jab_to_JCh(self) -> None: + def test_Jab_to_JCh(self, xp: ModuleType) -> None: """Test :func:`colour.models.common.Jab_to_JCh` definition.""" - np.testing.assert_allclose( - Jab_to_JCh(np.array([41.52787529, 52.63858304, 26.92317922])), - np.array([41.52787529, 59.12425901, 27.08848784]), + xp_assert_close( + Jab_to_JCh(xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp)), + [41.52787529, 59.12425901, 27.08848784], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Jab_to_JCh(np.array([55.11636304, -41.08791787, 30.91825778])), - np.array([55.11636304, 51.42135412, 143.03889556]), + xp_assert_close( + Jab_to_JCh(xp_as_array([55.11636304, -41.08791787, 30.91825778], xp=xp)), + [55.11636304, 51.42135412, 143.03889556], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Jab_to_JCh(np.array([29.80565520, 20.01830466, -48.34913874])), - np.array([29.80565520, 52.32945383, 292.49133666]), + xp_assert_close( + Jab_to_JCh(xp_as_array([29.80565520, 20.01830466, -48.34913874], xp=xp)), + [29.80565520, 52.32945383, 292.49133666], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Jab_to_JCh(self) -> None: + def test_n_dimensional_Jab_to_JCh(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.Jab_to_JCh` definition n-dimensional arrays support. """ - Lab = np.array([41.52787529, 52.63858304, 26.92317922]) - LCHab = Jab_to_JCh(Lab) + Lab = xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp) + LCHab = as_ndarray(Jab_to_JCh(Lab)) - Lab = np.tile(Lab, (6, 1)) - LCHab = np.tile(LCHab, (6, 1)) - np.testing.assert_allclose( - Jab_to_JCh(Lab), LCHab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + LCHab = xp.tile(xp_as_array(LCHab, xp=xp), (6, 1)) + xp_assert_close(Jab_to_JCh(Lab), LCHab, atol=TOLERANCE_ABSOLUTE_TESTS) - Lab = np.reshape(Lab, (2, 3, 3)) - LCHab = np.reshape(LCHab, (2, 3, 3)) - np.testing.assert_allclose( - Jab_to_JCh(Lab), LCHab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + LCHab = xp_reshape(xp_as_array(LCHab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Jab_to_JCh(Lab), LCHab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Jab_to_JCh(self) -> None: + def test_domain_range_scale_Jab_to_JCh(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.Jab_to_JCh` definition domain and range scale support. """ - Lab = np.array([41.52787529, 52.63858304, 26.92317922]) - LCHab = Jab_to_JCh(Lab) + Lab = xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp) + LCHab = as_ndarray(Jab_to_JCh(Lab)) d_r = ( ("reference", 1, 1), @@ -89,8 +97,8 @@ def test_domain_range_scale_Jab_to_JCh(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - Jab_to_JCh(Lab * factor_a), + xp_assert_close( + Jab_to_JCh(Lab * xp_as_array(factor_a, xp=xp)), LCHab * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -110,56 +118,52 @@ class TestJCh_to_Jab: methods. """ - def test_JCh_to_Jab(self) -> None: + def test_JCh_to_Jab(self, xp: ModuleType) -> None: """Test :func:`colour.models.common.JCh_to_Jab` definition.""" - np.testing.assert_allclose( - JCh_to_Jab(np.array([41.52787529, 59.12425901, 27.08848784])), - np.array([41.52787529, 52.63858304, 26.92317922]), + xp_assert_close( + JCh_to_Jab(xp_as_array([41.52787529, 59.12425901, 27.08848784], xp=xp)), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JCh_to_Jab(np.array([55.11636304, 51.42135412, 143.03889556])), - np.array([55.11636304, -41.08791787, 30.91825778]), + xp_assert_close( + JCh_to_Jab(xp_as_array([55.11636304, 51.42135412, 143.03889556], xp=xp)), + [55.11636304, -41.08791787, 30.91825778], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - JCh_to_Jab(np.array([29.80565520, 52.32945383, 292.49133666])), - np.array([29.80565520, 20.01830466, -48.34913874]), + xp_assert_close( + JCh_to_Jab(xp_as_array([29.80565520, 52.32945383, 292.49133666], xp=xp)), + [29.80565520, 20.01830466, -48.34913874], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_JCh_to_Jab(self) -> None: + def test_n_dimensional_JCh_to_Jab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.JCh_to_Jab` definition n-dimensional arrays support. """ - LCHab = np.array([41.52787529, 59.12425901, 27.08848784]) - Lab = JCh_to_Jab(LCHab) + LCHab = xp_as_array([41.52787529, 59.12425901, 27.08848784], xp=xp) + Lab = as_ndarray(JCh_to_Jab(LCHab)) - LCHab = np.tile(LCHab, (6, 1)) - Lab = np.tile(Lab, (6, 1)) - np.testing.assert_allclose( - JCh_to_Jab(LCHab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + LCHab = xp.tile(xp_as_array(LCHab, xp=xp), (6, 1)) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + xp_assert_close(JCh_to_Jab(LCHab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - LCHab = np.reshape(LCHab, (2, 3, 3)) - Lab = np.reshape(Lab, (2, 3, 3)) - np.testing.assert_allclose( - JCh_to_Jab(LCHab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + LCHab = xp_reshape(xp_as_array(LCHab, xp=xp), (2, 3, 3), xp=xp) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(JCh_to_Jab(LCHab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_JCh_to_Jab(self) -> None: + def test_domain_range_scale_JCh_to_Jab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.JCh_to_Jab` definition domain and range scale support. """ - LCHab = np.array([41.52787529, 59.12425901, 27.08848784]) - Lab = JCh_to_Jab(LCHab) + LCHab = xp_as_array([41.52787529, 59.12425901, 27.08848784], xp=xp) + Lab = as_ndarray(JCh_to_Jab(LCHab)) d_r = ( ("reference", 1, 1), @@ -168,8 +172,8 @@ def test_domain_range_scale_JCh_to_Jab(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - JCh_to_Jab(LCHab * factor_a), + xp_assert_close( + JCh_to_Jab(LCHab * xp_as_array(factor_a, xp=xp)), Lab * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -205,80 +209,84 @@ def setup_method(self) -> None: ] ) - def test_XYZ_to_Iab(self) -> None: + def test_XYZ_to_Iab(self, xp: ModuleType) -> None: """Test :func:`colour.models.common.XYZ_to_Iab` definition.""" - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Iab( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab, ), - np.array([0.38426191, 0.38487306, 0.18886838]), + [0.38426191, 0.38487306, 0.18886838], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Iab( - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp), self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab, ), - np.array([0.49437481, -0.19251742, 0.18080304]), + [0.49437481, -0.19251742, 0.18080304], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Iab( - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp), self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab, ), - np.array([0.35167774, -0.07525627, -0.30921279]), + [0.35167774, -0.07525627, -0.30921279], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Iab(self) -> None: + def test_n_dimensional_XYZ_to_Iab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.XYZ_to_Iab` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Iab = XYZ_to_Iab(XYZ, self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Iab = as_ndarray( + XYZ_to_Iab(XYZ, self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab) + ) - XYZ = np.tile(XYZ, (6, 1)) - Iab = np.tile(Iab, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_Iab(XYZ, self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab), Iab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Iab = np.reshape(Iab, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_Iab(XYZ, self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab), Iab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_Iab(self) -> None: + def test_domain_range_scale_XYZ_to_Iab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.XYZ_to_Iab` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Iab = XYZ_to_Iab(XYZ, self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Iab = as_ndarray( + XYZ_to_Iab(XYZ, self.LMS_to_LMS_p, self.M_XYZ_to_LMS, self.M_LMS_p_to_Iab) + ) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Iab( XYZ * factor, self.LMS_to_LMS_p, @@ -327,80 +335,84 @@ def setup_method(self) -> None: ) ) - def test_Iab_to_XYZ(self) -> None: + def test_Iab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.common.Iab_to_XYZ` definition.""" - np.testing.assert_allclose( + xp_assert_close( Iab_to_XYZ( - np.array([0.38426191, 0.38487306, 0.18886838]), + xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp), self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ, ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Iab_to_XYZ( - np.array([0.49437481, -0.19251742, 0.18080304]), + xp_as_array([0.49437481, -0.19251742, 0.18080304], xp=xp), self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ, ), - np.array([0.14222010, 0.23042768, 0.10495772]), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Iab_to_XYZ( - np.array([0.35167774, -0.07525627, -0.30921279]), + xp_as_array([0.35167774, -0.07525627, -0.30921279], xp=xp), self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ, ), - np.array([0.07818780, 0.06157201, 0.28099326]), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Iab_to_XYZ(self) -> None: + def test_n_dimensional_Iab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.Iab_to_XYZ` definition n-dimensional support. """ - Iab = np.array([0.38426191, 0.38487306, 0.18886838]) - XYZ = Iab_to_XYZ(Iab, self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ) + Iab = xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp) + XYZ = as_ndarray( + Iab_to_XYZ(Iab, self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ) + ) - Iab = np.tile(Iab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( Iab_to_XYZ(Iab, self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Iab = np.reshape(Iab, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( Iab_to_XYZ(Iab, self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_Iab_to_XYZ(self) -> None: + def test_domain_range_scale_Iab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.common.Iab_to_XYZ` definition domain and range scale support. """ - Iab = np.array([0.38426191, 0.38487306, 0.18886838]) - XYZ = Iab_to_XYZ(Iab, self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ) + Iab = xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp) + XYZ = as_ndarray( + Iab_to_XYZ(Iab, self.LMS_p_to_LMS, self.M_Iab_to_LMS_p, self.M_LMS_to_XYZ) + ) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Iab_to_XYZ( Iab * factor, self.LMS_p_to_LMS, diff --git a/colour/models/tests/test_din99.py b/colour/models/tests/test_din99.py index 27af969da4..a166f599e9 100644 --- a/colour/models/tests/test_din99.py +++ b/colour/models/tests/test_din99.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import DIN99_to_Lab, DIN99_to_XYZ, Lab_to_DIN99, XYZ_to_DIN99 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,106 +43,102 @@ class TestLab_to_DIN99: methods. """ - def test_Lab_to_DIN99(self) -> None: + def test_Lab_to_DIN99(self, xp: ModuleType) -> None: """Test :func:`colour.models.din99.Lab_to_DIN99` definition.""" - np.testing.assert_allclose( - Lab_to_DIN99(np.array([41.52787529, 52.63858304, 26.92317922])), - np.array([53.22821988, 28.41634656, 3.89839552]), + xp_assert_close( + Lab_to_DIN99(xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp)), + [53.22821988, 28.41634656, 3.89839552], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Lab_to_DIN99(np.array([55.11636304, -41.08791787, 30.91825778])), - np.array([66.08943912, -17.35290106, 16.09690691]), + xp_assert_close( + Lab_to_DIN99(xp_as_array([55.11636304, -41.08791787, 30.91825778], xp=xp)), + [66.08943912, -17.35290106, 16.09690691], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Lab_to_DIN99(np.array([29.80565520, 20.01830466, -48.34913874])), - np.array([40.71533366, 3.48714163, -21.45321411]), + xp_assert_close( + Lab_to_DIN99(xp_as_array([29.80565520, 20.01830466, -48.34913874], xp=xp)), + [40.71533366, 3.48714163, -21.45321411], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99( - np.array([41.52787529, 52.63858304, 26.92317922]), + xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp), method="DIN99b", ), - np.array([45.58303137, 34.71824493, 17.61622149]), + [45.58303137, 34.71824493, 17.61622149], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99( - np.array([41.52787529, 52.63858304, 26.92317922]), + xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp), method="DIN99c", ), - np.array([45.40284208, 32.75074741, 15.74603532]), + [45.40284208, 32.75074741, 15.74603532], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99( - np.array([41.52787529, 52.63858304, 26.92317922]), + xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp), method="DIN99d", ), - np.array([45.31204747, 31.42106716, 14.17004652]), + [45.31204747, 31.42106716, 14.17004652], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Lab_to_DIN99(self) -> None: + def test_n_dimensional_Lab_to_DIN99(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.Lab_to_DIN99` definition n-dimensional support. """ - Lab = np.array([41.52787529, 52.63858304, 26.92317922]) - Lab_99 = Lab_to_DIN99(Lab) + Lab = xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp) + Lab_99 = as_ndarray(Lab_to_DIN99(Lab)) - Lab = np.tile(Lab, (6, 1)) - Lab_99 = np.tile(Lab_99, (6, 1)) - np.testing.assert_allclose( - Lab_to_DIN99(Lab), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + Lab_99 = xp.tile(xp_as_array(Lab_99, xp=xp), (6, 1)) + xp_assert_close(Lab_to_DIN99(Lab), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS) - Lab = np.reshape(Lab, (2, 3, 3)) - Lab_99 = np.reshape(Lab_99, (2, 3, 3)) - np.testing.assert_allclose( - Lab_to_DIN99(Lab), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + Lab_99 = xp_reshape(xp_as_array(Lab_99, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Lab_to_DIN99(Lab), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Lab_to_DIN99(self) -> None: + def test_domain_range_scale_Lab_to_DIN99(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.Lab_to_DIN99` definition domain and range scale support. """ - Lab = np.array([41.52787529, 52.63858304, 26.92317922]) - Lab_99 = Lab_to_DIN99(Lab) - Lab_99_b = Lab_to_DIN99(Lab, method="DIN99b") - Lab_99_c = Lab_to_DIN99(Lab, method="DIN99c") - Lab_99_d = Lab_to_DIN99(Lab, method="DIN99d") + Lab = xp_as_array([41.52787529, 52.63858304, 26.92317922], xp=xp) + Lab_99 = as_ndarray(Lab_to_DIN99(Lab)) + Lab_99_b = as_ndarray(Lab_to_DIN99(Lab, method="DIN99b")) + Lab_99_c = as_ndarray(Lab_to_DIN99(Lab, method="DIN99c")) + Lab_99_d = as_ndarray(Lab_to_DIN99(Lab, method="DIN99d")) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99(Lab * factor), Lab_99 * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99((Lab * factor), method="DIN99b"), Lab_99_b * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99((Lab * factor), method="DIN99c"), Lab_99_c * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Lab_to_DIN99((Lab * factor), method="DIN99d"), Lab_99_d * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -154,106 +162,102 @@ class TestDIN99_to_Lab: methods. """ - def test_DIN99_to_Lab(self) -> None: + def test_DIN99_to_Lab(self, xp: ModuleType) -> None: """Test :func:`colour.models.din99.DIN99_to_Lab` definition.""" - np.testing.assert_allclose( - DIN99_to_Lab(np.array([53.22821988, 28.41634656, 3.89839552])), - np.array([41.52787529, 52.63858304, 26.92317922]), + xp_assert_close( + DIN99_to_Lab(xp_as_array([53.22821988, 28.41634656, 3.89839552], xp=xp)), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - DIN99_to_Lab(np.array([66.08943912, -17.35290106, 16.09690691])), - np.array([55.11636304, -41.08791787, 30.91825778]), + xp_assert_close( + DIN99_to_Lab(xp_as_array([66.08943912, -17.35290106, 16.09690691], xp=xp)), + [55.11636304, -41.08791787, 30.91825778], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - DIN99_to_Lab(np.array([40.71533366, 3.48714163, -21.45321411])), - np.array([29.80565520, 20.01830466, -48.34913874]), + xp_assert_close( + DIN99_to_Lab(xp_as_array([40.71533366, 3.48714163, -21.45321411], xp=xp)), + [29.80565520, 20.01830466, -48.34913874], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab( - np.array([45.58303137, 34.71824493, 17.61622149]), + xp_as_array([45.58303137, 34.71824493, 17.61622149], xp=xp), method="DIN99b", ), - np.array([41.52787529, 52.63858304, 26.92317922]), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab( - np.array([45.40284208, 32.75074741, 15.74603532]), + xp_as_array([45.40284208, 32.75074741, 15.74603532], xp=xp), method="DIN99c", ), - np.array([41.52787529, 52.63858304, 26.92317922]), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab( - np.array([45.31204747, 31.42106716, 14.17004652]), + xp_as_array([45.31204747, 31.42106716, 14.17004652], xp=xp), method="DIN99d", ), - np.array([41.52787529, 52.63858304, 26.92317922]), + [41.52787529, 52.63858304, 26.92317922], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_DIN99_to_Lab(self) -> None: + def test_n_dimensional_DIN99_to_Lab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.DIN99_to_Lab` definition n-dimensional support. """ - Lab_99 = np.array([53.22821988, 28.41634656, 3.89839552]) - Lab = DIN99_to_Lab(Lab_99) + Lab_99 = xp_as_array([53.22821988, 28.41634656, 3.89839552], xp=xp) + Lab = as_ndarray(DIN99_to_Lab(Lab_99)) - Lab_99 = np.tile(Lab_99, (6, 1)) - Lab = np.tile(Lab, (6, 1)) - np.testing.assert_allclose( - DIN99_to_Lab(Lab_99), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab_99 = xp.tile(xp_as_array(Lab_99, xp=xp), (6, 1)) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + xp_assert_close(DIN99_to_Lab(Lab_99), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - Lab_99 = np.reshape(Lab_99, (2, 3, 3)) - Lab = np.reshape(Lab, (2, 3, 3)) - np.testing.assert_allclose( - DIN99_to_Lab(Lab_99), Lab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab_99 = xp_reshape(xp_as_array(Lab_99, xp=xp), (2, 3, 3), xp=xp) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(DIN99_to_Lab(Lab_99), Lab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_DIN99_to_Lab(self) -> None: + def test_domain_range_scale_DIN99_to_Lab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.DIN99_to_Lab` definition domain and range scale support. """ - Lab_99 = np.array([53.22821988, 28.41634656, 3.89839552]) - Lab = DIN99_to_Lab(Lab_99) - Lab_b = DIN99_to_Lab(Lab_99, method="DIN99b") - Lab_c = DIN99_to_Lab(Lab_99, method="DIN99c") - Lab_d = DIN99_to_Lab(Lab_99, method="DIN99d") + Lab_99 = xp_as_array([53.22821988, 28.41634656, 3.89839552], xp=xp) + Lab = as_ndarray(DIN99_to_Lab(Lab_99)) + Lab_b = as_ndarray(DIN99_to_Lab(Lab_99, method="DIN99b")) + Lab_c = as_ndarray(DIN99_to_Lab(Lab_99, method="DIN99c")) + Lab_d = as_ndarray(DIN99_to_Lab(Lab_99, method="DIN99d")) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab(Lab_99 * factor), Lab * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab((Lab_99 * factor), method="DIN99b"), Lab_b * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab((Lab_99 * factor), method="DIN99c"), Lab_c * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_Lab((Lab_99 * factor), method="DIN99d"), Lab_d * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -277,70 +281,67 @@ class TestXYZ_to_DIN99: methods. """ - def test_XYZ_to_DIN99(self) -> None: + def test_XYZ_to_DIN99(self, xp: ModuleType) -> None: """Test :func:`colour.models.din99.XYZ_to_DIN99` definition.""" - np.testing.assert_allclose( - XYZ_to_DIN99(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([53.22821988, 28.41634656, 3.89839552]), + xp_assert_close( + XYZ_to_DIN99(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [53.22821988, 28.41634656, 3.89839552], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_DIN99(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([66.08943912, -17.35290106, 16.09690691]), + xp_assert_close( + XYZ_to_DIN99(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [66.08943912, -17.35290106, 16.09690691], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_DIN99(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([40.71533366, 3.48714163, -21.45321411]), + xp_assert_close( + XYZ_to_DIN99(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [40.71533366, 3.48714163, -21.45321411], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_DIN99( - np.array([0.20654008, 0.12197225, 0.05136952]), method="DIN99b" + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + method="DIN99b", ), - np.array([45.58303137, 34.71824493, 17.61622149]), + [45.58303137, 34.71824493, 17.61622149], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_DIN99(self) -> None: + def test_n_dimensional_XYZ_to_DIN99(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.XYZ_to_DIN99` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Lab_99 = XYZ_to_DIN99(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Lab_99 = as_ndarray(XYZ_to_DIN99(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Lab_99 = np.tile(Lab_99, (6, 1)) - np.testing.assert_allclose( - XYZ_to_DIN99(XYZ), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Lab_99 = xp.tile(xp_as_array(Lab_99, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_DIN99(XYZ), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Lab_99 = np.reshape(Lab_99, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_DIN99(XYZ), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Lab_99 = xp_reshape(xp_as_array(Lab_99, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_DIN99(XYZ), Lab_99, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_DIN99(self) -> None: + def test_domain_range_scale_XYZ_to_DIN99(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.XYZ_to_DIN99` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Lab_99 = XYZ_to_DIN99(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Lab_99 = as_ndarray(XYZ_to_DIN99(XYZ)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_DIN99(XYZ * factor_a), + xp_assert_close( + XYZ_to_DIN99(XYZ * xp_as_array(factor_a, xp=xp)), Lab_99 * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -360,71 +361,67 @@ class TestDIN99_to_XYZ: methods. """ - def test_DIN99_to_XYZ(self) -> None: + def test_DIN99_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.din99.DIN99_to_XYZ` definition.""" - np.testing.assert_allclose( - DIN99_to_XYZ(np.array([53.22821988, 28.41634656, 3.89839552])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + DIN99_to_XYZ(xp_as_array([53.22821988, 28.41634656, 3.89839552], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - DIN99_to_XYZ(np.array([66.08943912, -17.35290106, 16.09690691])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + DIN99_to_XYZ(xp_as_array([66.08943912, -17.35290106, 16.09690691], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - DIN99_to_XYZ(np.array([40.71533366, 3.48714163, -21.45321411])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + DIN99_to_XYZ(xp_as_array([40.71533366, 3.48714163, -21.45321411], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( DIN99_to_XYZ( - np.array([45.58303137, 34.71824493, 17.61622149]), + xp_as_array([45.58303137, 34.71824493, 17.61622149], xp=xp), method="DIN99b", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_DIN99_to_XYZ(self) -> None: + def test_n_dimensional_DIN99_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.DIN99_to_XYZ` definition n-dimensional support. """ - Lab_99 = np.array([53.22821988, 28.41634656, 3.89839552]) - XYZ = DIN99_to_XYZ(Lab_99) + Lab_99 = xp_as_array([53.22821988, 28.41634656, 3.89839552], xp=xp) + XYZ = as_ndarray(DIN99_to_XYZ(Lab_99)) - Lab_99 = np.tile(Lab_99, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - DIN99_to_XYZ(Lab_99), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab_99 = xp.tile(xp_as_array(Lab_99, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(DIN99_to_XYZ(Lab_99), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Lab_99 = np.reshape(Lab_99, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - DIN99_to_XYZ(Lab_99), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Lab_99 = xp_reshape(xp_as_array(Lab_99, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(DIN99_to_XYZ(Lab_99), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_DIN99_to_XYZ(self) -> None: + def test_domain_range_scale_DIN99_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.din99.DIN99_to_XYZ` definition domain and range scale support. """ - Lab_99 = np.array([53.22821988, 28.41634656, 3.89839552]) - XYZ = DIN99_to_XYZ(Lab_99) + Lab_99 = xp_as_array([53.22821988, 28.41634656, 3.89839552], xp=xp) + XYZ = as_ndarray(DIN99_to_XYZ(Lab_99)) d_r = (("reference", 1, 1), ("1", 0.01, 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - DIN99_to_XYZ(Lab_99 * factor_a), + xp_assert_close( + DIN99_to_XYZ(Lab_99 * xp_as_array(factor_a, xp=xp)), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_hdr_cie_lab.py b/colour/models/tests/test_hdr_cie_lab.py index 55fffffa8f..b3427c3d6b 100644 --- a/colour/models/tests/test_hdr_cie_lab.py +++ b/colour/models/tests/test_hdr_cie_lab.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -9,7 +14,14 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import XYZ_to_hdr_CIELab, hdr_CIELab_to_XYZ from colour.models.hdr_cie_lab import exponent_hdr_CIELab -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,37 +43,41 @@ class TestExponent_hdr_CIELab: definition unit tests methods. """ - def test_exponent_hdr_CIELab(self) -> None: + def test_exponent_hdr_CIELab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_cie_lab.exponent_hdr_CIELab` definition. """ - np.testing.assert_allclose( - exponent_hdr_CIELab(0.2, 100), - 0.473851073746817, + xp_assert_close( + exponent_hdr_CIELab(xp_as_array([0.2], xp=xp), xp_as_array([100], xp=xp)), + [0.473851073746817], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_hdr_CIELab(0.4, 100), - 0.656101486726362, + xp_assert_close( + exponent_hdr_CIELab(xp_as_array([0.4], xp=xp), xp_as_array([100], xp=xp)), + [0.656101486726362], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_hdr_CIELab(0.4, 100, method="Fairchild 2010"), - 1.326014370643925, + xp_assert_close( + exponent_hdr_CIELab( + xp_as_array([0.4], xp=xp), + xp_as_array([100], xp=xp), + method="Fairchild 2010", + ), + [1.326014370643925], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_hdr_CIELab(0.2, 1000), - 0.710776610620225, + xp_assert_close( + exponent_hdr_CIELab(xp_as_array([0.2], xp=xp), xp_as_array([1000], xp=xp)), + [0.710776610620225], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_exponent_hdr_CIELab(self) -> None: + def test_n_dimensional_exponent_hdr_CIELab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_cie_lab.exponent_hdr_CIELab` definition n-dimensional arrays support. @@ -69,36 +85,36 @@ def test_n_dimensional_exponent_hdr_CIELab(self) -> None: Y_s = 0.2 Y_abs = 100 - epsilon = exponent_hdr_CIELab(Y_s, Y_abs) + epsilon = as_ndarray(exponent_hdr_CIELab(Y_s, Y_abs)) - Y_s = np.tile(Y_s, 6) - Y_abs = np.tile(Y_abs, 6) - epsilon = np.tile(epsilon, 6) - np.testing.assert_allclose( + Y_s = xp.tile(xp_as_array(Y_s, xp=xp), (6,)) + Y_abs = xp.tile(xp_as_array(Y_abs, xp=xp), (6,)) + epsilon = xp.tile(xp_as_array(epsilon, xp=xp), (6,)) + xp_assert_close( exponent_hdr_CIELab(Y_s, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y_s = np.reshape(Y_s, (2, 3)) - Y_abs = np.reshape(Y_abs, (2, 3)) - epsilon = np.reshape(epsilon, (2, 3)) - np.testing.assert_allclose( + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3), xp=xp) + epsilon = xp_reshape(xp_as_array(epsilon, xp=xp), (2, 3), xp=xp) + xp_assert_close( exponent_hdr_CIELab(Y_s, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y_s = np.reshape(Y_s, (2, 3, 1)) - Y_abs = np.reshape(Y_abs, (2, 3, 1)) - epsilon = np.reshape(epsilon, (2, 3, 1)) - np.testing.assert_allclose( + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3, 1), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3, 1), xp=xp) + epsilon = xp_reshape(xp_as_array(epsilon, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( exponent_hdr_CIELab(Y_s, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_exponent_hdr_CIELab(self) -> None: + def test_domain_range_scale_exponent_hdr_CIELab(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.models.hdr_cie_lab.exponent_hdr_CIELab` definition domain and range scale support. @@ -106,12 +122,12 @@ def test_domain_range_scale_exponent_hdr_CIELab(self) -> None: Y_s = 0.2 Y_abs = 100 - epsilon = exponent_hdr_CIELab(Y_s, Y_abs) + epsilon = as_ndarray(exponent_hdr_CIELab(Y_s, Y_abs)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( exponent_hdr_CIELab(Y_s * factor, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -134,106 +150,111 @@ class TestXYZ_to_hdr_CIELab: tests methods. """ - def test_XYZ_to_hdr_CIELab(self) -> None: + def test_XYZ_to_hdr_CIELab(self, xp: ModuleType) -> None: """Test :func:`colour.models.hdr_cie_lab.XYZ_to_hdr_CIELab` definition.""" - np.testing.assert_allclose( - XYZ_to_hdr_CIELab(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([51.87002062, 60.47633850, 32.14551912]), + xp_assert_close( + XYZ_to_hdr_CIELab(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [51.87002062, 60.47633850, 32.14551912], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_hdr_CIELab( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([51.87002062, 44.49667330, -6.69619196]), + [51.87002062, 44.49667330, -6.69619196], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_hdr_CIELab( - np.array([0.20654008, 0.12197225, 0.05136952]), - np.array([0.44757, 0.40745]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), method="Fairchild 2010", ), - np.array([31.99621114, 95.08564341, -14.14047055]), + [31.99621114, 95.08564341, -14.14047055], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_hdr_CIELab(np.array([0.20654008, 0.12197225, 0.05136952]), Y_s=0.5), - np.array([23.10388654, 59.31425004, 23.69960142]), + xp_assert_close( + XYZ_to_hdr_CIELab( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), Y_s=0.5 + ), + [23.10388654, 59.31425004, 23.69960142], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_hdr_CIELab( - np.array([0.20654008, 0.12197225, 0.05136952]), Y_abs=1000 + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), Y_abs=1000 ), - np.array([29.77261805, 62.58315675, 27.31232673]), + [29.77261805, 62.58315675, 27.31232673], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_hdr_CIELab(self) -> None: + def test_n_dimensional_XYZ_to_hdr_CIELab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_cie_lab.XYZ_to_hdr_CIELab` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) Y_s = 0.2 Y_abs = 100 - Lab_hdr = XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs) + Lab_hdr = as_ndarray(XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs)) - XYZ = np.tile(XYZ, (6, 1)) - Lab_hdr = np.tile(Lab_hdr, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Lab_hdr = xp.tile(xp_as_array(Lab_hdr, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs), Lab_hdr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - illuminant = np.tile(illuminant, (6, 1)) - Y_s = np.tile(Y_s, 6) - Y_abs = np.tile(Y_abs, 6) - np.testing.assert_allclose( + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + Y_s = xp.tile(xp_as_array(Y_s, xp=xp), (6,)) + Y_abs = xp.tile(xp_as_array(Y_abs, xp=xp), (6,)) + xp_assert_close( XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs), Lab_hdr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - Y_s = np.reshape(Y_s, (2, 3)) - Y_abs = np.reshape(Y_abs, (2, 3)) - Lab_hdr = np.reshape(Lab_hdr, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3), xp=xp) + Lab_hdr = xp_reshape(xp_as_array(Lab_hdr, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs), Lab_hdr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_hdr_CIELab(self) -> None: + def test_domain_range_scale_XYZ_to_hdr_CIELab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_cie_lab.XYZ_to_hdr_CIELab` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - illuminant = np.array([0.31270, 0.32900]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) Y_s = 0.2 Y_abs = 100 - Lab_hdr = XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs) + Lab_hdr = as_ndarray(XYZ_to_hdr_CIELab(XYZ, illuminant, Y_s, Y_abs)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_hdr_CIELab( - XYZ * factor_a, illuminant, Y_s * factor_a, Y_abs + XYZ * xp_as_array(factor_a, xp=xp), + illuminant, + Y_s * xp_as_array(factor_a, xp=xp), + Y_abs, ), Lab_hdr * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -257,108 +278,113 @@ class TestHdr_CIELab_to_XYZ: tests methods. """ - def test_hdr_CIELab_to_XYZ(self) -> None: + def test_hdr_CIELab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.hdr_cie_lab.hdr_CIELab_to_XYZ` definition.""" - np.testing.assert_allclose( - hdr_CIELab_to_XYZ(np.array([51.87002062, 60.47633850, 32.14551912])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + hdr_CIELab_to_XYZ( + xp_as_array([51.87002062, 60.47633850, 32.14551912], xp=xp) + ), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hdr_CIELab_to_XYZ( - np.array([51.87002062, 44.49667330, -6.69619196]), - np.array([0.44757, 0.40745]), + xp_as_array([51.87002062, 44.49667330, -6.69619196], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hdr_CIELab_to_XYZ( - np.array([31.99621114, 95.08564341, -14.14047055]), - np.array([0.44757, 0.40745]), + xp_as_array([31.99621114, 95.08564341, -14.14047055], xp=xp), + xp_as_array([0.44757, 0.40745], xp=xp), method="Fairchild 2010", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hdr_CIELab_to_XYZ( - np.array([23.10388654, 59.31425004, 23.69960142]), Y_s=0.5 + xp_as_array([23.10388654, 59.31425004, 23.69960142], xp=xp), Y_s=0.5 ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hdr_CIELab_to_XYZ( - np.array([29.77261805, 62.58315675, 27.31232673]), Y_abs=1000 + xp_as_array([29.77261805, 62.58315675, 27.31232673], xp=xp), Y_abs=1000 ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_hdr_CIELab_to_XYZ(self) -> None: + def test_n_dimensional_hdr_CIELab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_cie_lab.hdr_CIELab_to_XYZ` definition n-dimensional support. """ - Lab_hdr = np.array([51.87002062, 60.47633850, 32.14551912]) - illuminant = np.array([0.31270, 0.32900]) + Lab_hdr = xp_as_array([51.87002062, 60.47633850, 32.14551912], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) Y_s = 0.2 Y_abs = 100 - XYZ = hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs) + XYZ = as_ndarray(hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs)) - Lab_hdr = np.tile(Lab_hdr, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + Lab_hdr = xp.tile(xp_as_array(Lab_hdr, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - illuminant = np.tile(illuminant, (6, 1)) - Y_s = np.tile(Y_s, 6) - Y_abs = np.tile(Y_abs, 6) - np.testing.assert_allclose( + illuminant = xp.tile(xp_as_array(illuminant, xp=xp), (6, 1)) + Y_s = xp.tile(xp_as_array(Y_s, xp=xp), (6,)) + Y_abs = xp.tile(xp_as_array(Y_abs, xp=xp), (6,)) + xp_assert_close( hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab_hdr = np.reshape(Lab_hdr, (2, 3, 3)) - illuminant = np.reshape(illuminant, (2, 3, 2)) - Y_s = np.reshape(Y_s, (2, 3)) - Y_abs = np.reshape(Y_abs, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + Lab_hdr = xp_reshape(xp_as_array(Lab_hdr, xp=xp), (2, 3, 3), xp=xp) + illuminant = xp_reshape(xp_as_array(illuminant, xp=xp), (2, 3, 2), xp=xp) + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_hdr_CIELab_to_XYZ(self) -> None: + def test_domain_range_scale_hdr_CIELab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_cie_lab.hdr_CIELab_to_XYZ` definition domain and range scale support. """ - Lab_hdr = np.array([26.46461067, -24.61332600, -4.84796811]) - illuminant = np.array([0.31270, 0.32900]) + Lab_hdr = xp_as_array([26.46461067, -24.61332600, -4.84796811], xp=xp) + illuminant = xp_as_array([0.31270, 0.32900], xp=xp) Y_s = 0.2 Y_abs = 100 - XYZ = hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs) + XYZ = as_ndarray(hdr_CIELab_to_XYZ(Lab_hdr, illuminant, Y_s, Y_abs)) d_r = (("reference", 1, 1, 1), ("1", 0.01, 1, 1), ("100", 1, 100, 100)) for scale, factor_a, factor_b, factor_c in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( hdr_CIELab_to_XYZ( - Lab_hdr * factor_a, illuminant, Y_s * factor_b, Y_abs + Lab_hdr * xp_as_array(factor_a, xp=xp), + illuminant, + Y_s * factor_b, + Y_abs, ), XYZ * factor_c, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_hdr_ipt.py b/colour/models/tests/test_hdr_ipt.py index 9274186d14..9403e74a89 100644 --- a/colour/models/tests/test_hdr_ipt.py +++ b/colour/models/tests/test_hdr_ipt.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -9,7 +14,14 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import XYZ_to_hdr_IPT, hdr_IPT_to_XYZ from colour.models.hdr_ipt import exponent_hdr_IPT -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,34 +43,38 @@ class TestExponent_hdr_IPT: definition unit tests methods. """ - def test_exponent_hdr_IPT(self) -> None: + def test_exponent_hdr_IPT(self, xp: ModuleType) -> None: """Test :func:`colour.models.hdr_ipt.exponent_hdr_IPT` definition.""" - np.testing.assert_allclose( - exponent_hdr_IPT(0.2, 100), - 0.482020919845900, + xp_assert_close( + exponent_hdr_IPT(xp_as_array([0.2], xp=xp), xp_as_array([100], xp=xp)), + [0.482020919845900], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_hdr_IPT(0.4, 100), - 0.667413581325092, + xp_assert_close( + exponent_hdr_IPT(xp_as_array([0.4], xp=xp), xp_as_array([100], xp=xp)), + [0.667413581325092], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_hdr_IPT(0.4, 100, method="Fairchild 2010"), - 1.219933220992410, + xp_assert_close( + exponent_hdr_IPT( + xp_as_array([0.4], xp=xp), + xp_as_array([100], xp=xp), + method="Fairchild 2010", + ), + [1.219933220992410], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - exponent_hdr_IPT(0.2, 1000), - 0.723031379768850, + xp_assert_close( + exponent_hdr_IPT(xp_as_array([0.2], xp=xp), xp_as_array([1000], xp=xp)), + [0.723031379768850], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_exponent_hdr_IPT(self) -> None: + def test_n_dimensional_exponent_hdr_IPT(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_ipt.exponent_hdr_IPT` definition n-dimensional arrays support. @@ -66,36 +82,36 @@ def test_n_dimensional_exponent_hdr_IPT(self) -> None: Y_s = 0.2 Y_abs = 100 - epsilon = exponent_hdr_IPT(Y_s, Y_abs) + epsilon = as_ndarray(exponent_hdr_IPT(Y_s, Y_abs)) - Y_s = np.tile(Y_s, 6) - Y_abs = np.tile(Y_abs, 6) - epsilon = np.tile(epsilon, 6) - np.testing.assert_allclose( + Y_s = xp.tile(xp_as_array(Y_s, xp=xp), (6,)) + Y_abs = xp.tile(xp_as_array(Y_abs, xp=xp), (6,)) + epsilon = xp.tile(xp_as_array(epsilon, xp=xp), (6,)) + xp_assert_close( exponent_hdr_IPT(Y_s, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y_s = np.reshape(Y_s, (2, 3)) - Y_abs = np.reshape(Y_abs, (2, 3)) - epsilon = np.reshape(epsilon, (2, 3)) - np.testing.assert_allclose( + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3), xp=xp) + epsilon = xp_reshape(xp_as_array(epsilon, xp=xp), (2, 3), xp=xp) + xp_assert_close( exponent_hdr_IPT(Y_s, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y_s = np.reshape(Y_s, (2, 3, 1)) - Y_abs = np.reshape(Y_abs, (2, 3, 1)) - epsilon = np.reshape(epsilon, (2, 3, 1)) - np.testing.assert_allclose( + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3, 1), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3, 1), xp=xp) + epsilon = xp_reshape(xp_as_array(epsilon, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( exponent_hdr_IPT(Y_s, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_exponent_hdr_IPT(self) -> None: + def test_domain_range_scale_exponent_hdr_IPT(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.models.hdr_ipt.exponent_hdr_IPT` definition domain and range scale support. @@ -103,12 +119,12 @@ def test_domain_range_scale_exponent_hdr_IPT(self) -> None: Y_s = 0.2 Y_abs = 100 - epsilon = exponent_hdr_IPT(Y_s, Y_abs) + epsilon = as_ndarray(exponent_hdr_IPT(Y_s, Y_abs)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( exponent_hdr_IPT(Y_s * factor, Y_abs), epsilon, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -131,89 +147,97 @@ class TestXYZ_to_hdr_IPT: methods. """ - def test_XYZ_to_hdr_IPT(self) -> None: + def test_XYZ_to_hdr_IPT(self, xp: ModuleType) -> None: """Test :func:`colour.models.hdr_ipt.XYZ_to_hdr_IPT` definition.""" - np.testing.assert_allclose( - XYZ_to_hdr_IPT(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([48.39376346, 42.44990202, 22.01954033]), + xp_assert_close( + XYZ_to_hdr_IPT(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [48.39376346, 42.44990202, 22.01954033], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_hdr_IPT( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="Fairchild 2010", ), - np.array([30.02873147, 83.93845061, 34.90287382]), + [30.02873147, 83.93845061, 34.90287382], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_hdr_IPT(np.array([0.20654008, 0.12197225, 0.05136952]), Y_s=0.5), - np.array([20.75088680, 37.98300971, 16.66974299]), + xp_assert_close( + XYZ_to_hdr_IPT( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), Y_s=0.5 + ), + [20.75088680, 37.98300971, 16.66974299], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_hdr_IPT(np.array([0.07818780, 0.06157201, 0.28099326]), Y_abs=1000), - np.array([23.83205010, -5.98739209, -32.74311745]), + xp_assert_close( + XYZ_to_hdr_IPT( + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp), Y_abs=1000 + ), + [23.83205010, -5.98739209, -32.74311745], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_hdr_IPT(self) -> None: + def test_n_dimensional_XYZ_to_hdr_IPT(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_ipt.XYZ_to_hdr_IPT` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) Y_s = 0.2 Y_abs = 100 - IPT_hdr = XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs) + IPT_hdr = as_ndarray(XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs)) - XYZ = np.tile(XYZ, (6, 1)) - IPT_hdr = np.tile(IPT_hdr, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + IPT_hdr = xp.tile(xp_as_array(IPT_hdr, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs), IPT_hdr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y_s = np.tile(Y_s, 6) - Y_abs = np.tile(Y_abs, 6) - np.testing.assert_allclose( + Y_s = xp.tile(xp_as_array(Y_s, xp=xp), (6,)) + Y_abs = xp.tile(xp_as_array(Y_abs, xp=xp), (6,)) + xp_assert_close( XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs), IPT_hdr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Y_s = np.reshape(Y_s, (2, 3)) - Y_abs = np.reshape(Y_abs, (2, 3)) - IPT_hdr = np.reshape(IPT_hdr, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3), xp=xp) + IPT_hdr = xp_reshape(xp_as_array(IPT_hdr, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs), IPT_hdr, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_hdr_IPT(self) -> None: + def test_domain_range_scale_XYZ_to_hdr_IPT(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_ipt.XYZ_to_hdr_IPT` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) Y_s = 0.2 Y_abs = 100 - IPT_hdr = XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs) + IPT_hdr = as_ndarray(XYZ_to_hdr_IPT(XYZ, Y_s, Y_abs)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_hdr_IPT(XYZ * factor_a, Y_s * factor_a, Y_abs), + xp_assert_close( + XYZ_to_hdr_IPT( + XYZ * xp_as_array(factor_a, xp=xp), + Y_s * xp_as_array(factor_a, xp=xp), + Y_abs, + ), IPT_hdr * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -236,91 +260,95 @@ class TestHdr_IPT_to_XYZ: methods. """ - def test_hdr_IPT_to_XYZ(self) -> None: + def test_hdr_IPT_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.hdr_ipt.hdr_IPT_to_XYZ` definition.""" - np.testing.assert_allclose( - hdr_IPT_to_XYZ(np.array([48.39376346, 42.44990202, 22.01954033])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + hdr_IPT_to_XYZ(xp_as_array([48.39376346, 42.44990202, 22.01954033], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hdr_IPT_to_XYZ( - np.array([30.02873147, 83.93845061, 34.90287382]), + xp_as_array([30.02873147, 83.93845061, 34.90287382], xp=xp), method="Fairchild 2010", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - hdr_IPT_to_XYZ(np.array([20.75088680, 37.98300971, 16.66974299]), Y_s=0.5), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + hdr_IPT_to_XYZ( + xp_as_array([20.75088680, 37.98300971, 16.66974299], xp=xp), Y_s=0.5 + ), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( hdr_IPT_to_XYZ( - np.array([23.83205010, -5.98739209, -32.74311745]), Y_abs=1000 + xp_as_array([23.83205010, -5.98739209, -32.74311745], xp=xp), Y_abs=1000 ), - np.array([0.07818780, 0.06157201, 0.28099326]), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_hdr_IPT_to_XYZ(self) -> None: + def test_n_dimensional_hdr_IPT_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_ipt.hdr_IPT_to_XYZ` definition n-dimensional support. """ - IPT_hdr = np.array([48.39376346, 42.44990202, 22.01954033]) + IPT_hdr = xp_as_array([48.39376346, 42.44990202, 22.01954033], xp=xp) Y_s = 0.2 Y_abs = 100 - XYZ = hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs) + XYZ = as_ndarray(hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs)) - IPT_hdr = np.tile(IPT_hdr, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + IPT_hdr = xp.tile(xp_as_array(IPT_hdr, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Y_s = np.tile(Y_s, 6) - Y_abs = np.tile(Y_abs, 6) - np.testing.assert_allclose( + Y_s = xp.tile(xp_as_array(Y_s, xp=xp), (6,)) + Y_abs = xp.tile(xp_as_array(Y_abs, xp=xp), (6,)) + xp_assert_close( hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - IPT_hdr = np.reshape(IPT_hdr, (2, 3, 3)) - Y_s = np.reshape(Y_s, (2, 3)) - Y_abs = np.reshape(Y_abs, (2, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + IPT_hdr = xp_reshape(xp_as_array(IPT_hdr, xp=xp), (2, 3, 3), xp=xp) + Y_s = xp_reshape(xp_as_array(Y_s, xp=xp), (2, 3), xp=xp) + Y_abs = xp_reshape(xp_as_array(Y_abs, xp=xp), (2, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_hdr_IPT_to_XYZ(self) -> None: + def test_domain_range_scale_hdr_IPT_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hdr_ipt.hdr_IPT_to_XYZ` definition domain and range scale support. """ - IPT_hdr = np.array([24.88927680, -11.44574144, 1.63147707]) + IPT_hdr = xp_as_array([24.88927680, -11.44574144, 1.63147707], xp=xp) Y_s = 0.2 Y_abs = 100 - XYZ = hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs) + XYZ = as_ndarray(hdr_IPT_to_XYZ(IPT_hdr, Y_s, Y_abs)) d_r = (("reference", 1, 1, 1), ("1", 0.01, 1, 1), ("100", 1, 100, 100)) for scale, factor_a, factor_b, factor_c in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - hdr_IPT_to_XYZ(IPT_hdr * factor_a, Y_s * factor_b, Y_abs), + xp_assert_close( + hdr_IPT_to_XYZ( + IPT_hdr * xp_as_array(factor_a, xp=xp), Y_s * factor_b, Y_abs + ), XYZ * factor_c, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_hunter_lab.py b/colour/models/tests/test_hunter_lab.py index 698b3a04ba..2e03a63858 100644 --- a/colour/models/tests/test_hunter_lab.py +++ b/colour/models/tests/test_hunter_lab.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -13,7 +18,14 @@ XYZ_to_Hunter_Lab, XYZ_to_K_ab_HunterLab1966, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -35,55 +47,59 @@ class TestXYZ_to_K_ab_HunterLab1966: definition unit tests methods. """ - def test_XYZ_to_K_ab_HunterLab1966(self) -> None: + def test_XYZ_to_K_ab_HunterLab1966(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.XYZ_to_K_ab_HunterLab1966` definition. """ - np.testing.assert_allclose( + xp_assert_close( XYZ_to_K_ab_HunterLab1966( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100 + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 ), - np.array([80.32152090, 14.59816495]), + [80.32152090, 14.59816495], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_K_ab_HunterLab1966( - np.array([0.14222010, 0.23042768, 0.10495772]) * 100 + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100 ), - np.array([66.65154834, 20.86664881]), + [66.65154834, 20.86664881], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_K_ab_HunterLab1966( - np.array([0.07818780, 0.06157201, 0.28099326]) * 100 + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100 ), - np.array([49.41960269, 34.14235426]), + [49.41960269, 34.14235426], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_K_ab_HunterLab1966(self) -> None: + def test_n_dimensional_XYZ_to_K_ab_HunterLab1966(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.XYZ_to_K_ab_HunterLab1966` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 - K_ab = XYZ_to_K_ab_HunterLab1966(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + K_ab = as_ndarray(XYZ_to_K_ab_HunterLab1966(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - K_ab = np.tile(K_ab, (6, 1)) - np.testing.assert_allclose( - XYZ_to_K_ab_HunterLab1966(XYZ), K_ab, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + K_ab = xp.tile(xp_as_array(K_ab, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_K_ab_HunterLab1966(XYZ), + K_ab, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - K_ab = np.reshape(K_ab, (2, 3, 2)) - np.testing.assert_allclose( - XYZ_to_K_ab_HunterLab1966(XYZ), K_ab, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + K_ab = xp_reshape(xp_as_array(K_ab, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close( + XYZ_to_K_ab_HunterLab1966(XYZ), + K_ab, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -104,61 +120,67 @@ class TestXYZ_to_Hunter_Lab: tests methods. """ - def test_XYZ_to_Hunter_Lab(self) -> None: + def test_XYZ_to_Hunter_Lab(self, xp: ModuleType) -> None: """Test :func:`colour.models.hunter_lab.XYZ_to_Hunter_Lab` definition.""" - np.testing.assert_allclose( - XYZ_to_Hunter_Lab(np.array([0.20654008, 0.12197225, 0.05136952]) * 100), - np.array([34.92452577, 47.06189858, 14.38615107]), + xp_assert_close( + XYZ_to_Hunter_Lab( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + ), + [34.92452577, 47.06189858, 14.38615107], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Hunter_Lab(np.array([0.14222010, 0.23042768, 0.10495772]) * 100), - np.array([48.00288325, -28.98551622, 18.75564181]), + xp_assert_close( + XYZ_to_Hunter_Lab( + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100 + ), + [48.00288325, -28.98551622, 18.75564181], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Hunter_Lab(np.array([0.07818780, 0.06157201, 0.28099326]) * 100), - np.array([24.81370791, 14.38300039, -53.25539126]), + xp_assert_close( + XYZ_to_Hunter_Lab( + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100 + ), + [24.81370791, 14.38300039, -53.25539126], atol=TOLERANCE_ABSOLUTE_TESTS, ) h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] A = h_i["A"] - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Lab( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, A.XYZ_n, A.K_ab, ), - np.array([34.92452577, 35.04243086, -2.47688619]), + [34.92452577, 35.04243086, -2.47688619], atol=TOLERANCE_ABSOLUTE_TESTS, ) D65 = h_i["D65"] - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Lab( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, D65.XYZ_n, D65.K_ab, ), - np.array([34.92452577, 47.06189858, 14.38615107]), + [34.92452577, 47.06189858, 14.38615107], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Lab( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, D65.XYZ_n, K_ab=None, ), - np.array([34.92452577, 47.05669614, 14.38385238]), + [34.92452577, 47.05669614, 14.38385238], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Hunter_Lab(self) -> None: + def test_n_dimensional_XYZ_to_Hunter_Lab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.XYZ_to_Hunter_Lab` definition n-dimensional support. @@ -167,38 +189,38 @@ def test_n_dimensional_XYZ_to_Hunter_Lab(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 XYZ_n = D65.XYZ_n K_ab = D65.K_ab - Lab = XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab) + Lab = as_ndarray(XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab)) - XYZ = np.tile(XYZ, (6, 1)) - Lab = np.tile(Lab, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_n = np.tile(XYZ_n, (6, 1)) - K_ab = np.tile(K_ab, (6, 1)) - np.testing.assert_allclose( + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + K_ab = xp.tile(xp_as_array(K_ab, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - K_ab = np.reshape(K_ab, (2, 3, 2)) - Lab = np.reshape(Lab, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + K_ab = xp_reshape(xp_as_array(K_ab, xp=xp), (2, 3, 2), xp=xp) + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab), Lab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_Hunter_Lab(self) -> None: + def test_domain_range_scale_XYZ_to_Hunter_Lab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.XYZ_to_Hunter_Lab` definition domain and range scale support. @@ -207,15 +229,15 @@ def test_domain_range_scale_XYZ_to_Hunter_Lab(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 XYZ_n = D65.XYZ_n K_ab = D65.K_ab - Lab = XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab) + Lab = as_ndarray(XYZ_to_Hunter_Lab(XYZ, XYZ_n, K_ab)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Lab(XYZ * factor, XYZ_n * factor, K_ab), Lab * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -239,61 +261,67 @@ class TestHunter_Lab_to_XYZ: tests methods. """ - def test_Hunter_Lab_to_XYZ(self) -> None: + def test_Hunter_Lab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.hunter_lab.Hunter_Lab_to_XYZ` definition.""" - np.testing.assert_allclose( - Hunter_Lab_to_XYZ(np.array([34.92452577, 47.06189858, 14.38615107])), - np.array([20.65400800, 12.19722500, 5.13695200]), + xp_assert_close( + Hunter_Lab_to_XYZ( + xp_as_array([34.92452577, 47.06189858, 14.38615107], xp=xp) + ), + [20.65400800, 12.19722500, 5.13695200], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Hunter_Lab_to_XYZ(np.array([48.00288325, -28.98551622, 18.75564181])), - np.array([14.22201000, 23.04276800, 10.49577200]), + xp_assert_close( + Hunter_Lab_to_XYZ( + xp_as_array([48.00288325, -28.98551622, 18.75564181], xp=xp) + ), + [14.22201000, 23.04276800, 10.49577200], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Hunter_Lab_to_XYZ(np.array([24.81370791, 14.38300039, -53.25539126])), - np.array([7.81878000, 6.15720100, 28.09932601]), + xp_assert_close( + Hunter_Lab_to_XYZ( + xp_as_array([24.81370791, 14.38300039, -53.25539126], xp=xp) + ), + [7.81878000, 6.15720100, 28.09932601], atol=TOLERANCE_ABSOLUTE_TESTS, ) h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] A = h_i["A"] - np.testing.assert_allclose( + xp_assert_close( Hunter_Lab_to_XYZ( - np.array([34.92452577, 35.04243086, -2.47688619]), + xp_as_array([34.92452577, 35.04243086, -2.47688619], xp=xp), A.XYZ_n, A.K_ab, ), - np.array([20.65400800, 12.19722500, 5.13695200]), + [20.65400800, 12.19722500, 5.13695200], atol=TOLERANCE_ABSOLUTE_TESTS, ) D65 = h_i["D65"] - np.testing.assert_allclose( + xp_assert_close( Hunter_Lab_to_XYZ( - np.array([34.92452577, 47.06189858, 14.38615107]), + xp_as_array([34.92452577, 47.06189858, 14.38615107], xp=xp), D65.XYZ_n, D65.K_ab, ), - np.array([20.65400800, 12.19722500, 5.13695200]), + [20.65400800, 12.19722500, 5.13695200], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Hunter_Lab_to_XYZ( - np.array([34.92452577, 47.05669614, 14.38385238]), + xp_as_array([34.92452577, 47.05669614, 14.38385238], xp=xp), D65.XYZ_n, K_ab=None, ), - np.array([20.65400800, 12.19722500, 5.13695200]), + [20.65400800, 12.19722500, 5.13695200], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Hunter_Lab_to_XYZ(self) -> None: + def test_n_dimensional_Hunter_Lab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.Hunter_Lab_to_XYZ` definition n-dimensional support. @@ -302,38 +330,38 @@ def test_n_dimensional_Hunter_Lab_to_XYZ(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - Lab = np.array([34.92452577, 47.06189858, 14.38615107]) + Lab = xp_as_array([34.92452577, 47.06189858, 14.38615107], xp=xp) XYZ_n = D65.XYZ_n K_ab = D65.K_ab - XYZ = Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab) + XYZ = as_ndarray(Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab)) - Lab = np.tile(Lab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + Lab = xp.tile(xp_as_array(Lab, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - K_ab = np.tile(K_ab, (6, 1)) - XYZ_n = np.tile(XYZ_n, (6, 1)) - np.testing.assert_allclose( + K_ab = xp.tile(xp_as_array(K_ab, xp=xp), (6, 1)) + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + xp_assert_close( Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - Lab = np.reshape(Lab, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - K_ab = np.reshape(K_ab, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + Lab = xp_reshape(xp_as_array(Lab, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + K_ab = xp_reshape(xp_as_array(K_ab, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_Hunter_Lab_to_XYZ(self) -> None: + def test_domain_range_scale_Hunter_Lab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.Hunter_Lab_to_XYZ` definition domain and range scale support. @@ -342,15 +370,15 @@ def test_domain_range_scale_Hunter_Lab_to_XYZ(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - Lab = np.array([34.92452577, 47.06189858, 14.38615107]) + Lab = xp_as_array([34.92452577, 47.06189858, 14.38615107], xp=xp) XYZ_n = D65.XYZ_n K_ab = D65.K_ab - XYZ = Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab) + XYZ = as_ndarray(Hunter_Lab_to_XYZ(Lab, XYZ_n, K_ab)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Hunter_Lab_to_XYZ(Lab * factor, XYZ_n * factor, K_ab), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_hunter_rdab.py b/colour/models/tests/test_hunter_rdab.py index 103af77492..56c045dd84 100644 --- a/colour/models/tests/test_hunter_rdab.py +++ b/colour/models/tests/test_hunter_rdab.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -9,7 +14,14 @@ from colour.colorimetry import TVS_ILLUMINANTS_HUNTERLAB from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import Hunter_Rdab_to_XYZ, XYZ_to_Hunter_Rdab -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -30,61 +42,67 @@ class TestXYZ_to_Hunter_Rdab: unit tests methods. """ - def test_XYZ_to_Hunter_Rdab(self) -> None: + def test_XYZ_to_Hunter_Rdab(self, xp: ModuleType) -> None: """Test :func:`colour.models.hunter_rdab.XYZ_to_Hunter_Rdab` definition.""" - np.testing.assert_allclose( - XYZ_to_Hunter_Rdab(np.array([0.20654008, 0.12197225, 0.05136952]) * 100), - np.array([12.19722500, 57.12537874, 17.46241341]), + xp_assert_close( + XYZ_to_Hunter_Rdab( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + ), + [12.19722500, 57.12537874, 17.46241341], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Hunter_Rdab(np.array([0.14222010, 0.23042768, 0.10495772]) * 100), - np.array([23.04276800, -32.40057474, 20.96542183]), + xp_assert_close( + XYZ_to_Hunter_Rdab( + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100 + ), + [23.04276800, -32.40057474, 20.96542183], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Hunter_Rdab(np.array([0.07818780, 0.06157201, 0.28099326]) * 100), - np.array([6.15720100, 18.13400284, -67.14408607]), + xp_assert_close( + XYZ_to_Hunter_Rdab( + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100 + ), + [6.15720100, 18.13400284, -67.14408607], atol=TOLERANCE_ABSOLUTE_TESTS, ) h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] A = h_i["A"] - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Rdab( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, A.XYZ_n, A.K_ab, ), - np.array([12.19722500, 42.53572838, -3.00653110]), + [12.19722500, 42.53572838, -3.00653110], atol=TOLERANCE_ABSOLUTE_TESTS, ) D65 = h_i["D65"] - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Rdab( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, D65.XYZ_n, D65.K_ab, ), - np.array([12.19722500, 57.12537874, 17.46241341]), + [12.19722500, 57.12537874, 17.46241341], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Rdab( - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, D65.XYZ_n, K_ab=None, ), - np.array([12.19722500, 57.11906384, 17.45962317]), + [12.19722500, 57.11906384, 17.45962317], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Hunter_Rdab(self) -> None: + def test_n_dimensional_XYZ_to_Hunter_Rdab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_rdab.XYZ_to_Hunter_Rdab` definition n-dimensional support. @@ -93,38 +111,38 @@ def test_n_dimensional_XYZ_to_Hunter_Rdab(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 XYZ_n = D65.XYZ_n K_ab = D65.K_ab - R_d_ab = XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab) + R_d_ab = as_ndarray(XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab)) - XYZ = np.tile(XYZ, (6, 1)) - R_d_ab = np.tile(R_d_ab, (6, 1)) - np.testing.assert_allclose( + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + R_d_ab = xp.tile(xp_as_array(R_d_ab, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab), R_d_ab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ_n = np.tile(XYZ_n, (6, 1)) - K_ab = np.tile(K_ab, (6, 1)) - np.testing.assert_allclose( + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + K_ab = xp.tile(xp_as_array(K_ab, xp=xp), (6, 1)) + xp_assert_close( XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab), R_d_ab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - K_ab = np.reshape(K_ab, (2, 3, 2)) - R_d_ab = np.reshape(R_d_ab, (2, 3, 3)) - np.testing.assert_allclose( + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + K_ab = xp_reshape(xp_as_array(K_ab, xp=xp), (2, 3, 2), xp=xp) + R_d_ab = xp_reshape(xp_as_array(R_d_ab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab), R_d_ab, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_Hunter_Rdab(self) -> None: + def test_domain_range_scale_XYZ_to_Hunter_Rdab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.XYZ_to_Hunter_Rdab` definition domain and range scale support. @@ -133,15 +151,15 @@ def test_domain_range_scale_XYZ_to_Hunter_Rdab(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 XYZ_n = D65.XYZ_n K_ab = D65.K_ab - R_d_ab = XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab) + R_d_ab = as_ndarray(XYZ_to_Hunter_Rdab(XYZ, XYZ_n, K_ab)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Hunter_Rdab(XYZ * factor, XYZ_n * factor, K_ab), R_d_ab * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -165,61 +183,67 @@ class TestHunter_Rdab_to_XYZ: unit tests methods. """ - def test_Hunter_Rdab_to_XYZ(self) -> None: + def test_Hunter_Rdab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.hunter_rdab.Hunter_Rdab_to_XYZ` definition.""" - np.testing.assert_allclose( - Hunter_Rdab_to_XYZ(np.array([12.19722500, 57.12537874, 17.46241341])), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_assert_close( + Hunter_Rdab_to_XYZ( + xp_as_array([12.19722500, 57.12537874, 17.46241341], xp=xp) + ), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Hunter_Rdab_to_XYZ(np.array([23.04276800, -32.40057474, 20.96542183])), - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, + xp_assert_close( + Hunter_Rdab_to_XYZ( + xp_as_array([23.04276800, -32.40057474, 20.96542183], xp=xp) + ), + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Hunter_Rdab_to_XYZ(np.array([6.15720100, 18.13400284, -67.14408607])), - np.array([0.07818780, 0.06157201, 0.28099326]) * 100, + xp_assert_close( + Hunter_Rdab_to_XYZ( + xp_as_array([6.15720100, 18.13400284, -67.14408607], xp=xp) + ), + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] A = h_i["A"] - np.testing.assert_allclose( + xp_assert_close( Hunter_Rdab_to_XYZ( - np.array([12.19722500, 42.53572838, -3.00653110]), + xp_as_array([12.19722500, 42.53572838, -3.00653110], xp=xp), A.XYZ_n, A.K_ab, ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) D65 = h_i["D65"] - np.testing.assert_allclose( + xp_assert_close( Hunter_Rdab_to_XYZ( - np.array([12.19722500, 57.12537874, 17.46241341]), + xp_as_array([12.19722500, 57.12537874, 17.46241341], xp=xp), D65.XYZ_n, D65.K_ab, ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Hunter_Rdab_to_XYZ( - np.array([12.19722500, 57.11906384, 17.45962317]), + xp_as_array([12.19722500, 57.11906384, 17.45962317], xp=xp), D65.XYZ_n, K_ab=None, ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_Hunter_Rdab_to_XYZ(self) -> None: + def test_n_dimensional_Hunter_Rdab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_rdab.Hunter_Rdab_to_XYZ` definition n-dimensional support. @@ -228,38 +252,38 @@ def test_n_dimensional_Hunter_Rdab_to_XYZ(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - R_d_ab = np.array([12.19722500, 57.12537874, 17.46241341]) + R_d_ab = xp_as_array([12.19722500, 57.12537874, 17.46241341], xp=xp) XYZ_n = D65.XYZ_n K_ab = D65.K_ab - XYZ = Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab) + XYZ = as_ndarray(Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab)) - R_d_ab = np.tile(R_d_ab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( + R_d_ab = xp.tile(xp_as_array(R_d_ab, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - K_ab = np.tile(K_ab, (6, 1)) - XYZ_n = np.tile(XYZ_n, (6, 1)) - np.testing.assert_allclose( + K_ab = xp.tile(xp_as_array(K_ab, xp=xp), (6, 1)) + XYZ_n = xp.tile(xp_as_array(XYZ_n, xp=xp), (6, 1)) + xp_assert_close( Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - R_d_ab = np.reshape(R_d_ab, (2, 3, 3)) - XYZ_n = np.reshape(XYZ_n, (2, 3, 3)) - K_ab = np.reshape(K_ab, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( + R_d_ab = xp_reshape(xp_as_array(R_d_ab, xp=xp), (2, 3, 3), xp=xp) + XYZ_n = xp_reshape(xp_as_array(XYZ_n, xp=xp), (2, 3, 3), xp=xp) + K_ab = xp_reshape(xp_as_array(K_ab, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_Hunter_Rdab_to_XYZ(self) -> None: + def test_domain_range_scale_Hunter_Rdab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.hunter_lab.Hunter_Rdab_to_XYZ` definition domain and range scale support. @@ -268,15 +292,15 @@ def test_domain_range_scale_Hunter_Rdab_to_XYZ(self) -> None: h_i = TVS_ILLUMINANTS_HUNTERLAB["CIE 1931 2 Degree Standard Observer"] D65 = h_i["D65"] - R_d_ab = np.array([12.19722500, 57.12537874, 17.46241341]) + R_d_ab = xp_as_array([12.19722500, 57.12537874, 17.46241341], xp=xp) XYZ_n = D65.XYZ_n K_ab = D65.K_ab - XYZ = Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab) + XYZ = as_ndarray(Hunter_Rdab_to_XYZ(R_d_ab, XYZ_n, K_ab)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Hunter_Rdab_to_XYZ(R_d_ab * factor, XYZ_n * factor, K_ab), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_icacb.py b/colour/models/tests/test_icacb.py index 328a79bfbe..60c876702b 100644 --- a/colour/models/tests/test_icacb.py +++ b/colour/models/tests/test_icacb.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import ICaCb_to_XYZ, XYZ_to_ICaCb -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,67 +42,63 @@ class TestXYZ_to_ICaCb: methods. """ - def test_XYZ_to_ICaCb(self) -> None: + def test_XYZ_to_ICaCb(self, xp: ModuleType) -> None: """Test :func:`colour.models.icacb.XYZ_to_ICaCb` definition.""" - np.testing.assert_allclose( - XYZ_to_ICaCb(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.06875297, 0.05753352, 0.02081548]), + xp_assert_close( + XYZ_to_ICaCb(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.06875297, 0.05753352, 0.02081548], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ICaCb(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.08666353, -0.02479011, 0.03099396]), + xp_assert_close( + XYZ_to_ICaCb(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.08666353, -0.02479011, 0.03099396], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ICaCb(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.05102472, -0.00965461, -0.05150706]), + xp_assert_close( + XYZ_to_ICaCb(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.05102472, -0.00965461, -0.05150706], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ICaCb(np.array([0.00000000, 0.00000000, 1.00000000])), - np.array([1702.0656419, 14738.00583456, 1239.66837927]), + xp_assert_close( + XYZ_to_ICaCb(xp_as_array([0.00000000, 0.00000000, 1.00000000], xp=xp)), + [1702.0656419, 14738.00583456, 1239.66837927], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_ICaCb(self) -> None: + def test_n_dimensional_XYZ_to_ICaCb(self, xp: ModuleType) -> None: """ Test :func:`colour.models.icacb.XYZ_to_ICaCb` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - ICaCb = XYZ_to_ICaCb(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + ICaCb = as_ndarray(XYZ_to_ICaCb(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - ICaCb = np.tile(ICaCb, (6, 1)) - np.testing.assert_allclose( - XYZ_to_ICaCb(XYZ), ICaCb, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + ICaCb = xp.tile(xp_as_array(ICaCb, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_ICaCb(XYZ), ICaCb, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - ICaCb = np.reshape(ICaCb, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_ICaCb(XYZ), ICaCb, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + ICaCb = xp_reshape(xp_as_array(ICaCb, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_ICaCb(XYZ), ICaCb, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_ICaCb(self) -> None: + def test_domain_range_scale_XYZ_to_ICaCb(self, xp: ModuleType) -> None: """ Test :func:`colour.models.icacb.XYZ_to_ICaCb` definition domain and range scale support. """ - XYZ = np.array([0.07818780, 0.06157201, 0.28099326]) - ICaCb = XYZ_to_ICaCb(XYZ) + XYZ = xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) + ICaCb = as_ndarray(XYZ_to_ICaCb(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ICaCb(XYZ * factor), ICaCb * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -107,67 +116,66 @@ def test_nan_XYZ_to_ICaCb(self) -> None: class TestICaCb_to_XYZ: """Test :func:`colour.models.icacb.ICaCb_to_XYZ` definition.""" - def test_XYZ_to_ICaCb(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_XYZ_to_ICaCb(self, xp: ModuleType) -> None: """Test :func:`colour.models.icacb.ICaCb_to_XYZ` definition.""" - np.testing.assert_allclose( - ICaCb_to_XYZ(np.array([0.06875297, 0.05753352, 0.02081548])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + ICaCb_to_XYZ(xp_as_array([0.06875297, 0.05753352, 0.02081548], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICaCb_to_XYZ(np.array([0.08666353, -0.02479011, 0.03099396])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + ICaCb_to_XYZ(xp_as_array([0.08666353, -0.02479011, 0.03099396], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICaCb_to_XYZ(np.array([0.05102472, -0.00965461, -0.05150706])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + ICaCb_to_XYZ(xp_as_array([0.05102472, -0.00965461, -0.05150706], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ICaCb_to_XYZ(np.array([1702.0656419, 14738.00583456, 1239.66837927])), - np.array([0.00000000, 0.00000000, 1.00000000]), + xp_assert_close( + ICaCb_to_XYZ( + xp_as_array([1702.0656419, 14738.00583456, 1239.66837927], xp=xp) + ), + [0.00000000, 0.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_ICaCb_to_XYZ(self) -> None: + def test_n_dimensional_ICaCb_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.icacb.ICaCb_to_XYZ` definition n-dimensional support. """ - ICaCb = np.array([0.06875297, 0.05753352, 0.02081548]) - XYZ = ICaCb_to_XYZ(ICaCb) + ICaCb = xp_as_array([0.06875297, 0.05753352, 0.02081548], xp=xp) + XYZ = as_ndarray(ICaCb_to_XYZ(ICaCb)) - ICaCb = np.tile(ICaCb, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - ICaCb_to_XYZ(ICaCb), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICaCb = xp.tile(xp_as_array(ICaCb, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(ICaCb_to_XYZ(ICaCb), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - ICaCb = np.reshape(ICaCb, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - ICaCb_to_XYZ(ICaCb), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICaCb = xp_reshape(xp_as_array(ICaCb, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(ICaCb_to_XYZ(ICaCb), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_ICaCb_to_XYZ(self) -> None: + def test_domain_range_scale_ICaCb_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.icacb.ICaCb_to_XYZ` definition domain and range scale support. """ - ICaCb = np.array([0.06875297, 0.05753352, 0.02081548]) - XYZ = ICaCb_to_XYZ(ICaCb) + ICaCb = xp_as_array([0.06875297, 0.05753352, 0.02081548], xp=xp) + XYZ = as_ndarray(ICaCb_to_XYZ(ICaCb)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( ICaCb_to_XYZ(ICaCb * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_igpgtg.py b/colour/models/tests/test_igpgtg.py index bf3524e377..912d8f7a1e 100644 --- a/colour/models/tests/test_igpgtg.py +++ b/colour/models/tests/test_igpgtg.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import IgPgTg_to_XYZ, XYZ_to_IgPgTg -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,61 +41,57 @@ class TestXYZ_to_IgPgTg: methods. """ - def test_XYZ_to_IgPgTg(self) -> None: + def test_XYZ_to_IgPgTg(self, xp: ModuleType) -> None: """Test :func:`colour.models.igpgtg.XYZ_to_IgPgTg` definition.""" - np.testing.assert_allclose( - XYZ_to_IgPgTg(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.42421258, 0.18632491, 0.10689223]), + xp_assert_close( + XYZ_to_IgPgTg(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.42421258, 0.18632491, 0.10689223], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_IgPgTg(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.50912820, -0.14804331, 0.11921472]), + xp_assert_close( + XYZ_to_IgPgTg(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.50912820, -0.14804331, 0.11921472], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_IgPgTg(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.29095152, -0.04057508, -0.18220795]), + xp_assert_close( + XYZ_to_IgPgTg(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.29095152, -0.04057508, -0.18220795], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_IgPgTg(self) -> None: + def test_n_dimensional_XYZ_to_IgPgTg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.igpgtg.XYZ_to_IgPgTg` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - IgPgTg = XYZ_to_IgPgTg(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IgPgTg = as_ndarray(XYZ_to_IgPgTg(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - IgPgTg = np.tile(IgPgTg, (6, 1)) - np.testing.assert_allclose( - XYZ_to_IgPgTg(XYZ), IgPgTg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + IgPgTg = xp.tile(xp_as_array(IgPgTg, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_IgPgTg(XYZ), IgPgTg, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - IgPgTg = np.reshape(IgPgTg, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_IgPgTg(XYZ), IgPgTg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + IgPgTg = xp_reshape(xp_as_array(IgPgTg, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_IgPgTg(XYZ), IgPgTg, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_IgPgTg(self) -> None: + def test_domain_range_scale_XYZ_to_IgPgTg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.igpgtg.XYZ_to_IgPgTg` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - IgPgTg = XYZ_to_IgPgTg(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IgPgTg = as_ndarray(XYZ_to_IgPgTg(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_IgPgTg(XYZ * factor), IgPgTg * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -107,61 +115,57 @@ class TestIgPgTg_to_XYZ: methods. """ - def test_IgPgTg_to_XYZ(self) -> None: + def test_IgPgTg_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.igpgtg.IgPgTg_to_XYZ` definition.""" - np.testing.assert_allclose( - IgPgTg_to_XYZ(np.array([0.42421258, 0.18632491, 0.10689223])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + IgPgTg_to_XYZ(xp_as_array([0.42421258, 0.18632491, 0.10689223], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IgPgTg_to_XYZ(np.array([0.50912820, -0.14804331, 0.11921472])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + IgPgTg_to_XYZ(xp_as_array([0.50912820, -0.14804331, 0.11921472], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IgPgTg_to_XYZ(np.array([0.29095152, -0.04057508, -0.18220795])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + IgPgTg_to_XYZ(xp_as_array([0.29095152, -0.04057508, -0.18220795], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_IgPgTg_to_XYZ(self) -> None: + def test_n_dimensional_IgPgTg_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.igpgtg.IgPgTg_to_XYZ` definition n-dimensional support. """ - IgPgTg = np.array([0.42421258, 0.18632491, 0.10689223]) - XYZ = IgPgTg_to_XYZ(IgPgTg) + IgPgTg = xp_as_array([0.42421258, 0.18632491, 0.10689223], xp=xp) + XYZ = as_ndarray(IgPgTg_to_XYZ(IgPgTg)) - IgPgTg = np.tile(IgPgTg, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - IgPgTg_to_XYZ(IgPgTg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + IgPgTg = xp.tile(xp_as_array(IgPgTg, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(IgPgTg_to_XYZ(IgPgTg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - IgPgTg = np.reshape(IgPgTg, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - IgPgTg_to_XYZ(IgPgTg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + IgPgTg = xp_reshape(xp_as_array(IgPgTg, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(IgPgTg_to_XYZ(IgPgTg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_IgPgTg_to_XYZ(self) -> None: + def test_domain_range_scale_IgPgTg_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.igpgtg.IgPgTg_to_XYZ` definition domain and range scale support. """ - IgPgTg = np.array([0.42421258, 0.18632491, 0.10689223]) - XYZ = IgPgTg_to_XYZ(IgPgTg) + IgPgTg = xp_as_array([0.42421258, 0.18632491, 0.10689223], xp=xp) + XYZ = as_ndarray(IgPgTg_to_XYZ(IgPgTg)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( IgPgTg_to_XYZ(IgPgTg * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_ipt.py b/colour/models/tests/test_ipt.py index 73703f0738..500b2025a6 100644 --- a/colour/models/tests/test_ipt.py +++ b/colour/models/tests/test_ipt.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import IPT_hue_angle, IPT_to_XYZ, XYZ_to_IPT -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -27,57 +39,57 @@ class TestXYZ_to_IPT: """Define :func:`colour.models.ipt.XYZ_to_IPT` definition unit tests methods.""" - def test_XYZ_to_IPT(self) -> None: + def test_XYZ_to_IPT(self, xp: ModuleType) -> None: """Test :func:`colour.models.ipt.XYZ_to_IPT` definition.""" - np.testing.assert_allclose( - XYZ_to_IPT(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.38426191, 0.38487306, 0.18886838]), + xp_assert_close( + XYZ_to_IPT(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.38426191, 0.38487306, 0.18886838], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_IPT(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.49437481, -0.19251742, 0.18080304]), + xp_assert_close( + XYZ_to_IPT(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.49437481, -0.19251742, 0.18080304], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_IPT(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.35167774, -0.07525627, -0.30921279]), + xp_assert_close( + XYZ_to_IPT(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [0.35167774, -0.07525627, -0.30921279], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_IPT(self) -> None: + def test_n_dimensional_XYZ_to_IPT(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ipt.XYZ_to_IPT` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - IPT = XYZ_to_IPT(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IPT = as_ndarray(XYZ_to_IPT(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - IPT = np.tile(IPT, (6, 1)) - np.testing.assert_allclose(XYZ_to_IPT(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + IPT = xp.tile(xp_as_array(IPT, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_IPT(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - IPT = np.reshape(IPT, (2, 3, 3)) - np.testing.assert_allclose(XYZ_to_IPT(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + IPT = xp_reshape(xp_as_array(IPT, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_IPT(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_IPT(self) -> None: + def test_domain_range_scale_XYZ_to_IPT(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ipt.XYZ_to_IPT` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - IPT = XYZ_to_IPT(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IPT = as_ndarray(XYZ_to_IPT(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_IPT(XYZ * factor), IPT * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -98,57 +110,57 @@ class TestIPT_to_XYZ: methods. """ - def test_IPT_to_XYZ(self) -> None: + def test_IPT_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.ipt.IPT_to_XYZ` definition.""" - np.testing.assert_allclose( - IPT_to_XYZ(np.array([0.38426191, 0.38487306, 0.18886838])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + IPT_to_XYZ(xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IPT_to_XYZ(np.array([0.49437481, -0.19251742, 0.18080304])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + IPT_to_XYZ(xp_as_array([0.49437481, -0.19251742, 0.18080304], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IPT_to_XYZ(np.array([0.35167774, -0.07525627, -0.30921279])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + IPT_to_XYZ(xp_as_array([0.35167774, -0.07525627, -0.30921279], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_IPT_to_XYZ(self) -> None: + def test_n_dimensional_IPT_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ipt.IPT_to_XYZ` definition n-dimensional support. """ - IPT = np.array([0.38426191, 0.38487306, 0.18886838]) - XYZ = IPT_to_XYZ(IPT) + IPT = xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp) + XYZ = as_ndarray(IPT_to_XYZ(IPT)) - IPT = np.tile(IPT, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(IPT_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + IPT = xp.tile(xp_as_array(IPT, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(IPT_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - IPT = np.reshape(IPT, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(IPT_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + IPT = xp_reshape(xp_as_array(IPT, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(IPT_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_IPT_to_XYZ(self) -> None: + def test_domain_range_scale_IPT_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ipt.IPT_to_XYZ` definition domain and range scale support. """ - IPT = np.array([0.38426191, 0.38487306, 0.18886838]) - XYZ = IPT_to_XYZ(IPT) + IPT = xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp) + XYZ = as_ndarray(IPT_to_XYZ(IPT)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( IPT_to_XYZ(IPT * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -169,62 +181,58 @@ class TestIPTHueAngle: methods. """ - def test_IPT_hue_angle(self) -> None: + def test_IPT_hue_angle(self, xp: ModuleType) -> None: """Test :func:`colour.models.ipt.IPT_hue_angle` definition.""" - np.testing.assert_allclose( - IPT_hue_angle(np.array([0.38426191, 0.38487306, 0.18886838])), + xp_assert_close( + IPT_hue_angle(xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp)), 26.138526939899490, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IPT_hue_angle(np.array([0.49437481, -0.19251742, 0.18080304])), + xp_assert_close( + IPT_hue_angle(xp_as_array([0.49437481, -0.19251742, 0.18080304], xp=xp)), 136.797287973958500, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IPT_hue_angle(np.array([0.35167774, -0.07525627, -0.30921279])), + xp_assert_close( + IPT_hue_angle(xp_as_array([0.35167774, -0.07525627, -0.30921279], xp=xp)), 256.321284526533300, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_IPT_hue_angle(self) -> None: + def test_n_dimensional_IPT_hue_angle(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ipt.IPT_hue_angle` definition n-dimensional support. """ - IPT = np.array([0.38426191, 0.38487306, 0.18886838]) - hue = IPT_hue_angle(IPT) + IPT = xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp) + hue = as_ndarray(IPT_hue_angle(IPT)) - IPT = np.tile(IPT, (6, 1)) - hue = np.tile(hue, 6) - np.testing.assert_allclose( - IPT_hue_angle(IPT), hue, atol=TOLERANCE_ABSOLUTE_TESTS - ) + IPT = xp.tile(xp_as_array(IPT, xp=xp), (6, 1)) + hue = xp.tile(xp_as_array(hue, xp=xp), (6,)) + xp_assert_close(IPT_hue_angle(IPT), hue, atol=TOLERANCE_ABSOLUTE_TESTS) - IPT = np.reshape(IPT, (2, 3, 3)) - hue = np.reshape(hue, (2, 3)) - np.testing.assert_allclose( - IPT_hue_angle(IPT), hue, atol=TOLERANCE_ABSOLUTE_TESTS - ) + IPT = xp_reshape(xp_as_array(IPT, xp=xp), (2, 3, 3), xp=xp) + hue = xp_reshape(xp_as_array(hue, xp=xp), (2, 3), xp=xp) + xp_assert_close(IPT_hue_angle(IPT), hue, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_IPT_hue_angle(self) -> None: + def test_domain_range_scale_IPT_hue_angle(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ipt.IPT_hue_angle` definition domain and range scale support. """ - IPT = np.array([0.38426191, 0.38487306, 0.18886838]) - hue = IPT_hue_angle(IPT) + IPT = xp_as_array([0.38426191, 0.38487306, 0.18886838], xp=xp) + hue = as_ndarray(IPT_hue_angle(IPT)) d_r = (("reference", 1, 1), ("1", 1, 1 / 360), ("100", 100, 1 / 3.6)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - IPT_hue_angle(IPT * factor_a), + xp_assert_close( + IPT_hue_angle(IPT * xp_as_array(factor_a, xp=xp)), hue * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_jzazbz.py b/colour/models/tests/test_jzazbz.py index 2493799a33..f2ae2054de 100644 --- a/colour/models/tests/test_jzazbz.py +++ b/colour/models/tests/test_jzazbz.py @@ -2,13 +2,27 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import Izazbz_to_XYZ, Jzazbz_to_XYZ, XYZ_to_Izazbz, XYZ_to_Jzazbz -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,80 +45,77 @@ class TestXYZ_to_Izazbz: tests methods. """ - def test_XYZ_to_Izazbz(self) -> None: + @pytest.mark.mps_xfail( + "MPS float32: aliased methods diverge under exact-equality assert" + ) + def test_XYZ_to_Izazbz(self, xp: ModuleType) -> None: """Test :func:`colour.models.jzazbz.XYZ_to_Izazbz` definition.""" - np.testing.assert_allclose( - XYZ_to_Izazbz(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.01207793, 0.00924302, 0.00526007]), + xp_assert_close( + XYZ_to_Izazbz(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.01207793, 0.00924302, 0.00526007], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Izazbz(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.01397346, -0.00608426, 0.00534077]), + xp_assert_close( + XYZ_to_Izazbz(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.01397346, -0.00608426, 0.00534077], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Izazbz(np.array([0.96907232, 1.00000000, 1.12179215])), - np.array([0.03927203, 0.00064174, -0.00052906]), + xp_assert_close( + XYZ_to_Izazbz(xp_as_array([0.96907232, 1.00000000, 1.12179215], xp=xp)), + [0.03927203, 0.00064174, -0.00052906], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Izazbz( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="Safdar 2021", ), - np.array([0.01049146, 0.00924302, 0.00526007]), + [0.01049146, 0.00924302, 0.00526007], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal( + xp_assert_equal( XYZ_to_Izazbz( - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp), method="Safdar 2021", ), - XYZ_to_Izazbz( - np.array([0.20654008, 0.12197225, 0.05136952]), method="ZCAM" - ), + XYZ_to_Izazbz([0.20654008, 0.12197225, 0.05136952], method="ZCAM"), ) - def test_n_dimensional_XYZ_to_Izazbz(self) -> None: + def test_n_dimensional_XYZ_to_Izazbz(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.XYZ_to_Izazbz` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Izazbz = XYZ_to_Izazbz(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Izazbz = as_ndarray(XYZ_to_Izazbz(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Izazbz = np.tile(Izazbz, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Izazbz(XYZ), Izazbz, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Izazbz = xp.tile(xp_as_array(Izazbz, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Izazbz(XYZ), Izazbz, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Izazbz = np.reshape(Izazbz, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_Izazbz(XYZ), Izazbz, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Izazbz = xp_reshape(xp_as_array(Izazbz, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_Izazbz(XYZ), Izazbz, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_Izazbz(self) -> None: + def test_domain_range_scale_XYZ_to_Izazbz(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.XYZ_to_Izazbz` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Izazbz = XYZ_to_Izazbz(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Izazbz = as_ndarray(XYZ_to_Izazbz(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Izazbz(XYZ * factor), Izazbz * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -128,86 +139,83 @@ class TestIzazbz_to_XYZ: methods. """ - def test_Izazbz_to_XYZ(self) -> None: + @pytest.mark.mps_xfail( + "MPS float32: aliased methods diverge under exact-equality assert" + ) + def test_Izazbz_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.jzazbz.Izazbz_to_XYZ` definition.""" - np.testing.assert_allclose( - Izazbz_to_XYZ(np.array([0.01207793, 0.00924302, 0.00526007])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + Izazbz_to_XYZ(xp_as_array([0.01207793, 0.00924302, 0.00526007], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Izazbz_to_XYZ(np.array([0.01397346, -0.00608426, 0.00534077])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + Izazbz_to_XYZ(xp_as_array([0.01397346, -0.00608426, 0.00534077], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Izazbz_to_XYZ(np.array([0.03927203, 0.00064174, -0.00052906])), - np.array([0.96907232, 1.00000000, 1.12179215]), + xp_assert_close( + Izazbz_to_XYZ(xp_as_array([0.03927203, 0.00064174, -0.00052906], xp=xp)), + [0.96907232, 1.00000000, 1.12179215], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - Izazbz_to_XYZ(np.array([0.03927203, 0.00064174, -0.00052906])), - np.array([0.96907232, 1.00000000, 1.12179215]), + xp_assert_close( + Izazbz_to_XYZ(xp_as_array([0.03927203, 0.00064174, -0.00052906], xp=xp)), + [0.96907232, 1.00000000, 1.12179215], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( Izazbz_to_XYZ( - np.array([0.01049146, 0.00924302, 0.00526007]), + xp_as_array([0.01049146, 0.00924302, 0.00526007], xp=xp), method="Safdar 2021", ), - np.array([0.20654008, 0.12197225, 0.05136952]), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_array_equal( + xp_assert_equal( Izazbz_to_XYZ( - np.array([0.01049146, 0.00924302, 0.00526007]), + xp_as_array([0.01049146, 0.00924302, 0.00526007], xp=xp), method="Safdar 2021", ), - Izazbz_to_XYZ( - np.array([0.01049146, 0.00924302, 0.00526007]), method="ZCAM" - ), + Izazbz_to_XYZ([0.01049146, 0.00924302, 0.00526007], method="ZCAM"), ) - def test_n_dimensional_Izazbz_to_XYZ(self) -> None: + def test_n_dimensional_Izazbz_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.Izazbz_to_XYZ` definition n-dimensional support. """ - Izazbz = np.array([0.01207793, 0.00924302, 0.00526007]) - XYZ = Izazbz_to_XYZ(Izazbz) + Izazbz = xp_as_array([0.01207793, 0.00924302, 0.00526007], xp=xp) + XYZ = as_ndarray(Izazbz_to_XYZ(Izazbz)) - Izazbz = np.tile(Izazbz, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - Izazbz_to_XYZ(Izazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Izazbz = xp.tile(xp_as_array(Izazbz, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(Izazbz_to_XYZ(Izazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Izazbz = np.reshape(Izazbz, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - Izazbz_to_XYZ(Izazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Izazbz = xp_reshape(xp_as_array(Izazbz, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Izazbz_to_XYZ(Izazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Izazbz_to_XYZ(self) -> None: + def test_domain_range_scale_Izazbz_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.Izazbz_to_XYZ` definition domain and range scale support. """ - Izazbz = np.array([0.01207793, 0.00924302, 0.00526007]) - XYZ = Izazbz_to_XYZ(Izazbz) + Izazbz = xp_as_array([0.01207793, 0.00924302, 0.00526007], xp=xp) + XYZ = as_ndarray(Izazbz_to_XYZ(Izazbz)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Izazbz_to_XYZ(Izazbz * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -231,63 +239,57 @@ class TestXYZ_to_Jzazbz: tests methods. """ - def test_XYZ_to_Jzazbz(self) -> None: + def test_XYZ_to_Jzazbz(self, xp: ModuleType) -> None: """Test :func:`colour.models.jzazbz.XYZ_to_Jzazbz` definition.""" - np.testing.assert_allclose( - XYZ_to_Jzazbz(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.00535048, 0.00924302, 0.00526007]), + xp_assert_close( + XYZ_to_Jzazbz(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.00535048, 0.00924302, 0.00526007], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Jzazbz(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.00619681, -0.00608426, 0.00534077]), + xp_assert_close( + XYZ_to_Jzazbz(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.00619681, -0.00608426, 0.00534077], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Jzazbz(np.array([0.96907232, 1.00000000, 1.12179215])), - np.array([0.01766826, 0.00064174, -0.00052906]), + xp_assert_close( + XYZ_to_Jzazbz(xp_as_array([0.96907232, 1.00000000, 1.12179215], xp=xp)), + [0.01766826, 0.00064174, -0.00052906], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Jzazbz(self) -> None: + def test_n_dimensional_XYZ_to_Jzazbz(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.XYZ_to_Jzazbz` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Jzazbz = XYZ_to_Jzazbz(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Jzazbz = as_ndarray(XYZ_to_Jzazbz(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Jzazbz = np.tile(Jzazbz, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Jzazbz(XYZ), Jzazbz, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Jzazbz = xp.tile(xp_as_array(Jzazbz, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Jzazbz(XYZ), Jzazbz, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Jzazbz = np.reshape(Jzazbz, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_Jzazbz(XYZ), Jzazbz, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Jzazbz = xp_reshape(xp_as_array(Jzazbz, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_Jzazbz(XYZ), Jzazbz, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_Jzazbz(self) -> None: + def test_domain_range_scale_XYZ_to_Jzazbz(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.XYZ_to_Jzazbz` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Jzazbz = XYZ_to_Jzazbz(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Jzazbz = as_ndarray(XYZ_to_Jzazbz(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_equal( - XYZ_to_Jzazbz(XYZ * factor), Jzazbz * factor - ) + xp_assert_equal(XYZ_to_Jzazbz(XYZ * factor), Jzazbz * factor) @ignore_numpy_errors def test_nan_XYZ_to_Jzazbz(self) -> None: @@ -307,61 +309,57 @@ class TestJzazbz_to_XYZ: methods. """ - def test_Jzazbz_to_XYZ(self) -> None: + def test_Jzazbz_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.jzazbz.Jzazbz_to_XYZ` definition.""" - np.testing.assert_allclose( - Jzazbz_to_XYZ(np.array([0.00535048, 0.00924302, 0.00526007])), - np.array([0.20654008, 0.12197225, 0.05136952]), - atol=1e-6, + xp_assert_close( + Jzazbz_to_XYZ(xp_as_array([0.00535048, 0.00924302, 0.00526007], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - np.testing.assert_allclose( - Jzazbz_to_XYZ(np.array([0.00619681, -0.00608426, 0.00534077])), - np.array([0.14222010, 0.23042768, 0.10495772]), - atol=1e-6, + xp_assert_close( + Jzazbz_to_XYZ(xp_as_array([0.00619681, -0.00608426, 0.00534077], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - np.testing.assert_allclose( - Jzazbz_to_XYZ(np.array([0.01766826, 0.00064174, -0.00052906])), - np.array([0.96907232, 1.00000000, 1.12179215]), - atol=1e-6, + xp_assert_close( + Jzazbz_to_XYZ(xp_as_array([0.01766826, 0.00064174, -0.00052906], xp=xp)), + [0.96907232, 1.00000000, 1.12179215], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - def test_n_dimensional_Jzazbz_to_XYZ(self) -> None: + def test_n_dimensional_Jzazbz_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.Jzazbz_to_XYZ` definition n-dimensional support. """ - Jzazbz = np.array([0.00535048, 0.00924302, 0.00526007]) - XYZ = Jzazbz_to_XYZ(Jzazbz) + Jzazbz = xp_as_array([0.00535048, 0.00924302, 0.00526007], xp=xp) + XYZ = as_ndarray(Jzazbz_to_XYZ(Jzazbz)) - Jzazbz = np.tile(Jzazbz, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - Jzazbz_to_XYZ(Jzazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Jzazbz = xp.tile(xp_as_array(Jzazbz, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(Jzazbz_to_XYZ(Jzazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Jzazbz = np.reshape(Jzazbz, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - Jzazbz_to_XYZ(Jzazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Jzazbz = xp_reshape(xp_as_array(Jzazbz, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Jzazbz_to_XYZ(Jzazbz), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Jzazbz_to_XYZ(self) -> None: + def test_domain_range_scale_Jzazbz_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.jzazbz.Jzazbz_to_XYZ` definition domain and range scale support. """ - Jzazbz = np.array([0.00535048, 0.00924302, 0.00526007]) - XYZ = Jzazbz_to_XYZ(Jzazbz) + Jzazbz = xp_as_array([0.00535048, 0.00924302, 0.00526007], xp=xp) + XYZ = as_ndarray(Jzazbz_to_XYZ(Jzazbz)) d_r = (("reference", 1), ("1", 1), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Jzazbz_to_XYZ(Jzazbz * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_oklab.py b/colour/models/tests/test_oklab.py index c6524e2e9e..5e09711d7e 100644 --- a/colour/models/tests/test_oklab.py +++ b/colour/models/tests/test_oklab.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import Oklab_to_XYZ, XYZ_to_Oklab -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,61 +42,57 @@ class TestXYZ_to_Oklab: tests methods. """ - def test_XYZ_to_Oklab(self) -> None: + def test_XYZ_to_Oklab(self, xp: ModuleType) -> None: """Test :func:`colour.models.oklab.XYZ_to_Oklab` definition.""" - np.testing.assert_allclose( - XYZ_to_Oklab(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.51634019, 0.15469500, 0.06289579]), + xp_assert_close( + XYZ_to_Oklab(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.51634019, 0.15469500, 0.06289579], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Oklab(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.59910746, -0.11139207, 0.07508465]), + xp_assert_close( + XYZ_to_Oklab(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.59910746, -0.11139207, 0.07508465], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Oklab(np.array([0.96907232, 1.00000000, 1.12179215])), - np.array([1.00121561, 0.00899591, -0.00535107]), + xp_assert_close( + XYZ_to_Oklab(xp_as_array([0.96907232, 1.00000000, 1.12179215], xp=xp)), + [1.00121561, 0.00899591, -0.00535107], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Oklab(self) -> None: + def test_n_dimensional_XYZ_to_Oklab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.oklab.XYZ_to_Oklab` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Oklab = XYZ_to_Oklab(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Oklab = as_ndarray(XYZ_to_Oklab(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Oklab = np.tile(Oklab, (6, 1)) - np.testing.assert_allclose( - XYZ_to_Oklab(XYZ), Oklab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Oklab = xp.tile(xp_as_array(Oklab, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Oklab(XYZ), Oklab, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Oklab = np.reshape(Oklab, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_Oklab(XYZ), Oklab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Oklab = xp_reshape(xp_as_array(Oklab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_Oklab(XYZ), Oklab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_Oklab(self) -> None: + def test_domain_range_scale_XYZ_to_Oklab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.oklab.XYZ_to_Oklab` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Oklab = XYZ_to_Oklab(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Oklab = as_ndarray(XYZ_to_Oklab(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Oklab(XYZ * factor), Oklab * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -107,61 +116,57 @@ class TestOklab_to_XYZ: methods. """ - def test_Oklab_to_XYZ(self) -> None: + def test_Oklab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.oklab.Oklab_to_XYZ` definition.""" - np.testing.assert_allclose( - Oklab_to_XYZ(np.array([0.51634019, 0.15469500, 0.06289579])), - np.array([0.20654008, 0.12197225, 0.05136952]), - atol=1e-6, + xp_assert_close( + Oklab_to_XYZ(xp_as_array([0.51634019, 0.15469500, 0.06289579], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - np.testing.assert_allclose( - Oklab_to_XYZ(np.array([0.59910746, -0.11139207, 0.07508465])), - np.array([0.14222010, 0.23042768, 0.10495772]), - atol=1e-6, + xp_assert_close( + Oklab_to_XYZ(xp_as_array([0.59910746, -0.11139207, 0.07508465], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - np.testing.assert_allclose( - Oklab_to_XYZ(np.array([1.00121561, 0.00899591, -0.00535107])), - np.array([0.96907232, 1.00000000, 1.12179215]), - atol=1e-6, + xp_assert_close( + Oklab_to_XYZ(xp_as_array([1.00121561, 0.00899591, -0.00535107], xp=xp)), + [0.96907232, 1.00000000, 1.12179215], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - def test_n_dimensional_Oklab_to_XYZ(self) -> None: + def test_n_dimensional_Oklab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.oklab.Oklab_to_XYZ` definition n-dimensional support. """ - Oklab = np.array([0.51634019, 0.15469500, 0.06289579]) - XYZ = Oklab_to_XYZ(Oklab) + Oklab = xp_as_array([0.51634019, 0.15469500, 0.06289579], xp=xp) + XYZ = as_ndarray(Oklab_to_XYZ(Oklab)) - Oklab = np.tile(Oklab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - Oklab_to_XYZ(Oklab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Oklab = xp.tile(xp_as_array(Oklab, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(Oklab_to_XYZ(Oklab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Oklab = np.reshape(Oklab, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - Oklab_to_XYZ(Oklab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Oklab = xp_reshape(xp_as_array(Oklab, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Oklab_to_XYZ(Oklab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Oklab_to_XYZ(self) -> None: + def test_domain_range_scale_Oklab_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.oklab.Oklab_to_XYZ` definition domain and range scale support. """ - Oklab = np.array([0.51634019, 0.15469500, 0.06289579]) - XYZ = Oklab_to_XYZ(Oklab) + Oklab = xp_as_array([0.51634019, 0.15469500, 0.06289579], xp=xp) + XYZ = as_ndarray(Oklab_to_XYZ(Oklab)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_equal( + xp_assert_equal( Oklab_to_XYZ(Oklab * factor), XYZ * factor, ) diff --git a/colour/models/tests/test_osa_ucs.py b/colour/models/tests/test_osa_ucs.py index 9714cd873f..9001a07a79 100644 --- a/colour/models/tests/test_osa_ucs.py +++ b/colour/models/tests/test_osa_ucs.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import OSA_UCS_to_XYZ, XYZ_to_OSA_UCS -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,61 +42,63 @@ class TestXYZ_to_OSA_UCS: methods. """ - def test_XYZ_to_OSA_UCS(self) -> None: + def test_XYZ_to_OSA_UCS(self, xp: ModuleType) -> None: """Test :func:`colour.models.osa_ucs.XYZ_to_OSA_UCS` definition.""" - np.testing.assert_allclose( - XYZ_to_OSA_UCS(np.array([0.20654008, 0.12197225, 0.05136952]) * 100), - np.array([-3.00499790, 2.99713697, -9.66784231]), + xp_assert_close( + XYZ_to_OSA_UCS( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + ), + [-3.00499790, 2.99713697, -9.66784231], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_OSA_UCS(np.array([0.14222010, 0.23042768, 0.10495772]) * 100), - np.array([-1.64657491, 4.59201565, 5.31738757]), + xp_assert_close( + XYZ_to_OSA_UCS( + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100 + ), + [-1.64657491, 4.59201565, 5.31738757], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_OSA_UCS(np.array([0.07818780, 0.06157201, 0.28099326]) * 100), - np.array([-5.08589672, -7.91062749, 0.98107575]), + xp_assert_close( + XYZ_to_OSA_UCS( + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100 + ), + [-5.08589672, -7.91062749, 0.98107575], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_OSA_UCS(self) -> None: + def test_n_dimensional_XYZ_to_OSA_UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.osa_ucs.XYZ_to_OSA_UCS` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 - Ljg = XYZ_to_OSA_UCS(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + Ljg = as_ndarray(XYZ_to_OSA_UCS(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Ljg = np.tile(Ljg, (6, 1)) - np.testing.assert_allclose( - XYZ_to_OSA_UCS(XYZ), Ljg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Ljg = xp.tile(xp_as_array(Ljg, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_OSA_UCS(XYZ), Ljg, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Ljg = np.reshape(Ljg, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_OSA_UCS(XYZ), Ljg, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Ljg = xp_reshape(xp_as_array(Ljg, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_OSA_UCS(XYZ), Ljg, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_OSA_UCS(self) -> None: + def test_domain_range_scale_XYZ_to_OSA_UCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.osa_ucs.XYZ_to_OSA_UCS` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) * 100 - Ljg = XYZ_to_OSA_UCS(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100 + Ljg = as_ndarray(XYZ_to_OSA_UCS(XYZ)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_OSA_UCS(XYZ * factor), Ljg * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -107,70 +122,67 @@ class TestOSA_UCS_to_XYZ: methods. """ - def test_OSA_UCS_to_XYZ(self) -> None: + @pytest.mark.mps_xfail("MPS float32 precision divergence") + def test_OSA_UCS_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.osa_ucs.OSA_UCS_to_XYZ` definition.""" - np.testing.assert_allclose( + xp_assert_close( OSA_UCS_to_XYZ( - np.array([-3.00499790, 2.99713697, -9.66784231]), + xp_as_array([-3.00499790, 2.99713697, -9.66784231], xp=xp), {"disp": False}, ), - np.array([0.20654008, 0.12197225, 0.05136952]) * 100, - atol=5e-5, + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) * 100, + atol=TOLERANCE_ABSOLUTE_TESTS * 500, ) - np.testing.assert_allclose( + xp_assert_close( OSA_UCS_to_XYZ( - np.array([-1.64657491, 4.59201565, 5.31738757]), + xp_as_array([-1.64657491, 4.59201565, 5.31738757], xp=xp), {"disp": False}, ), - np.array([0.14222010, 0.23042768, 0.10495772]) * 100, - atol=5e-5, + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) * 100, + atol=TOLERANCE_ABSOLUTE_TESTS * 500, ) - np.testing.assert_allclose( + xp_assert_close( OSA_UCS_to_XYZ( - np.array([-5.08589672, -7.91062749, 0.98107575]), + xp_as_array([-5.08589672, -7.91062749, 0.98107575], xp=xp), {"disp": False}, ), - np.array([0.07818780, 0.06157201, 0.28099326]) * 100, - atol=5e-5, + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) * 100, + atol=TOLERANCE_ABSOLUTE_TESTS * 500, ) - def test_n_dimensional_OSA_UCS_to_XYZ(self) -> None: + def test_n_dimensional_OSA_UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.osa_ucs.OSA_UCS_to_XYZ` definition n-dimensional support. """ - Ljg = np.array([-3.00499790, 2.99713697, -9.66784231]) - XYZ = OSA_UCS_to_XYZ(Ljg) + Ljg = xp_as_array([-3.00499790, 2.99713697, -9.66784231], xp=xp) + XYZ = as_ndarray(OSA_UCS_to_XYZ(Ljg)) - Ljg = np.tile(Ljg, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - OSA_UCS_to_XYZ(Ljg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Ljg = xp.tile(xp_as_array(Ljg, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(OSA_UCS_to_XYZ(Ljg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Ljg = np.reshape(Ljg, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - OSA_UCS_to_XYZ(Ljg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Ljg = xp_reshape(xp_as_array(Ljg, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(OSA_UCS_to_XYZ(Ljg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_OSA_UCS_to_XYZ(self) -> None: + def test_domain_range_scale_OSA_UCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.osa_ucs.OSA_UCS_to_XYZ` definition domain and range scale support. """ - Ljg = np.array([-3.00499790, 2.99713697, -9.66784231]) - XYZ = OSA_UCS_to_XYZ(Ljg) + Ljg = xp_as_array([-3.00499790, 2.99713697, -9.66784231], xp=xp) + XYZ = as_ndarray(OSA_UCS_to_XYZ(Ljg)) d_r = (("reference", 1), ("1", 0.01), ("100", 1)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( OSA_UCS_to_XYZ(Ljg * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_prolab.py b/colour/models/tests/test_prolab.py index cc5e77cd5a..19ebe4ec6a 100644 --- a/colour/models/tests/test_prolab.py +++ b/colour/models/tests/test_prolab.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import ProLab_to_XYZ, XYZ_to_ProLab -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,61 +41,57 @@ class TestXYZ_to_ProLab: tests methods. """ - def test_XYZ_to_ProLab(self) -> None: + def test_XYZ_to_ProLab(self, xp: ModuleType) -> None: """Test :func:`colour.models.ProLab.XYZ_to_ProLab` definition.""" - np.testing.assert_allclose( - XYZ_to_ProLab(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([48.7948929, 35.31503175, 13.30044932]), + xp_assert_close( + XYZ_to_ProLab(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [48.7948929, 35.31503175, 13.30044932], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ProLab(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([64.45929636, -21.67007419, 13.25749056]), + xp_assert_close( + XYZ_to_ProLab(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [64.45929636, -21.67007419, 13.25749056], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_ProLab(np.array([0.96907232, 1.00000000, 0.12179215])), - np.array([100.0, 5.47367608, 37.26313098]), + xp_assert_close( + XYZ_to_ProLab(xp_as_array([0.96907232, 1.00000000, 0.12179215], xp=xp)), + [100.0, 5.47367608, 37.26313098], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_ProLab(self) -> None: + def test_n_dimensional_XYZ_to_ProLab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.prolab.XYZ_to_ProLab` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) ProLab = XYZ_to_ProLab(XYZ) - XYZ = np.tile(XYZ, (6, 1)) - ProLab = np.tile(ProLab, (6, 1)) - np.testing.assert_allclose( - XYZ_to_ProLab(XYZ), ProLab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(XYZ, (6, 1)) + ProLab = xp.tile(ProLab, (6, 1)) + xp_assert_close(XYZ_to_ProLab(XYZ), ProLab, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - ProLab = np.reshape(ProLab, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_ProLab(XYZ), ProLab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(XYZ, (2, 3, 3), xp=xp) + ProLab = xp_reshape(ProLab, (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_ProLab(XYZ), ProLab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_ProLab(self) -> None: + def test_domain_range_scale_XYZ_to_ProLab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.prolab.XYZ_to_ProLab` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) ProLab = XYZ_to_ProLab(XYZ) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ProLab(XYZ * factor), ProLab * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -107,61 +115,57 @@ class TestProLab_to_XYZ: methods. """ - def test_ProLab_to_XYZ(self) -> None: + def test_ProLab_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.ProLab.ProLab_to_XYZ` definition.""" - np.testing.assert_allclose( - ProLab_to_XYZ(np.array([48.7948929, 35.31503175, 13.30044932])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + ProLab_to_XYZ(xp_as_array([48.7948929, 35.31503175, 13.30044932], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ProLab_to_XYZ(np.array([64.45929636, -21.67007419, 13.25749056])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + ProLab_to_XYZ(xp_as_array([64.45929636, -21.67007419, 13.25749056], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - ProLab_to_XYZ(np.array([100.0, 5.47367608, 37.26313098])), - np.array([0.96907232, 1.00000000, 0.12179215]), + xp_assert_close( + ProLab_to_XYZ(xp_as_array([100.0, 5.47367608, 37.26313098], xp=xp)), + [0.96907232, 1.00000000, 0.12179215], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_ProLab(self) -> None: + def test_n_dimensional_XYZ_to_ProLab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.prolab.XYZ_to_ProLab` definition n-dimensional support. """ - ProLab = np.array([48.7948929, 35.31503175, 13.30044932]) + ProLab = xp_as_array([48.7948929, 35.31503175, 13.30044932], xp=xp) XYZ = ProLab_to_XYZ(ProLab) - ProLab = np.tile(ProLab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - ProLab_to_XYZ(ProLab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ProLab = xp.tile(ProLab, (6, 1)) + XYZ = xp.tile(XYZ, (6, 1)) + xp_assert_close(ProLab_to_XYZ(ProLab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - ProLab = np.reshape(ProLab, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - ProLab_to_XYZ(ProLab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ProLab = xp_reshape(ProLab, (2, 3, 3), xp=xp) + XYZ = xp_reshape(XYZ, (2, 3, 3), xp=xp) + xp_assert_close(ProLab_to_XYZ(ProLab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_ProLab(self) -> None: + def test_domain_range_scale_XYZ_to_ProLab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.prolab.XYZ_to_ProLab` definition domain and range scale support. """ - ProLab = np.array([48.7948929, 35.31503175, 13.30044932]) - XYZ = XYZ_to_ProLab(ProLab) + ProLab = xp_as_array([48.7948929, 35.31503175, 13.30044932], xp=xp) + XYZ = as_ndarray(XYZ_to_ProLab(ProLab)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_ProLab(ProLab * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_ragoo2021.py b/colour/models/tests/test_ragoo2021.py index 096f0f6b39..06e663afae 100644 --- a/colour/models/tests/test_ragoo2021.py +++ b/colour/models/tests/test_ragoo2021.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import IPT_Ragoo2021_to_XYZ, XYZ_to_IPT_Ragoo2021 -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,63 +41,65 @@ class TestXYZ_to_IPT_Ragoo2021: unit tests methods. """ - def test_XYZ_to_IPT_Ragoo2021(self) -> None: + def test_XYZ_to_IPT_Ragoo2021(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ragoo2021.XYZ_to_IPT_Ragoo2021` definition. """ - np.testing.assert_allclose( - XYZ_to_IPT_Ragoo2021(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.42248243, 0.29105140, 0.20410663]), + xp_assert_close( + XYZ_to_IPT_Ragoo2021( + xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + ), + [0.42248243, 0.29105140, 0.20410663], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_IPT_Ragoo2021(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.54745257, -0.22795249, 0.10109646]), + xp_assert_close( + XYZ_to_IPT_Ragoo2021( + xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp) + ), + [0.54745257, -0.22795249, 0.10109646], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_IPT_Ragoo2021(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([0.32151337, 0.06071424, -0.27388774]), + xp_assert_close( + XYZ_to_IPT_Ragoo2021( + xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp) + ), + [0.32151337, 0.06071424, -0.27388774], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_IPT_Ragoo2021(self) -> None: + def test_n_dimensional_XYZ_to_IPT_Ragoo2021(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ragoo2021.XYZ_to_IPT_Ragoo2021` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - IPT = XYZ_to_IPT_Ragoo2021(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IPT = as_ndarray(XYZ_to_IPT_Ragoo2021(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - IPT = np.tile(IPT, (6, 1)) - np.testing.assert_allclose( - XYZ_to_IPT_Ragoo2021(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + IPT = xp.tile(xp_as_array(IPT, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_IPT_Ragoo2021(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - IPT = np.reshape(IPT, (2, 3, 3)) - np.testing.assert_allclose( - XYZ_to_IPT_Ragoo2021(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + IPT = xp_reshape(xp_as_array(IPT, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_IPT_Ragoo2021(XYZ), IPT, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_IPT_Ragoo2021(self) -> None: + def test_domain_range_scale_XYZ_to_IPT_Ragoo2021(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ragoo2021.XYZ_to_IPT_Ragoo2021` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - IPT = XYZ_to_IPT_Ragoo2021(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IPT = as_ndarray(XYZ_to_IPT_Ragoo2021(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_IPT_Ragoo2021(XYZ * factor), IPT * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -109,63 +123,65 @@ class TestIPT_Ragoo2021_to_XYZ: unit tests methods. """ - def test_IPT_Ragoo2021_to_XYZ(self) -> None: + def test_IPT_Ragoo2021_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ragoo2021.IPT_Ragoo2021_to_XYZ` definition. """ - np.testing.assert_allclose( - IPT_Ragoo2021_to_XYZ(np.array([0.42248243, 0.29105140, 0.20410663])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + IPT_Ragoo2021_to_XYZ( + xp_as_array([0.42248243, 0.29105140, 0.20410663], xp=xp) + ), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IPT_Ragoo2021_to_XYZ(np.array([0.54745257, -0.22795249, 0.10109646])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + IPT_Ragoo2021_to_XYZ( + xp_as_array([0.54745257, -0.22795249, 0.10109646], xp=xp) + ), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - IPT_Ragoo2021_to_XYZ(np.array([0.32151337, 0.06071424, -0.27388774])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + IPT_Ragoo2021_to_XYZ( + xp_as_array([0.32151337, 0.06071424, -0.27388774], xp=xp) + ), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_IPT_Ragoo2021_to_XYZ(self) -> None: + def test_n_dimensional_IPT_Ragoo2021_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ragoo2021.IPT_Ragoo2021_to_XYZ` definition n-dimensional support. """ - IPT = np.array([0.42248243, 0.29105140, 0.20410663]) - XYZ = IPT_Ragoo2021_to_XYZ(IPT) + IPT = xp_as_array([0.42248243, 0.29105140, 0.20410663], xp=xp) + XYZ = as_ndarray(IPT_Ragoo2021_to_XYZ(IPT)) - IPT = np.tile(IPT, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - IPT_Ragoo2021_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + IPT = xp.tile(xp_as_array(IPT, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(IPT_Ragoo2021_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - IPT = np.reshape(IPT, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - IPT_Ragoo2021_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS - ) + IPT = xp_reshape(xp_as_array(IPT, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(IPT_Ragoo2021_to_XYZ(IPT), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_IPT_Ragoo2021_to_XYZ(self) -> None: + def test_domain_range_scale_IPT_Ragoo2021_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.ragoo2021.IPT_Ragoo2021_to_XYZ` definition domain and range scale support. """ - IPT = np.array([0.42248243, 0.29105140, 0.20410663]) - XYZ = IPT_Ragoo2021_to_XYZ(IPT) + IPT = xp_as_array([0.42248243, 0.29105140, 0.20410663], xp=xp) + XYZ = as_ndarray(IPT_Ragoo2021_to_XYZ(IPT)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( IPT_Ragoo2021_to_XYZ(IPT * factor), XYZ * factor, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/models/tests/test_sucs.py b/colour/models/tests/test_sucs.py index 6f6a07ab96..68ad56fbb4 100644 --- a/colour/models/tests/test_sucs.py +++ b/colour/models/tests/test_sucs.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -15,7 +20,14 @@ sUCS_ICh_to_sUCS_Iab, sUCS_to_XYZ, ) -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "UltraMo114(Molin Li), Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -37,58 +49,58 @@ class TestXYZ_to_sUCS: """Define :func:`colour.models.sucs.XYZ_to_sUCS` definition unit tests methods.""" - def test_XYZ_to_sUCS(self) -> None: + def test_XYZ_to_sUCS(self, xp: ModuleType) -> None: """Test :func:`colour.models.sucs.XYZ_to_sUCS` definition.""" - np.testing.assert_allclose( - XYZ_to_sUCS(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([42.62923653, 36.97646831, 14.12301358]), + xp_assert_close( + XYZ_to_sUCS(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [42.62923653, 36.97646831, 14.12301358], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_sUCS(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([51.93649255, -18.89245582, 15.76112395]), + xp_assert_close( + XYZ_to_sUCS(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [51.93649255, -18.89245582, 15.76112395], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_sUCS(np.array([0.07818780, 0.06157201, 0.28099326])), - np.array([29.79456846, -6.83806757, -25.33884097]), + xp_assert_close( + XYZ_to_sUCS(xp_as_array([0.07818780, 0.06157201, 0.28099326], xp=xp)), + [29.79456846, -6.83806757, -25.33884097], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_sUCS(self) -> None: + def test_n_dimensional_XYZ_to_sUCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.XYZ_to_sUCS` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Iab = XYZ_to_sUCS(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Iab = as_ndarray(XYZ_to_sUCS(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Iab = np.tile(Iab, (6, 1)) - np.testing.assert_allclose(XYZ_to_sUCS(XYZ), Iab, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_sUCS(XYZ), Iab, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Iab = np.reshape(Iab, (2, 3, 3)) - np.testing.assert_allclose(XYZ_to_sUCS(XYZ), Iab, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_sUCS(XYZ), Iab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_sUCS(self) -> None: + def test_domain_range_scale_XYZ_to_sUCS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.XYZ_to_sUCS` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Iab = XYZ_to_sUCS(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Iab = as_ndarray(XYZ_to_sUCS(XYZ)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_sUCS(XYZ * factor_a), + xp_assert_close( + XYZ_to_sUCS(XYZ * xp_as_array(factor_a, xp=xp)), Iab * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -108,58 +120,58 @@ class TestsUCS_to_XYZ: methods. """ - def test_sUCS_to_XYZ(self) -> None: + def test_sUCS_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.sucs.sUCS_to_XYZ` definition.""" - np.testing.assert_allclose( - sUCS_to_XYZ(np.array([42.62923653, 36.97646831, 14.12301358])), - np.array([0.20654008, 0.12197225, 0.05136952]), + xp_assert_close( + sUCS_to_XYZ(xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_to_XYZ(np.array([51.93649255, -18.89245582, 15.76112395])), - np.array([0.14222010, 0.23042768, 0.10495772]), + xp_assert_close( + sUCS_to_XYZ(xp_as_array([51.93649255, -18.89245582, 15.76112395], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_to_XYZ(np.array([29.79456846, -6.83806757, -25.33884097])), - np.array([0.07818780, 0.06157201, 0.28099326]), + xp_assert_close( + sUCS_to_XYZ(xp_as_array([29.79456846, -6.83806757, -25.33884097], xp=xp)), + [0.07818780, 0.06157201, 0.28099326], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sUCS_to_XYZ(self) -> None: + def test_n_dimensional_sUCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_to_XYZ` definition n-dimensional support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - XYZ = sUCS_to_XYZ(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + XYZ = as_ndarray(sUCS_to_XYZ(Iab)) - Iab = np.tile(Iab, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(sUCS_to_XYZ(Iab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(sUCS_to_XYZ(Iab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Iab = np.reshape(Iab, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(sUCS_to_XYZ(Iab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(sUCS_to_XYZ(Iab), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_sUCS_to_XYZ(self) -> None: + def test_domain_range_scale_sUCS_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_to_XYZ` definition domain and range scale support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - XYZ = sUCS_to_XYZ(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + XYZ = as_ndarray(sUCS_to_XYZ(Iab)) d_r = (("reference", 1, 1), ("1", 0.01, 1), ("100", 1, 100)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sUCS_to_XYZ(Iab * factor_a), + xp_assert_close( + sUCS_to_XYZ(Iab * xp_as_array(factor_a, xp=xp)), XYZ * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -179,58 +191,58 @@ class TestsUCSChroma: methods. """ - def test_sUCS_hue_angle(self) -> None: + def test_sUCS_hue_angle(self, xp: ModuleType) -> None: """Test :func:`colour.models.sucs.sUCS_chroma` definition.""" - np.testing.assert_allclose( - sUCS_chroma(np.array([42.62923653, 36.97646831, 14.12301358])), + xp_assert_close( + sUCS_chroma(xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp)), 40.420511061137226, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_chroma(np.array([51.93649255, -18.89245582, 15.76112395])), + xp_assert_close( + sUCS_chroma(xp_as_array([51.93649255, -18.89245582, 15.76112395], xp=xp)), 29.437831501432590, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_chroma(np.array([29.79456846, -6.83806757, -25.33884097])), + xp_assert_close( + sUCS_chroma(xp_as_array([29.79456846, -6.83806757, -25.33884097], xp=xp)), 30.800979756091614, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sUCS_hue_angle(self) -> None: + def test_n_dimensional_sUCS_hue_angle(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_chroma` definition n-dimensional support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) C = sUCS_chroma(Iab) - Iab = np.tile(Iab, (6, 1)) - C = np.tile(C, 6) - np.testing.assert_allclose(sUCS_chroma(Iab), C, atol=TOLERANCE_ABSOLUTE_TESTS) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + C = xp.tile(xp_as_array(C, xp=xp), (6,)) + xp_assert_close(sUCS_chroma(Iab), C, atol=TOLERANCE_ABSOLUTE_TESTS) - Iab = np.reshape(Iab, (2, 3, 3)) - C = np.reshape(C, (2, 3)) - np.testing.assert_allclose(sUCS_chroma(Iab), C, atol=TOLERANCE_ABSOLUTE_TESTS) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + C = xp_reshape(xp_as_array(C, xp=xp), (2, 3), xp=xp) + xp_assert_close(sUCS_chroma(Iab), C, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_sUCS_chroma(self) -> None: + def test_domain_range_scale_sUCS_chroma(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_chroma` definition domain and range scale support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - C = sUCS_chroma(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + C = as_ndarray(sUCS_chroma(Iab)) d_r = (("reference", 1, 1), ("1", 0.01, 0.01), ("100", 1, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sUCS_chroma(Iab * factor_a), + xp_assert_close( + sUCS_chroma(Iab * xp_as_array(factor_a, xp=xp)), C * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -250,62 +262,62 @@ class TestsUCSHueAngle: methods. """ - def test_sUCS_hue_angle(self) -> None: + def test_sUCS_hue_angle(self, xp: ModuleType) -> None: """Test :func:`colour.models.sucs.sUCS_hue_angle` definition.""" - np.testing.assert_allclose( - sUCS_hue_angle(np.array([42.62923653, 36.97646831, 14.12301358])), + xp_assert_close( + sUCS_hue_angle(xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp)), 20.904156072136217, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_hue_angle(np.array([51.93649255, -18.89245582, 15.76112395])), + xp_assert_close( + sUCS_hue_angle( + xp_as_array([51.93649255, -18.89245582, 15.76112395], xp=xp) + ), 140.163281067124470, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_hue_angle(np.array([29.79456846, -6.83806757, -25.33884097])), + xp_assert_close( + sUCS_hue_angle( + xp_as_array([29.79456846, -6.83806757, -25.33884097], xp=xp) + ), 254.897631851863850, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sUCS_hue_angle(self) -> None: + def test_n_dimensional_sUCS_hue_angle(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_hue_angle` definition n-dimensional support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - hue = sUCS_hue_angle(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + hue = as_ndarray(sUCS_hue_angle(Iab)) - Iab = np.tile(Iab, (6, 1)) - hue = np.tile(hue, 6) - np.testing.assert_allclose( - sUCS_hue_angle(Iab), hue, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + hue = xp.tile(xp_as_array(hue, xp=xp), (6,)) + xp_assert_close(sUCS_hue_angle(Iab), hue, atol=TOLERANCE_ABSOLUTE_TESTS) - Iab = np.reshape(Iab, (2, 3, 3)) - hue = np.reshape(hue, (2, 3)) - np.testing.assert_allclose( - sUCS_hue_angle(Iab), hue, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + hue = xp_reshape(xp_as_array(hue, xp=xp), (2, 3), xp=xp) + xp_assert_close(sUCS_hue_angle(Iab), hue, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_sUCS_hue_angle(self) -> None: + def test_domain_range_scale_sUCS_hue_angle(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_hue_angle` definition domain and range scale support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - hue = sUCS_hue_angle(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + hue = as_ndarray(sUCS_hue_angle(Iab)) d_r = (("reference", 1, 1), ("1", 1, 1 / 360), ("100", 100, 1 / 3.6)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sUCS_hue_angle(Iab * factor_a), + xp_assert_close( + sUCS_hue_angle(Iab * xp_as_array(factor_a, xp=xp)), hue * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -325,56 +337,58 @@ class TestsUCS_Iab_to_sUCS_ICh: methods. """ - def test_sUCS_Iab_to_sUCS_ICh(self) -> None: + def test_sUCS_Iab_to_sUCS_ICh(self, xp: ModuleType) -> None: """Test :func:`colour.models.sucs.sUCS_Iab_to_sUCS_ICh` definition.""" - np.testing.assert_allclose( - sUCS_Iab_to_sUCS_ICh(np.array([42.62923653, 36.97646831, 14.12301358])), - np.array([42.62923653, 40.42051106, 20.90415607]), + xp_assert_close( + sUCS_Iab_to_sUCS_ICh( + xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + ), + [42.62923653, 40.42051106, 20.90415607], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_Iab_to_sUCS_ICh(np.array([51.93649255, -18.89245582, 15.76112395])), - np.array([51.93649255, 29.43783150, 140.16328107]), + xp_assert_close( + sUCS_Iab_to_sUCS_ICh( + xp_as_array([51.93649255, -18.89245582, 15.76112395], xp=xp) + ), + [51.93649255, 29.43783150, 140.16328107], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_Iab_to_sUCS_ICh(np.array([29.79456846, -6.83806757, -25.33884097])), - np.array([29.79456846, 30.80097976, 254.89763185]), + xp_assert_close( + sUCS_Iab_to_sUCS_ICh( + xp_as_array([29.79456846, -6.83806757, -25.33884097], xp=xp) + ), + [29.79456846, 30.80097976, 254.89763185], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sUCS_Iab_to_sUCS_ICh(self) -> None: + def test_n_dimensional_sUCS_Iab_to_sUCS_ICh(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_Iab_to_sUCS_ICh` definition n-dimensional support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - ICh = sUCS_Iab_to_sUCS_ICh(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + ICh = as_ndarray(sUCS_Iab_to_sUCS_ICh(Iab)) - Iab = np.tile(Iab, (6, 1)) - ICh = np.tile(ICh, (6, 1)) - np.testing.assert_allclose( - sUCS_Iab_to_sUCS_ICh(Iab), ICh, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + ICh = xp.tile(xp_as_array(ICh, xp=xp), (6, 1)) + xp_assert_close(sUCS_Iab_to_sUCS_ICh(Iab), ICh, atol=TOLERANCE_ABSOLUTE_TESTS) - Iab = np.reshape(Iab, (2, 3, 3)) - ICh = np.reshape(ICh, (2, 3, 3)) - np.testing.assert_allclose( - sUCS_Iab_to_sUCS_ICh(Iab), ICh, atol=TOLERANCE_ABSOLUTE_TESTS - ) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + ICh = xp_reshape(xp_as_array(ICh, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(sUCS_Iab_to_sUCS_ICh(Iab), ICh, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_sUCS_Iab_to_sUCS_ICh(self) -> None: + def test_domain_range_scale_sUCS_Iab_to_sUCS_ICh(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_Iab_to_sUCS_ICh` definition domain and range scale support. """ - Iab = np.array([42.62923653, 36.97646831, 14.12301358]) - ICh = sUCS_Iab_to_sUCS_ICh(Iab) + Iab = xp_as_array([42.62923653, 36.97646831, 14.12301358], xp=xp) + ICh = as_ndarray(sUCS_Iab_to_sUCS_ICh(Iab)) d_r = ( ("reference", 1, 1), @@ -383,8 +397,8 @@ def test_domain_range_scale_sUCS_Iab_to_sUCS_ICh(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sUCS_Iab_to_sUCS_ICh(Iab * factor_a), + xp_assert_close( + sUCS_Iab_to_sUCS_ICh(Iab * xp_as_array(factor_a, xp=xp)), ICh * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -406,56 +420,58 @@ class TestsUCS_ICh_to_sUCS_Iab: methods. """ - def test_sUCS_ICh_to_sUCS_Iab(self) -> None: + def test_sUCS_ICh_to_sUCS_Iab(self, xp: ModuleType) -> None: """Test :func:`colour.models.sucs.sUCS_ICh_to_sUCS_Iab` definition.""" - np.testing.assert_allclose( - sUCS_ICh_to_sUCS_Iab(np.array([42.62923653, 40.42051106, 20.90415607])), - np.array([42.62923653, 36.97646831, 14.12301358]), + xp_assert_close( + sUCS_ICh_to_sUCS_Iab( + xp_as_array([42.62923653, 40.42051106, 20.90415607], xp=xp) + ), + [42.62923653, 36.97646831, 14.12301358], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_ICh_to_sUCS_Iab(np.array([51.93649255, 29.43783150, 140.16328107])), - np.array([51.93649255, -18.89245582, 15.76112395]), + xp_assert_close( + sUCS_ICh_to_sUCS_Iab( + xp_as_array([51.93649255, 29.43783150, 140.16328107], xp=xp) + ), + [51.93649255, -18.89245582, 15.76112395], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sUCS_ICh_to_sUCS_Iab(np.array([29.79456846, 30.80097976, 254.89763185])), - np.array([29.79456846, -6.83806757, -25.33884097]), + xp_assert_close( + sUCS_ICh_to_sUCS_Iab( + xp_as_array([29.79456846, 30.80097976, 254.89763185], xp=xp) + ), + [29.79456846, -6.83806757, -25.33884097], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sUCS_ICh_to_sUCS_Iab(self) -> None: + def test_n_dimensional_sUCS_ICh_to_sUCS_Iab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_ICh_to_sUCS_Iab` definition n-dimensional support. """ - ICh = np.array([42.62923653, 40.42051106, 20.90415607]) - Iab = sUCS_ICh_to_sUCS_Iab(ICh) + ICh = xp_as_array([42.62923653, 40.42051106, 20.90415607], xp=xp) + Iab = as_ndarray(sUCS_ICh_to_sUCS_Iab(ICh)) - ICh = np.tile(ICh, (6, 1)) - Iab = np.tile(Iab, (6, 1)) - np.testing.assert_allclose( - sUCS_ICh_to_sUCS_Iab(ICh), Iab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICh = xp.tile(xp_as_array(ICh, xp=xp), (6, 1)) + Iab = xp.tile(xp_as_array(Iab, xp=xp), (6, 1)) + xp_assert_close(sUCS_ICh_to_sUCS_Iab(ICh), Iab, atol=TOLERANCE_ABSOLUTE_TESTS) - ICh = np.reshape(ICh, (2, 3, 3)) - Iab = np.reshape(Iab, (2, 3, 3)) - np.testing.assert_allclose( - sUCS_ICh_to_sUCS_Iab(ICh), Iab, atol=TOLERANCE_ABSOLUTE_TESTS - ) + ICh = xp_reshape(xp_as_array(ICh, xp=xp), (2, 3, 3), xp=xp) + Iab = xp_reshape(xp_as_array(Iab, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(sUCS_ICh_to_sUCS_Iab(ICh), Iab, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_sUCS_ICh_to_sUCS_Iab(self) -> None: + def test_domain_range_scale_sUCS_ICh_to_sUCS_Iab(self, xp: ModuleType) -> None: """ Test :func:`colour.models.sucs.sUCS_ICh_to_sUCS_Iab` definition domain and range scale support. """ - ICh = np.array([42.62923653, 40.42051106, 20.90415607]) - Iab = sUCS_ICh_to_sUCS_Iab(ICh) + ICh = xp_as_array([42.62923653, 40.42051106, 20.90415607], xp=xp) + Iab = as_ndarray(sUCS_ICh_to_sUCS_Iab(ICh)) d_r = ( ("reference", 1, 1), @@ -464,8 +480,8 @@ def test_domain_range_scale_sUCS_ICh_to_sUCS_Iab(self) -> None: ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sUCS_ICh_to_sUCS_Iab(ICh * factor_a), + xp_assert_close( + sUCS_ICh_to_sUCS_Iab(ICh * xp_as_array(factor_a, xp=xp)), Iab * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/models/tests/test_yrg.py b/colour/models/tests/test_yrg.py index bbab9c8370..525748a570 100644 --- a/colour/models/tests/test_yrg.py +++ b/colour/models/tests/test_yrg.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models import LMS_to_Yrg, XYZ_to_Yrg, Yrg_to_LMS, Yrg_to_XYZ -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,57 +43,57 @@ class TestLMS_to_Yrg: methods. """ - def test_LMS_to_Yrg(self) -> None: + def test_LMS_to_Yrg(self, xp: ModuleType) -> None: """Test :func:`colour.models.yrg.LMS_to_Yrg` definition.""" - np.testing.assert_allclose( - LMS_to_Yrg(np.array([0.15639195, 0.06741689, 0.03281398])), - np.array([0.13137801, 0.49037644, 0.37777391]), + xp_assert_close( + LMS_to_Yrg(xp_as_array([0.15639195, 0.06741689, 0.03281398], xp=xp)), + [0.13137801, 0.49037644, 0.37777391], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - LMS_to_Yrg(np.array([0.23145723, 0.22601133, 0.05033211])), - np.array([0.23840767, 0.20110504, 0.69668437]), + xp_assert_close( + LMS_to_Yrg(xp_as_array([0.23145723, 0.22601133, 0.05033211], xp=xp)), + [0.23840767, 0.20110504, 0.69668437], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - LMS_to_Yrg(np.array([1.07423297, 0.91295620, 0.61375713])), - np.array([1.05911888, 0.22010094, 0.53660290]), + xp_assert_close( + LMS_to_Yrg(xp_as_array([1.07423297, 0.91295620, 0.61375713], xp=xp)), + [1.05911888, 0.22010094, 0.53660290], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_LMS_to_Yrg(self) -> None: + def test_n_dimensional_LMS_to_Yrg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.LMS_to_Yrg` definition n-dimensional support. """ - LMS = np.array([0.15639195, 0.06741689, 0.03281398]) - Yrg = LMS_to_Yrg(LMS) + LMS = xp_as_array([0.15639195, 0.06741689, 0.03281398], xp=xp) + Yrg = as_ndarray(LMS_to_Yrg(LMS)) - LMS = np.tile(LMS, (6, 1)) - Yrg = np.tile(Yrg, (6, 1)) - np.testing.assert_allclose(LMS_to_Yrg(LMS), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) + LMS = xp.tile(xp_as_array(LMS, xp=xp), (6, 1)) + Yrg = xp.tile(xp_as_array(Yrg, xp=xp), (6, 1)) + xp_assert_close(LMS_to_Yrg(LMS), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) - LMS = np.reshape(LMS, (2, 3, 3)) - Yrg = np.reshape(Yrg, (2, 3, 3)) - np.testing.assert_allclose(LMS_to_Yrg(LMS), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) + LMS = xp_reshape(xp_as_array(LMS, xp=xp), (2, 3, 3), xp=xp) + Yrg = xp_reshape(xp_as_array(Yrg, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(LMS_to_Yrg(LMS), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_LMS_to_Yrg(self) -> None: + def test_domain_range_scale_LMS_to_Yrg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.LMS_to_Yrg` definition domain and range scale support. """ - LMS = np.array([0.15639195, 0.06741689, 0.03281398]) - Yrg = LMS_to_Yrg(LMS) + LMS = xp_as_array([0.15639195, 0.06741689, 0.03281398], xp=xp) + Yrg = as_ndarray(LMS_to_Yrg(LMS)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( LMS_to_Yrg(LMS * factor), Yrg * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -101,57 +113,57 @@ class TestYrg_to_LMS: Define :func:`colour.models.yrg.Yrg_to_LMS` definition unit tests methods. """ - def test_Yrg_to_LMS(self) -> None: + def test_Yrg_to_LMS(self, xp: ModuleType) -> None: """Test :func:`colour.models.yrg.Yrg_to_LMS` definition.""" - np.testing.assert_allclose( - Yrg_to_LMS(np.array([0.13137801, 0.49037644, 0.37777391])), - np.array([0.15639195, 0.06741689, 0.03281398]), - atol=1e-4, + xp_assert_close( + Yrg_to_LMS(xp_as_array([0.13137801, 0.49037644, 0.37777391], xp=xp)), + [0.15639195, 0.06741689, 0.03281398], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( - Yrg_to_LMS(np.array([0.23840767, 0.20110504, 0.69668437])), - np.array([0.23145723, 0.22601133, 0.05033211]), - atol=1e-4, + xp_assert_close( + Yrg_to_LMS(xp_as_array([0.23840767, 0.20110504, 0.69668437], xp=xp)), + [0.23145723, 0.22601133, 0.05033211], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( - Yrg_to_LMS(np.array([1.05911888, 0.22010094, 0.53660290])), - np.array([1.07423297, 0.91295620, 0.61375713]), - atol=1e-4, + xp_assert_close( + Yrg_to_LMS(xp_as_array([1.05911888, 0.22010094, 0.53660290], xp=xp)), + [1.07423297, 0.91295620, 0.61375713], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - def test_n_dimensional_Yrg_to_LMS(self) -> None: + def test_n_dimensional_Yrg_to_LMS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.Yrg_to_LMS` definition n-dimensional support. """ - Yrg = np.array([0.00535048, 0.00924302, 0.00526007]) - LMS = Yrg_to_LMS(Yrg) + Yrg = xp_as_array([0.00535048, 0.00924302, 0.00526007], xp=xp) + LMS = as_ndarray(Yrg_to_LMS(Yrg)) - Yrg = np.tile(Yrg, (6, 1)) - LMS = np.tile(LMS, (6, 1)) - np.testing.assert_allclose(Yrg_to_LMS(Yrg), LMS, atol=TOLERANCE_ABSOLUTE_TESTS) + Yrg = xp.tile(xp_as_array(Yrg, xp=xp), (6, 1)) + LMS = xp.tile(xp_as_array(LMS, xp=xp), (6, 1)) + xp_assert_close(Yrg_to_LMS(Yrg), LMS, atol=TOLERANCE_ABSOLUTE_TESTS) - Yrg = np.reshape(Yrg, (2, 3, 3)) - LMS = np.reshape(LMS, (2, 3, 3)) - np.testing.assert_allclose(Yrg_to_LMS(Yrg), LMS, atol=TOLERANCE_ABSOLUTE_TESTS) + Yrg = xp_reshape(xp_as_array(Yrg, xp=xp), (2, 3, 3), xp=xp) + LMS = xp_reshape(xp_as_array(LMS, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Yrg_to_LMS(Yrg), LMS, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Yrg_to_LMS(self) -> None: + def test_domain_range_scale_Yrg_to_LMS(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.Yrg_to_LMS` definition domain and range scale support. """ - Yrg = np.array([0.00535048, 0.00924302, 0.00526007]) - LMS = Yrg_to_LMS(Yrg) + Yrg = xp_as_array([0.00535048, 0.00924302, 0.00526007], xp=xp) + LMS = as_ndarray(Yrg_to_LMS(Yrg)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Yrg_to_LMS(Yrg * factor), LMS * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -172,57 +184,57 @@ class TestXYZ_to_Yrg: methods. """ - def test_XYZ_to_Yrg(self) -> None: + def test_XYZ_to_Yrg(self, xp: ModuleType) -> None: """Test :func:`colour.models.yrg.XYZ_to_Yrg` definition.""" - np.testing.assert_allclose( - XYZ_to_Yrg(np.array([0.20654008, 0.12197225, 0.05136952])), - np.array([0.13137801, 0.49037645, 0.37777388]), + xp_assert_close( + XYZ_to_Yrg(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.13137801, 0.49037645, 0.37777388], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Yrg(np.array([0.14222010, 0.23042768, 0.10495772])), - np.array([0.23840767, 0.20110503, 0.69668437]), + xp_assert_close( + XYZ_to_Yrg(xp_as_array([0.14222010, 0.23042768, 0.10495772], xp=xp)), + [0.23840767, 0.20110503, 0.69668437], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - XYZ_to_Yrg(np.array([0.96907232, 1.00000000, 1.12179215])), - np.array([1.05911888, 0.22010094, 0.53660290]), + xp_assert_close( + XYZ_to_Yrg(xp_as_array([0.96907232, 1.00000000, 1.12179215], xp=xp)), + [1.05911888, 0.22010094, 0.53660290], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_Yrg(self) -> None: + def test_n_dimensional_XYZ_to_Yrg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.XYZ_to_Yrg` definition n-dimensional support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Yrg = XYZ_to_Yrg(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Yrg = as_ndarray(XYZ_to_Yrg(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - Yrg = np.tile(Yrg, (6, 1)) - np.testing.assert_allclose(XYZ_to_Yrg(XYZ), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + Yrg = xp.tile(xp_as_array(Yrg, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_Yrg(XYZ), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) - XYZ = np.reshape(XYZ, (2, 3, 3)) - Yrg = np.reshape(Yrg, (2, 3, 3)) - np.testing.assert_allclose(XYZ_to_Yrg(XYZ), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + Yrg = xp_reshape(xp_as_array(Yrg, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_Yrg(XYZ), Yrg, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_XYZ_to_Yrg(self) -> None: + def test_domain_range_scale_XYZ_to_Yrg(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.XYZ_to_Yrg` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - Yrg = XYZ_to_Yrg(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + Yrg = as_ndarray(XYZ_to_Yrg(XYZ)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( XYZ_to_Yrg(XYZ * factor), Yrg * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -242,60 +254,60 @@ class TestYrg_to_XYZ: Define :func:`colour.models.yrg.Yrg_to_XYZ` definition unit tests methods. """ - def test_Yrg_to_XYZ(self) -> None: + def test_Yrg_to_XYZ(self, xp: ModuleType) -> None: """Test :func:`colour.models.yrg.Yrg_to_XYZ` definition.""" - np.testing.assert_allclose( - Yrg_to_XYZ(np.array([0.13137801, 0.49037645, 0.37777388])), - np.array([0.20654008, 0.12197225, 0.05136952]), - atol=1e-4, + xp_assert_close( + Yrg_to_XYZ(xp_as_array([0.13137801, 0.49037645, 0.37777388], xp=xp)), + [0.20654008, 0.12197225, 0.05136952], + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) - np.testing.assert_allclose( - Yrg_to_XYZ(np.array([0.23840767, 0.20110503, 0.69668437])), - np.array([0.14222010, 0.23042768, 0.10495772]), - atol=2e-4, + xp_assert_close( + Yrg_to_XYZ(xp_as_array([0.23840767, 0.20110503, 0.69668437], xp=xp)), + [0.14222010, 0.23042768, 0.10495772], + atol=TOLERANCE_ABSOLUTE_TESTS * 2000, ) - np.testing.assert_allclose( - Yrg_to_XYZ(np.array([1.05911888, 0.22010094, 0.53660290])), - np.array([0.96907232, 1.00000000, 1.12179215]), - atol=2e-4, + xp_assert_close( + Yrg_to_XYZ(xp_as_array([1.05911888, 0.22010094, 0.53660290], xp=xp)), + [0.96907232, 1.00000000, 1.12179215], + atol=TOLERANCE_ABSOLUTE_TESTS * 2000, ) - def test_n_dimensional_Yrg_to_XYZ(self) -> None: + def test_n_dimensional_Yrg_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.Yrg_to_XYZ` definition n-dimensional support. """ - Yrg = np.array([0.13137801, 0.49037645, 0.37777388]) - XYZ = Yrg_to_XYZ(Yrg) + Yrg = xp_as_array([0.13137801, 0.49037645, 0.37777388], xp=xp) + XYZ = as_ndarray(Yrg_to_XYZ(Yrg)) - Yrg = np.tile(Yrg, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose(Yrg_to_XYZ(Yrg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + Yrg = xp.tile(xp_as_array(Yrg, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close(Yrg_to_XYZ(Yrg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - Yrg = np.reshape(Yrg, (2, 3, 3)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose(Yrg_to_XYZ(Yrg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + Yrg = xp_reshape(xp_as_array(Yrg, xp=xp), (2, 3, 3), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(Yrg_to_XYZ(Yrg), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_Yrg_to_XYZ(self) -> None: + def test_domain_range_scale_Yrg_to_XYZ(self, xp: ModuleType) -> None: """ Test :func:`colour.models.yrg.Yrg_to_XYZ` definition domain and range scale support. """ - Yrg = np.array([0.13137801, 0.49037645, 0.37777388]) - XYZ = Yrg_to_XYZ(Yrg) + Yrg = xp_as_array([0.13137801, 0.49037645, 0.37777388], xp=xp) + XYZ = as_ndarray(Yrg_to_XYZ(Yrg)) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( Yrg_to_XYZ(Yrg * factor), XYZ * factor, - atol=1e-4, + atol=TOLERANCE_ABSOLUTE_TESTS * 1000, ) @ignore_numpy_errors diff --git a/colour/notation/hexadecimal.py b/colour/notation/hexadecimal.py index 86c5c42cdf..96005cec56 100644 --- a/colour/notation/hexadecimal.py +++ b/colour/notation/hexadecimal.py @@ -27,8 +27,10 @@ ) from colour.models import eotf_inverse_sRGB, eotf_sRGB from colour.utilities import ( + array_namespace, as_float_array, as_int_array, + as_ndarray, from_range_1, to_domain_1, usage_warning, @@ -78,15 +80,17 @@ def RGB_to_HEX(RGB: Domain1) -> NDArrayStr: RGB = to_domain_1(RGB) - if np.any(RGB < 0): + xp = array_namespace(RGB) + + if xp.any(RGB < 0): usage_warning( '"RGB" array contains negative values, those will be clipped, ' "unpredictable results may occur!" ) - RGB = as_float_array(np.clip(RGB, 0, np.inf)) + RGB = as_float_array(xp.clip(RGB, 0, float("inf"))) - if np.any(RGB > 1): + if xp.any(RGB > 1): usage_warning( '"RGB" array contains values over 1 and will be normalised, ' "unpredictable results may occur!" @@ -96,7 +100,7 @@ def RGB_to_HEX(RGB: Domain1) -> NDArrayStr: to_HEX = np.vectorize("{:02x}".format) - HEX = to_HEX(as_int_array(RGB * 255, dtype=np.uint8)).astype(object) + HEX = to_HEX(as_int_array(as_ndarray(RGB) * 255, dtype=np.uint8)).astype(object) return np.asarray("#") + HEX[..., 0] + HEX[..., 1] + HEX[..., 2] diff --git a/colour/notation/munsell/centore2014.py b/colour/notation/munsell/centore2014.py index 7032ec415d..33e0bef99c 100644 --- a/colour/notation/munsell/centore2014.py +++ b/colour/notation/munsell/centore2014.py @@ -133,10 +133,12 @@ from colour.utilities import ( CACHE_REGISTRY, Lookup, + array_namespace, as_float, as_float_array, as_float_scalar, as_int_scalar, + as_ndarray, attest, domain_range_scale, from_range_1, @@ -150,6 +152,13 @@ tsplit, tstack, usage_warning, + xp_as_array, + xp_as_float_array, + xp_degrees, + xp_radians, + xp_reshape, + xp_round, + xp_squeeze, ) from colour.volume import is_within_macadam_limits @@ -271,13 +280,16 @@ def _munsell_specifications() -> NDArrayFloat: if is_caching_enabled() and "All" in _CACHE_MUNSELL_SPECIFICATIONS: return _CACHE_MUNSELL_SPECIFICATIONS["All"] - munsell_specifications = np.array( + xp = array_namespace() + + munsell_specifications = xp_as_float_array( [ munsell_colour_to_munsell_specification( MUNSELL_COLOUR_FORMAT.format(*colour[0]) ) for colour in MUNSELL_COLOURS_ALL - ] + ], + xp=xp, ) _CACHE_MUNSELL_SPECIFICATIONS["All"] = munsell_specifications @@ -286,7 +298,7 @@ def _munsell_specifications() -> NDArrayFloat: def _munsell_maximum_chromas_from_renotation() -> Tuple[ - Tuple[Tuple[float, float], float], ... + Tuple[Tuple[float, float, float], float], ... ]: """ Return the maximum *Munsell* chromas from *Munsell Renotation System* @@ -339,7 +351,11 @@ def _munsell_scale_factor() -> NDArrayFloat: Domain-range scale factor for the *Munsell Renotation System*. """ - return np.array([10, 10, 50 if get_domain_range_scale() == "1" else 2, 10]) + xp = array_namespace() + + return xp_as_float_array( + [10, 10, 50 if get_domain_range_scale() == "1" else 2, 10], xp=xp + ) def _munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat: @@ -357,7 +373,7 @@ def _munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat: *CIE xyY* colourspace array. """ - specification = normalise_munsell_specification(specification) + specification = as_ndarray(normalise_munsell_specification(specification)) if is_grey_munsell_colour(specification): specification = to_domain_10(specification) @@ -379,13 +395,15 @@ def _munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat: f"domain [0, 10]!", ) + xp = array_namespace(specification) + with domain_range_scale("ignore"): Y = luminance_ASTMD1535(value) if is_integer(value): - value_minus = value_plus = round(value) + value_minus = value_plus = round(float(value)) else: - value_minus = np.floor(value) + value_minus = xp.floor(value) value_plus = value_minus + 1 specification_minus = as_float_array( @@ -410,9 +428,9 @@ def _munsell_specification_to_xyY(specification: ArrayLike) -> NDArrayFloat: Y_minus = luminance_ASTMD1535(value_minus) Y_plus = luminance_ASTMD1535(value_plus) - Y_minus_plus = np.squeeze([Y_minus, Y_plus]) - x_minus_plus = np.squeeze([x_minus, x_plus]) - y_minus_plus = np.squeeze([y_minus, y_plus]) + Y_minus_plus = xp_as_float_array([Y_minus, Y_plus], xp=xp, like=specification) + x_minus_plus = xp_as_float_array([x_minus, x_plus], xp=xp, like=specification) + y_minus_plus = xp_as_float_array([y_minus, y_plus], xp=xp, like=specification) x = as_float(LinearInterpolator(Y_minus_plus, x_minus_plus)(Y)) y = as_float(LinearInterpolator(Y_minus_plus, y_minus_plus)(Y)) @@ -476,9 +494,15 @@ def munsell_specification_to_xyY_Centore2014(specification: ArrayLike) -> NDArra specification = as_float_array(specification) shape = specification.shape - xyY = [_munsell_specification_to_xyY(a) for a in np.reshape(specification, (-1, 4))] + xp = array_namespace(specification) - return np.reshape(as_float_array(xyY), (*shape[:-1], 3)) + xyY = [ + _munsell_specification_to_xyY(a) + for a in xp_reshape(specification, (-1, 4), xp=xp) + ] + + result = xp_as_float_array(xyY, xp=xp, like=specification) + return xp_reshape(result, (*shape[:-1], 3), xp=xp) def munsell_colour_to_xyY_Centore2014(munsell_colour: ArrayLike) -> Range1: @@ -516,15 +540,23 @@ def munsell_colour_to_xyY_Centore2014(munsell_colour: ArrayLike) -> Range1: array([0.31006 , 0.31616 , 0.7461345...]) """ - munsell_colour = np.array(munsell_colour) + munsell_colour = np.asarray(munsell_colour) shape = munsell_colour.shape - specification = np.array( - [munsell_colour_to_munsell_specification(a) for a in np.ravel(munsell_colour)] + xp = array_namespace() + + specification = xp_as_float_array( + [ + munsell_colour_to_munsell_specification(a) + for a in np.reshape(munsell_colour, (-1,)) + ], + xp=xp, ) return munsell_specification_to_xyY_Centore2014( - from_range_10(np.reshape(specification, (*shape, 4)), _munsell_scale_factor()) + from_range_10( + xp_reshape(specification, (*shape, 4), xp=xp), _munsell_scale_factor() + ) ) @@ -553,7 +585,9 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: converging to a result. """ - xyY = as_float_array(xyY) + xyY = as_float_array(as_ndarray(xyY)) + + xp = array_namespace(xyY) x, y, Y = tsplit(xyY) Y = to_domain_1(Y) @@ -568,7 +602,7 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: value = munsell_value_ASTMD1535(Y * 100) if is_integer(value): - value = np.around(value) + value = xp_round(value, xp=xp) with domain_range_scale("ignore"): x_center, y_center, Y_center = tsplit(_munsell_specification_to_xyY(value)) @@ -576,7 +610,7 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: rho_input, phi_input, _z_input = tsplit( cartesian_to_cylindrical([x - x_center, y - y_center, Y_center]) ) - phi_input = np.degrees(phi_input) + phi_input = xp_degrees(phi_input) grey_threshold = THRESHOLD_INTEGER @@ -590,7 +624,7 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: X_r, Y_r, Z_r = xyY_to_XYZ([x_i, y_i, Y]) with sdiv_mode(): - XYZ_r = np.array([(1 / Y_r) * X_r, 1, (1 / Y_r) * Z_r]) + XYZ_r = xp_as_float_array([(1 / Y_r) * X_r, 1, (1 / Y_r) * Z_r], xp=xp) Lab = XYZ_to_Lab(XYZ, XYZ_to_xy(XYZ_r)) LCHab = Lab_to_LCHab(Lab) @@ -635,7 +669,7 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: [x_current - x_center, y_current - y_center, Y_center] ) ) - phi_current = np.degrees(phi_current) + phi_current = xp_degrees(phi_current) phi_current_difference = (360 - phi_input + phi_current) % 360 if phi_current_difference > 180: phi_current_difference -= 360 @@ -649,8 +683,8 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: extrapolate = False while ( - np.sign(np.min(phi_differences_data)) - == np.sign(np.max(phi_differences_data)) + xp.sign(xp.min(xp_as_float_array(phi_differences_data, xp=xp))) + == xp.sign(xp.max(xp_as_float_array(phi_differences_data, xp=xp))) and extrapolate is False ): iterations_inner += 1 @@ -678,7 +712,7 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: if hue_angle_difference_inner > 180: hue_angle_difference_inner -= 360 - hue_inner, code_inner = hue_angle_to_hue(hue_angle_inner) + hue_inner, code_inner = hue_angle_to_hue(hue_angle_inner) # pyright: ignore with domain_range_scale("ignore"): x_inner, y_inner, _Y_inner = _munsell_specification_to_xyY( @@ -699,19 +733,19 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: rho_inner, phi_inner, _z_inner = cartesian_to_cylindrical( [x_inner - x_center, y_inner - y_center, Y_center] ) - phi_inner = np.degrees(phi_inner) + phi_inner = xp_degrees(phi_inner) phi_inner_difference = (360 - phi_input + phi_inner) % 360 if phi_inner_difference > 180: phi_inner_difference -= 360 phi_differences_data.append(phi_inner_difference) - hue_angles.append(hue_angle_inner) - hue_angles_differences_data.append(hue_angle_difference_inner) + hue_angles.append(hue_angle_inner) # pyright: ignore + hue_angles_differences_data.append(hue_angle_difference_inner) # pyright: ignore - phi_differences = np.array(phi_differences_data) - hue_angles_differences = np.array(hue_angles_differences_data) + phi_differences = xp_as_float_array(phi_differences_data, xp=xp) + hue_angles_differences = xp_as_float_array(hue_angles_differences_data, xp=xp) - phi_differences_indexes = phi_differences.argsort() + phi_differences_indexes = xp.argsort(phi_differences) phi_differences = phi_differences[phi_differences_indexes] hue_angles_differences = hue_angles_differences[phi_differences_indexes] @@ -737,8 +771,8 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: difference = euclidean_distance([x, y], [x_current, y_current]) if difference < convergence_threshold: return from_range_10( - np.array(specification_current), - np.array([10, 10, chroma_scale, 10]), + xp_as_float_array(specification_current, xp=xp), + xp_as_float_array([10, 10, chroma_scale, 10], xp=xp), ) # TODO: Consider refactoring implementation. @@ -773,7 +807,11 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: iterations_maximum_inner = 16 iterations_inner = 0 - while not (np.min(rho_bounds_data) < rho_input < np.max(rho_bounds_data)): + while not ( + xp.min(xp_as_float_array(rho_bounds_data, xp=xp)) + < rho_input + < xp.max(xp_as_float_array(rho_bounds_data, xp=xp)) + ): iterations_inner += 1 if iterations_inner > iterations_maximum_inner: @@ -808,10 +846,10 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: rho_bounds_data.append(rho_inner) chroma_bounds_data.append(chroma_inner) - rho_bounds = np.array(rho_bounds_data) - chroma_bounds = np.array(chroma_bounds_data) + rho_bounds = xp_as_float_array(rho_bounds_data, xp=xp) + chroma_bounds = xp_as_float_array(chroma_bounds_data, xp=xp) - rhos_bounds_indexes = rho_bounds.argsort() + rhos_bounds_indexes = xp.argsort(rho_bounds) rho_bounds = rho_bounds[rhos_bounds_indexes] chroma_bounds = chroma_bounds[rhos_bounds_indexes] @@ -827,8 +865,8 @@ def _xyY_to_munsell_specification(xyY: ArrayLike) -> NDArrayFloat: difference = euclidean_distance([x, y], [x_current, y_current]) if difference < convergence_threshold: return from_range_10( - np.array(specification_current), - np.array([10, 10, chroma_scale, 10]), + xp_as_float_array(specification_current, xp=xp), + xp_as_float_array([10, 10, chroma_scale, 10], xp=xp), ) # NOTE: This exception is likely never reached in practice: 300K iterations @@ -902,9 +940,14 @@ def xyY_to_munsell_specification_Centore2014(xyY: ArrayLike) -> NDArrayFloat: xyY = as_float_array(xyY) shape = xyY.shape - specification = [_xyY_to_munsell_specification(a) for a in np.reshape(xyY, (-1, 3))] + xp = array_namespace(xyY) + + specification = [ + _xyY_to_munsell_specification(a) for a in xp_reshape(xyY, (-1, 3), xp=xp) + ] - return np.reshape(as_float_array(specification), (*shape[:-1], 4)) + result = xp_as_float_array(specification, xp=xp, like=xyY) + return xp_reshape(result, (*shape[:-1], 4), xp=xp) def xyY_to_munsell_colour_Centore2014( @@ -959,14 +1002,18 @@ def xyY_to_munsell_colour_Centore2014( shape = specification.shape decimals = (hue_decimals, value_decimals, chroma_decimals) - munsell_colour = np.reshape( - np.array( + xp = array_namespace(specification) + + munsell_colour = xp_reshape( + xp_as_array( [ munsell_specification_to_munsell_colour(a, *decimals) - for a in np.reshape(specification, (-1, 4)) - ] + for a in xp_reshape(specification, (-1, 4), xp=xp) + ], + xp=xp, ), shape[:-1], + xp=xp, ) return str(munsell_colour) if shape == (4,) else munsell_colour @@ -1056,9 +1103,12 @@ def is_grey_munsell_colour(specification: ArrayLike) -> bool: specification = as_float_array(specification) - specification = np.squeeze(specification[~np.isnan(specification)]) + xp = array_namespace(specification) + + filtered = specification[~xp.isnan(specification)] + specification = xp_squeeze(filtered, xp=xp) - return is_numeric(as_float(specification)) + return specification.ndim == 0 and is_numeric(float(specification)) def normalise_munsell_specification(specification: ArrayLike) -> NDArrayFloat: @@ -1085,8 +1135,12 @@ def normalise_munsell_specification(specification: ArrayLike) -> NDArrayFloat: specification = as_float_array(specification) + xp = array_namespace(specification) + if is_grey_munsell_colour(specification): - return specification * np.array([np.nan, 1, np.nan, np.nan]) + return specification * xp_as_float_array( + [float("nan"), 1, float("nan"), float("nan")], xp=xp + ) hue, value, chroma, code = specification @@ -1095,9 +1149,13 @@ def normalise_munsell_specification(specification: ArrayLike) -> NDArrayFloat: hue, code = 10, (code + 1) % 10 if chroma == 0: - return tstack([np.nan, value, np.nan, np.nan]) + return tstack( + [float("nan"), as_ndarray(value), float("nan"), float("nan")] # pyright: ignore + ) - return tstack([hue, value, chroma, code]) + return tstack( + [as_ndarray(hue), as_ndarray(value), as_ndarray(chroma), as_ndarray(code)] + ) def munsell_colour_to_munsell_specification( @@ -1169,20 +1227,20 @@ def munsell_specification_to_munsell_colour( if is_grey_munsell_colour(specification): return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals) - hue = round(hue, hue_decimals) + hue = round(float(hue), hue_decimals) attest( 0 <= hue <= 10, f'"{specification!r}" specification hue must be normalised to domain [0, 10]!', ) - value = round(value, value_decimals) + value = round(float(value), value_decimals) attest( 0 <= value <= 10, f'"{specification!r}" specification value must be normalised to ' f"domain [0, 10]!", ) - chroma = round(chroma, chroma_decimals) + chroma = round(float(chroma), chroma_decimals) attest( 0 <= chroma <= 50, f'"{specification!r}" specification chroma must be normalised to ' @@ -1190,7 +1248,7 @@ def munsell_specification_to_munsell_colour( ) code_values = MUNSELL_HUE_LETTER_CODES.values() - code = round(code, 1) + code = round(float(code), 1) attest( code in code_values, f'"{specification!r}" specification code must one of "{code_values}"!', @@ -1249,7 +1307,9 @@ def xyY_from_renotation( array([0.71..., 1.41..., 0.23...]) """ - specification = normalise_munsell_specification(specification) + specification = as_ndarray(normalise_munsell_specification(specification)).astype( + float + ) try: index = np.argwhere( @@ -1264,7 +1324,7 @@ def xyY_from_renotation( ) ) - return MUNSELL_COLOURS_ALL[as_int_scalar(index[0])][1] + return MUNSELL_COLOURS_ALL[int(index.flat[0])][1] except Exception as exception: error = ( @@ -1341,6 +1401,8 @@ def bounding_hues_from_renotation(hue_and_code: ArrayLike) -> NDArrayFloat: hue, code = as_float_array(hue_and_code) + xp = array_namespace(hue) + hue_cw: float code_cw: float hue_ccw: float @@ -1356,7 +1418,7 @@ def bounding_hues_from_renotation(hue_and_code: ArrayLike) -> NDArrayFloat: hue_ccw = hue_cw code_ccw = code_cw else: - hue_cw = 2.5 * np.floor(hue / 2.5) + hue_cw = 2.5 * xp.floor(hue / 2.5) hue_ccw = (hue_cw + 2.5) % 10 if hue_ccw == 0: hue_ccw = 10 @@ -1370,7 +1432,12 @@ def bounding_hues_from_renotation(hue_and_code: ArrayLike) -> NDArrayFloat: code_cw = code code_ccw = code - return as_float_array([(hue_cw, code_cw), (hue_ccw, code_ccw)]) + return as_float_array( + [ + (float(as_ndarray(hue_cw)), float(as_ndarray(code_cw))), + (float(as_ndarray(hue_ccw)), float(as_ndarray(code_ccw))), + ] + ) def hue_to_hue_angle(hue_and_code: ArrayLike) -> float: @@ -1559,7 +1626,7 @@ def interpolation_method_from_renotation_ovoid( f'"{specification}" specification value must be an int!', ) - value = round(value) + value = round(float(value)) attest( 2 <= chroma <= 50, @@ -1568,11 +1635,12 @@ def interpolation_method_from_renotation_ovoid( ) attest( - abs(2 * (chroma / 2 - round(chroma / 2))) <= THRESHOLD_INTEGER, + abs(float(2 * (chroma / 2 - round(float(chroma) / 2)))) + <= THRESHOLD_INTEGER, f'"{specification}" specification chroma must be an int and multiple of 2!', ) - chroma = 2 * round(chroma / 2) + chroma = 2 * round(float(chroma) / 2) interpolation_method = 0 @@ -1823,7 +1891,7 @@ def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat: array([0.31006..., 0.31616...]) """ - specification = normalise_munsell_specification(specification) + specification = as_ndarray(normalise_munsell_specification(specification)) if is_grey_munsell_colour(specification): return CCS_ILLUMINANT_MUNSELL @@ -1840,7 +1908,7 @@ def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat: f'"{specification}" specification value must be an int!', ) - value = round(value) + value = round(float(value)) attest( 2 <= chroma <= 50, @@ -1863,7 +1931,7 @@ def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat: or abs(hue - 7.5) < THRESHOLD_INTEGER or abs(hue - 10) < THRESHOLD_INTEGER ): - hue = 2.5 * round(hue / 2.5) + hue = 2.5 * round(float(hue) / 2.5) x, y, _Y = xyY_from_renotation([hue, value, chroma, code]) @@ -1880,14 +1948,14 @@ def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat: rho_minus, phi_minus, _z_minus = cartesian_to_cylindrical( [x_minus - x_grey, y_minus - y_grey, Y_minus] ) - phi_minus = np.degrees(phi_minus) + phi_minus = xp_degrees(phi_minus) specification_plus = (hue_plus, value, chroma, code_plus) x_plus, y_plus, Y_plus = xyY_from_renotation(specification_plus) rho_plus, phi_plus, _z_plus = cartesian_to_cylindrical( [x_plus - x_grey, y_plus - y_grey, Y_plus] ) - phi_plus = np.degrees(phi_plus) + phi_plus = xp_degrees(phi_plus) hue_angle_lower = hue_to_hue_angle([hue_minus, code_minus]) hue_angle = hue_to_hue_angle([hue, code]) @@ -1913,17 +1981,21 @@ def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat: f'Interpolation method must be one of: "{"Linear, Radial"}"', ) - hue_angle_lower_upper = np.squeeze([hue_angle_lower, hue_angle_upper]) + specification = as_float_array(specification) + + xp = array_namespace(specification) + + hue_angle_lower_upper = xp_as_float_array([hue_angle_lower, hue_angle_upper], xp=xp) if interpolation_method == "Linear": - x_minus_plus = np.squeeze([x_minus, x_plus]) - y_minus_plus = np.squeeze([y_minus, y_plus]) + x_minus_plus = xp_as_float_array([x_minus, x_plus], xp=xp) + y_minus_plus = xp_as_float_array([y_minus, y_plus], xp=xp) x = LinearInterpolator(hue_angle_lower_upper, x_minus_plus)(hue_angle) y = LinearInterpolator(hue_angle_lower_upper, y_minus_plus)(hue_angle) elif interpolation_method == "Radial": - rho_minus_plus = np.squeeze([rho_minus, rho_plus]) - phi_minus_plus = np.squeeze([phi_minus, phi_plus]) + rho_minus_plus = xp_as_float_array([rho_minus, rho_plus], xp=xp) + phi_minus_plus = xp_as_float_array([phi_minus, phi_plus], xp=xp) rho = as_float_array( LinearInterpolator(hue_angle_lower_upper, rho_minus_plus)(hue_angle) @@ -1932,7 +2004,7 @@ def xy_from_renotation_ovoid(specification: ArrayLike) -> NDArrayFloat: LinearInterpolator(hue_angle_lower_upper, phi_minus_plus)(hue_angle) ) - rho_phi = np.squeeze([rho, np.radians(phi)]) + rho_phi = xp_as_float_array([rho, xp_radians(phi)], xp=xp) x, y = tsplit(polar_to_cartesian(rho_phi) + tstack([x_grey, y_grey])) return tstack([x, y]) @@ -1996,7 +2068,12 @@ def LCHab_to_munsell_specification(LCHab: ArrayLike) -> NDArrayFloat: value = L / 10 chroma = C / 5 - return tstack(cast("ArrayLike", [hue, value, chroma, code])) + return tstack( + cast( + "ArrayLike", + [as_ndarray(hue), as_ndarray(value), as_ndarray(chroma), as_ndarray(code)], + ) + ) def maximum_chroma_from_renotation(hue_and_value_and_code: ArrayLike) -> float: @@ -2027,6 +2104,8 @@ def maximum_chroma_from_renotation(hue_and_value_and_code: ArrayLike) -> float: hue, value, code = as_float_array(hue_and_value_and_code) + xp = array_namespace(hue) + # Ideal white, no chroma. if value >= 9.99: return 0 @@ -2040,7 +2119,7 @@ def maximum_chroma_from_renotation(hue_and_value_and_code: ArrayLike) -> float: value_minus = value value_plus = value else: - value_minus = np.floor(value) + value_minus = xp.floor(value) value_plus = value_minus + 1 hue_code_cw, hue_code_ccw = bounding_hues_from_renotation([hue, code]) @@ -2051,38 +2130,37 @@ def maximum_chroma_from_renotation(hue_and_value_and_code: ArrayLike) -> float: specification_for_indexes = [chroma[0] for chroma in maximum_chromas] ma_limit_mcw = maximum_chromas[ - specification_for_indexes.index( - (hue_cw, value_minus, code_cw) # pyright: ignore - ) + specification_for_indexes.index((hue_cw, value_minus, code_cw)) ][1] ma_limit_mccw = maximum_chromas[ - specification_for_indexes.index( - (hue_ccw, value_minus, code_ccw) # pyright: ignore - ) + specification_for_indexes.index((hue_ccw, value_minus, code_ccw)) ][1] if value_plus <= 9: ma_limit_pcw = maximum_chromas[ - specification_for_indexes.index( - (hue_cw, value_plus, code_cw) # pyright: ignore - ) + specification_for_indexes.index((hue_cw, value_plus, code_cw)) ][1] ma_limit_pccw = maximum_chromas[ - specification_for_indexes.index( - (hue_ccw, value_plus, code_ccw) # pyright: ignore - ) + specification_for_indexes.index((hue_ccw, value_plus, code_ccw)) ][1] - max_chroma = np.min([ma_limit_mcw, ma_limit_mccw, ma_limit_pcw, ma_limit_pccw]) + max_chroma = xp.min( + xp_as_float_array( + [ma_limit_mcw, ma_limit_mccw, ma_limit_pcw, ma_limit_pccw], xp=xp + ) + ) else: L = as_float_scalar(luminance_ASTMD1535(value)) L9 = as_float_scalar(luminance_ASTMD1535(9)) L10 = as_float_scalar(luminance_ASTMD1535(10)) - max_chroma = np.min( - [ - LinearInterpolator([L9, L10], [ma_limit_mcw, 0])(L), - LinearInterpolator([L9, L10], [ma_limit_mccw, 0])(L), - ] + max_chroma = xp.min( + xp_as_float_array( + [ + LinearInterpolator([L9, L10], [ma_limit_mcw, 0])(L), + LinearInterpolator([L9, L10], [ma_limit_mccw, 0])(L), + ], + xp=xp, + ) ) return as_float_scalar(max_chroma) @@ -2118,11 +2196,15 @@ def munsell_specification_to_xy(specification: ArrayLike) -> NDArrayFloat: array([0.31006..., 0.31616...]) """ - specification = normalise_munsell_specification(specification) + specification = as_ndarray(normalise_munsell_specification(specification)) if is_grey_munsell_colour(specification): return CCS_ILLUMINANT_MUNSELL + specification = as_float_array(specification) + + xp = array_namespace(specification) + hue, value, chroma, code = specification attest( @@ -2135,12 +2217,12 @@ def munsell_specification_to_xy(specification: ArrayLike) -> NDArrayFloat: f'"{specification}" specification value must be an int!', ) - value = round(value) + value = round(float(value)) if chroma % 2 == 0: chroma_minus = chroma_plus = chroma else: - chroma_minus = 2 * np.floor(chroma / 2) + chroma_minus = 2 * xp.floor(chroma / 2) chroma_plus = chroma_minus + 2 if chroma_minus == 0: @@ -2156,9 +2238,9 @@ def munsell_specification_to_xy(specification: ArrayLike) -> NDArrayFloat: x = x_minus y = y_minus else: - chroma_minus_plus = np.squeeze([chroma_minus, chroma_plus]) - x_minus_plus = np.squeeze([x_minus, x_plus]) - y_minus_plus = np.squeeze([y_minus, y_plus]) + chroma_minus_plus = xp_as_float_array([chroma_minus, chroma_plus], xp=xp) + x_minus_plus = xp_as_float_array([x_minus, x_plus], xp=xp) + y_minus_plus = xp_as_float_array([y_minus, y_plus], xp=xp) x = LinearInterpolator(chroma_minus_plus, x_minus_plus)(chroma) y = LinearInterpolator(chroma_minus_plus, y_minus_plus)(chroma) diff --git a/colour/notation/munsell/onnx.py b/colour/notation/munsell/onnx.py index 3b460df84e..f692f5d4b7 100644 --- a/colour/notation/munsell/onnx.py +++ b/colour/notation/munsell/onnx.py @@ -39,6 +39,7 @@ required, to_domain_1, to_domain_10, + xp_round, ) __author__ = "Colour Developers" @@ -528,7 +529,7 @@ def xyY_to_munsell_colour_Onnx( decimals = (hue_decimals, value_decimals, chroma_decimals) specification_flat = np.reshape(specification, (-1, 4)).copy() - specification_flat[..., 3] = np.round(specification_flat[..., 3]) + specification_flat[..., 3] = xp_round(specification_flat[..., 3], xp=np) munsell_colour = np.reshape( np.array( diff --git a/colour/notation/munsell/tests/test__init__.py b/colour/notation/munsell/tests/test__init__.py index 17c12424ff..c939664f9f 100644 --- a/colour/notation/munsell/tests/test__init__.py +++ b/colour/notation/munsell/tests/test__init__.py @@ -18,7 +18,11 @@ xyY_to_munsell_specification, xyY_to_munsell_specification_Centore2014, ) -from colour.utilities import is_onnxruntime_installed, is_scipy_installed +from colour.utilities import ( + is_onnxruntime_installed, + is_scipy_installed, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -49,7 +53,7 @@ def test_munsell_specification_to_xyY(self) -> None: specification = np.array([7.18927191, 5.34025196, 16.05861170, 3.00000000]) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY(specification), munsell_specification_to_xyY_Centore2014(specification), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -63,7 +67,7 @@ def test_munsell_specification_to_xyY_Centore2014(self) -> None: specification = np.array([7.18927191, 5.34025196, 16.05861170, 3.00000000]) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY(specification, method="Centore 2014"), munsell_specification_to_xyY_Centore2014(specification), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -85,10 +89,10 @@ def test_munsell_specification_to_xyY_Onnx(self) -> None: specification = np.array([7.18927191, 5.34025196, 16.05861170, 3.00000000]) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY(specification, method="ONNX"), munsell_specification_to_xyY_Onnx(specification), - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) def test_munsell_specification_to_xyY_raise_exception(self) -> None: @@ -97,12 +101,10 @@ def test_munsell_specification_to_xyY_raise_exception(self) -> None: definition raised exception. """ - pytest.raises( - ValueError, - munsell_specification_to_xyY, - np.array([7.18927191, 5.34025196, 16.05861170, 3.00000000]), - method="Invalid", - ) + with pytest.raises(ValueError): + munsell_specification_to_xyY( + np.array([7.18927191, 5.34025196, 16.0586117, 3.0]), method="Invalid" + ) class TestMunsellColour_to_xyY: @@ -117,7 +119,7 @@ def test_munsell_colour_to_xyY(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY("4.2YR 8.1/5.3"), munsell_colour_to_xyY_Centore2014("4.2YR 8.1/5.3"), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -129,7 +131,7 @@ def test_munsell_colour_to_xyY_Centore2014(self) -> None: definition with the *Centore 2014* method. """ - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY("4.2YR 8.1/5.3", method="Centore 2014"), munsell_colour_to_xyY_Centore2014("4.2YR 8.1/5.3"), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -149,10 +151,10 @@ def test_munsell_colour_to_xyY_Onnx(self) -> None: munsell_colour_to_xyY_Onnx, ) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY("4.2YR 8.1/5.3", method="ONNX"), munsell_colour_to_xyY_Onnx("4.2YR 8.1/5.3"), - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) def test_munsell_colour_to_xyY_raise_exception(self) -> None: @@ -161,12 +163,8 @@ def test_munsell_colour_to_xyY_raise_exception(self) -> None: definition raised exception. """ - pytest.raises( - ValueError, - munsell_colour_to_xyY, - "4.2YR 8.1/5.3", - method="Invalid", - ) + with pytest.raises(ValueError): + munsell_colour_to_xyY("4.2YR 8.1/5.3", method="Invalid") class TestxyY_to_munsell_specification: @@ -186,7 +184,7 @@ def test_xyY_to_munsell_specification(self) -> None: xyY = np.array([0.16623068, 0.45684550, 0.22399519]) - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification(xyY), xyY_to_munsell_specification_Centore2014(xyY), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -203,7 +201,7 @@ def test_xyY_to_munsell_specification_Centore2014(self) -> None: xyY = np.array([0.16623068, 0.45684550, 0.22399519]) - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification(xyY, method="Centore 2014"), xyY_to_munsell_specification_Centore2014(xyY), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -225,10 +223,10 @@ def test_xyY_to_munsell_specification_Onnx(self) -> None: xyY = np.array([0.16623068, 0.45684550, 0.22399519]) - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification(xyY, method="ONNX"), xyY_to_munsell_specification_Onnx(xyY), - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) def test_xyY_to_munsell_specification_raise_exception(self) -> None: @@ -237,12 +235,10 @@ def test_xyY_to_munsell_specification_raise_exception(self) -> None: definition raised exception. """ - pytest.raises( - ValueError, - xyY_to_munsell_specification, - np.array([0.16623068, 0.45684550, 0.22399519]), - method="Invalid", - ) + with pytest.raises(ValueError): + xyY_to_munsell_specification( + np.array([0.16623068, 0.4568455, 0.22399519]), method="Invalid" + ) class TestxyY_to_munsell_colour: @@ -305,9 +301,7 @@ def test_xyY_to_munsell_colour_raise_exception(self) -> None: definition raised exception. """ - pytest.raises( - ValueError, - xyY_to_munsell_colour, - np.array([0.38736945, 0.35751656, 0.59362000]), - method="Invalid", - ) + with pytest.raises(ValueError): + xyY_to_munsell_colour( + np.array([0.38736945, 0.35751656, 0.59362]), method="Invalid" + ) diff --git a/colour/notation/munsell/tests/test_centore2014.py b/colour/notation/munsell/tests/test_centore2014.py index da6ff41084..e47276bc6b 100644 --- a/colour/notation/munsell/tests/test_centore2014.py +++ b/colour/notation/munsell/tests/test_centore2014.py @@ -15,7 +15,7 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat + from colour.hints import NDArrayFloat, ModuleType from colour.notation.munsell import ( CCS_ILLUMINANT_MUNSELL, @@ -45,10 +45,15 @@ from colour.utilities import ( as_array, as_float_array, + as_ndarray, domain_range_scale, ignore_numpy_errors, is_scipy_installed, tstack, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, ) __author__ = "Colour Developers" @@ -1102,7 +1107,7 @@ class TestMunsellSpecification_to_xyY_Centore2014: definition unit tests methods. """ - def test_munsell_specification_to_xyY_Centore2014(self) -> None: + def test_munsell_specification_to_xyY_Centore2014(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_specification_to_xyY_Centore2014` @@ -1113,8 +1118,8 @@ def test_munsell_specification_to_xyY_Centore2014(self) -> None: as_float_array(list(MUNSELL_SPECIFICATIONS[..., 0])), as_float_array(list(MUNSELL_SPECIFICATIONS[..., 1])), ) - np.testing.assert_allclose( - munsell_specification_to_xyY_Centore2014(specification), + xp_assert_close( + munsell_specification_to_xyY_Centore2014(xp_as_array(specification, xp=xp)), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1127,14 +1132,15 @@ def test_munsell_specification_to_xyY_Centore2014(self) -> None: nan_array = np.full(specification.shape, np.nan) specification = tstack([nan_array, specification, nan_array, nan_array]) - np.testing.assert_allclose( - munsell_specification_to_xyY_Centore2014(specification), + xp_assert_close( + munsell_specification_to_xyY_Centore2014(xp_as_array(specification, xp=xp)), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_n_dimensional_munsell_specification_to_xyY_Centore2014( self, + xp: ModuleType, ) -> None: """ Test @@ -1142,46 +1148,48 @@ def test_n_dimensional_munsell_specification_to_xyY_Centore2014( definition n-dimensional arrays support. """ - specification = np.array([7.18927191, 5.34025196, 16.05861170, 3.00000000]) - xyY = munsell_specification_to_xyY_Centore2014(specification) + specification = xp_as_array( + [7.18927191, 5.34025196, 16.05861170, 3.00000000], xp=xp + ) + xyY = as_ndarray(munsell_specification_to_xyY_Centore2014(specification)) - specification = np.tile(specification, (6, 1)) - xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + specification = xp.tile(xp_as_array(specification, xp=xp), (6, 1)) + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + xp_assert_close( munsell_specification_to_xyY_Centore2014(specification), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, ) - specification = np.reshape(specification, (2, 3, 4)) - xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 4), xp=xp) + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( munsell_specification_to_xyY_Centore2014(specification), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, ) - specification = np.array([np.nan, 8.9, np.nan, np.nan]) - xyY = munsell_specification_to_xyY_Centore2014(specification) + specification = xp_as_array([np.nan, 8.9, np.nan, np.nan], xp=xp) + xyY = as_ndarray(munsell_specification_to_xyY_Centore2014(specification)) - specification = np.tile(specification, (6, 1)) - xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + specification = xp.tile(xp_as_array(specification, xp=xp), (6, 1)) + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + xp_assert_close( munsell_specification_to_xyY_Centore2014(specification), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, ) - specification = np.reshape(specification, (2, 3, 4)) - xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 4), xp=xp) + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( munsell_specification_to_xyY_Centore2014(specification), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_domain_range_scale_munsell_specification_to_xyY_Centore2014( - self, + self, xp: ModuleType ) -> None: """ Test @@ -1189,8 +1197,10 @@ def test_domain_range_scale_munsell_specification_to_xyY_Centore2014( definition domain and range scale support. """ - specification = np.array([7.18927191, 5.34025196, 16.05861170, 3.00000000]) - xyY = munsell_specification_to_xyY_Centore2014(specification) + specification = xp_as_array( + [7.18927191, 5.34025196, 16.05861170, 3.00000000], xp=xp + ) + xyY = as_ndarray(munsell_specification_to_xyY_Centore2014(specification)) d_r = ( ("reference", 1, 1), @@ -1199,8 +1209,10 @@ def test_domain_range_scale_munsell_specification_to_xyY_Centore2014( ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - munsell_specification_to_xyY_Centore2014(specification * factor_a), + xp_assert_close( + munsell_specification_to_xyY_Centore2014( + specification * xp_as_array(factor_a, xp=xp) + ), xyY * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1229,6 +1241,7 @@ class TestMunsellColour_to_xyY_Centore2014: def test_domain_range_scale_munsell_colour_to_xyY_Centore2014( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -1246,7 +1259,7 @@ def test_domain_range_scale_munsell_colour_to_xyY_Centore2014( ) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Centore2014(munsell_colour), xyY * factor, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1254,6 +1267,7 @@ def test_domain_range_scale_munsell_colour_to_xyY_Centore2014( def test_n_dimensional_munsell_colour_to_xyY_Centore2014( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -1266,7 +1280,7 @@ def test_n_dimensional_munsell_colour_to_xyY_Centore2014( munsell_colour = np.tile(munsell_colour, 6) xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Centore2014(munsell_colour), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1274,7 +1288,7 @@ def test_n_dimensional_munsell_colour_to_xyY_Centore2014( munsell_colour = np.reshape(munsell_colour, (2, 3)) xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Centore2014(munsell_colour), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1285,7 +1299,7 @@ def test_n_dimensional_munsell_colour_to_xyY_Centore2014( munsell_colour = np.tile(munsell_colour, 6) xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Centore2014(munsell_colour), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1293,7 +1307,7 @@ def test_n_dimensional_munsell_colour_to_xyY_Centore2014( munsell_colour = np.reshape(munsell_colour, (2, 3)) xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Centore2014(munsell_colour), xyY, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -1307,7 +1321,8 @@ class TestxyY_to_munsell_specification_Centore2014: definition unit tests methods. """ - def test_xyY_to_munsell_specification_Centore2014(self) -> None: + @pytest.mark.mps_xfail("MPS float32 iterative non-convergence") + def test_xyY_to_munsell_specification_Centore2014(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.xyY_to_munsell_specification_Centore2014` @@ -1322,10 +1337,10 @@ def test_xyY_to_munsell_specification_Centore2014(self) -> None: as_float_array(list(MUNSELL_SPECIFICATIONS[..., 1])), ) - np.testing.assert_allclose( - xyY_to_munsell_specification_Centore2014(xyY), + xp_assert_close( + xyY_to_munsell_specification_Centore2014(xp_as_array(xyY, xp=xp)), specification, - atol=5e-5, + atol=TOLERANCE_ABSOLUTE_TESTS * 500, ) specification, xyY = ( @@ -1336,14 +1351,15 @@ def test_xyY_to_munsell_specification_Centore2014(self) -> None: nan_array = np.full(specification.shape, np.nan) specification = tstack([nan_array, specification, nan_array, nan_array]) - np.testing.assert_allclose( - xyY_to_munsell_specification_Centore2014(xyY), + xp_assert_close( + xyY_to_munsell_specification_Centore2014(xp_as_array(xyY, xp=xp)), specification, - atol=0.00001, + atol=TOLERANCE_ABSOLUTE_TESTS * 100, ) def test_n_dimensional_xyY_to_munsell_specification_Centore2014( self, + xp: ModuleType, ) -> None: """ Test @@ -1354,27 +1370,27 @@ def test_n_dimensional_xyY_to_munsell_specification_Centore2014( if not is_scipy_installed(): # pragma: no cover return - xyY = [0.16623068, 0.45684550, 0.22399519] - specification = xyY_to_munsell_specification_Centore2014(xyY) + xyY = xp_as_array([0.16623068, 0.45684550, 0.22399519], xp=xp) + specification = as_ndarray(xyY_to_munsell_specification_Centore2014(xyY)) - xyY = np.tile(xyY, (6, 1)) - specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( + xyY = xp.tile(xp_as_array(xyY, xp=xp), (6, 1)) + specification = xp.tile(xp_as_array(specification, xp=xp), (6, 1)) + xp_assert_close( xyY_to_munsell_specification_Centore2014(xyY), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) - xyY = np.reshape(xyY, (2, 3, 3)) - specification = np.reshape(specification, (2, 3, 4)) - np.testing.assert_allclose( + xyY = xp_reshape(xp_as_array(xyY, xp=xp), (2, 3, 3), xp=xp) + specification = xp_reshape(xp_as_array(specification, xp=xp), (2, 3, 4), xp=xp) + xp_assert_close( xyY_to_munsell_specification_Centore2014(xyY), specification, atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_raise_exception_xyY_to_munsell_specification_Centore2014( - self, + self, xp: ModuleType ) -> None: """ Test @@ -1385,14 +1401,13 @@ def test_raise_exception_xyY_to_munsell_specification_Centore2014( if not is_scipy_installed(): # pragma: no cover return - pytest.raises( - RuntimeError, - xyY_to_munsell_specification_Centore2014, - np.array([0.90615118, 0.57945103, 0.91984064]), - ) + with pytest.raises(RuntimeError): + xyY_to_munsell_specification_Centore2014( + xp_as_array([0.90615118, 0.57945103, 0.91984064], xp=xp) + ) def test_domain_range_scale_xyY_to_munsell_specification_Centore2014( - self, + self, xp: ModuleType ) -> None: """ Test @@ -1403,8 +1418,8 @@ def test_domain_range_scale_xyY_to_munsell_specification_Centore2014( if not is_scipy_installed(): # pragma: no cover return - xyY = [0.16623068, 0.45684550, 0.22399519] - specification = xyY_to_munsell_specification_Centore2014(xyY) + xyY = xp_as_array([0.16623068, 0.45684550, 0.22399519], xp=xp) + specification = as_ndarray(xyY_to_munsell_specification_Centore2014(xyY)) d_r = ( ("reference", 1, 1), @@ -1413,10 +1428,12 @@ def test_domain_range_scale_xyY_to_munsell_specification_Centore2014( ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - xyY_to_munsell_specification_Centore2014(xyY * factor_a), + xp_assert_close( + xyY_to_munsell_specification_Centore2014( + xyY * xp_as_array(factor_a, xp=xp) + ), specification * factor_b, - atol=2e-5, + atol=TOLERANCE_ABSOLUTE_TESTS * 200, ) @ignore_numpy_errors @@ -1446,6 +1463,7 @@ class TestxyY_to_munsell_colour_Centore2014: def test_domain_range_scale_xyY_to_munsell_colour_Centore2014( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -1470,6 +1488,7 @@ def test_domain_range_scale_xyY_to_munsell_colour_Centore2014( def test_n_dimensional_xyY_to_munsell_colour_Centore2014( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -1485,22 +1504,22 @@ def test_n_dimensional_xyY_to_munsell_colour_Centore2014( xyY = np.tile(xyY, (6, 1)) munsell_colour = np.tile(munsell_colour, 6) - np.testing.assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) xyY = np.reshape(xyY, (2, 3, 3)) munsell_colour = np.reshape(munsell_colour, (2, 3)) - np.testing.assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) xyY = [*list(CCS_ILLUMINANT_MUNSELL), 1.0] munsell_colour = xyY_to_munsell_colour_Centore2014(xyY) xyY = np.tile(xyY, (6, 1)) munsell_colour = np.tile(munsell_colour, 6) - np.testing.assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) xyY = np.reshape(xyY, (2, 3, 3)) munsell_colour = np.reshape(munsell_colour, (2, 3)) - np.testing.assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Centore2014(xyY), munsell_colour) class TestParseMunsellColour: @@ -1515,21 +1534,21 @@ def test_parse_munsell_colour(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( parse_munsell_colour("N5.2"), - np.array([np.nan, 5.2, np.nan, np.nan]), + [np.nan, 5.2, np.nan, np.nan], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( parse_munsell_colour("0YR 2.0/4.0"), - np.array([0.0, 2.0, 4.0, 6]), + [0.0, 2.0, 4.0, 6], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( parse_munsell_colour("4.2YR 8.1/5.3"), - np.array([4.2, 8.1, 5.3, 6]), + [4.2, 8.1, 5.3, 6], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1539,7 +1558,8 @@ def test_raise_exception_parse_munsell_colour(self) -> None: definition raised exception. """ - pytest.raises(ValueError, parse_munsell_colour, "4.2YQ 8.1/5.3") + with pytest.raises(ValueError): + parse_munsell_colour("4.2YQ 8.1/5.3") class TestIsGreyMunsellColour: @@ -1548,7 +1568,7 @@ class TestIsGreyMunsellColour: unit tests methods. """ - def test_is_grey_munsell_colour(self) -> None: + def test_is_grey_munsell_colour(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.is_grey_munsell_colour` definition. @@ -1556,11 +1576,11 @@ def test_is_grey_munsell_colour(self) -> None: assert is_grey_munsell_colour(5.2) - assert not is_grey_munsell_colour(np.array([0.0, 2.0, 4.0, 6])) + assert not is_grey_munsell_colour(xp_as_array([0.0, 2.0, 4.0, 6], xp=xp)) - assert not is_grey_munsell_colour(np.array([4.2, 8.1, 5.3, 6])) + assert not is_grey_munsell_colour(xp_as_array([4.2, 8.1, 5.3, 6], xp=xp)) - assert is_grey_munsell_colour(np.array([np.nan, 0.5, np.nan, np.nan])) + assert is_grey_munsell_colour(xp_as_array([np.nan, 0.5, np.nan, np.nan], xp=xp)) class TestNormaliseMunsellSpecification: @@ -1575,27 +1595,27 @@ def test_normalise_munsell_specification(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( normalise_munsell_specification((0.0, 2.0, 4.0, 6)), - np.array([10.0, 2.0, 4.0, 7]), + [10.0, 2.0, 4.0, 7], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_munsell_specification((0.0, 2.0, 4.0, 8)), - np.array([10.0, 2.0, 4.0, 9]), + [10.0, 2.0, 4.0, 9], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_munsell_specification((0, 2.0, 4.0, 10)), - np.array([10.0, 2.0, 4.0, 1]), + [10.0, 2.0, 4.0, 1], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( normalise_munsell_specification(0.5), - np.array([np.nan, 0.5, np.nan, np.nan]), + [np.nan, 0.5, np.nan, np.nan], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1612,33 +1632,33 @@ def test_munsell_colour_to_munsell_specification(self) -> None: munsell_colour_to_munsell_specification` definition. """ - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_munsell_specification("0.0YR 2.0/4.0"), - np.array([10.0, 2.0, 4.0, 7]), + [10.0, 2.0, 4.0, 7], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_munsell_specification("0.0RP 2.0/4.0"), - np.array([10.0, 2.0, 4.0, 9]), + [10.0, 2.0, 4.0, 9], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_munsell_specification("10.0B 2.0/4.0"), - np.array([10.0, 2.0, 4.0, 1]), + [10.0, 2.0, 4.0, 1], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_munsell_specification("N5.2"), - np.array([np.nan, 5.2, np.nan, np.nan]), + [np.nan, 5.2, np.nan, np.nan], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_munsell_specification("0.0YR 2.0/0.0"), - np.array([np.nan, 2.0, np.nan, np.nan]), + [np.nan, 2.0, np.nan, np.nan], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1649,41 +1669,51 @@ class TestMunsellSpecificationToMunsellColour: munsell_specification_to_munsell_colour` definition unit tests methods. """ - def test_munsell_specification_to_munsell_colour(self) -> None: + def test_munsell_specification_to_munsell_colour(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.\ munsell_specification_to_munsell_colour` definition. """ assert ( - munsell_specification_to_munsell_colour(np.array([10.0, 2.0, 4.0, 7])) + munsell_specification_to_munsell_colour( + xp_as_array([10.0, 2.0, 4.0, 7], xp=xp) + ) == "10.0R 2.0/4.0" ) assert ( - munsell_specification_to_munsell_colour(np.array([10.0, 2.0, 4.0, 9])) + munsell_specification_to_munsell_colour( + xp_as_array([10.0, 2.0, 4.0, 9], xp=xp) + ) == "10.0P 2.0/4.0" ) assert ( - munsell_specification_to_munsell_colour(np.array([10.0, 2.0, 4.0, 1])) + munsell_specification_to_munsell_colour( + xp_as_array([10.0, 2.0, 4.0, 1], xp=xp) + ) == "10.0B 2.0/4.0" ) assert ( munsell_specification_to_munsell_colour( - np.array([np.nan, 5.2, np.nan, np.nan]) + xp_as_array([np.nan, 5.2, np.nan, np.nan], xp=xp) ) == "N5.2" ) assert ( - munsell_specification_to_munsell_colour(np.array([0.0, 2.0, 4.0, 7])) + munsell_specification_to_munsell_colour( + xp_as_array([0.0, 2.0, 4.0, 7], xp=xp) + ) == "10.0RP 2.0/4.0" ) assert ( - munsell_specification_to_munsell_colour(np.array([10.0, 0.0, 4.0, 7])) + munsell_specification_to_munsell_colour( + xp_as_array([10.0, 0.0, 4.0, 7], xp=xp) + ) == "N0.0" ) @@ -1694,25 +1724,25 @@ class Test_xyY_fromRenotation: unit tests methods. """ - def test_xyY_from_renotation(self) -> None: + def test_xyY_from_renotation(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.xyY_from_renotation` definition. """ - np.testing.assert_array_equal( - xyY_from_renotation([2.5, 0.2, 2.0, 4]), - np.array([0.713, 1.414, 0.237]), + xp_assert_equal( + xyY_from_renotation(xp_as_array([2.5, 0.2, 2.0, 4], xp=xp)), + [0.713, 1.414, 0.237], ) - np.testing.assert_array_equal( - xyY_from_renotation([5.0, 0.2, 2.0, 4]), - np.array([0.449, 1.145, 0.237]), + xp_assert_equal( + xyY_from_renotation(xp_as_array([5.0, 0.2, 2.0, 4], xp=xp)), + [0.449, 1.145, 0.237], ) - np.testing.assert_array_equal( - xyY_from_renotation([7.5, 0.2, 2.0, 4]), - np.array([0.262, 0.837, 0.237]), + xp_assert_equal( + xyY_from_renotation(xp_as_array([7.5, 0.2, 2.0, 4], xp=xp)), + [0.262, 0.837, 0.237], ) @@ -1722,17 +1752,19 @@ class TestIsSpecificationInRenotation: definition unit tests methods. """ - def test_is_specification_in_renotation(self) -> None: + def test_is_specification_in_renotation(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.is_specification_in_renotation` definition. """ - assert is_specification_in_renotation(np.array([2.5, 0.2, 2.0, 4])) + assert is_specification_in_renotation(xp_as_array([2.5, 0.2, 2.0, 4], xp=xp)) - assert is_specification_in_renotation(np.array([5.0, 0.2, 2.0, 4])) + assert is_specification_in_renotation(xp_as_array([5.0, 0.2, 2.0, 4], xp=xp)) - assert not is_specification_in_renotation(np.array([25.0, 0.2, 2.0, 4])) + assert not is_specification_in_renotation( + xp_as_array([25.0, 0.2, 2.0, 4], xp=xp) + ) class TestBoundingHuesFromRenotation: @@ -1741,7 +1773,7 @@ class TestBoundingHuesFromRenotation: definition unit tests methods. """ - def test_bounding_hues_from_renotation(self) -> None: + def test_bounding_hues_from_renotation(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.bounding_hues_from_renotation` definition. @@ -1749,15 +1781,15 @@ def test_bounding_hues_from_renotation(self) -> None: for i, (specification, _xyY) in enumerate(MUNSELL_SPECIFICATIONS): hue, _value, _chroma, code = specification - np.testing.assert_array_equal( - bounding_hues_from_renotation([hue, code]), + xp_assert_equal( + bounding_hues_from_renotation(xp_as_array([hue, code], xp=xp)), MUNSELL_BOUNDING_HUES[i], ) # Test hue == 0 case - np.testing.assert_array_equal( - bounding_hues_from_renotation([0.0, 1]), - np.array([[10.0, 2.0], [10.0, 2.0]]), + xp_assert_equal( + bounding_hues_from_renotation(xp_as_array([0.0, 1.0], xp=xp)), + [[10.0, 2.0], [10.0, 2.0]], ) @@ -1784,7 +1816,7 @@ def test_hue_angle_to_hue(self) -> None: """Test :func:`colour.notation.munsell.hue_angle_to_hue` definition.""" for hue, code, angle in MUNSELL_HUE_TO_ANGLE: - np.testing.assert_array_equal(hue_angle_to_hue(angle), (hue, code)) + xp_assert_equal(hue_angle_to_hue(angle), (hue, code)) class TestHueTo_ASTM_hue: @@ -1806,7 +1838,7 @@ class TestInterpolationMethodFromRenotationOvoid: interpolation_method_from_renotation_ovoid` definition unit tests methods. """ - def test_interpolation_method_from_renotation_ovoid(self) -> None: + def test_interpolation_method_from_renotation_ovoid(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.\ interpolation_method_from_renotation_ovoid` definition. @@ -1820,13 +1852,15 @@ def test_interpolation_method_from_renotation_ovoid(self) -> None: assert ( interpolation_method_from_renotation_ovoid( - np.array([np.nan, 5.2, np.nan, np.nan]) + xp_as_array([np.nan, 5.2, np.nan, np.nan], xp=xp) ) is None ) assert ( - interpolation_method_from_renotation_ovoid(np.array([2.5, 10.0, 2.0, 4])) + interpolation_method_from_renotation_ovoid( + xp_as_array([2.5, 10.0, 2.0, 4], xp=xp) + ) is None ) @@ -1837,7 +1871,7 @@ class Test_xy_fromRenotationOvoid: unit tests methods. """ - def test_xy_from_renotation_ovoid(self) -> None: + def test_xy_from_renotation_ovoid(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.xy_from_renotation_ovoid` definition. @@ -1845,17 +1879,17 @@ def test_xy_from_renotation_ovoid(self) -> None: for i, (specification, _xyY) in enumerate(MUNSELL_EVEN_SPECIFICATIONS): if is_specification_in_renotation(specification): - np.testing.assert_allclose( - xy_from_renotation_ovoid(specification), + xp_assert_close( + xy_from_renotation_ovoid(xp_as_array(specification, xp=xp)), MUNSELL_XY_FROM_RENOTATION_OVOID[i], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test grey Munsell colour case (coverage for line 2347) - np.testing.assert_allclose( - xy_from_renotation_ovoid([np.nan, 8, np.nan, np.nan]), - np.array([0.31006, 0.31616]), - atol=0.00001, + xp_assert_close( + xy_from_renotation_ovoid(xp_as_array([np.nan, 8, np.nan, np.nan], xp=xp)), + [0.31006, 0.31616], + atol=TOLERANCE_ABSOLUTE_TESTS * 100, ) @@ -1865,49 +1899,49 @@ class TestLCHabToMunsellSpecification: definition unit tests methods. """ - def test_LCHab_to_munsell_specification(self) -> None: + def test_LCHab_to_munsell_specification(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.LCHab_to_munsell_specification` definition. """ - np.testing.assert_allclose( + xp_assert_close( LCHab_to_munsell_specification( - np.array([100.00000000, 21.57210357, 272.22819350]) + xp_as_array([100.00000000, 21.57210357, 272.22819350], xp=xp) ), - np.array([5.618942638888882, 10.0, 4.314420714000000, 10]), + [5.618942638888882, 10.0, 4.314420714000000, 10], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LCHab_to_munsell_specification( - np.array([100.00000000, 426.67945353, 72.39590835]) + xp_as_array([100.00000000, 426.67945353, 72.39590835], xp=xp) ), - np.array([0.109974541666666, 10.0, 85.335890706000001, 5]), + [0.109974541666666, 10.0, 85.335890706000001, 5], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LCHab_to_munsell_specification( - np.array([100.00000000, 74.05216981, 276.45318193]) + xp_as_array([100.00000000, 74.05216981, 276.45318193], xp=xp) ), - np.array([6.792550536111119, 10.0, 14.810433961999999, 10]), + [6.792550536111119, 10.0, 14.810433961999999, 10], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LCHab_to_munsell_specification( - np.array([100.00000000, 21.57210357, 0.00000000]) + xp_as_array([100.00000000, 21.57210357, 0.00000000], xp=xp) ), - np.array([10.000000000000000, 10.0, 4.314420714000000, 8]), + [10.000000000000000, 10.0, 4.314420714000000, 8], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LCHab_to_munsell_specification( - np.array([100.00000000, 21.57210357, 36.00000000]) + xp_as_array([100.00000000, 21.57210357, 36.00000000], xp=xp) ), - np.array([10.000000000000000, 10.0, 4.314420714000000, 7]), + [10.000000000000000, 10.0, 4.314420714000000, 7], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -1918,17 +1952,25 @@ class TestMaximumChromaFromRenotation: definition unit tests methods. """ - def test_maximum_chroma_from_renotation(self) -> None: + def test_maximum_chroma_from_renotation(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.maximum_chroma_from_renotation` definition. """ - assert maximum_chroma_from_renotation([2.5, 5, 5]) == 14.0 + assert ( + maximum_chroma_from_renotation(xp_as_array([2.5, 5.0, 5.0], xp=xp)) == 14.0 + ) - assert maximum_chroma_from_renotation([8.675, 1.225, 10]) == 48.0 + assert ( + maximum_chroma_from_renotation(xp_as_array([8.675, 1.225, 10.0], xp=xp)) + == 48.0 + ) - assert maximum_chroma_from_renotation([6.875, 3.425, 1]) == 16.0 + assert ( + maximum_chroma_from_renotation(xp_as_array([6.875, 3.425, 1.0], xp=xp)) + == 16.0 + ) class TestMunsellSpecification_to_xy: @@ -1937,22 +1979,22 @@ class TestMunsellSpecification_to_xy: definition unit tests methods. """ - def test_munsell_specification_to_xy(self) -> None: + def test_munsell_specification_to_xy(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_specification_to_xy` definition. """ for specification, xyY in MUNSELL_EVEN_SPECIFICATIONS: - np.testing.assert_allclose( - munsell_specification_to_xy(specification), + xp_assert_close( + munsell_specification_to_xy(xp_as_array(specification, xp=xp)), xyY[0:2], atol=TOLERANCE_ABSOLUTE_TESTS, ) for specification, xyY in MUNSELL_GREYS_SPECIFICATIONS: - np.testing.assert_allclose( - munsell_specification_to_xy(specification[0]), + xp_assert_close( + munsell_specification_to_xy(xp_as_array(specification[0], xp=xp)), xyY[0:2], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/notation/munsell/tests/test_onnx.py b/colour/notation/munsell/tests/test_onnx.py index f9f8f7d12f..0dd6830b0a 100644 --- a/colour/notation/munsell/tests/test_onnx.py +++ b/colour/notation/munsell/tests/test_onnx.py @@ -5,8 +5,12 @@ from __future__ import annotations import contextlib +import typing from itertools import product +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np import pytest @@ -25,6 +29,8 @@ domain_range_scale, ignore_numpy_errors, is_onnxruntime_installed, + xp_assert_close, + xp_assert_equal, ) __author__ = "Colour Developers" @@ -63,14 +69,15 @@ def test_munsell_specification_to_xyY_Onnx(self) -> None: as_float_array(list(MUNSELL_SPECIFICATIONS[..., 0])), as_float_array(list(MUNSELL_SPECIFICATIONS[..., 1])), ) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY_Onnx(specification), xyY, - atol=5e-2, + atol=TOLERANCE_ABSOLUTE_TESTS * 500000, ) def test_n_dimensional_munsell_specification_to_xyY_Onnx( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -83,18 +90,18 @@ def test_n_dimensional_munsell_specification_to_xyY_Onnx( specification = np.tile(specification, (6, 1)) xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY_Onnx(specification), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) specification = np.reshape(specification, (2, 3, 4)) xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY_Onnx(specification), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) specification = np.array([np.nan, 8.9, np.nan, np.nan]) @@ -102,22 +109,23 @@ def test_n_dimensional_munsell_specification_to_xyY_Onnx( specification = np.tile(specification, (6, 1)) xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY_Onnx(specification), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) specification = np.reshape(specification, (2, 3, 4)) xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY_Onnx(specification), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) def test_domain_range_scale_munsell_specification_to_xyY_Onnx( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -135,7 +143,7 @@ def test_domain_range_scale_munsell_specification_to_xyY_Onnx( ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_specification_to_xyY_Onnx(specification * factor_a), xyY * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -167,6 +175,7 @@ class TestMunsellColour_to_xyY_Onnx: def test_domain_range_scale_munsell_colour_to_xyY_Onnx( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -184,13 +193,13 @@ def test_domain_range_scale_munsell_colour_to_xyY_Onnx( ) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Onnx(munsell_colour), xyY * factor, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_colour_to_xyY_Onnx(self) -> None: + def test_n_dimensional_munsell_colour_to_xyY_Onnx(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_colour_to_xyY_Onnx` definition n-dimensional arrays support. @@ -201,18 +210,18 @@ def test_n_dimensional_munsell_colour_to_xyY_Onnx(self) -> None: munsell_colour = np.tile(munsell_colour, 6) xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Onnx(munsell_colour), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) munsell_colour = np.reshape(munsell_colour, (2, 3)) xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Onnx(munsell_colour), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) munsell_colour = "N8.9" @@ -220,18 +229,18 @@ def test_n_dimensional_munsell_colour_to_xyY_Onnx(self) -> None: munsell_colour = np.tile(munsell_colour, 6) xyY = np.tile(xyY, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Onnx(munsell_colour), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) munsell_colour = np.reshape(munsell_colour, (2, 3)) xyY = np.reshape(xyY, (2, 3, 3)) - np.testing.assert_allclose( + xp_assert_close( munsell_colour_to_xyY_Onnx(munsell_colour), xyY, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) @@ -257,14 +266,15 @@ def test_xyY_to_munsell_specification_Onnx(self) -> None: as_float_array(list(MUNSELL_SPECIFICATIONS[..., 1])), ) - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification_Onnx(xyY), specification, - atol=1, + atol=TOLERANCE_ABSOLUTE_TESTS * 10000000, ) def test_n_dimensional_xyY_to_munsell_specification_Onnx( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -277,22 +287,23 @@ def test_n_dimensional_xyY_to_munsell_specification_Onnx( xyY = np.tile(xyY, (6, 1)) specification = np.tile(specification, (6, 1)) - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification_Onnx(xyY), specification, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) xyY = np.reshape(xyY, (2, 3, 3)) specification = np.reshape(specification, (2, 3, 4)) - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification_Onnx(xyY), specification, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) def test_domain_range_scale_xyY_to_munsell_specification_Onnx( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -310,7 +321,7 @@ def test_domain_range_scale_xyY_to_munsell_specification_Onnx( ) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( xyY_to_munsell_specification_Onnx(xyY * factor_a), specification * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -342,6 +353,7 @@ class TestxyY_to_munsell_colour_Onnx: def test_domain_range_scale_xyY_to_munsell_colour_Onnx( self, + xp: ModuleType, # noqa: ARG002 ) -> None: """ Test @@ -361,7 +373,7 @@ def test_domain_range_scale_xyY_to_munsell_colour_Onnx( with domain_range_scale(scale): assert xyY_to_munsell_colour_Onnx(xyY * factor) == munsell_colour - def test_n_dimensional_xyY_to_munsell_colour_Onnx(self) -> None: + def test_n_dimensional_xyY_to_munsell_colour_Onnx(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.xyY_to_munsell_colour_Onnx` definition n-dimensional arrays support. @@ -372,19 +384,19 @@ def test_n_dimensional_xyY_to_munsell_colour_Onnx(self) -> None: xyY = np.tile(xyY, (6, 1)) munsell_colour = np.tile(munsell_colour, 6) - np.testing.assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) xyY = np.reshape(xyY, (2, 3, 3)) munsell_colour = np.reshape(munsell_colour, (2, 3)) - np.testing.assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) xyY = [0.38736945, 0.35751656, 0.59362000] munsell_colour = xyY_to_munsell_colour_Onnx(xyY) xyY = np.tile(xyY, (6, 1)) munsell_colour = np.tile(munsell_colour, 6) - np.testing.assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) xyY = np.reshape(xyY, (2, 3, 3)) munsell_colour = np.reshape(munsell_colour, (2, 3)) - np.testing.assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) + xp_assert_equal(xyY_to_munsell_colour_Onnx(xyY), munsell_colour) diff --git a/colour/notation/munsell/tests/test_value.py b/colour/notation/munsell/tests/test_value.py index a2e34c1348..d498c32270 100644 --- a/colour/notation/munsell/tests/test_value.py +++ b/colour/notation/munsell/tests/test_value.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -17,6 +22,9 @@ from colour.utilities import ( domain_range_scale, ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, ) __author__ = "Colour Developers" @@ -43,31 +51,31 @@ class TestMunsellValuePriest1920: unit tests methods. """ - def test_munsell_value_Priest1920(self) -> None: + def test_munsell_value_Priest1920(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Priest1920` definition. """ - np.testing.assert_allclose( - munsell_value_Priest1920(12.23634268), + xp_assert_close( + munsell_value_Priest1920(xp_as_array(12.23634268, xp=xp)), 3.498048410185314, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Priest1920(22.89399987), + xp_assert_close( + munsell_value_Priest1920(xp_as_array(22.89399987, xp=xp)), 4.7847674833788947, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Priest1920(6.29022535), + xp_assert_close( + munsell_value_Priest1920(xp_as_array(6.29022535, xp=xp)), 2.5080321668591092, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_Priest1920(self) -> None: + def test_n_dimensional_munsell_value_Priest1920(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Priest1920` definition n-dimensional arrays support. @@ -76,25 +84,31 @@ def test_n_dimensional_munsell_value_Priest1920(self) -> None: Y = 12.23634268 V = munsell_value_Priest1920(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_Priest1920(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_Priest1920(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_Priest1920(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_Priest1920(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_Priest1920(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_Priest1920(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_Priest1920(self) -> None: + def test_domain_range_scale_munsell_value_Priest1920(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_Priest1920` definition domain and range scale support. @@ -106,7 +120,7 @@ def test_domain_range_scale_munsell_value_Priest1920(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_Priest1920(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -128,31 +142,31 @@ class TestMunsellValueMunsell1933: definition unit tests methods. """ - def test_munsell_value_Munsell1933(self) -> None: + def test_munsell_value_Munsell1933(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Munsell1933` definition. """ - np.testing.assert_allclose( - munsell_value_Munsell1933(12.23634268), + xp_assert_close( + munsell_value_Munsell1933(xp_as_array(12.23634268, xp=xp)), 4.1627702416858083, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Munsell1933(22.89399987), + xp_assert_close( + munsell_value_Munsell1933(xp_as_array(22.89399987, xp=xp)), 5.5914543020790592, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Munsell1933(6.29022535), + xp_assert_close( + munsell_value_Munsell1933(xp_as_array(6.29022535, xp=xp)), 3.0141971134091761, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_Munsell1933(self) -> None: + def test_n_dimensional_munsell_value_Munsell1933(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Munsell1933` definition n-dimensional arrays support. @@ -161,25 +175,31 @@ def test_n_dimensional_munsell_value_Munsell1933(self) -> None: Y = 12.23634268 V = munsell_value_Munsell1933(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_Munsell1933(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_Munsell1933(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_Munsell1933(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_Munsell1933(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_Munsell1933(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_Munsell1933(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_Munsell1933(self) -> None: + def test_domain_range_scale_munsell_value_Munsell1933(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_Munsell1933` definition domain and range scale support. @@ -191,7 +211,7 @@ def test_domain_range_scale_munsell_value_Munsell1933(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_Munsell1933(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -213,31 +233,31 @@ class TestMunsellValueMoon1943: unit tests methods. """ - def test_munsell_value_Moon1943(self) -> None: + def test_munsell_value_Moon1943(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Moon1943` definition. """ - np.testing.assert_allclose( - munsell_value_Moon1943(12.23634268), + xp_assert_close( + munsell_value_Moon1943(xp_as_array(12.23634268, xp=xp)), 4.0688120634976421, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Moon1943(22.89399987), + xp_assert_close( + munsell_value_Moon1943(xp_as_array(22.89399987, xp=xp)), 5.3133627855494412, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Moon1943(6.29022535), + xp_assert_close( + munsell_value_Moon1943(xp_as_array(6.29022535, xp=xp)), 3.0645015037679695, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_Moon1943(self) -> None: + def test_n_dimensional_munsell_value_Moon1943(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Moon1943` definition n-dimensional arrays support. @@ -246,25 +266,31 @@ def test_n_dimensional_munsell_value_Moon1943(self) -> None: Y = 12.23634268 V = munsell_value_Moon1943(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_Moon1943(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_Moon1943(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_Moon1943(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_Moon1943(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_Moon1943(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_Moon1943(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_Moon1943(self) -> None: + def test_domain_range_scale_munsell_value_Moon1943(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_Moon1943` definition domain and range scale support. @@ -276,7 +302,7 @@ def test_domain_range_scale_munsell_value_Moon1943(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_Moon1943(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -298,31 +324,31 @@ class TestMunsellValueSaunderson1944: definition unit tests methods. """ - def test_munsell_value_Saunderson1944(self) -> None: + def test_munsell_value_Saunderson1944(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Saunderson1944` definition. """ - np.testing.assert_allclose( - munsell_value_Saunderson1944(12.23634268), + xp_assert_close( + munsell_value_Saunderson1944(xp_as_array(12.23634268, xp=xp)), 4.0444736723175119, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Saunderson1944(22.89399987), + xp_assert_close( + munsell_value_Saunderson1944(xp_as_array(22.89399987, xp=xp)), 5.3783324022305923, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Saunderson1944(6.29022535), + xp_assert_close( + munsell_value_Saunderson1944(xp_as_array(6.29022535, xp=xp)), 2.9089633927316823, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_Saunderson1944(self) -> None: + def test_n_dimensional_munsell_value_Saunderson1944(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Saunderson1944` definition n-dimensional arrays support. @@ -331,25 +357,34 @@ def test_n_dimensional_munsell_value_Saunderson1944(self) -> None: Y = 12.23634268 V = munsell_value_Saunderson1944(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_Saunderson1944(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_Saunderson1944(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_Saunderson1944(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_Saunderson1944(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_Saunderson1944(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_Saunderson1944(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_Saunderson1944(self) -> None: + def test_domain_range_scale_munsell_value_Saunderson1944( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Saunderson1944` definition domain and range scale support. @@ -361,7 +396,7 @@ def test_domain_range_scale_munsell_value_Saunderson1944(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_Saunderson1944(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -385,31 +420,31 @@ class TestMunsellValueLadd1955: unit tests methods. """ - def test_munsell_value_Ladd1955(self) -> None: + def test_munsell_value_Ladd1955(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Ladd1955` definition. """ - np.testing.assert_allclose( - munsell_value_Ladd1955(12.23634268), + xp_assert_close( + munsell_value_Ladd1955(xp_as_array(12.23634268, xp=xp)), 4.0511633044287088, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Ladd1955(22.89399987), + xp_assert_close( + munsell_value_Ladd1955(xp_as_array(22.89399987, xp=xp)), 5.3718647913936772, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_Ladd1955(6.29022535), + xp_assert_close( + munsell_value_Ladd1955(xp_as_array(6.29022535, xp=xp)), 2.9198269939751613, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_Ladd1955(self) -> None: + def test_n_dimensional_munsell_value_Ladd1955(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_Ladd1955` definition n-dimensional arrays support. @@ -418,25 +453,31 @@ def test_n_dimensional_munsell_value_Ladd1955(self) -> None: Y = 12.23634268 V = munsell_value_Ladd1955(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_Ladd1955(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_Ladd1955(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_Ladd1955(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_Ladd1955(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_Ladd1955(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_Ladd1955(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_Ladd1955(self) -> None: + def test_domain_range_scale_munsell_value_Ladd1955(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_Ladd1955` definition domain and range scale support. @@ -448,7 +489,7 @@ def test_domain_range_scale_munsell_value_Ladd1955(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_Ladd1955(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -470,31 +511,31 @@ class TestMunsellValueMcCamy1992: unit tests methods. """ - def test_munsell_value_McCamy1987(self) -> None: + def test_munsell_value_McCamy1987(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_McCamy1987` definition. """ - np.testing.assert_allclose( - munsell_value_McCamy1987(12.23634268), + xp_assert_close( + munsell_value_McCamy1987(xp_as_array(12.23634268, xp=xp)), 4.081434853194113, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_McCamy1987(22.89399987), + xp_assert_close( + munsell_value_McCamy1987(xp_as_array(22.89399987, xp=xp)), 5.394083970919982, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - munsell_value_McCamy1987(6.29022535), + xp_assert_close( + munsell_value_McCamy1987(xp_as_array(6.29022535, xp=xp)), 2.9750160800320096, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_McCamy1987(self) -> None: + def test_n_dimensional_munsell_value_McCamy1987(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_McCamy1987` definition n-dimensional arrays support. @@ -503,25 +544,31 @@ def test_n_dimensional_munsell_value_McCamy1987(self) -> None: Y = 12.23634268 V = munsell_value_McCamy1987(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_McCamy1987(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_McCamy1987(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_McCamy1987(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_McCamy1987(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_McCamy1987(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_McCamy1987(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_McCamy1987(self) -> None: + def test_domain_range_scale_munsell_value_McCamy1987(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_McCamy1987` definition domain and range scale support. @@ -533,7 +580,7 @@ def test_domain_range_scale_munsell_value_McCamy1987(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_McCamy1987(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -555,31 +602,31 @@ class TestMunsellValueASTMD1535: definition unit tests methods. """ - def test_munsell_value_ASTMD1535(self) -> None: + def test_munsell_value_ASTMD1535(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_ASTMD1535` definition. """ - np.testing.assert_allclose( + xp_assert_close( munsell_value_ASTMD1535(12.23634268), 4.0824437076525664, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( munsell_value_ASTMD1535(22.89399987), 5.3913268228155395, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( munsell_value_ASTMD1535(6.29022535), 2.9761930839606454, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_munsell_value_ASTMD1535(self) -> None: + def test_n_dimensional_munsell_value_ASTMD1535(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.munsell.munsell_value_ASTMD1535` definition n-dimensional arrays support. @@ -588,25 +635,31 @@ def test_n_dimensional_munsell_value_ASTMD1535(self) -> None: Y = 12.23634268 V = munsell_value_ASTMD1535(Y) - V = np.tile(V, 6) - Y = np.tile(Y, 6) - np.testing.assert_allclose( - munsell_value_ASTMD1535(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp.tile(xp_as_array(V, xp=xp), (6,)) + Y = xp.tile(xp_as_array(Y, xp=xp), (6,)) + xp_assert_close( + munsell_value_ASTMD1535(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3)) - Y = np.reshape(Y, (2, 3)) - np.testing.assert_allclose( - munsell_value_ASTMD1535(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3), xp=xp) + xp_assert_close( + munsell_value_ASTMD1535(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - V = np.reshape(V, (2, 3, 1)) - Y = np.reshape(Y, (2, 3, 1)) - np.testing.assert_allclose( - munsell_value_ASTMD1535(Y), V, atol=TOLERANCE_ABSOLUTE_TESTS + V = xp_reshape(xp_as_array(V, xp=xp), (2, 3, 1), xp=xp) + Y = xp_reshape(xp_as_array(Y, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + munsell_value_ASTMD1535(Y), + V, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_munsell_value_ASTMD1535(self) -> None: + def test_domain_range_scale_munsell_value_ASTMD1535(self, xp: ModuleType) -> None: # noqa: ARG002 """ Test :func:`colour.notation.munsell.munsell_value_ASTMD1535` definition domain and range scale support. @@ -618,7 +671,7 @@ def test_domain_range_scale_munsell_value_ASTMD1535(self) -> None: d_r = (("reference", 1, 1), ("1", 0.01, 0.1), ("100", 1, 10)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( munsell_value_ASTMD1535(Y * factor_a), V * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/notation/munsell/value.py b/colour/notation/munsell/value.py index 142216d972..455493fb7d 100644 --- a/colour/notation/munsell/value.py +++ b/colour/notation/munsell/value.py @@ -46,8 +46,6 @@ import typing -import numpy as np - from colour.algebra import ( Extrapolator, LinearInterpolator, @@ -67,6 +65,7 @@ from colour.utilities import ( CACHE_REGISTRY, CanonicalMapping, + array_namespace, as_float, from_range_10, to_domain_100, @@ -117,7 +116,9 @@ def _munsell_value_ASTMD1535_interpolator() -> Extrapolator: "ASTM D1535-08 Interpolator" ] - munsell_values = np.arange(0, 10, 0.001) + xp = array_namespace() + + munsell_values = xp.arange(0, 10, 0.001) interpolator = LinearInterpolator( luminance_ASTMD1535(munsell_values), munsell_values ) @@ -173,7 +174,9 @@ def munsell_value_Priest1920( Y = to_domain_100(Y) - V = 10 * np.sqrt(Y / 100) + xp = array_namespace(Y) + + V = 10 * xp.sqrt(Y / 100) return as_float(from_range_10(V)) @@ -221,7 +224,9 @@ def munsell_value_Munsell1933( Y = to_domain_100(Y) - V = np.sqrt(1.4742 * Y - 0.004743 * (Y * Y)) + xp = array_namespace(Y) + + V = xp.sqrt(1.4742 * Y - 0.004743 * (Y * Y)) return as_float(from_range_10(V)) @@ -409,17 +414,19 @@ def munsell_value_McCamy1987( Y = to_domain_100(Y) + xp = array_namespace(Y) + with sdiv_mode(): - V = np.where( + V = xp.where( Y <= 0.9, 0.87445 * spow(Y, 0.9967), 2.49268 * spow(Y, 1 / 3) - 1.5614 - (0.985 / (((0.1073 * Y - 3.084) ** 2) + 7.54)) + sdiv(0.0133, spow(Y, 2.3)) - + 0.0084 * np.sin(4.1 * spow(Y, 1 / 3) + 1) - + sdiv(0.0221, Y) * np.sin(0.39 * (Y - 2)) - - (sdiv(0.0037, 0.44 * Y)) * np.sin(1.28 * (Y - 0.53)), + + 0.0084 * xp.sin(4.1 * spow(Y, 1 / 3) + 1) + + sdiv(0.0221, Y) * xp.sin(0.39 * (Y - 2)) + - (sdiv(0.0037, 0.44 * Y)) * xp.sin(1.28 * (Y - 0.53)), ) return as_float(from_range_10(V)) diff --git a/colour/notation/tests/test_css_color_3.py b/colour/notation/tests/test_css_color_3.py index d3f5bc037f..6b8bb59268 100644 --- a/colour/notation/tests/test_css_color_3.py +++ b/colour/notation/tests/test_css_color_3.py @@ -2,10 +2,9 @@ from __future__ import annotations -import numpy as np - from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.notation import keyword_to_RGB_CSSColor3 +from colour.utilities import xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -31,16 +30,12 @@ def test_keyword_to_RGB_CSSColor3(self) -> None: definition. """ - np.testing.assert_array_equal( - keyword_to_RGB_CSSColor3("black"), np.array([0, 0, 0]) - ) + xp_assert_equal(keyword_to_RGB_CSSColor3("black"), [0, 0, 0]) - np.testing.assert_array_equal( - keyword_to_RGB_CSSColor3("white"), np.array([1, 1, 1]) - ) + xp_assert_equal(keyword_to_RGB_CSSColor3("white"), [1, 1, 1]) - np.testing.assert_allclose( + xp_assert_close( keyword_to_RGB_CSSColor3("aliceblue"), - np.array([0.94117647, 0.97254902, 1.00000000]), + [0.94117647, 0.97254902, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/notation/tests/test_hexadecimal.py b/colour/notation/tests/test_hexadecimal.py index a61ea7be85..5e397eede4 100644 --- a/colour/notation/tests/test_hexadecimal.py +++ b/colour/notation/tests/test_hexadecimal.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing from itertools import product +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.notation.hexadecimal import HEX_to_RGB, RGB_to_HEX -from colour.utilities import domain_range_scale, ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + domain_range_scale, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,46 +41,59 @@ class TestRGB_to_HEX: tests methods. """ - def test_RGB_to_HEX(self) -> None: + def test_RGB_to_HEX(self, xp: ModuleType) -> None: """Test :func:`colour.notation.hexadecimal.RGB_to_HEX` definition.""" - assert RGB_to_HEX(np.array([0.45620519, 0.03081071, 0.04091952])) == "#74070a" + assert ( + RGB_to_HEX(xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp)) + == "#74070a" + ) - assert RGB_to_HEX(np.array([0.00000000, 0.00000000, 0.00000000])) == "#000000" + assert ( + RGB_to_HEX(xp_as_array([0.00000000, 0.00000000, 0.00000000], xp=xp)) + == "#000000" + ) - assert RGB_to_HEX(np.array([1.00000000, 1.00000000, 1.00000000])) == "#ffffff" + assert ( + RGB_to_HEX(xp_as_array([1.00000000, 1.00000000, 1.00000000], xp=xp)) + == "#ffffff" + ) - np.testing.assert_equal( + xp_assert_equal( RGB_to_HEX( - np.array( + xp_as_array( [ [10.00000000, 1.00000000, 1.00000000], [1.00000000, 1.00000000, 1.00000000], [0.00000000, 1.00000000, 0.00000000], - ] + ], + xp=xp, ) ), ["#fe0e0e", "#0e0e0e", "#000e00"], ) - def test_n_dimensional_RGB_to_HEX(self) -> None: + def test_n_dimensional_RGB_to_HEX(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.hexadecimal.RGB_to_HEX` definition n-dimensional arrays support. """ - RGB = np.array([0.45620519, 0.03081071, 0.04091952]) + RGB = xp_as_array([0.45620519, 0.03081071, 0.04091952], xp=xp) HEX = RGB_to_HEX(RGB) - RGB = np.tile(RGB, (6, 1)) + RGB = xp_as_array(np.tile(as_ndarray(RGB), (6, 1)), xp=xp) HEX = np.tile(HEX, 6) assert RGB_to_HEX(RGB).tolist() == HEX.tolist() - RGB = np.reshape(RGB, (2, 3, 3)) + RGB = xp_reshape(RGB, (2, 3, 3), xp=xp) HEX = np.reshape(HEX, (2, 3)) assert RGB_to_HEX(RGB).tolist() == HEX.tolist() - def test_domain_range_scale_RGB_to_HEX(self) -> None: + def test_domain_range_scale_RGB_to_HEX( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.notation.hexadecimal.RGB_to_HEX` definition domain and range scale support. @@ -103,42 +128,45 @@ class TestHEX_to_RGB: def test_HEX_to_RGB(self) -> None: """Test :func:`colour.notation.hexadecimal.HEX_to_RGB` definition.""" - np.testing.assert_allclose( + xp_assert_close( HEX_to_RGB("#74070a"), - np.array([0.45620519, 0.03081071, 0.04091952]), - atol=1e-1, + [0.45620519, 0.03081071, 0.04091952], + atol=TOLERANCE_ABSOLUTE_TESTS * 1e06, ) - np.testing.assert_allclose( + xp_assert_close( HEX_to_RGB("#000000"), - np.array([0.00000000, 0.00000000, 0.00000000]), + [0.00000000, 0.00000000, 0.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( HEX_to_RGB("#ffffff"), - np.array([1.00000000, 1.00000000, 1.00000000]), + [1.00000000, 1.00000000, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_HEX_to_RGB(self) -> None: + def test_n_dimensional_HEX_to_RGB(self, xp: ModuleType) -> None: """ Test :func:`colour.notation.hexadecimal.HEX_to_RGB` definition n-dimensional arrays support. """ HEX = "#74070a" - RGB = HEX_to_RGB(HEX) + RGB = xp_as_array(HEX_to_RGB(HEX), xp=xp) HEX = np.tile(HEX, 6) - RGB = np.tile(RGB, (6, 1)) - np.testing.assert_allclose(HEX_to_RGB(HEX), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_as_array(np.tile(as_ndarray(RGB), (6, 1)), xp=xp) + xp_assert_close(HEX_to_RGB(HEX), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) HEX = np.reshape(HEX, (2, 3)) - RGB = np.reshape(RGB, (2, 3, 3)) - np.testing.assert_allclose(HEX_to_RGB(HEX), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) + RGB = xp_reshape(RGB, (2, 3, 3), xp=xp) + xp_assert_close(HEX_to_RGB(HEX), RGB, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_domain_range_scale_HEX_to_RGB(self) -> None: + def test_domain_range_scale_HEX_to_RGB( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.notation.hexadecimal.HEX_to_RGB` definition domain and range scale support. @@ -150,4 +178,4 @@ def test_domain_range_scale_HEX_to_RGB(self) -> None: d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_array_equal(HEX_to_RGB(HEX), RGB * factor) + xp_assert_equal(HEX_to_RGB(HEX), RGB * factor) diff --git a/colour/phenomena/interference.py b/colour/phenomena/interference.py index 172fa5bcd2..127d200304 100644 --- a/colour/phenomena/interference.py +++ b/colour/phenomena/interference.py @@ -20,10 +20,15 @@ from typing import TYPE_CHECKING -import numpy as np - from colour.phenomena.tmm import matrix_transfer_tmm -from colour.utilities import as_float_array, tstack +from colour.utilities import ( + array_namespace, + as_float_array, + tstack, + xp_as_float_array, + xp_atleast_1d, + xp_radians, +) if TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat @@ -145,7 +150,9 @@ def light_water_refractive_index_Schiebener1990( LL = light_water_molar_refraction_Schiebener1990(wavelength, temperature, density) - return np.sqrt((2 * LL + 1 / p_s) / (1 / p_s - LL)) + xp = array_namespace(LL) + + return xp.sqrt((2 * LL + 1 / p_s) / (1 / p_s - LL)) def thin_film_tmm( @@ -206,6 +213,7 @@ def thin_film_tmm( -------- Basic usage: + >>> import numpy as np >>> R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, 555) >>> R.shape, T.shape ((1, 1, 1, 2), (1, 1, 1, 2)) @@ -274,10 +282,14 @@ def thin_film_tmm( :cite:`Byrnes2016` """ - t = np.atleast_1d(as_float_array(t)) + wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) + + t = xp_atleast_1d(xp_as_float_array(t, xp=xp, like=wavelength), xp=xp) # Handle thickness broadcasting: reshape from (T,) to (T, 1) for single-layer - t = t[:, np.newaxis] if len(t) > 1 else t + t = t[:, None] if len(t) > 1 else t return multilayer_tmm(n=n, t=t, wavelength=wavelength, theta=theta) @@ -345,6 +357,7 @@ def multilayer_tmm( -------- Single layer: + >>> import numpy as np >>> R, T = multilayer_tmm([1.0, 1.5, 1.0], [250], 555) >>> R.shape, T.shape ((1, 1, 1, 2), (1, 1, 1, 2)) @@ -404,11 +417,15 @@ def multilayer_tmm( :cite:`Byrnes2016` """ - theta = np.atleast_1d(as_float_array(theta)) + wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) + + theta = xp_atleast_1d(xp_as_float_array(theta, xp=xp, like=wavelength), xp=xp) result = matrix_transfer_tmm( n=n, - t=np.atleast_1d(as_float_array(t)), + t=xp_atleast_1d(xp_as_float_array(t, xp=xp, like=wavelength), xp=xp), theta=theta, wavelength=wavelength, ) @@ -420,8 +437,8 @@ def multilayer_tmm( # T = thickness_count, A = angles_count, W = wavelengths_count # Reflectance (Byrnes Eq. 15, 23) - r_s = np.abs(result.M_s[:, :, :, 1, 0] / result.M_s[:, :, :, 0, 0]) ** 2 - r_p = np.abs(result.M_p[:, :, :, 1, 0] / result.M_p[:, :, :, 0, 0]) ** 2 + r_s = xp.abs(result.M_s[:, :, :, 1, 0] / result.M_s[:, :, :, 0, 0]) ** 2 + r_p = xp.abs(result.M_p[:, :, :, 1, 0] / result.M_p[:, :, :, 0, 0]) ** 2 # Transmittance (Byrnes Eq. 14, 21-22) t_s = 1 / result.M_s[:, :, :, 0, 0] @@ -429,17 +446,17 @@ def multilayer_tmm( # Transmittance correction factor: Re[n_f cos(θ_f) / n_i cos(θ_i)] # result.theta has shape (A, M) where M = media_count - cos_theta_i = np.cos(np.radians(theta))[:, None] # (A, 1) - cos_theta_f = np.cos(np.radians(result.theta[:, -1]))[:, None] # (A, 1) - transmittance_correction = np.real( + cos_theta_i = xp.cos(xp_radians(theta))[:, None] # (A, 1) + cos_theta_f = xp.cos(xp_radians(result.theta[:, -1]))[:, None] # (A, 1) + transmittance_correction = xp.real( (n_substrate * cos_theta_f) / (n_incident * cos_theta_i) ) # (A, 1) # Broadcast to thickness dimension: (1, A, 1) transmittance_correction = transmittance_correction[None, :, :] - t_s = np.abs(t_s) ** 2 * transmittance_correction # (T, A, W) - t_p = np.abs(t_p) ** 2 * transmittance_correction # (T, A, W) + t_s = xp.abs(t_s) ** 2 * transmittance_correction # (T, A, W) + t_p = xp.abs(t_p) ** 2 * transmittance_correction # (T, A, W) # Stack results: (T, A, W, 2) R = tstack([r_s, r_p]) diff --git a/colour/phenomena/rayleigh.py b/colour/phenomena/rayleigh.py index b21f2520f9..6e82132d1e 100644 --- a/colour/phenomena/rayleigh.py +++ b/colour/phenomena/rayleigh.py @@ -23,8 +23,6 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode from colour.colorimetry import ( SPECTRAL_SHAPE_DEFAULT, @@ -36,7 +34,15 @@ if typing.TYPE_CHECKING: from colour.hints import ArrayLike, Callable, NDArrayFloat -from colour.utilities import as_float, as_float_array, filter_kwargs +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + filter_kwargs, + xp_as_float_array, + xp_radians, + xp_resize, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -110,13 +116,15 @@ def air_refraction_index_Penndorf1957( np.float64(1.0002777...) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) - n = 6432.8 + 2949810 / (146 - wl ** (-2)) + 25540 / (41 - wl ** (-2)) - n /= 1.0e8 - n += +1 + n = ( + 6432.8 + + 2949810 / (146 - wavelength ** (-2)) + + 25540 / (41 - wavelength ** (-2)) + ) - return n + return n / 1.0e8 + 1 def air_refraction_index_Edlen1966( @@ -143,13 +151,15 @@ def air_refraction_index_Edlen1966( np.float64(1.0002777...) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) - n = 8342.13 + 2406030 / (130 - wl ** (-2)) + 15997 / (38.9 - wl ** (-2)) - n /= 1.0e8 - n += +1 + n = ( + 8342.13 + + 2406030 / (130 - wavelength ** (-2)) + + 15997 / (38.9 - wavelength ** (-2)) + ) - return n + return n / 1.0e8 + 1 def air_refraction_index_Peck1972( @@ -176,13 +186,15 @@ def air_refraction_index_Peck1972( np.float64(1.0002777...) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) - n = 8060.51 + 2480990 / (132.274 - wl ** (-2)) + 17455.7 / (39.32957 - wl ** (-2)) - n /= 1.0e8 - n += +1 + n = ( + 8060.51 + + 2480990 / (132.274 - wavelength ** (-2)) + + 17455.7 / (39.32957 - wavelength ** (-2)) + ) - return n + return n / 1.0e8 + 1 def air_refraction_index_Bodhaine1999( @@ -212,15 +224,15 @@ def air_refraction_index_Bodhaine1999( np.float64(1.0002777...) """ - wl = as_float_array(wavelength) - CO2_c = as_float_array(CO2_concentration) + wavelength = as_float_array(wavelength) + CO2_concentration = as_float_array(CO2_concentration) # Converting from parts per million (ppm) to parts per volume (ppv). - CO2_c = CO2_c * 1e-6 - - n = (1 + 0.54 * (CO2_c - 300e-6)) * (air_refraction_index_Peck1972(wl) - 1) + 1 + CO2_concentration = CO2_concentration * 1e-6 - return as_float(n) + return (1 + 0.54 * (CO2_concentration - 300e-6)) * ( + air_refraction_index_Peck1972(wavelength) - 1 + ) + 1 def N2_depolarisation(wavelength: ArrayLike) -> NDArrayFloat: @@ -244,9 +256,9 @@ def N2_depolarisation(wavelength: ArrayLike) -> NDArrayFloat: np.float64(1.0350291...) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) - return 1.034 + 3.17 * 1.0e-4 * (1 / wl**2) + return 1.034 + 3.17 * 1.0e-4 * (1 / wavelength**2) def O2_depolarisation(wavelength: ArrayLike) -> NDArrayFloat: @@ -270,9 +282,13 @@ def O2_depolarisation(wavelength: ArrayLike) -> NDArrayFloat: np.float64(1.1020225...) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) - return 1.096 + 1.385 * 1.0e-3 * (1 / wl**2) + 1.448 * 1.0e-4 * (1 / wl**4) + return ( + 1.096 + + 1.385 * 1.0e-3 * (1 / wavelength**2) + + 1.448 * 1.0e-4 * (1 / wavelength**4) + ) def F_air_Penndorf1957(wavelength: ArrayLike) -> NDArrayFloat: @@ -302,9 +318,13 @@ def F_air_Penndorf1957(wavelength: ArrayLike) -> NDArrayFloat: np.float64(1.0608) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) - return as_float(np.resize(np.array([1.0608]), wl.shape)) + return as_float( + xp_resize(xp_as_float_array([1.0608], xp=xp), wavelength.shape, xp=xp) + ) def F_air_Young1981(wavelength: ArrayLike) -> NDArrayFloat: @@ -334,9 +354,13 @@ def F_air_Young1981(wavelength: ArrayLike) -> NDArrayFloat: np.float64(1.048) """ - wl = as_float_array(wavelength) + wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) - return as_float(np.resize(np.array([1.0480]), wl.shape)) + return as_float( + xp_resize(xp_as_float_array([1.0480], xp=xp), wavelength.shape, xp=xp) + ) def F_air_Bates1984(wavelength: ArrayLike) -> NDArrayFloat: @@ -399,13 +423,13 @@ def F_air_Bodhaine1999( O2 = O2_depolarisation(wavelength) N2 = N2_depolarisation(wavelength) - CO2_c = as_float_array(CO2_concentration) + CO2_concentration = as_float_array(CO2_concentration) # Converting from parts per million (ppm) to parts per volume per percent. - CO2_c = CO2_c * 1e-4 + CO2_concentration = CO2_concentration * 1e-4 - return (78.084 * N2 + 20.946 * O2 + 0.934 * 1 + CO2_c * 1.15) / ( - 78.084 + 20.946 + 0.934 + CO2_c + return (78.084 * N2 + 20.946 * O2 + 0.934 * 1 + CO2_concentration * 1.15) / ( + 78.084 + 20.946 + 0.934 + CO2_concentration ) @@ -445,11 +469,11 @@ def molecular_density( np.float64(2.5468999...e+19) """ - T = as_float_array(temperature) + temperature = as_float_array(temperature) avogadro_constant = as_float_array(avogadro_constant) with sdiv_mode(): - return (avogadro_constant / 22.4141) * sdiv(273.15, T) * (1 / 1000) + return (avogadro_constant / 22.4141) * sdiv(273.15, temperature) * (1 / 1000) def mean_molecular_weights( @@ -477,9 +501,9 @@ def mean_molecular_weights( CO2_concentration = as_float_array(CO2_concentration) - CO2_c = CO2_concentration * 1.0e-6 + CO2_concentration = CO2_concentration * 1.0e-6 - return 15.0556 * CO2_c + 28.9595 + return 15.0556 * CO2_concentration + 28.9595 def gravity_List1968( @@ -519,7 +543,9 @@ def gravity_List1968( latitude = as_float_array(latitude) altitude = as_float_array(altitude) - cos2phi = np.cos(2 * np.radians(latitude)) + xp = array_namespace(latitude, altitude) + + cos2phi = xp.cos(xp_radians(2 * latitude)) # Sea level acceleration of gravity. g0 = 980.6160 * (1 - 0.0026373 * cos2phi + 0.0000059 * cos2phi**2) @@ -584,26 +610,44 @@ def scattering_cross_section( np.float64(4.3466692...e-27) """ - wl = as_float_array(wavelength) - CO2_c = as_float_array(CO2_concentration) + wavelength = as_float_array(wavelength) + CO2_concentration = as_float_array(CO2_concentration) temperature = as_float_array(temperature) - wl_micrometers = wl * 10e3 + xp = array_namespace(wavelength, CO2_concentration, temperature) + + wavelength_micrometers = wavelength * 10e3 N_s = molecular_density(temperature, avogadro_constant) - n_s = n_s_function(wl_micrometers) - # n_s = n_s_function(**filter_kwargs( - # n_s_function, wavelength=wl_micrometers, CO2_concentration=CO2_c)) + n_s = n_s_function(wavelength_micrometers) F_air = F_air_function( **filter_kwargs( - F_air_function, wavelength=wl_micrometers, CO2_concentration=CO2_c + F_air_function, + wavelength=wavelength_micrometers, + CO2_concentration=CO2_concentration, ) ) - sigma = 24 * np.pi**3 * (n_s**2 - 1) ** 2 / (wl**4 * N_s**2 * (n_s**2 + 2) ** 2) - sigma *= F_air - - return sigma + # *Bodhaine et al. (1999)* Eq. (2): + # + # sigma = 24 * pi^3 * (n_s^2 - 1)^2 + # ----------------------------- * F(air) + # wavelength^4 * N_s^2 * (n_s^2 + 2)^2 + # + # ``N_s`` is Avogadro-scale (~2.5e19) so ``N_s ** 2 ~ 6.3e38`` overflows + # the ~3.4e38 float32 normal range; the algebraically equivalent + # ``/ N_s / N_s`` keeps every intermediate within float32 (largest + # ~3e12, smallest ~4e-27) and so dispatches cleanly across every + # *Array API* backend including *PyTorch MPS*. + return ( + 24 + * xp.pi**3 + * (n_s**2 - 1) ** 2 + * F_air + / (wavelength**4 * (n_s**2 + 2) ** 2) + / N_s + / N_s + ) def rayleigh_optical_depth( @@ -665,27 +709,35 @@ def rayleigh_optical_depth( """ wavelength = as_float_array(wavelength) - CO2_c = as_float_array(CO2_concentration) - pressure = as_float_array(pressure) - latitude = as_float_array(latitude) - altitude = as_float_array(altitude) - avogadro_constant = as_float_array(avogadro_constant) - # Conversion from pascal to dyne/cm2. - P = as_float_array(pressure * 10) + + xp = array_namespace(wavelength) + + # All scalar / array inputs are promoted to ``wavelength``'s namespace + # and device so that the final ``sigma * P * N_A / (m_a * g)`` + # multiplication dispatches uniformly; mixing a backend tensor + # with a *NumPy* scalar default would fail on non-numpy devices + # (e.g. *PyTorch MPS* refusing implicit device round-trips). + CO2_concentration = xp_as_float_array(CO2_concentration, xp=xp, like=wavelength) + pressure = xp_as_float_array(pressure, xp=xp, like=wavelength) + latitude = xp_as_float_array(latitude, xp=xp, like=wavelength) + altitude = xp_as_float_array(altitude, xp=xp, like=wavelength) + avogadro_constant = xp_as_float_array(avogadro_constant, xp=xp, like=wavelength) sigma = scattering_cross_section( wavelength, - CO2_c, + CO2_concentration, temperature, avogadro_constant, n_s_function, F_air_function, ) - m_a = mean_molecular_weights(CO2_c) + m_a = mean_molecular_weights(CO2_concentration) g = gravity_List1968(latitude, altitude) - T_R = sigma * (P * avogadro_constant) / (m_a * g) + # *Bodhaine et al. (1999)* Eq. (1): ``T_R = sigma * (P * N_A) / (m_a * g)`` + # where ``P`` converts from pascal to dyne/cm^2 (factor 10). + T_R = sigma * (pressure * 10) * avogadro_constant / (m_a * g) return as_float(T_R) diff --git a/colour/phenomena/sky/cie2003.py b/colour/phenomena/sky/cie2003.py index 67bc948edc..87e1d4f8ea 100644 --- a/colour/phenomena/sky/cie2003.py +++ b/colour/phenomena/sky/cie2003.py @@ -21,12 +21,16 @@ import typing from dataclasses import dataclass -import numpy as np - if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import MixinDataclassIterable, as_float, as_float_array +from colour.utilities import ( + MixinDataclassIterable, + array_namespace, + as_float, + as_float_array, + xp_as_float_array, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -277,6 +281,7 @@ def sky_luminance_gradation_CIE2003( Examples -------- + >>> import numpy as np >>> sky_luminance_gradation_CIE2003(0, 4.0, -0.70) # doctest: +ELLIPSIS np.float64(2.9863412...) >>> sky_luminance_gradation_CIE2003( # doctest: +ELLIPSIS @@ -286,12 +291,15 @@ def sky_luminance_gradation_CIE2003( """ Z = as_float_array(Z) - a = as_float_array(a) - b = as_float_array(b) - phi = np.where( - np.pi / 2 > Z, - 1 + a * np.exp(b / np.cos(Z)), + xp = array_namespace(Z) + + a = xp_as_float_array(a, xp=xp, like=Z) + b = xp_as_float_array(b, xp=xp, like=Z) + + phi = xp.where( + xp.pi / 2 > Z, + 1 + a * xp.exp(b / xp.cos(Z)), 1.0, ) @@ -332,6 +340,7 @@ def sky_scattering_indicatrix_CIE2003( Examples -------- + >>> import numpy as np >>> sky_scattering_indicatrix_CIE2003( # doctest: +ELLIPSIS ... np.radians(30), 10, -3.0, 0.45 ... ) @@ -339,11 +348,14 @@ def sky_scattering_indicatrix_CIE2003( """ chi = as_float_array(chi) - c = as_float_array(c) - d = as_float_array(d) - e = as_float_array(e) - f = 1 + c * (np.exp(d * chi) - np.exp(d * np.pi / 2)) + e * np.cos(chi) ** 2 + xp = array_namespace(chi) + + c = xp_as_float_array(c, xp=xp, like=chi) + d = xp_as_float_array(d, xp=xp, like=chi) + e = xp_as_float_array(e, xp=xp, like=chi) + + f = 1 + c * (xp.exp(d * chi) - xp.exp(d * xp.pi / 2)) + e * xp.cos(chi) ** 2 return as_float(f) @@ -396,6 +408,7 @@ def sky_luminance_distribution_CIE2003( Examples -------- + >>> import numpy as np >>> sky_luminance_distribution_CIE2003( # doctest: +ELLIPSIS ... 1, np.radians(45), np.radians(180), np.radians(30), np.radians(0) ... ) @@ -411,9 +424,12 @@ def sky_luminance_distribution_CIE2003( raise ValueError(error) Z = as_float_array(Z) - alpha = as_float_array(alpha) - Z_s = as_float_array(Z_s) - alpha_s = as_float_array(alpha_s) + + xp = array_namespace(Z) + + alpha = xp_as_float_array(alpha, xp=xp, like=Z) + Z_s = xp_as_float_array(Z_s, xp=xp, like=Z) + alpha_s = xp_as_float_array(alpha_s, xp=xp, like=Z) parameters = CIE_STANDARD_SKY_PARAMETERS[sky_type] a = parameters.a @@ -423,9 +439,9 @@ def sky_luminance_distribution_CIE2003( e = parameters.e # Angular distance between sky element and the sun (Equation 1). - chi = np.arccos( - np.cos(Z_s) * np.cos(Z) - + np.sin(Z_s) * np.sin(Z) * np.cos(np.abs(alpha - alpha_s)) + chi = xp.acos( + xp.cos(Z_s) * xp.cos(Z) + + xp.sin(Z_s) * xp.sin(Z) * xp.cos(xp.abs(alpha - alpha_s)) ) # Relative luminance (Equations 3-7). @@ -468,6 +484,7 @@ def sky_luminance_distribution_overcast_CIE2003( Examples -------- + >>> import numpy as np >>> sky_luminance_distribution_overcast_CIE2003(0) # doctest: +ELLIPSIS np.float64(1.0) >>> sky_luminance_distribution_overcast_CIE2003( # doctest: +ELLIPSIS @@ -478,6 +495,8 @@ def sky_luminance_distribution_overcast_CIE2003( Z = as_float_array(Z) - gamma = np.pi / 2 - Z + xp = array_namespace(Z) + + gamma = xp.pi / 2 - Z - return as_float((1 + 2 * np.sin(gamma)) / 3) + return as_float((1 + 2 * xp.sin(gamma)) / 3) diff --git a/colour/phenomena/sky/tests/test_cie2003.py b/colour/phenomena/sky/tests/test_cie2003.py index 86dc5a16cd..4c4fa60337 100644 --- a/colour/phenomena/sky/tests/test_cie2003.py +++ b/colour/phenomena/sky/tests/test_cie2003.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np import pytest @@ -12,7 +17,12 @@ sky_luminance_gradation_CIE2003, sky_scattering_indicatrix_CIE2003, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -35,49 +45,55 @@ class TestSkyLuminanceGradationCIE2003: definition unit tests methods. """ - def test_sky_luminance_gradation_CIE2003(self) -> None: + def test_sky_luminance_gradation_CIE2003(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.\ sky_luminance_gradation_CIE2003` definition. """ # Type 1: a=4.0, b=-0.70. At zenith (Z=0): 1 + 4*exp(-0.70). - np.testing.assert_allclose( - sky_luminance_gradation_CIE2003(0, 4.0, -0.70), + xp_assert_close( + sky_luminance_gradation_CIE2003(xp_as_array([0.0], xp=xp), 4.0, -0.70), 1 + 4.0 * np.exp(-0.70), atol=TOLERANCE_ABSOLUTE_TESTS, ) # At horizon (Z=pi/2): always 1. - np.testing.assert_allclose( - sky_luminance_gradation_CIE2003(np.pi / 2, 4.0, -0.70), + xp_assert_close( + sky_luminance_gradation_CIE2003( + xp_as_array([np.pi / 2], xp=xp), 4.0, -0.70 + ), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Type 5: a=0, b=-1.0. Uniform: phi(Z) = 1 for all Z. - np.testing.assert_allclose( - sky_luminance_gradation_CIE2003(0, 0, -1.0), + xp_assert_close( + sky_luminance_gradation_CIE2003(xp_as_array([0.0], xp=xp), 0, -1.0), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sky_luminance_gradation_CIE2003(np.radians(45), 0, -1.0), + xp_assert_close( + sky_luminance_gradation_CIE2003( + xp_as_array([np.radians(45)], xp=xp), 0, -1.0 + ), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sky_luminance_gradation_CIE2003(self) -> None: + def test_n_dimensional_sky_luminance_gradation_CIE2003( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.sky.\ sky_luminance_gradation_CIE2003` definition n-dimensional support. """ - Z = np.array([0, np.radians(30), np.radians(60)]) + Z = xp_as_array([0, np.radians(30), np.radians(60)], xp=xp) result = sky_luminance_gradation_CIE2003(Z, 4.0, -0.70) - assert result.shape == (3,) + assert as_ndarray(result).shape == (3,) @ignore_numpy_errors def test_nan_sky_luminance_gradation_CIE2003(self) -> None: @@ -95,42 +111,50 @@ class TestSkyScatteringIndicatrixCIE2003: definition unit tests methods. """ - def test_sky_scattering_indicatrix_CIE2003(self) -> None: + def test_sky_scattering_indicatrix_CIE2003(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.\ sky_scattering_indicatrix_CIE2003` definition. """ # When c=0, e=0: f(chi) = 1 for any chi. - np.testing.assert_allclose( - sky_scattering_indicatrix_CIE2003(np.radians(45), 0, -1.0, 0), + xp_assert_close( + sky_scattering_indicatrix_CIE2003( + xp_as_array([np.radians(45)], xp=xp), 0, -1.0, 0 + ), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) # At chi=0 (looking at sun): f(0) = 1 + c*(1 - exp(d*pi/2)) + e. - np.testing.assert_allclose( - sky_scattering_indicatrix_CIE2003(0, 10, -3.0, 0.45), + xp_assert_close( + sky_scattering_indicatrix_CIE2003( + xp_as_array([0.0], xp=xp), 10, -3.0, 0.45 + ), 1 + 10 * (1 - np.exp(-3.0 * np.pi / 2)) + 0.45, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - sky_scattering_indicatrix_CIE2003(np.radians(30), 10, -3.0, 0.45), + xp_assert_close( + sky_scattering_indicatrix_CIE2003( + xp_as_array([np.radians(30)], xp=xp), 10, -3.0, 0.45 + ), 3.32646285329632, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_sky_scattering_indicatrix_CIE2003(self) -> None: + def test_n_dimensional_sky_scattering_indicatrix_CIE2003( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.sky.\ sky_scattering_indicatrix_CIE2003` definition n-dimensional support. """ - chi = np.array([0, np.radians(45), np.radians(90)]) + chi = xp_as_array([0, np.radians(45), np.radians(90)], xp=xp) result = sky_scattering_indicatrix_CIE2003(chi, 10, -3.0, 0.45) - assert result.shape == (3,) + assert as_ndarray(result).shape == (3,) @ignore_numpy_errors def test_nan_sky_scattering_indicatrix_CIE2003(self) -> None: @@ -148,34 +172,44 @@ class TestSkyLuminanceDistributionCIE2003: sky_luminance_distribution_CIE2003` definition unit tests methods. """ - def test_sky_luminance_distribution_CIE2003(self) -> None: + def test_sky_luminance_distribution_CIE2003(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.\ sky_luminance_distribution_CIE2003` definition. """ # Type 1 (CIE Standard Overcast Sky): at zenith, L/Lz = 1. - np.testing.assert_allclose( - sky_luminance_distribution_CIE2003(1, 0, 0, np.radians(30), np.radians(0)), + xp_assert_close( + sky_luminance_distribution_CIE2003( + 1, + xp_as_array([0.0], xp=xp), + xp_as_array([0.0], xp=xp), + np.radians(30), + np.radians(0), + ), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Type 5 (Uniform sky): L/Lz = 1 everywhere. - np.testing.assert_allclose( + xp_assert_close( sky_luminance_distribution_CIE2003( - 5, np.radians(45), np.radians(90), np.radians(30), np.radians(0) + 5, + xp_as_array([np.radians(45)], xp=xp), + xp_as_array([np.radians(90)], xp=xp), + np.radians(30), + np.radians(0), ), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) # Type 1: overcast sky at specific position. - np.testing.assert_allclose( + xp_assert_close( sky_luminance_distribution_CIE2003( 1, - np.radians(45), - np.radians(180), + xp_as_array([np.radians(45)], xp=xp), + xp_as_array([np.radians(180)], xp=xp), np.radians(30), np.radians(0), ), @@ -184,11 +218,11 @@ def test_sky_luminance_distribution_CIE2003(self) -> None: ) # Type 12: clear sky at specific position. - np.testing.assert_allclose( + xp_assert_close( sky_luminance_distribution_CIE2003( 12, - np.radians(45), - np.radians(180), + xp_as_array([np.radians(45)], xp=xp), + xp_as_array([np.radians(180)], xp=xp), np.radians(30), np.radians(0), ), @@ -208,19 +242,21 @@ def test_sky_type_validation(self) -> None: with pytest.raises(ValueError): sky_luminance_distribution_CIE2003(16, 0, 0, 0, 0) - def test_n_dimensional_sky_luminance_distribution_CIE2003(self) -> None: + def test_n_dimensional_sky_luminance_distribution_CIE2003( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.sky.\ sky_luminance_distribution_CIE2003` definition n-dimensional support. """ - Z = np.array([0, np.radians(30), np.radians(60)]) - alpha = np.array([0, np.radians(90), np.radians(180)]) + Z = xp_as_array([0, np.radians(30), np.radians(60)], xp=xp) + alpha = xp_as_array([0, np.radians(90), np.radians(180)], xp=xp) result = sky_luminance_distribution_CIE2003( 1, Z, alpha, np.radians(30), np.radians(0) ) - assert result.shape == (3,) + assert as_ndarray(result).shape == (3,) @ignore_numpy_errors def test_nan_sky_luminance_distribution_CIE2003(self) -> None: @@ -238,35 +274,39 @@ class TestSkyLuminanceDistributionOvercastCIE2003: sky_luminance_distribution_overcast_CIE2003` definition unit tests methods. """ - def test_sky_luminance_distribution_overcast_CIE2003(self) -> None: + def test_sky_luminance_distribution_overcast_CIE2003(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.\ sky_luminance_distribution_overcast_CIE2003` definition. """ # At zenith (Z=0, gamma=pi/2): (1 + 2*1) / 3 = 1. - np.testing.assert_allclose( - sky_luminance_distribution_overcast_CIE2003(0), + xp_assert_close( + sky_luminance_distribution_overcast_CIE2003(xp_as_array([0.0], xp=xp)), 1.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) # At horizon (Z=pi/2, gamma=0): (1 + 0) / 3 = 1/3. - np.testing.assert_allclose( - sky_luminance_distribution_overcast_CIE2003(np.pi / 2), + xp_assert_close( + sky_luminance_distribution_overcast_CIE2003( + xp_as_array([np.pi / 2], xp=xp) + ), 1 / 3, atol=TOLERANCE_ABSOLUTE_TESTS, ) # At 45 degrees elevation (Z=pi/4, gamma=pi/4). - np.testing.assert_allclose( - sky_luminance_distribution_overcast_CIE2003(np.pi / 4), + xp_assert_close( + sky_luminance_distribution_overcast_CIE2003( + xp_as_array([np.pi / 4], xp=xp) + ), (1 + 2 * np.sin(np.pi / 4)) / 3, atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_n_dimensional_sky_luminance_distribution_overcast_CIE2003( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.phenomena.sky.\ @@ -274,10 +314,10 @@ def test_n_dimensional_sky_luminance_distribution_overcast_CIE2003( support. """ - Z = np.array([0, np.radians(30), np.radians(60)]) + Z = xp_as_array([0, np.radians(30), np.radians(60)], xp=xp) result = sky_luminance_distribution_overcast_CIE2003(Z) - assert result.shape == (3,) + assert as_ndarray(result).shape == (3,) @ignore_numpy_errors def test_nan_sky_luminance_distribution_overcast_CIE2003(self) -> None: diff --git a/colour/phenomena/sky/tests/test_wilkie2021.py b/colour/phenomena/sky/tests/test_wilkie2021.py index f7a4f1dacf..53186f7f57 100644 --- a/colour/phenomena/sky/tests/test_wilkie2021.py +++ b/colour/phenomena/sky/tests/test_wilkie2021.py @@ -3,11 +3,17 @@ from __future__ import annotations import os +import typing from dataclasses import fields +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np import pytest +from colour.algebra import linear_interpolation_index_and_factor +from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.phenomena.sky.wilkie2021 import ( PATH_PRAGUE_SKY_MODEL_DATASET_GROUND, SkyDataset_Wilkie2021, @@ -17,6 +23,7 @@ sky_transmittance_Wilkie2021, sun_radiance_Wilkie2021, ) +from colour.utilities import as_ndarray, xp_as_array, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -267,8 +274,8 @@ def test_read(self) -> None: assert isinstance(dataset, SkyDataset_Wilkie2021) assert dataset.channels == 11 - np.testing.assert_allclose(dataset.channel_start, 320.0) - np.testing.assert_allclose(dataset.channel_width, 40.0) + xp_assert_close(dataset.channel_start, 320.0) + xp_assert_close(dataset.channel_width, 40.0) assert len(dataset.visibilities_radiance) >= 1 assert len(dataset.albedos_radiance) >= 1 assert len(dataset.altitudes_radiance) >= 1 @@ -312,6 +319,104 @@ def test_required_attributes(self) -> None: assert attribute in field_names +class TestLinearInterpolationIndexAndFactor: + """ + Define :func:`colour.algebra.linear_interpolation_index_and_factor` + definition unit tests methodataset. + """ + + def test_linear_interpolation_index_and_factor(self, xp: ModuleType) -> None: + """ + Test :func:`colour.algebra.\ +linear_interpolation_index_and_factor` definition. + """ + + break_points = xp_as_array([0.0, 1.0, 2.0, 3.0], xp=xp) + + # Exact match at start. + index, factor = linear_interpolation_index_and_factor( + xp_as_array(0.0, xp=xp), break_points + ) + assert index == 0 + xp_assert_close(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) + + # Exact match at end (index = last, factor = 0). + index, factor = linear_interpolation_index_and_factor( + xp_as_array(3.0, xp=xp), break_points + ) + assert index == 3 + xp_assert_close(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) + + # Midpoint. + index, factor = linear_interpolation_index_and_factor( + xp_as_array(1.5, xp=xp), break_points + ) + assert index == 1 + xp_assert_close(factor, 0.5, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) + + # Clamped below. + index, factor = linear_interpolation_index_and_factor( + xp_as_array(-1.0, xp=xp), break_points + ) + assert index == 0 + xp_assert_close(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) + + # Clamped above (same as end: index = last, factor = 0). + index, factor = linear_interpolation_index_and_factor( + xp_as_array(5.0, xp=xp), break_points + ) + assert index == 3 + xp_assert_close(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) + + # Degenerate (identical break points). + index, factor = linear_interpolation_index_and_factor( + xp_as_array(1.0, xp=xp), xp_as_array([1.0, 1.0], xp=xp) + ) + xp_assert_close(factor, 0.0, atol=TOLERANCE_ABSOLUTE_TESTS * 0.001) + + +@pytest.mark.skipif( + not DATASET_AVAILABLE, + reason=f"Prague Sky Model dataset not found at {DATASET_PATH}", +) +class TestLoadDatasetWilkie2021: + """ + Define :class:`colour.phenomena.sky.wilkie2021.SkyDataset_Wilkie2021` + class unit tests methodataset. + """ + + def test_SkyDataset_Wilkie2021(self) -> None: + """ + Test :class:`colour.phenomena.sky.wilkie2021.\ +SkyDataset_Wilkie2021` class. + """ + + dataset = SkyDataset_Wilkie2021(DATASET_PATH) + + assert isinstance(dataset, SkyDataset_Wilkie2021) + assert dataset.channels == 11 + xp_assert_close( + dataset.channel_start, + 320.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + xp_assert_close( + dataset.channel_width, + 40.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + assert len(dataset.visibilities_radiance) >= 1 + assert len(dataset.albedos_radiance) >= 1 + assert len(dataset.altitudes_radiance) >= 1 + assert len(dataset.elevations_radiance) >= 1 + assert len(dataset.data_radiance) > 0 + assert len(dataset.data_transmittance_u) > 0 + assert len(dataset.data_transmittance_v) > 0 + + with pytest.raises(FileNotFoundError): + SkyDataset_Wilkie2021("/nonexistent/path.dat") + + @pytest.mark.skipif( not DATASET_AVAILABLE, reason=f"Prague Sky Model dataset not found at {DATASET_PATH}", @@ -322,7 +427,7 @@ class TestComputeSkyParametersWilkie2021: compute_sky_parameters_Wilkie2021` definition unit tests methods. """ - def test_compute_sky_parameters_Wilkie2021(self) -> None: + def test_compute_sky_parameters_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ compute_sky_parameters_Wilkie2021` definition. @@ -330,8 +435,8 @@ def test_compute_sky_parameters_Wilkie2021(self) -> None: for name, condition in TEST_SKY_CONDITIONS.items(): parameters = compute_sky_parameters_Wilkie2021( - np.array(condition["view_point"], dtype=float), - np.array(condition["view_direction"], dtype=float), + xp_as_array(condition["view_point"], xp=xp), + xp_as_array(condition["view_direction"], xp=xp), condition["sun_elevation"], condition["sun_azimuth"], condition["visibility"], @@ -339,40 +444,40 @@ def test_compute_sky_parameters_Wilkie2021(self) -> None: ) reference = condition["parameters"] - np.testing.assert_allclose( + xp_assert_close( parameters.theta, reference["theta"], - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, err_msg=f"{name}: theta", ) - np.testing.assert_allclose( + xp_assert_close( parameters.gamma, reference["gamma"], - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, err_msg=f"{name}: gamma", ) - np.testing.assert_allclose( + xp_assert_close( parameters.shadow, reference["shadow"], - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, err_msg=f"{name}: shadow", ) - np.testing.assert_allclose( + xp_assert_close( parameters.zero, reference["zero"], - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, err_msg=f"{name}: zero", ) - np.testing.assert_allclose( + xp_assert_close( parameters.elevation, reference["elevation"], - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, err_msg=f"{name}: elevation", ) - np.testing.assert_allclose( + xp_assert_close( parameters.altitude, reference["altitude"], - atol=1e-3, + atol=TOLERANCE_ABSOLUTE_TESTS * 10000, err_msg=f"{name}: altitude", ) @@ -387,7 +492,8 @@ class TestSkyRadianceWilkie2021: definition unit tests methods. """ - def test_sky_radiance_Wilkie2021(self) -> None: + @pytest.mark.mps_tolerance_absolute(2e-1) + def test_sky_radiance_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_radiance_Wilkie2021` definition. @@ -397,25 +503,28 @@ def test_sky_radiance_Wilkie2021(self) -> None: for name, condition in TEST_SKY_CONDITIONS.items(): parameters = compute_sky_parameters_Wilkie2021( - np.array(condition["view_point"], dtype=float), - np.array(condition["view_direction"], dtype=float), + xp_as_array(condition["view_point"], xp=xp), + xp_as_array(condition["view_direction"], xp=xp), condition["sun_elevation"], condition["sun_azimuth"], condition["visibility"], condition["albedo"], ) - result = sky_radiance_Wilkie2021(dataset, parameters, WAVELENGTHS) + result = sky_radiance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp) + ) reference = np.array(condition["sky_radiance"]) - np.testing.assert_allclose( + xp_assert_close( result, reference, rtol=1e-5, err_msg=f"{name}: sky_radiance", + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_sky_radiance_Wilkie2021_out_of_range(self) -> None: + def test_sky_radiance_Wilkie2021_out_of_range(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_radiance_Wilkie2021` definition with out-of-range wavelengths. @@ -424,17 +533,19 @@ def test_sky_radiance_Wilkie2021_out_of_range(self) -> None: dataset = SkyDataset_Wilkie2021(DATASET_PATH) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), - np.array([0, 0, 1.0]), + xp_as_array([0, 0, 0.0], xp=xp), + xp_as_array([0, 0, 1.0], xp=xp), 0.5236, 0.0, 50.0, 0.5, ) - result = sky_radiance_Wilkie2021(dataset, parameters, np.array([100.0, 5000.0])) - np.testing.assert_array_equal(result, 0.0) + result = sky_radiance_Wilkie2021( + dataset, parameters, xp_as_array([100.0, 5000.0], xp=xp) + ) + xp_assert_equal(result, 0.0) - def test_n_dimensional_sky_radiance_Wilkie2021(self) -> None: + def test_n_dimensional_sky_radiance_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_radiance_Wilkie2021` definition n-dimensional support. @@ -443,27 +554,32 @@ def test_n_dimensional_sky_radiance_Wilkie2021(self) -> None: dataset = SkyDataset_Wilkie2021(DATASET_PATH) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), - np.array([0, 0, 1.0]), + xp_as_array([0, 0, 0.0], xp=xp), + xp_as_array([0, 0, 1.0], xp=xp), 0.5236, 0.0, 50.0, 0.5, ) - result = sky_radiance_Wilkie2021(dataset, parameters, WAVELENGTHS) - assert result.shape == (6,) + result = sky_radiance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp) + ) + assert as_ndarray(result).shape == (6,) - directions = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]], dtype=float) + # Batched directions. + directions = xp_as_array([[0, 0, 1], [1, 0, 0], [0, 1, 0]], xp=xp) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), + xp_as_array([0, 0, 0.0], xp=xp), directions, 0.5236, 0.0, 50.0, 0.5, ) - result = sky_radiance_Wilkie2021(dataset, parameters, WAVELENGTHS) - assert result.shape == (3, 6) + result = sky_radiance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp) + ) + assert as_ndarray(result).shape == (3, 6) @pytest.mark.skipif( @@ -476,7 +592,7 @@ class TestSunRadianceWilkie2021: definition unit tests methods. """ - def test_sun_radiance_Wilkie2021(self) -> None: + def test_sun_radiance_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sun_radiance_Wilkie2021` definition. @@ -486,18 +602,20 @@ def test_sun_radiance_Wilkie2021(self) -> None: for name, condition in TEST_SKY_CONDITIONS.items(): parameters = compute_sky_parameters_Wilkie2021( - np.array(condition["view_point"], dtype=float), - np.array(condition["view_direction"], dtype=float), + xp_as_array(condition["view_point"], xp=xp), + xp_as_array(condition["view_direction"], xp=xp), condition["sun_elevation"], condition["sun_azimuth"], condition["visibility"], condition["albedo"], ) - result = sun_radiance_Wilkie2021(dataset, parameters, WAVELENGTHS) - np.testing.assert_array_equal(result, 0.0, err_msg=f"{name}: sun_radiance") + result = sun_radiance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp) + ) + xp_assert_equal(result, 0.0, err_msg=f"{name}: sun_radiance") - def test_sun_radiance_Wilkie2021_toward_sun(self) -> None: + def test_sun_radiance_Wilkie2021_toward_sun(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sun_radiance_Wilkie2021` definition when looking at the sun. @@ -505,25 +623,29 @@ def test_sun_radiance_Wilkie2021_toward_sun(self) -> None: dataset = SkyDataset_Wilkie2021(DATASET_PATH) - sun_direction = np.array( + # Look directly at the sun (gamma ~ 0): should be positive. + sun_dir = xp_as_array( [ np.cos(0.0) * np.cos(0.5236), np.sin(0.0) * np.cos(0.5236), np.sin(0.5236), - ] + ], + xp=xp, ) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), - sun_direction, + xp_as_array([0, 0, 0.0], xp=xp), + sun_dir, 0.5236, 0.0, 50.0, 0.5, ) - result = sun_radiance_Wilkie2021(dataset, parameters, WAVELENGTHS) - assert np.all(result[:6] > 0) + result = sun_radiance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp) + ) + assert np.all(as_ndarray(result)[:6] > 0) - def test_n_dimensional_sun_radiance_Wilkie2021(self) -> None: + def test_n_dimensional_sun_radiance_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sun_radiance_Wilkie2021` definition n-dimensional support. @@ -531,18 +653,21 @@ def test_n_dimensional_sun_radiance_Wilkie2021(self) -> None: dataset = SkyDataset_Wilkie2021(DATASET_PATH) - directions = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]], dtype=float) + # Batched directions (none hitting sun). + directions = xp_as_array([[0, 0, 1], [1, 0, 0], [0, 1, 0]], xp=xp) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), + xp_as_array([0, 0, 0.0], xp=xp), directions, 0.5236, 0.0, 50.0, 0.5, ) - result = sun_radiance_Wilkie2021(dataset, parameters, WAVELENGTHS) - assert result.shape == (3, 6) - np.testing.assert_array_equal(result, 0.0) + result = sun_radiance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp) + ) + assert as_ndarray(result).shape == (3, 6) + xp_assert_equal(result, 0.0) @pytest.mark.skipif( @@ -555,7 +680,7 @@ class TestSkyTransmittanceWilkie2021: sky_transmittance_Wilkie2021` definition unit tests methods. """ - def test_sky_transmittance_Wilkie2021(self) -> None: + def test_sky_transmittance_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_transmittance_Wilkie2021` definition. @@ -565,8 +690,8 @@ def test_sky_transmittance_Wilkie2021(self) -> None: for name, condition in TEST_SKY_CONDITIONS.items(): parameters = compute_sky_parameters_Wilkie2021( - np.array(condition["view_point"], dtype=float), - np.array(condition["view_direction"], dtype=float), + xp_as_array(condition["view_point"], xp=xp), + xp_as_array(condition["view_direction"], xp=xp), condition["sun_elevation"], condition["sun_azimuth"], condition["visibility"], @@ -574,18 +699,19 @@ def test_sky_transmittance_Wilkie2021(self) -> None: ) result = sky_transmittance_Wilkie2021( - dataset, parameters, WAVELENGTHS, np.inf + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp), np.inf ) reference = np.array(condition["transmittance"]) - np.testing.assert_allclose( + xp_assert_close( result, reference, rtol=1e-5, err_msg=f"{name}: transmittance", + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_sky_transmittance_Wilkie2021_bounded(self) -> None: + def test_sky_transmittance_Wilkie2021_bounded(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_transmittance_Wilkie2021` definition values are in [0, 1]. @@ -594,18 +720,20 @@ def test_sky_transmittance_Wilkie2021_bounded(self) -> None: dataset = SkyDataset_Wilkie2021(DATASET_PATH) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), - np.array([0, 0, 1.0]), + xp_as_array([0, 0, 0.0], xp=xp), + xp_as_array([0, 0, 1.0], xp=xp), 0.5236, 0.0, 50.0, 0.5, ) - result = sky_transmittance_Wilkie2021(dataset, parameters, WAVELENGTHS, np.inf) - assert np.all(result >= 0.0) - assert np.all(result <= 1.0) + result = sky_transmittance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp), np.inf + ) + assert np.all(as_ndarray(result) >= 0.0) + assert np.all(as_ndarray(result) <= 1.0) - def test_sky_transmittance_Wilkie2021_out_of_range(self) -> None: + def test_sky_transmittance_Wilkie2021_out_of_range(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_transmittance_Wilkie2021` definition with out-of-range wavelengths. @@ -622,11 +750,11 @@ def test_sky_transmittance_Wilkie2021_out_of_range(self) -> None: 0.5, ) result = sky_transmittance_Wilkie2021( - dataset, parameters, np.array([100.0, 5000.0]), np.inf + dataset, parameters, xp_as_array([100.0, 5000.0], xp=xp), np.inf ) - np.testing.assert_array_equal(result, 0.0) + xp_assert_equal(result, 0.0) - def test_n_dimensional_sky_transmittance_Wilkie2021(self) -> None: + def test_n_dimensional_sky_transmittance_Wilkie2021(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.sky.wilkie2021.\ sky_transmittance_Wilkie2021` definition n-dimensional support. @@ -634,16 +762,19 @@ def test_n_dimensional_sky_transmittance_Wilkie2021(self) -> None: dataset = SkyDataset_Wilkie2021(DATASET_PATH) - directions = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]], dtype=float) + # Batched directions. + directions = xp_as_array([[0, 0, 1], [1, 0, 0], [0, 1, 0]], xp=xp) parameters = compute_sky_parameters_Wilkie2021( - np.array([0, 0, 0.0]), + xp_as_array([0, 0, 0.0], xp=xp), directions, 0.5236, 0.0, 50.0, 0.5, ) - result = sky_transmittance_Wilkie2021(dataset, parameters, WAVELENGTHS, np.inf) - assert result.shape == (3, 6) - assert np.all(result >= 0.0) - assert np.all(result <= 1.0) + result = sky_transmittance_Wilkie2021( + dataset, parameters, xp_as_array(WAVELENGTHS, xp=xp), np.inf + ) + assert as_ndarray(result).shape == (3, 6) + assert np.all(as_ndarray(result) >= 0.0) + assert np.all(as_ndarray(result) <= 1.0) diff --git a/colour/phenomena/sky/wilkie2021.py b/colour/phenomena/sky/wilkie2021.py index 2b6d222a8e..429fd9340d 100644 --- a/colour/phenomena/sky/wilkie2021.py +++ b/colour/phenomena/sky/wilkie2021.py @@ -37,13 +37,23 @@ from colour.hints import ArrayLike, NDArrayFloat, NDArrayReal from colour.algebra import lerp, linear_interpolation_index_and_factor +from colour.constants import DTYPE_INT_DEFAULT from colour.geometry.intersection import intersect_ray_circle_2d from colour.utilities import ( MixinDataclassIterable, Structure, + array_namespace, as_float_array, as_int_array, + as_ndarray, download_url, + xp_as_array, + xp_as_float_array, + xp_as_int_array, + xp_astype, + xp_atleast_1d, + xp_broadcast_to, + xp_degrees, zeros, ) @@ -627,7 +637,7 @@ def read(self) -> SkyDataset_Wilkie2021: self.metadata_radiance = metadata_radiance - data_radiance = np.zeros( + data_radiance = zeros( metadata_radiance.total_coefficients_all_configurations, dtype=np.float32, ) @@ -747,7 +757,7 @@ def read(self) -> SkyDataset_Wilkie2021: len(metadata_polarisation.sun_break_points) + len(metadata_polarisation.zenith_break_points) ) * metadata_polarisation.rank - data_polarisation = np.zeros( + data_polarisation = zeros( metadata_polarisation.total_coefficients_all_configurations, dtype=np.float32, ) @@ -848,74 +858,84 @@ def compute_sky_parameters_Wilkie2021( view_point_array = as_float_array(view_point) view_direction_array = as_float_array(view_direction) - view_direction_array = view_direction_array / np.linalg.norm( + + xp = array_namespace(view_point_array, view_direction_array) + + view_direction_array = view_direction_array / xp.linalg.vector_norm( view_direction_array, axis=-1, keepdims=True ) - center = np.array([0.0, 0.0, -CONSTANTS_WILKIE2021.planet_radius]) + center = xp_as_float_array([0.0, 0.0, -CONSTANTS_WILKIE2021.planet_radius], xp=xp) to_view_point = view_point_array - center - to_view_point_normalised = to_view_point / np.linalg.norm(to_view_point) + to_view_point_normalised = to_view_point / xp.linalg.vector_norm(to_view_point) distance_to_view = ( - np.linalg.norm(to_view_point) + CONSTANTS_WILKIE2021.safety_altitude + xp.linalg.vector_norm(to_view_point) + CONSTANTS_WILKIE2021.safety_altitude ) to_shifted_view_point = to_view_point_normalised * distance_to_view shifted_view_point = center + to_shifted_view_point - altitude = max(distance_to_view - CONSTANTS_WILKIE2021.planet_radius, 0.0) + altitude = xp.maximum( + distance_to_view - CONSTANTS_WILKIE2021.planet_radius, + xp_as_float_array(0.0, xp=xp), + ) # Direction to sun. - direction_to_sun = np.array( + sun_azimuth = xp_as_float_array(sun_azimuth, xp=xp) # pyright: ignore + sun_elevation = xp_as_float_array(sun_elevation, xp=xp) # pyright: ignore + direction_to_sun = xp.stack( [ - np.cos(sun_azimuth) * np.cos(sun_elevation), - np.sin(sun_azimuth) * np.cos(sun_elevation), - np.sin(sun_elevation), + xp.cos(sun_azimuth) * xp.cos(sun_elevation), + xp.sin(sun_azimuth) * xp.cos(sun_elevation), + xp.sin(sun_elevation), ] ) # Solar elevation at view point. - dot_zenith_sun = float(np.dot(to_view_point_normalised, direction_to_sun)) - elevation = 0.5 * np.pi - np.arccos(dot_zenith_sun) + dot_zenith_sun = xp.sum(to_view_point_normalised * direction_to_sun) + elevation = 0.5 * xp.pi - xp.acos(dot_zenith_sun) # Altitude-corrected view direction. if distance_to_view > CONSTANTS_WILKIE2021.planet_radius: look_at_point = shifted_view_point + view_direction_array correction = ( - np.sqrt(distance_to_view**2 - CONSTANTS_WILKIE2021.planet_radius**2) + xp.sqrt( + xp_as_float_array( + distance_to_view**2 - CONSTANTS_WILKIE2021.planet_radius**2, xp=xp + ) + ) / distance_to_view ) to_new_origin = to_view_point_normalised * (distance_to_view - correction) new_origin = center + to_new_origin correct_view = look_at_point - new_origin - correct_view_n = correct_view / np.linalg.norm( + correct_view_n = correct_view / xp.linalg.vector_norm( correct_view, axis=-1, keepdims=True ) else: correct_view_n = view_direction_array # Gamma (sun angle) - no correction. Shape: (...,). - gamma = np.arccos( - np.clip(np.sum(view_direction_array * direction_to_sun, axis=-1), -1, 1) + gamma = xp.acos( + xp.clip(xp.sum(view_direction_array * direction_to_sun, axis=-1), -1, 1) ) # Shadow angle - requires correction. - shadow_angle = sun_elevation + np.pi * 0.5 - shadow_direction = np.array( + shadow_angle = xp_as_float_array(sun_elevation + xp.pi * 0.5, xp=xp) + shadow_direction = xp.stack( [ - np.cos(shadow_angle) * np.cos(sun_azimuth), - np.cos(shadow_angle) * np.sin(sun_azimuth), - np.sin(shadow_angle), + xp.cos(shadow_angle) * xp.cos(sun_azimuth), + xp.cos(shadow_angle) * xp.sin(sun_azimuth), + xp.sin(shadow_angle), ] ) - shadow = np.arccos( - np.clip(np.sum(correct_view_n * shadow_direction, axis=-1), -1, 1) - ) + shadow = xp.acos(xp.clip(xp.sum(correct_view_n * shadow_direction, axis=-1), -1, 1)) # Zenith angle (corrected and uncorrected). Shape: (...,). - zero = np.arccos( - np.clip(np.sum(correct_view_n * to_view_point_normalised, axis=-1), -1, 1) + zero = xp.acos( + xp.clip(xp.sum(correct_view_n * to_view_point_normalised, axis=-1), -1, 1) ) - theta = np.arccos( - np.clip(np.sum(view_direction_array * to_view_point_normalised, axis=-1), -1, 1) + theta = xp.acos( + xp.clip(xp.sum(view_direction_array * to_view_point_normalised, axis=-1), -1, 1) ) return SkyParameters_Wilkie2021( @@ -923,8 +943,8 @@ def compute_sky_parameters_Wilkie2021( gamma=gamma, shadow=shadow, zero=zero, - elevation=float(elevation), - altitude=float(altitude), + elevation=elevation, + altitude=altitude, visibility=visibility, albedo=albedo, ) @@ -961,7 +981,14 @@ def _reconstruct_sky_model( Reconstructed values, shape ``(*S, W)``. """ - result = np.zeros(offsets.shape, dtype=np.float64) + xp = array_namespace(offsets, data) + + result = xp.zeros(offsets.shape, dtype=gamma_factor.dtype) + + # When running on a non-CPU device (e.g., MPS), convert the coefficient + # data array to a device tensor once so that advanced indexing works + # without round-tripping through numpy. + data = xp_as_float_array(data, xp=xp, like=gamma_factor) for rank_index in range(metadata.rank): sun_index = as_int_array( @@ -972,8 +999,8 @@ def _reconstruct_sky_model( ) sun_value = lerp( gamma_factor[..., None], - as_float_array(data[sun_index]), - as_float_array(data[sun_index + 1]), + data[sun_index], + data[sun_index + 1], ) zenith_index = as_int_array( @@ -984,11 +1011,11 @@ def _reconstruct_sky_model( ) zenith_value = lerp( alpha_factor[..., None], - as_float_array(data[zenith_index]), - as_float_array(data[zenith_index + 1]), + data[zenith_index], + data[zenith_index + 1], ) - result += sun_value * zenith_value + result = result + sun_value * zenith_value if len(metadata.emphasis_break_points) > 0: emphasis_index = as_int_array( @@ -996,13 +1023,13 @@ def _reconstruct_sky_model( ) emphasis_value = lerp( zero_factor[..., None], - as_float_array(data[emphasis_index]), - as_float_array(data[emphasis_index + 1]), + data[emphasis_index], + data[emphasis_index + 1], ) - result *= emphasis_value + result = result * emphasis_value # The emphasis term is non-negative by construction in the # *Prague Sky Model* reference; clipping floors out fitting noise. - np.maximum(result, 0.0, out=result) + result = xp.maximum(result, xp_as_float_array(0.0, xp=xp)) return result @@ -1021,7 +1048,9 @@ def _evaluate_sky_model( shape ``(W,)``, the result has shape ``(*S, W)``. """ - wavelength = np.atleast_1d(wavelength) + xp = array_namespace(wavelength) + + wavelength = xp_atleast_1d(wavelength, xp=xp) wavelength_count = len(wavelength) # Angle parameters. @@ -1034,7 +1063,7 @@ def _evaluate_sky_model( zero = as_float_array(parameters.zero) if len(metadata.emphasis_break_points) > 0: - alpha_value = np.where(elevation < 0, shadow, zero) + alpha_value = xp.where(xp_as_array(elevation < 0, xp=xp), shadow, zero) alpha_index, alpha_factor = linear_interpolation_index_and_factor( alpha_value, metadata.zenith_break_points ) @@ -1045,8 +1074,8 @@ def _evaluate_sky_model( alpha_index, alpha_factor = linear_interpolation_index_and_factor( zero, metadata.zenith_break_points ) - zero_index = np.zeros_like(gamma_index) - zero_factor = np.zeros_like(gamma_factor) + zero_index = xp.zeros_like(gamma_index) + zero_factor = xp.zeros_like(gamma_factor) # Configuration parameters. visibility_index, visibility_factor = linear_interpolation_index_and_factor( @@ -1059,44 +1088,52 @@ def _evaluate_sky_model( parameters.altitude, dataset.altitudes_radiance ) elevation_index, elevation_factor = linear_interpolation_index_and_factor( - np.degrees(elevation), dataset.elevations_radiance + xp_degrees(elevation), dataset.elevations_radiance ) + visibility_index = xp_as_int_array(visibility_index, xp=xp) + visibility_factor = xp_as_float_array(visibility_factor, xp=xp) + albedo_index = xp_as_int_array(albedo_index, xp=xp) + albedo_factor = xp_as_float_array(albedo_factor, xp=xp) + altitude_index = xp_as_int_array(altitude_index, xp=xp) + altitude_factor = xp_as_float_array(altitude_factor, xp=xp) + elevation_index = xp_as_int_array(elevation_index, xp=xp) + elevation_factor = xp_as_float_array(elevation_factor, xp=xp) + # Broadcast all parameter arrays to the common batch shape. - shape = np.broadcast_shapes( - np.shape(gamma_index), - np.shape(alpha_index), - np.shape(zero_index), - np.shape(visibility_index), - np.shape(albedo_index), - np.shape(altitude_index), - np.shape(elevation_index), + shape = xp.broadcast_shapes( + gamma_index.shape, + alpha_index.shape, + zero_index.shape, + visibility_index.shape, + albedo_index.shape, + altitude_index.shape, + elevation_index.shape, ) - gamma_index = np.broadcast_to(gamma_index, shape) - gamma_factor = np.broadcast_to(gamma_factor, shape) - alpha_index = np.broadcast_to(alpha_index, shape) - alpha_factor = np.broadcast_to(alpha_factor, shape) - zero_index = np.broadcast_to(zero_index, shape) - zero_factor = np.broadcast_to(zero_factor, shape) - visibility_index = np.broadcast_to(visibility_index, shape) - visibility_factor = np.broadcast_to(visibility_factor, shape) - albedo_index = np.broadcast_to(albedo_index, shape) - albedo_factor = np.broadcast_to(albedo_factor, shape) - altitude_index = np.broadcast_to(altitude_index, shape) - altitude_factor = np.broadcast_to(altitude_factor, shape) - elevation_index = np.broadcast_to(elevation_index, shape) - elevation_factor = np.broadcast_to(elevation_factor, shape) + gamma_index = xp_broadcast_to(gamma_index, shape, xp=xp) + gamma_factor = xp_broadcast_to(gamma_factor, shape, xp=xp) + alpha_index = xp_broadcast_to(alpha_index, shape, xp=xp) + alpha_factor = xp_broadcast_to(alpha_factor, shape, xp=xp) + zero_index = xp_broadcast_to(zero_index, shape, xp=xp) + zero_factor = xp_broadcast_to(zero_factor, shape, xp=xp) + visibility_index = xp_broadcast_to(visibility_index, shape, xp=xp) + visibility_factor = xp_broadcast_to(visibility_factor, shape, xp=xp) + albedo_index = xp_broadcast_to(albedo_index, shape, xp=xp) + albedo_factor = xp_broadcast_to(albedo_factor, shape, xp=xp) + altitude_index = xp_broadcast_to(altitude_index, shape, xp=xp) + altitude_factor = xp_broadcast_to(altitude_factor, shape, xp=xp) + elevation_index = xp_broadcast_to(elevation_index, shape, xp=xp) + elevation_factor = xp_broadcast_to(elevation_factor, shape, xp=xp) # Filter wavelength within dataset range. wavelength_end = dataset.channel_start + dataset.channels * dataset.channel_width valid = (wavelength >= dataset.channel_start) & (wavelength < wavelength_end) - if not np.any(valid): - return np.zeros((*shape, wavelength_count), dtype=np.float64) + if not bool(xp.any(valid)): + return xp.zeros((*shape, wavelength_count), dtype=wavelength.dtype) channel_indices = as_int_array( - np.floor((wavelength[valid] - dataset.channel_start) / dataset.channel_width) + xp.floor((wavelength[valid] - dataset.channel_start) / dataset.channel_width) ) - n_valid = len(channel_indices) # Precompute strides for offset calculation. elevation_count = len(dataset.elevations_radiance) @@ -1104,19 +1141,25 @@ def _evaluate_sky_model( albedo_count = len(dataset.albedos_radiance) total_coefficients_single = metadata.total_coefficients_single_configuration - # 4D hypercube corners over (visibility, albedo, altitude, elevation). - grid_shape = (16, *shape, n_valid) - grid = np.zeros(grid_shape, dtype=np.float64) + # 16-point interpolation grid over (visibility, albedo, altitude, elevation), + # shape ``(16, *S, n_valid)``. Building via ``stack`` rather than indexed + # assignment keeps the path Array API compatible across backends. + grid_parts = [] for i in range(16): - visibility_grid_index = np.minimum( - visibility_index + i // 8, len(dataset.visibilities_radiance) - 1 + visibility_grid_index = xp.minimum( + visibility_index + i // 8, + xp_as_int_array(len(dataset.visibilities_radiance) - 1, xp=xp), + ) + albedo_grid_index = xp.minimum( + albedo_index + (i % 8) // 4, xp_as_int_array(albedo_count - 1, xp=xp) ) - albedo_grid_index = np.minimum(albedo_index + (i % 8) // 4, albedo_count - 1) - altitude_grid_index = np.minimum( - altitude_index + (i % 4) // 2, altitude_count - 1 + altitude_grid_index = xp.minimum( + altitude_index + (i % 4) // 2, xp_as_int_array(altitude_count - 1, xp=xp) + ) + elevation_grid_index = xp.minimum( + elevation_index + i % 2, xp_as_int_array(elevation_count - 1, xp=xp) ) - elevation_grid_index = np.minimum(elevation_index + i % 2, elevation_count - 1) offsets = total_coefficients_single * ( channel_indices @@ -1133,18 +1176,22 @@ def _evaluate_sky_model( * visibility_grid_index[..., None] ) - grid[i] = _reconstruct_sky_model( - data, - offsets, - gamma_index, - gamma_factor, - alpha_index, - alpha_factor, - zero_index, - zero_factor, - metadata, + grid_parts.append( + _reconstruct_sky_model( + data, + offsets, + gamma_index, + gamma_factor, + alpha_index, + alpha_factor, + zero_index, + zero_factor, + metadata, + ) ) + grid = xp.stack(grid_parts) + # 4-level hierarchical interpolation (elevation, altitude, albedo, # visibility). factors = [elevation_factor, altitude_factor, albedo_factor, visibility_factor] @@ -1155,11 +1202,17 @@ def _evaluate_sky_model( result_valid = grid[0] - # Place valid wavelength into full result. - result = np.zeros((*shape, wavelength_count), dtype=np.float64) - result[..., valid] = result_valid + # Scatter valid wavelength results into full-size output by padding + # ``result_valid`` with a zero column that invalid wavelengths index + # into via the cumulative-sum lookup below. + padding = xp.zeros((*result_valid.shape[:-1], 1), dtype=result_valid.dtype) + padded = xp.concat([result_valid, padding], axis=-1) - return result + n_valid = result_valid.shape[-1] + valid_indices = xp.cumsum(xp_astype(valid, DTYPE_INT_DEFAULT, xp=xp), axis=0) - 1 + lookup = xp.where(valid, valid_indices, n_valid) + + return xp.take(padded, lookup, axis=-1) def sky_radiance_Wilkie2021( @@ -1228,45 +1281,56 @@ def sun_radiance_Wilkie2021( wavelength = as_float_array(wavelength) gamma = as_float_array(parameters.gamma) - shape = np.shape(gamma) + + xp = array_namespace(wavelength, gamma) + + shape = gamma.shape wavelength_count = len(wavelength) - result = np.zeros((*shape, wavelength_count), dtype=np.float64) + result = xp.zeros((*shape, wavelength_count), dtype=gamma.dtype) # Mask: only directions hitting the sun disk. hits_sun = gamma <= CONSTANTS_WILKIE2021.sun_radius - if not np.any(hits_sun): + if not bool(xp.any(hits_sun)): return result valid_wavelength = (wavelength >= CONSTANTS_WILKIE2021.sun_radiance_start) & ( wavelength < CONSTANTS_WILKIE2021.sun_radiance_end ) - if not np.any(valid_wavelength): + if not bool(xp.any(valid_wavelength)): return result # Interpolate solar radiance from table. index_float = ( wavelength[valid_wavelength] - CONSTANTS_WILKIE2021.sun_radiance_start ) / CONSTANTS_WILKIE2021.sun_radiance_step - index_integer = as_int_array(np.floor(index_float)) + index_integer = as_int_array(xp.floor(index_float)) index_fraction = index_float - index_integer - index_integer = np.clip(index_integer, 0, len(SUN_RAD_TABLE) - 2) + index_integer = xp.clip(index_integer, 0, len(SUN_RAD_TABLE) - 2) + sun_rad_table = xp_as_float_array(SUN_RAD_TABLE, xp=xp, like=wavelength) sun_radiance_value = ( - SUN_RAD_TABLE[index_integer] * (1.0 - index_fraction) - + SUN_RAD_TABLE[index_integer + 1] * index_fraction + sun_rad_table[index_integer] * (1.0 - index_fraction) + + sun_rad_table[index_integer + 1] * index_fraction ) # Compute transmittance towards the sun. transmittance = sky_transmittance_Wilkie2021( - dataset, parameters, wavelength[valid_wavelength], np.inf + dataset, parameters, wavelength[valid_wavelength], float("inf") ) - result[..., valid_wavelength] = sun_radiance_value * transmittance - result *= hits_sun[..., None] + result = sun_radiance_value * transmittance - return result + padding = xp.zeros((*result.shape[:-1], 1), dtype=result.dtype) + padded = xp.concat([result, padding], axis=-1) + n_valid = result.shape[-1] + valid_indices = ( + xp.cumsum(xp_astype(valid_wavelength, DTYPE_INT_DEFAULT, xp=xp), axis=0) - 1 + ) + lookup = xp.where(valid_wavelength, valid_indices, n_valid) + + return xp.take(padded, lookup, axis=-1) * hits_sun[..., None] def sky_polarisation_Wilkie2021( @@ -1322,19 +1386,26 @@ def _compute_transmittance_interpolation( """Compute transmittance-specific interpolation index and factor.""" value = as_float_array(value) - index = np.minimum(as_int_array(value * count), count - 1) + + xp = array_namespace(value) + + index = xp.minimum(as_int_array(value * count), xp_as_int_array(count - 1, xp=xp)) lower = index / count upper = (index + 1) / count denominator = upper**power - lower**power - factor = np.where( + factor = xp.where( (index < count - 1) & (denominator != 0), - np.clip( + xp.clip( (value**power - lower**power) - / np.where(denominator == 0, 1.0, denominator), + / xp.where( + xp_as_array(denominator == 0, xp=xp), + xp_as_float_array(1.0, xp=xp), + denominator, + ), 0.0, 1.0, ), - 0.0, + xp_as_float_array(0.0, xp=xp), ) return index, factor @@ -1350,19 +1421,26 @@ def _compute_transmittance_parameters( theta = as_float_array(theta) altitude = as_float_array(altitude) - ray_direction_x = np.sin(theta) - ray_direction_y = np.cos(theta) + xp = array_namespace(theta, altitude) + + altitude = xp_as_float_array(altitude, xp=xp, like=theta) + + ray_direction_x = xp.sin(theta) + ray_direction_y = xp.cos(theta) ray_position_y = CONSTANTS_WILKIE2021.planet_radius + altitude - shape = np.broadcast_shapes(np.shape(theta), np.shape(altitude)) - ray_origin = np.stack( - [np.broadcast_to(0.0, shape), np.broadcast_to(ray_position_y, shape)], + shape = xp.broadcast_shapes(theta.shape, altitude.shape) + ray_origin = xp.stack( + [ + xp_broadcast_to(xp_as_float_array(0.0, xp=xp, like=theta), shape, xp=xp), + xp_broadcast_to(ray_position_y, shape, xp=xp), + ], axis=-1, ) - ray_direction = np.stack( + ray_direction = xp.stack( [ - np.broadcast_to(ray_direction_x, shape), - np.broadcast_to(ray_direction_y, shape), + xp_broadcast_to(ray_direction_x, shape, xp=xp), + xp_broadcast_to(ray_direction_y, shape, xp=xp), ], axis=-1, ) @@ -1377,7 +1455,9 @@ def _compute_transmittance_parameters( distance_low_atmosphere = intersect_ray_circle_2d( ray_origin, ray_direction, atmosphere_edge ) - distance_low = np.where(theta <= 0.5 * np.pi, distance_low_atmosphere, 0.0) + distance_low = xp.where( + theta <= 0.5 * xp.pi, distance_low_atmosphere, xp_as_float_array(0.0, xp=xp) + ) # High altitude: planet first, atmosphere edge if planet missed. distance_planet = intersect_ray_circle_2d( @@ -1386,21 +1466,23 @@ def _compute_transmittance_parameters( distance_atmosphere = intersect_ray_circle_2d( ray_origin, ray_direction, atmosphere_edge ) - distance_high = np.where( - np.isnan(distance_planet), distance_atmosphere, distance_planet + distance_high = xp.where( + xp.isnan(distance_planet), distance_atmosphere, distance_planet ) - distance_to_intersection = np.where(is_low_altitude, distance_low, distance_high) - distance_to_intersection = np.minimum(distance_to_intersection, distance) + distance_to_intersection = xp.where(is_low_altitude, distance_low, distance_high) + distance_to_intersection = xp.minimum( + distance_to_intersection, xp_as_float_array(distance, xp=xp) + ) intersection_x = ray_direction_x * distance_to_intersection intersection_y = ray_direction_y * distance_to_intersection + ray_position_y - intersection_distance = np.sqrt( + intersection_distance = xp.sqrt( intersection_x * intersection_x + intersection_y * intersection_y ) - altitude_parameter = np.clip( + altitude_parameter = xp.clip( intersection_distance - CONSTANTS_WILKIE2021.planet_radius, 0.0, CONSTANTS_WILKIE2021.atmosphere_width, @@ -1410,11 +1492,12 @@ def _compute_transmittance_parameters( ) ** (1.0 / 3.0) distance_parameter = ( - np.arccos( - np.clip( + xp.acos( + xp.clip( intersection_y - / np.maximum( - intersection_distance, CONSTANTS_WILKIE2021.distance_epsilon + / xp.maximum( + intersection_distance, + xp_as_float_array(CONSTANTS_WILKIE2021.distance_epsilon, xp=xp), ), -1, 1, @@ -1422,11 +1505,11 @@ def _compute_transmittance_parameters( ) * CONSTANTS_WILKIE2021.planet_radius ) - distance_parameter = np.sqrt( + distance_parameter = xp.sqrt( distance_parameter / CONSTANTS_WILKIE2021.distance_to_edge ) - distance_parameter = np.sqrt(distance_parameter) - distance_parameter = np.minimum(1.0, distance_parameter) + distance_parameter = xp.sqrt(distance_parameter) + distance_parameter = xp.minimum(xp_as_float_array(1.0, xp=xp), distance_parameter) altitude_interpolation = _compute_transmittance_interpolation( altitude_parameter, dataset.altitude_dimension, 3 @@ -1457,6 +1540,8 @@ def _reconstruct_transmittance( altitude_i, altitude_f = altitude_interpolation distance_i, distance_f = distance_interpolation + xp = array_namespace(altitude_i, distance_i, visibility_index) + altitude_d = dataset.altitude_dimension distance_d = dataset.distance_dimension rank = dataset.rank_transmittance @@ -1472,17 +1557,16 @@ def _reconstruct_transmittance( + channel_indices ) * rank - v_coefficient_offsets = v_coefficient_base[..., None] + np.arange(rank) - v_coefficients = as_float_array( - dataset.data_transmittance_v[as_int_array(v_coefficient_offsets)] + v_coefficient_offsets = v_coefficient_base[..., None] + xp.arange(rank) + v_coefficients = xp_as_float_array( + dataset.data_transmittance_v[as_ndarray(as_int_array(v_coefficient_offsets))], + xp=xp, + like=altitude_i, ) # U-coefficients: iterate 2x2 grid (altitude x distance). - transmittance = np.zeros( - (*np.shape(altitude_i), len(channel_indices), 4), dtype=np.float64 - ) + transmittance_parts = [] - grid_index = 0 for altitude_offset in range(2): a = altitude_i + altitude_offset altitude_valid = a < altitude_d @@ -1494,25 +1578,30 @@ def _reconstruct_transmittance( altitude_index[..., None] * altitude_d * distance_d * rank + (d[..., None] * altitude_d + a[..., None]) * rank ) - u_coefficient_offsets = u_coefficient_base[..., None] + np.arange(rank) + u_coefficient_offsets = u_coefficient_base[..., None] + xp.arange(rank) - safe_u_offsets = np.clip( + safe_u_offsets = xp.clip( u_coefficient_offsets, 0, len(dataset.data_transmittance_u) - 1 ) - u_coefficients = as_float_array( - dataset.data_transmittance_u[as_int_array(safe_u_offsets)] + u_coefficients = xp_as_float_array( + dataset.data_transmittance_u[as_ndarray(as_int_array(safe_u_offsets))], + xp=xp, + like=altitude_i, ) - dot = np.sum(u_coefficients * v_coefficients, axis=-1) + dot = xp.sum(u_coefficients * v_coefficients, axis=-1) mask = altitude_valid & distance_valid - transmittance[..., grid_index] = np.where(mask[..., None], dot, 0.0) - grid_index += 1 + transmittance_parts.append( + xp.where(mask[..., None], dot, xp_as_float_array(0.0, xp=xp)) + ) + + transmittance = xp.stack(transmittance_parts, axis=-1) # Bilinear interpolation over distance then altitude. low = lerp(distance_f[..., None], transmittance[..., 0], transmittance[..., 1]) high = lerp(distance_f[..., None], transmittance[..., 2], transmittance[..., 3]) - low = np.maximum(low, 0.0) - high = np.maximum(high, 0.0) + low = xp.maximum(low, xp_as_float_array(0.0, xp=xp)) + high = xp.maximum(high, xp_as_float_array(0.0, xp=xp)) return lerp(altitude_f[..., None], low, high) @@ -1548,18 +1637,21 @@ def sky_transmittance_Wilkie2021( """ wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength) + wavelength_count = len(wavelength) - theta = as_float_array(parameters.theta) - shape = np.shape(theta) + theta = xp_as_float_array(parameters.theta, xp=xp, like=wavelength) + shape = theta.shape wavelength_end = dataset.channel_start + dataset.channels * dataset.channel_width valid = (wavelength >= dataset.channel_start) & (wavelength < wavelength_end) - if not np.any(valid): - return np.zeros((*shape, wavelength_count), dtype=np.float64) + if not bool(xp.any(valid)): + return xp.zeros((*shape, wavelength_count), dtype=wavelength.dtype) channel_indices = as_int_array( - np.floor((wavelength[valid] - dataset.channel_start) / dataset.channel_width) + xp.floor((wavelength[valid] - dataset.channel_start) / dataset.channel_width) ) visibility_index, visibility_factor = linear_interpolation_index_and_factor( @@ -1570,15 +1662,20 @@ def sky_transmittance_Wilkie2021( ) # Broadcast config indices to common shape. - shape = np.broadcast_shapes( - np.shape(theta), - np.shape(visibility_index), - np.shape(altitude_index), + visibility_index = xp_as_float_array(visibility_index, xp=xp, like=wavelength) + visibility_factor = xp_as_float_array(visibility_factor, xp=xp, like=wavelength) + altitude_index = xp_as_float_array(altitude_index, xp=xp, like=wavelength) + altitude_factor = xp_as_float_array(altitude_factor, xp=xp, like=wavelength) + + shape = xp.broadcast_shapes( + theta.shape, + visibility_index.shape, + altitude_index.shape, ) - visibility_index = np.broadcast_to(visibility_index, shape) - visibility_factor = np.broadcast_to(visibility_factor, shape) - altitude_index = np.broadcast_to(altitude_index, shape) - altitude_factor = np.broadcast_to(altitude_factor, shape) + visibility_index = xp_broadcast_to(visibility_index, shape, xp=xp) + visibility_factor = xp_broadcast_to(visibility_factor, shape, xp=xp) + altitude_index = xp_broadcast_to(altitude_index, shape, xp=xp) + altitude_factor = xp_broadcast_to(altitude_factor, shape, xp=xp) altitude_interpolation, distance_interpolation = _compute_transmittance_parameters( dataset, parameters.theta, distance, parameters.altitude @@ -1597,7 +1694,10 @@ def sky_transmittance_Wilkie2021( transmittance_altitude_high = _reconstruct_transmittance( dataset, visibility_index, - np.minimum(altitude_index + 1, len(dataset.altitudes_transmittance) - 1), + xp.minimum( + altitude_index + 1, + xp_as_int_array(len(dataset.altitudes_transmittance) - 1, xp=xp), + ), altitude_interpolation, distance_interpolation, channel_indices, @@ -1608,7 +1708,10 @@ def sky_transmittance_Wilkie2021( transmittance_visibility_high = _reconstruct_transmittance( dataset, - np.minimum(visibility_index + 1, len(dataset.visibilities_transmittance) - 1), + xp.minimum( + visibility_index + 1, + xp_as_int_array(len(dataset.visibilities_transmittance) - 1, xp=xp), + ), altitude_index, altitude_interpolation, distance_interpolation, @@ -1616,8 +1719,14 @@ def sky_transmittance_Wilkie2021( ) transmittance_visibility_altitude_high = _reconstruct_transmittance( dataset, - np.minimum(visibility_index + 1, len(dataset.visibilities_transmittance) - 1), - np.minimum(altitude_index + 1, len(dataset.altitudes_transmittance) - 1), + xp.minimum( + visibility_index + 1, + xp_as_int_array(len(dataset.visibilities_transmittance) - 1, xp=xp), + ), + xp.minimum( + altitude_index + 1, + xp_as_int_array(len(dataset.altitudes_transmittance) - 1, xp=xp), + ), altitude_interpolation, distance_interpolation, channel_indices, @@ -1633,9 +1742,12 @@ def sky_transmittance_Wilkie2021( # Transmittance is stored as square root. transmittance = transmittance * transmittance - transmittance = np.clip(transmittance, 0.0, 1.0) + transmittance = xp.clip(transmittance, 0.0, 1.0) - result = np.zeros((*shape, wavelength_count), dtype=np.float64) - result[..., valid] = transmittance + padding = xp.zeros((*transmittance.shape[:-1], 1), dtype=transmittance.dtype) + padded = xp.concat([transmittance, padding], axis=-1) + n_valid = transmittance.shape[-1] + valid_indices = xp.cumsum(xp_astype(valid, DTYPE_INT_DEFAULT, xp=xp), axis=0) - 1 + lookup = xp.where(valid, valid_indices, n_valid) - return result + return xp.take(padded, lookup, axis=-1) diff --git a/colour/phenomena/tests/test_interference.py b/colour/phenomena/tests/test_interference.py index f029b192c0..c359f56fce 100644 --- a/colour/phenomena/tests/test_interference.py +++ b/colour/phenomena/tests/test_interference.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -11,7 +16,13 @@ multilayer_tmm, thin_film_tmm, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -34,32 +45,36 @@ class TestLightWaterMolarRefractionSchiebener1990: light_water_molar_refraction_Schiebener1990` definition unit tests methods. """ - def test_light_water_molar_refraction_Schiebener1990(self) -> None: + def test_light_water_molar_refraction_Schiebener1990(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.\ light_water_molar_refraction_Schiebener1990` definition. """ - np.testing.assert_allclose( - light_water_molar_refraction_Schiebener1990(589), + xp_assert_close( + light_water_molar_refraction_Schiebener1990(xp_as_array([589.0], xp=xp)), 0.206211470522, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - light_water_molar_refraction_Schiebener1990(400, 300, 1000), + xp_assert_close( + light_water_molar_refraction_Schiebener1990( + xp_as_array([400.0], xp=xp), 300, 1000 + ), 0.211842881763, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - light_water_molar_refraction_Schiebener1990(700, 280, 998), + xp_assert_close( + light_water_molar_refraction_Schiebener1990( + xp_as_array([700.0], xp=xp), 280, 998 + ), 0.204829756928, atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_n_dimensional_light_water_molar_refraction_Schiebener1990( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.phenomena.interference.\ @@ -69,25 +84,25 @@ def test_n_dimensional_light_water_molar_refraction_Schiebener1990( wl = 589 LL = light_water_molar_refraction_Schiebener1990(wl) - wl = np.tile(wl, 6) - LL = np.tile(LL, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + LL = xp.tile(xp_as_array(LL, xp=xp), (6,)) + xp_assert_close( light_water_molar_refraction_Schiebener1990(wl), LL, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - LL = np.reshape(LL, (2, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + LL = xp_reshape(xp_as_array(LL, xp=xp), (2, 3), xp=xp) + xp_assert_close( light_water_molar_refraction_Schiebener1990(wl), LL, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - LL = np.reshape(LL, (2, 3, 1)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + LL = xp_reshape(xp_as_array(LL, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( light_water_molar_refraction_Schiebener1990(wl), LL, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -111,32 +126,32 @@ class TestLightWaterRefractiveIndexSchiebener1990: light_water_refractive_index_Schiebener1990` definition unit tests methods. """ - def test_light_water_refractive_index_Schiebener1990(self) -> None: + def test_light_water_refractive_index_Schiebener1990(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.\ light_water_refractive_index_Schiebener1990` definition. """ - np.testing.assert_allclose( - light_water_refractive_index_Schiebener1990(400), + xp_assert_close( + light_water_refractive_index_Schiebener1990(xp_as_array([400.0], xp=xp)), 1.344143366618, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - light_water_refractive_index_Schiebener1990(500), + xp_assert_close( + light_water_refractive_index_Schiebener1990(xp_as_array([500.0], xp=xp)), 1.337363795367, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - light_water_refractive_index_Schiebener1990(600), + xp_assert_close( + light_water_refractive_index_Schiebener1990(xp_as_array([600.0], xp=xp)), 1.333585122179, atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_n_dimensional_light_water_refractive_index_Schiebener1990( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.phenomena.interference.\ @@ -146,25 +161,25 @@ def test_n_dimensional_light_water_refractive_index_Schiebener1990( wl = 400 n = light_water_refractive_index_Schiebener1990(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close( light_water_refractive_index_Schiebener1990(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close( light_water_refractive_index_Schiebener1990(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( light_water_refractive_index_Schiebener1990(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -188,36 +203,43 @@ class TestThinFilmTmm: definition unit tests methods. """ - def test_thin_film_tmm(self) -> None: + def test_thin_film_tmm(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.thin_film_tmm` definition. """ # Test single wavelength - returns (R, T) tuple - R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, 500) + R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, xp_as_array(500.0, xp=xp)) + R, T = as_ndarray(R), as_ndarray(T) assert R.shape == (1, 1, 1, 2) # (W, A, T, 2) - [R_s, R_p] assert T.shape == (1, 1, 1, 2) # (W, A, T, 2) - [T_s, T_p] assert np.all((R >= 0) & (R <= 1)) assert np.all((T >= 0) & (T <= 1)) # Test energy conservation - np.testing.assert_allclose(R + T, 1.0, atol=1e-6) + xp_assert_close(R + T, 1.0, atol=TOLERANCE_ABSOLUTE_TESTS * 10) # Test multiple wavelengths - R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, [400, 500, 600]) + R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, xp_as_array([400, 500, 600], xp=xp)) + R, T = as_ndarray(R), as_ndarray(T) assert R.shape == (3, 1, 1, 2) # (W=3, A=1, T=1, 2) - Spectroscopy Convention assert T.shape == (3, 1, 1, 2) assert np.all((R >= 0) & (R <= 1)) assert np.all((T >= 0) & (T <= 1)) # Test that s and p polarisations are similar at normal incidence - R_normal, _ = thin_film_tmm([1.0, 1.5, 1.0], 250, 500, theta=0) - np.testing.assert_allclose( - R_normal[0, 0, 0, 0], R_normal[0, 0, 0, 1], atol=1e-10 + R_normal, _ = thin_film_tmm( + [1.0, 1.5, 1.0], 250, xp_as_array(500.0, xp=xp), theta=0 + ) + R_normal = as_ndarray(R_normal) + xp_assert_close( + R_normal[0, 0, 0, 0], + R_normal[0, 0, 0, 1], + atol=TOLERANCE_ABSOLUTE_TESTS * 0.001, ) - def test_n_dimensional_thin_film_tmm(self) -> None: + def test_n_dimensional_thin_film_tmm(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.thin_film_tmm` definition n-dimensional arrays support. @@ -226,12 +248,12 @@ def test_n_dimensional_thin_film_tmm(self) -> None: wl = 555 R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, wl) - wl = np.tile(wl, 6) - R = np.tile(R, (6, 1, 1, 1)) - T = np.tile(T, (6, 1, 1, 1)) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + R = xp.tile(xp_as_array(R, xp=xp), (6, 1, 1, 1)) + T = xp.tile(xp_as_array(T, xp=xp), (6, 1, 1, 1)) R_array, T_array = thin_film_tmm([1.0, 1.5, 1.0], 250, wl) - np.testing.assert_allclose(R_array, R, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(T_array, T, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(R_array, R, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(T_array, T, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_thin_film_tmm(self) -> None: @@ -246,7 +268,7 @@ def test_nan_thin_film_tmm(self) -> None: np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), ) - def test_thin_film_tmm_complex_n(self) -> None: + def test_thin_film_tmm_complex_n(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.thin_film_tmm` with complex refractive indices (absorbing layers). @@ -254,25 +276,27 @@ def test_thin_film_tmm_complex_n(self) -> None: # Absorbing layer: n = 2.0 + 0.5j n_absorbing = 2.0 + 0.5j - R, T = thin_film_tmm([1.0, n_absorbing, 1.0], 250, 500) + R, T = thin_film_tmm([1.0, n_absorbing, 1.0], 250, xp_as_array([500.0], xp=xp)) assert R.shape == (1, 1, 1, 2) assert T.shape == (1, 1, 1, 2) - assert np.all((R >= 0) & (R <= 1)) - assert np.all((T >= 0) & (T <= 1)) + assert np.all((as_ndarray(R) >= 0) & (as_ndarray(R) <= 1)) + assert np.all((as_ndarray(T) >= 0) & (as_ndarray(T) <= 1)) # For absorbing media: R + T < 1 (absorption A = 1 - R - T > 0) - R_avg = np.mean(R) - T_avg = np.mean(T) + R_avg = np.mean(as_ndarray(R)) + T_avg = np.mean(as_ndarray(T)) A = 1 - R_avg - T_avg assert A > 0, f"Expected absorption > 0, got A = {A}" # Silver mirror: n ≈ 0.18 + 3.15j at 500nm n_silver = 0.18 + 3.15j - R_silver, _ = thin_film_tmm([1.0, n_silver, 1.0], 50, 500) + R_silver, _ = thin_film_tmm( + [1.0, n_silver, 1.0], 50, xp_as_array([500.0], xp=xp) + ) # Silver should have high reflectance - assert np.mean(R_silver) > 0.5 + assert np.mean(as_ndarray(R_silver)) > 0.5 class TestMultilayerTmm: @@ -281,34 +305,44 @@ class TestMultilayerTmm: definition unit tests methods. """ - def test_multilayer_tmm(self) -> None: + def test_multilayer_tmm(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.multilayer_tmm` definition. """ # Test single layer (should match thin_film_tmm) - R_multi, T_multi = multilayer_tmm([1.0, 1.5, 1.0], [250], 500) - R_single, T_single = thin_film_tmm([1.0, 1.5, 1.0], 250, 500) - np.testing.assert_allclose(R_multi, R_single, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(T_multi, T_single, atol=TOLERANCE_ABSOLUTE_TESTS) + R_multi, T_multi = multilayer_tmm( + [1.0, 1.5, 1.0], [250], xp_as_array(500.0, xp=xp) + ) + R_single, T_single = thin_film_tmm( + [1.0, 1.5, 1.0], 250, xp_as_array(500.0, xp=xp) + ) + xp_assert_close(R_multi, as_ndarray(R_single), atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(T_multi, as_ndarray(T_single), atol=TOLERANCE_ABSOLUTE_TESTS) # Test multiple layers - R, T = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], [400, 500, 600]) + R, T = multilayer_tmm( + [1.0, 1.5, 2.0, 1.0], + [250, 150], + xp_as_array([400, 500, 600], xp=xp), + ) + R, T = as_ndarray(R), as_ndarray(T) assert R.shape == (3, 1, 1, 2) # (W=3, A=1, T=1, 2) - Spectroscopy Convention assert T.shape == (3, 1, 1, 2) assert np.all((R >= 0) & (R <= 1)) assert np.all((T >= 0) & (T <= 1)) # Test energy conservation - np.testing.assert_allclose(R + T, 1.0, atol=1e-6) + xp_assert_close(R + T, 1.0, atol=TOLERANCE_ABSOLUTE_TESTS * 10) # Test with different substrate - R_sub, T_sub = multilayer_tmm([1.0, 1.5, 1.5], [250], 500) + R_sub, T_sub = multilayer_tmm([1.0, 1.5, 1.5], [250], xp_as_array(500.0, xp=xp)) + R_sub, T_sub = as_ndarray(R_sub), as_ndarray(T_sub) assert R_sub.shape == (1, 1, 1, 2) assert T_sub.shape == (1, 1, 1, 2) - def test_n_dimensional_multilayer_tmm(self) -> None: + def test_n_dimensional_multilayer_tmm(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.multilayer_tmm` definition n-dimensional arrays support. @@ -317,12 +351,12 @@ def test_n_dimensional_multilayer_tmm(self) -> None: wl = 555 R, T = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], wl) - wl = np.tile(wl, 6) - R = np.tile(R, (6, 1, 1, 1)) - T = np.tile(T, (6, 1, 1, 1)) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + R = xp.tile(xp_as_array(R, xp=xp), (6, 1, 1, 1)) + T = xp.tile(xp_as_array(T, xp=xp), (6, 1, 1, 1)) R_array, T_array = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], wl) - np.testing.assert_allclose(R_array, R, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(T_array, T, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(R_array, R, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(T_array, T, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_multilayer_tmm(self) -> None: @@ -337,7 +371,7 @@ def test_nan_multilayer_tmm(self) -> None: np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), ) - def test_multilayer_tmm_complex_n(self) -> None: + def test_multilayer_tmm_complex_n(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.multilayer_tmm` with complex refractive indices. @@ -346,9 +380,10 @@ def test_multilayer_tmm_complex_n(self) -> None: # Stack of two absorbing layers: air | layer1 | layer2 | air n_layers = [1.0, 2.0 + 0.5j, 1.8 + 0.3j, 1.0] thicknesses = [200, 300] - wavelengths = np.array([400, 500, 600]) + wavelengths = xp_as_array([400, 500, 600], xp=xp) R, T = multilayer_tmm(n_layers, thicknesses, wavelengths) + R, T = as_ndarray(R), as_ndarray(T) # Check shapes and validity assert R.shape == (3, 1, 1, 2) # (W=3, A=1, T=1, 2) - Spectroscopy Convention @@ -367,7 +402,7 @@ def test_multilayer_tmm_complex_n(self) -> None: # High reflectance expected for metal assert np.mean(R_metal) > 0.5 - def test_multilayer_tmm_mixed_structures(self) -> None: + def test_multilayer_tmm_mixed_structures(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.interference.multilayer_tmm` with mixed transparent and absorbing layers. @@ -404,11 +439,12 @@ def test_multilayer_tmm_mixed_structures(self) -> None: 1.0, ] # air, Real, Complex, Real, air thicknesses_mixed = [150, 200, 250] - wavelengths = np.array([450, 550, 650]) + wavelengths = xp_as_array([450, 550, 650], xp=xp) R_mixed, T_mixed = multilayer_tmm( n_layers_mixed, thicknesses_mixed, wavelengths ) + R_mixed, T_mixed = as_ndarray(R_mixed), as_ndarray(T_mixed) # Check shapes and validity assert R_mixed.shape == ( @@ -428,4 +464,4 @@ def test_multilayer_tmm_mixed_structures(self) -> None: R_real, T_real = multilayer_tmm(n_layers_real, thicknesses_real, 550) # For lossless media: R + T = 1 - np.testing.assert_allclose(R_real + T_real, 1.0, atol=1e-6) + xp_assert_close(R_real + T_real, 1.0, atol=TOLERANCE_ABSOLUTE_TESTS * 10) diff --git a/colour/phenomena/tests/test_rayleigh.py b/colour/phenomena/tests/test_rayleigh.py index 75dc4b3933..244dd66dff 100644 --- a/colour/phenomena/tests/test_rayleigh.py +++ b/colour/phenomena/tests/test_rayleigh.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -25,7 +30,12 @@ mean_molecular_weights, molecular_density, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -485,31 +495,33 @@ class TestAirRefractionIndexPenndorf1957: air_refraction_index_Penndorf1957` definition unit tests methods. """ - def test_air_refraction_index_Penndorf1957(self) -> None: + def test_air_refraction_index_Penndorf1957(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.\ air_refraction_index_Penndorf1957` definition. """ - np.testing.assert_allclose( - air_refraction_index_Penndorf1957(0.360), + xp_assert_close( + air_refraction_index_Penndorf1957(xp_as_array([0.360], xp=xp)), 1.000285316795146, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Penndorf1957(0.555), + xp_assert_close( + air_refraction_index_Penndorf1957(xp_as_array([0.555], xp=xp)), 1.000277729533864, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Penndorf1957(0.830), + xp_assert_close( + air_refraction_index_Penndorf1957(xp_as_array([0.830], xp=xp)), 1.000274856640486, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_air_refraction_index_Penndorf1957(self) -> None: + def test_n_dimensional_air_refraction_index_Penndorf1957( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.rayleigh.\ air_refraction_index_Penndorf1957` definition n-dimensional arrays support. @@ -518,25 +530,25 @@ def test_n_dimensional_air_refraction_index_Penndorf1957(self) -> None: wl = 0.360 n = air_refraction_index_Penndorf1957(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close( air_refraction_index_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close( air_refraction_index_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( air_refraction_index_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -560,31 +572,31 @@ class TestAirRefractionIndexEdlen1966: definition unit tests methods. """ - def test_air_refraction_index_Edlen1966(self) -> None: + def test_air_refraction_index_Edlen1966(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.\ rayleigh.air_refraction_index_Edlen1966` definition. """ - np.testing.assert_allclose( - air_refraction_index_Edlen1966(0.360), + xp_assert_close( + air_refraction_index_Edlen1966(xp_as_array([0.360], xp=xp)), 1.000285308809879, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Edlen1966(0.555), + xp_assert_close( + air_refraction_index_Edlen1966(xp_as_array([0.555], xp=xp)), 1.000277727690364, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Edlen1966(0.830), + xp_assert_close( + air_refraction_index_Edlen1966(xp_as_array([0.830], xp=xp)), 1.000274862218835, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_air_refraction_index_Edlen1966(self) -> None: + def test_n_dimensional_air_refraction_index_Edlen1966(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.\ air_refraction_index_Edlen1966` definition n-dimensional arrays support. @@ -593,25 +605,25 @@ def test_n_dimensional_air_refraction_index_Edlen1966(self) -> None: wl = 0.360 n = air_refraction_index_Edlen1966(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close( air_refraction_index_Edlen1966(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close( air_refraction_index_Edlen1966(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( air_refraction_index_Edlen1966(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -635,31 +647,31 @@ class TestAirRefractionIndexPeck1972: definition unit tests methods. """ - def test_air_refraction_index_Peck1972(self) -> None: + def test_air_refraction_index_Peck1972(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.air_refraction_index_Peck1972` definition. """ - np.testing.assert_allclose( - air_refraction_index_Peck1972(0.360), + xp_assert_close( + air_refraction_index_Peck1972(xp_as_array([0.360], xp=xp)), 1.000285310285056, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Peck1972(0.555), + xp_assert_close( + air_refraction_index_Peck1972(xp_as_array([0.555], xp=xp)), 1.000277726541484, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Peck1972(0.830), + xp_assert_close( + air_refraction_index_Peck1972(xp_as_array([0.830], xp=xp)), 1.000274859144804, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_air_refraction_index_Peck1972(self) -> None: + def test_n_dimensional_air_refraction_index_Peck1972(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.air_refraction_index_Peck1972` definition n-dimensional arrays support. @@ -668,22 +680,28 @@ def test_n_dimensional_air_refraction_index_Peck1972(self) -> None: wl = 0.360 n = air_refraction_index_Peck1972(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - air_refraction_index_Peck1972(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close( + air_refraction_index_Peck1972(wl), + n, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - air_refraction_index_Peck1972(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close( + air_refraction_index_Peck1972(wl), + n, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - air_refraction_index_Peck1972(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + air_refraction_index_Peck1972(wl), + n, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -704,49 +722,51 @@ class TestAirRefractionIndexBodhaine1999: air_refraction_index_Bodhaine1999` definition unit tests methods. """ - def test_air_refraction_index_Bodhaine1999(self) -> None: + def test_air_refraction_index_Bodhaine1999(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.\ air_refraction_index_Bodhaine1999` definition. """ - np.testing.assert_allclose( - air_refraction_index_Bodhaine1999(0.360), + xp_assert_close( + air_refraction_index_Bodhaine1999(xp_as_array([0.360], xp=xp)), 1.000285310285056, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Bodhaine1999(0.555), + xp_assert_close( + air_refraction_index_Bodhaine1999(xp_as_array([0.555], xp=xp)), 1.000277726541484, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Bodhaine1999(0.830), + xp_assert_close( + air_refraction_index_Bodhaine1999(xp_as_array([0.830], xp=xp)), 1.000274859144804, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Bodhaine1999(0.360, 0), + xp_assert_close( + air_refraction_index_Bodhaine1999(xp_as_array([0.360], xp=xp), 0), 1.000285264064789, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Bodhaine1999(0.555, 360), + xp_assert_close( + air_refraction_index_Bodhaine1999(xp_as_array([0.555], xp=xp), 360), 1.000277735539824, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - air_refraction_index_Bodhaine1999(0.830, 620), + xp_assert_close( + air_refraction_index_Bodhaine1999(xp_as_array([0.830], xp=xp), 620), 1.000274906640464, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_air_refraction_index_Bodhaine1999(self) -> None: + def test_n_dimensional_air_refraction_index_Bodhaine1999( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.rayleigh.\ air_refraction_index_Bodhaine1999` definition n-dimensional arrays support. @@ -755,25 +775,25 @@ def test_n_dimensional_air_refraction_index_Bodhaine1999(self) -> None: wl = 0.360 n = air_refraction_index_Bodhaine1999(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close( air_refraction_index_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close( air_refraction_index_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, ) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( air_refraction_index_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -796,28 +816,28 @@ class TestN2Depolarisation: unit tests methods. """ - def test_N2_depolarisation(self) -> None: + def test_N2_depolarisation(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.rayleigh.N2_depolarisation` definition.""" - np.testing.assert_allclose( - N2_depolarisation(0.360), + xp_assert_close( + N2_depolarisation(xp_as_array([0.360], xp=xp)), 1.036445987654321, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - N2_depolarisation(0.555), + xp_assert_close( + N2_depolarisation(xp_as_array([0.555], xp=xp)), 1.035029137245354, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - N2_depolarisation(0.830), + xp_assert_close( + N2_depolarisation(xp_as_array([0.830], xp=xp)), 1.034460153868486, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_N2_depolarisation(self) -> None: + def test_n_dimensional_N2_depolarisation(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.N2_depolarisation` definition n-dimensional arrays support. @@ -826,23 +846,17 @@ def test_n_dimensional_N2_depolarisation(self) -> None: wl = 0.360 n = N2_depolarisation(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - N2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close(N2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - N2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close(N2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - N2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(N2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_N2_depolarisation(self) -> None: @@ -860,28 +874,28 @@ class TestO2Depolarisation: unit tests methods. """ - def test_O2_depolarisation(self) -> None: + def test_O2_depolarisation(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.rayleigh.O2_depolarisation` definition.""" - np.testing.assert_allclose( - O2_depolarisation(0.360), + xp_assert_close( + O2_depolarisation(xp_as_array([0.360], xp=xp)), 1.115307746532541, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - O2_depolarisation(0.555), + xp_assert_close( + O2_depolarisation(xp_as_array([0.555], xp=xp)), 1.102022536201071, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - O2_depolarisation(0.830), + xp_assert_close( + O2_depolarisation(xp_as_array([0.830], xp=xp)), 1.098315561269013, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_O2_depolarisation(self) -> None: + def test_n_dimensional_O2_depolarisation(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.O2_depolarisation` definition n-dimensional arrays support. @@ -890,23 +904,17 @@ def test_n_dimensional_O2_depolarisation(self) -> None: wl = 0.360 n = O2_depolarisation(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - O2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close(O2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - O2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close(O2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - O2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(O2_depolarisation(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_O2_depolarisation(self) -> None: @@ -924,15 +932,19 @@ class TestF_airPenndorf1957: unit tests methods. """ - def test_F_air_Penndorf1957(self) -> None: + def test_F_air_Penndorf1957(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.F_air_Penndorf1957` definition. """ - assert F_air_Penndorf1957(0.360) == 1.0608 + xp_assert_close( + F_air_Penndorf1957(xp_as_array([0.360], xp=xp)), + 1.0608, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_n_dimensional_F_air_Penndorf1957(self) -> None: + def test_n_dimensional_F_air_Penndorf1957(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.F_air_Penndorf1957` definition n-dimensional arrays support. @@ -941,23 +953,17 @@ def test_n_dimensional_F_air_Penndorf1957(self) -> None: wl = 0.360 n = F_air_Penndorf1957(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - F_air_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close(F_air_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - F_air_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close(F_air_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - F_air_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(F_air_Penndorf1957(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_F_air_Penndorf1957(self) -> None: @@ -975,12 +981,16 @@ class TestF_airYoung1981: unit tests methods. """ - def test_F_air_Young1981(self) -> None: + def test_F_air_Young1981(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.rayleigh.F_air_Young1981` definition.""" - assert F_air_Young1981(0.360) == 1.0480 + xp_assert_close( + F_air_Young1981(xp_as_array([0.360], xp=xp)), + 1.0480, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_n_dimensional_F_air_Young1981(self) -> None: + def test_n_dimensional_F_air_Young1981(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.F_air_Young1981` definition n-dimensional arrays support. @@ -989,23 +999,17 @@ def test_n_dimensional_F_air_Young1981(self) -> None: wl = 0.360 n = F_air_Young1981(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - F_air_Young1981(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close(F_air_Young1981(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - F_air_Young1981(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close(F_air_Young1981(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - F_air_Young1981(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(F_air_Young1981(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_F_air_Young1981(self) -> None: @@ -1023,28 +1027,28 @@ class TestF_airBates1984: tests methods. """ - def test_F_air_Bates1984(self) -> None: + def test_F_air_Bates1984(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.rayleigh.F_air_Bates1984` definition.""" - np.testing.assert_allclose( - F_air_Bates1984(0.360), + xp_assert_close( + F_air_Bates1984(xp_as_array([0.360], xp=xp)), 1.051997277711708, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bates1984(0.555), + xp_assert_close( + F_air_Bates1984(xp_as_array([0.555], xp=xp)), 1.048153579718658, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bates1984(0.830), + xp_assert_close( + F_air_Bates1984(xp_as_array([0.830], xp=xp)), 1.046947068600589, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_F_air_Bates1984(self) -> None: + def test_n_dimensional_F_air_Bates1984(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.F_air_Bates1984` definition n-dimensional arrays support. @@ -1053,23 +1057,17 @@ def test_n_dimensional_F_air_Bates1984(self) -> None: wl = 0.360 n = F_air_Bates1984(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - F_air_Bates1984(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close(F_air_Bates1984(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - F_air_Bates1984(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close(F_air_Bates1984(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - F_air_Bates1984(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(F_air_Bates1984(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_F_air_Bates1984(self) -> None: @@ -1087,49 +1085,49 @@ class TestF_airBodhaine1999: unit tests methods. """ - def test_F_air_Bodhaine1999(self) -> None: + def test_F_air_Bodhaine1999(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.F_air_Bodhaine1999` definition. """ - np.testing.assert_allclose( - F_air_Bodhaine1999(0.360), + xp_assert_close( + F_air_Bodhaine1999(xp_as_array([0.360], xp=xp)), 1.052659005129014, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bodhaine1999(0.555), + xp_assert_close( + F_air_Bodhaine1999(xp_as_array([0.555], xp=xp)), 1.048769718142427, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bodhaine1999(0.830), + xp_assert_close( + F_air_Bodhaine1999(xp_as_array([0.830], xp=xp)), 1.047548896943893, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bodhaine1999(0.360, 0), + xp_assert_close( + F_air_Bodhaine1999(xp_as_array([0.360], xp=xp), 0), 1.052629792313939, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bodhaine1999(0.555, 360), + xp_assert_close( + F_air_Bodhaine1999(xp_as_array([0.555], xp=xp), 360), 1.048775791959338, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - F_air_Bodhaine1999(0.830, 620), + xp_assert_close( + F_air_Bodhaine1999(xp_as_array([0.830], xp=xp), 620), 1.047581672775155, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_F_air_Bodhaine1999(self) -> None: + def test_n_dimensional_F_air_Bodhaine1999(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.F_air_Bodhaine1999` definition n-dimensional arrays support. @@ -1138,23 +1136,17 @@ def test_n_dimensional_F_air_Bodhaine1999(self) -> None: wl = 0.360 n = F_air_Bodhaine1999(wl) - wl = np.tile(wl, 6) - n = np.tile(n, 6) - np.testing.assert_allclose( - F_air_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp.tile(xp_as_array(wl, xp=xp), (6,)) + n = xp.tile(xp_as_array(n, xp=xp), (6,)) + xp_assert_close(F_air_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3)) - n = np.reshape(n, (2, 3)) - np.testing.assert_allclose( - F_air_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3), xp=xp) + xp_assert_close(F_air_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) - wl = np.reshape(wl, (2, 3, 1)) - n = np.reshape(n, (2, 3, 1)) - np.testing.assert_allclose( - F_air_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS - ) + wl = xp_reshape(xp_as_array(wl, xp=xp), (2, 3, 1), xp=xp) + n = xp_reshape(xp_as_array(n, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(F_air_Bodhaine1999(wl), n, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_F_air_Bodhaine1999(self) -> None: @@ -1173,22 +1165,28 @@ class TestMolecularDensity: unit tests methods. """ - def test_molecular_density(self) -> None: + def test_molecular_density(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.rayleigh.molecular_density` definition.""" - np.testing.assert_allclose( - molecular_density(200), 3.669449208173649e19, atol=10000 + xp_assert_close( + molecular_density(xp_as_array([200.0], xp=xp)), + 3.669449208173649e19, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e11, ) - np.testing.assert_allclose( - molecular_density(300), 2.4462994721157665e19, atol=10000 + xp_assert_close( + molecular_density(xp_as_array([300.0], xp=xp)), + 2.4462994721157665e19, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e11, ) - np.testing.assert_allclose( - molecular_density(400), 1.834724604086825e19, atol=10000 + xp_assert_close( + molecular_density(xp_as_array([400.0], xp=xp)), + 1.834724604086825e19, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e11, ) - def test_n_dimensional_molecular_density(self) -> None: + def test_n_dimensional_molecular_density(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.molecular_density` definition n-dimensional arrays support. @@ -1197,22 +1195,28 @@ def test_n_dimensional_molecular_density(self) -> None: temperature = 200 N_s = molecular_density(temperature) - temperature = np.tile(temperature, 6) - N_s = np.tile(N_s, 6) - np.testing.assert_allclose( - molecular_density(temperature), N_s, atol=TOLERANCE_ABSOLUTE_TESTS + temperature = xp.tile(xp_as_array(temperature, xp=xp), (6,)) + N_s = xp.tile(xp_as_array(N_s, xp=xp), (6,)) + xp_assert_close( + molecular_density(temperature), + N_s, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - temperature = np.reshape(temperature, (2, 3)) - N_s = np.reshape(N_s, (2, 3)) - np.testing.assert_allclose( - molecular_density(temperature), N_s, atol=TOLERANCE_ABSOLUTE_TESTS + temperature = xp_reshape(xp_as_array(temperature, xp=xp), (2, 3), xp=xp) + N_s = xp_reshape(xp_as_array(N_s, xp=xp), (2, 3), xp=xp) + xp_assert_close( + molecular_density(temperature), + N_s, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - temperature = np.reshape(temperature, (2, 3, 1)) - N_s = np.reshape(N_s, (2, 3, 1)) - np.testing.assert_allclose( - molecular_density(temperature), N_s, atol=TOLERANCE_ABSOLUTE_TESTS + temperature = xp_reshape(xp_as_array(temperature, xp=xp), (2, 3, 1), xp=xp) + N_s = xp_reshape(xp_as_array(N_s, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + molecular_density(temperature), + N_s, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -1231,29 +1235,31 @@ class TestMeanMolecularWeights: definition unit tests methods. """ - def test_mean_molecular_weights(self) -> None: + def test_mean_molecular_weights(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.mean_molecular_weights` definition. """ - np.testing.assert_allclose( - mean_molecular_weights(0), 28.9595, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + mean_molecular_weights(xp_as_array([0.0], xp=xp)), + 28.9595, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - mean_molecular_weights(360), + xp_assert_close( + mean_molecular_weights(xp_as_array([360.0], xp=xp)), 28.964920015999997, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - mean_molecular_weights(620), + xp_assert_close( + mean_molecular_weights(xp_as_array([620.0], xp=xp)), 28.968834471999998, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_mean_molecular_weights(self) -> None: + def test_n_dimensional_mean_molecular_weights(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.mean_molecular_weights` definition n-dimensional arrays support. @@ -1262,22 +1268,28 @@ def test_n_dimensional_mean_molecular_weights(self) -> None: CO2_c = 300 m_a = mean_molecular_weights(CO2_c) - CO2_c = np.tile(CO2_c, 6) - m_a = np.tile(m_a, 6) - np.testing.assert_allclose( - mean_molecular_weights(CO2_c), m_a, atol=TOLERANCE_ABSOLUTE_TESTS + CO2_c = xp.tile(xp_as_array(CO2_c, xp=xp), (6,)) + m_a = xp.tile(xp_as_array(m_a, xp=xp), (6,)) + xp_assert_close( + mean_molecular_weights(CO2_c), + m_a, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - CO2_c = np.reshape(CO2_c, (2, 3)) - m_a = np.reshape(m_a, (2, 3)) - np.testing.assert_allclose( - mean_molecular_weights(CO2_c), m_a, atol=TOLERANCE_ABSOLUTE_TESTS + CO2_c = xp_reshape(xp_as_array(CO2_c, xp=xp), (2, 3), xp=xp) + m_a = xp_reshape(xp_as_array(m_a, xp=xp), (2, 3), xp=xp) + xp_assert_close( + mean_molecular_weights(CO2_c), + m_a, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - CO2_c = np.reshape(CO2_c, (2, 3, 1)) - m_a = np.reshape(m_a, (2, 3, 1)) - np.testing.assert_allclose( - mean_molecular_weights(CO2_c), m_a, atol=TOLERANCE_ABSOLUTE_TESTS + CO2_c = xp_reshape(xp_as_array(CO2_c, xp=xp), (2, 3, 1), xp=xp) + m_a = xp_reshape(xp_as_array(m_a, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( + mean_molecular_weights(CO2_c), + m_a, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -1296,44 +1308,44 @@ class TestGravityList1968: unit tests methods. """ - def test_gravity_List1968(self) -> None: + def test_gravity_List1968(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.rayleigh.gravity_List1968` definition.""" - np.testing.assert_allclose( - gravity_List1968(0.0, 0.0), + xp_assert_close( + gravity_List1968(xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp)), 978.03560706, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gravity_List1968(45.0, 1500.0), + xp_assert_close( + gravity_List1968(xp_as_array([45.0], xp=xp), xp_as_array([1500.0], xp=xp)), 980.15334386, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - gravity_List1968(48.8567, 35.0), + xp_assert_close( + gravity_List1968(xp_as_array([48.8567], xp=xp), xp_as_array([35.0], xp=xp)), 980.95241784, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_gravity_List1968(self) -> None: + def test_n_dimensional_gravity_List1968(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.gravity_List1968` definition n-dimensional arrays support. """ g = 978.03560706 - np.testing.assert_allclose(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) - g = np.tile(g, 6) - np.testing.assert_allclose(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) + g = xp.tile(xp_as_array(g, xp=xp), (6,)) + xp_assert_close(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) - g = np.reshape(g, (2, 3)) - np.testing.assert_allclose(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) + g = xp_reshape(xp_as_array(g, xp=xp), (2, 3), xp=xp) + xp_assert_close(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) - g = np.reshape(g, (2, 3, 1)) - np.testing.assert_allclose(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) + g = xp_reshape(xp_as_array(g, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(gravity_List1968(), g, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_gravity_List1968(self) -> None: @@ -1352,67 +1364,73 @@ class TestScatteringCrossSection: definition unit tests methods. """ - def test_scattering_cross_section(self) -> None: + def test_scattering_cross_section(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.scattering_cross_section` definition. """ - np.testing.assert_allclose( - scattering_cross_section(360 * 10e-8), + xp_assert_close( + scattering_cross_section(xp_as_array([360 * 10e-8], xp=xp)), 2.600908533851937e-26, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8), + xp_assert_close( + scattering_cross_section(xp_as_array([555 * 10e-8], xp=xp)), 4.346669248087624e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(830 * 10e-8), + xp_assert_close( + scattering_cross_section(xp_as_array([830 * 10e-8], xp=xp)), 8.501515434751428e-28, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8, 0), + xp_assert_close( + scattering_cross_section(xp_as_array([555 * 10e-8], xp=xp), 0), 4.346543336839102e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8, 360), + xp_assert_close( + scattering_cross_section(xp_as_array([555 * 10e-8], xp=xp), 360), 4.346694421271718e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8, 620), + xp_assert_close( + scattering_cross_section(xp_as_array([555 * 10e-8], xp=xp), 620), 4.346803470171720e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8, temperature=200), + xp_assert_close( + scattering_cross_section( + xp_as_array([555 * 10e-8], xp=xp), temperature=200 + ), 2.094012829135068e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8, temperature=300), + xp_assert_close( + scattering_cross_section( + xp_as_array([555 * 10e-8], xp=xp), temperature=300 + ), 4.711528865553901e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - np.testing.assert_allclose( - scattering_cross_section(555 * 10e-8, temperature=400), + xp_assert_close( + scattering_cross_section( + xp_as_array([555 * 10e-8], xp=xp), temperature=400 + ), 8.376051316540270e-27, - atol=1e-30, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e-23, ) - def test_n_dimensional_scattering_cross_section(self) -> None: + def test_n_dimensional_scattering_cross_section(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.scattering_cross_section` definition n-dimensional arrays support. @@ -1421,18 +1439,18 @@ def test_n_dimensional_scattering_cross_section(self) -> None: wl = 360 * 10e-8 sigma = scattering_cross_section(wl) - sigma = np.tile(sigma, 6) - np.testing.assert_allclose( + sigma = xp.tile(xp_as_array(sigma, xp=xp), (6,)) + xp_assert_close( scattering_cross_section(wl), sigma, atol=TOLERANCE_ABSOLUTE_TESTS ) - sigma = np.reshape(sigma, (2, 3)) - np.testing.assert_allclose( + sigma = xp_reshape(xp_as_array(sigma, xp=xp), (2, 3), xp=xp) + xp_assert_close( scattering_cross_section(wl), sigma, atol=TOLERANCE_ABSOLUTE_TESTS ) - sigma = np.reshape(sigma, (2, 3, 1)) - np.testing.assert_allclose( + sigma = xp_reshape(xp_as_array(sigma, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close( scattering_cross_section(wl), sigma, atol=TOLERANCE_ABSOLUTE_TESTS ) @@ -1453,103 +1471,113 @@ class TestRayleighOpticalDepth: definition unit tests methods. """ - def test_rayleigh_optical_depth(self) -> None: + def test_rayleigh_optical_depth(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.rayleigh_optical_depth` definition. """ - np.testing.assert_allclose( - rayleigh_optical_depth(360 * 10e-8), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([360 * 10e-8], xp=xp)), 0.560246579231107, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp)), 0.093629074056042, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(830 * 10e-8), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([830 * 10e-8], xp=xp)), 0.018312619911882, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, 0), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), 0), 0.093640964348049, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, 360), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), 360), 0.093626696247360, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, 620), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), 620), 0.093616393371777, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, temperature=200), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), temperature=200), 0.045105912380991, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, temperature=300), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), temperature=300), 0.101488302857230, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, temperature=400), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), temperature=400), 0.180423649523964, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, pressure=101325), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), pressure=101325), 0.093629074056042, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, pressure=100325), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), pressure=100325), 0.092705026939772, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, pressure=99325), + xp_assert_close( + rayleigh_optical_depth(xp_as_array([555 * 10e-8], xp=xp), pressure=99325), 0.091780979823502, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, latitude=0, altitude=0), + xp_assert_close( + rayleigh_optical_depth( + xp_as_array([555 * 10e-8], xp=xp), latitude=0, altitude=0 + ), 0.093629074056041, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, latitude=45, altitude=1500), + xp_assert_close( + rayleigh_optical_depth( + xp_as_array([555 * 10e-8], xp=xp), + latitude=45, + altitude=1500, + ), 0.093426777407767, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - rayleigh_optical_depth(555 * 10e-8, latitude=48.8567, altitude=35), + xp_assert_close( + rayleigh_optical_depth( + xp_as_array([555 * 10e-8], xp=xp), + latitude=48.8567, + altitude=35, + ), 0.093350672894038, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_rayleigh_optical_depth(self) -> None: + def test_n_dimensional_rayleigh_optical_depth(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.rayleigh.rayleigh_optical_depth` definition n-dimensional arrays support. @@ -1558,20 +1586,14 @@ def test_n_dimensional_rayleigh_optical_depth(self) -> None: wl = 360 * 10e-8 T_R = rayleigh_optical_depth(wl) - T_R = np.tile(T_R, 6) - np.testing.assert_allclose( - rayleigh_optical_depth(wl), T_R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + T_R = xp.tile(xp_as_array(T_R, xp=xp), (6,)) + xp_assert_close(rayleigh_optical_depth(wl), T_R, atol=TOLERANCE_ABSOLUTE_TESTS) - T_R = np.reshape(T_R, (2, 3)) - np.testing.assert_allclose( - rayleigh_optical_depth(wl), T_R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + T_R = xp_reshape(xp_as_array(T_R, xp=xp), (2, 3), xp=xp) + xp_assert_close(rayleigh_optical_depth(wl), T_R, atol=TOLERANCE_ABSOLUTE_TESTS) - T_R = np.reshape(T_R, (2, 3, 1)) - np.testing.assert_allclose( - rayleigh_optical_depth(wl), T_R, atol=TOLERANCE_ABSOLUTE_TESTS - ) + T_R = xp_reshape(xp_as_array(T_R, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(rayleigh_optical_depth(wl), T_R, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_rayleigh_optical_depth(self) -> None: @@ -1596,7 +1618,7 @@ def test_sd_rayleigh_scattering(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( sd_rayleigh_scattering().values, DATA_SD_RAYLEIGH_SCATTERING, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/phenomena/tests/test_tmm.py b/colour/phenomena/tests/test_tmm.py index 2ad38bc544..df31e96d15 100644 --- a/colour/phenomena/tests/test_tmm.py +++ b/colour/phenomena/tests/test_tmm.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -14,7 +19,13 @@ polarised_light_transmission_coefficient, snell_law, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -40,35 +51,35 @@ class TestSnellLaw: methods. """ - def test_snell_law(self) -> None: + def test_snell_law(self, xp: ModuleType) -> None: """Test :func:`colour.phenomena.tmm.snell_law` definition.""" - np.testing.assert_allclose( - snell_law(1.0, 1.5, 30.0), + xp_assert_close( + snell_law(1.0, 1.5, xp_as_array([30.0], xp=xp)), 19.4712206345, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - snell_law(1.0, 1.33, 45.0), + xp_assert_close( + snell_law(1.0, 1.33, xp_as_array([45.0], xp=xp)), 32.117631278, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - snell_law(1.5, 1.0, 19.47), + xp_assert_close( + snell_law(1.5, 1.0, xp_as_array([19.47], xp=xp)), 30.0, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) # Test normal incidence (0 degrees) - np.testing.assert_allclose( - snell_law(1.0, 1.5, 0.0), + xp_assert_close( + snell_law(1.0, 1.5, xp_as_array([0.0], xp=xp)), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_snell_law(self) -> None: + def test_n_dimensional_snell_law(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.snell_law` definition n-dimensional arrays support. @@ -79,17 +90,17 @@ def test_n_dimensional_snell_law(self) -> None: theta_i = 30.0 theta_t = snell_law(n_1, n_2, theta_i) - theta_i = np.tile(theta_i, 6) - theta_t = np.tile(theta_t, 6) - np.testing.assert_allclose( + theta_i = xp.tile(xp_as_array(theta_i, xp=xp), (6,)) + theta_t = xp.tile(xp_as_array(theta_t, xp=xp), (6,)) + xp_assert_close( snell_law(n_1, n_2, theta_i), theta_t, atol=TOLERANCE_ABSOLUTE_TESTS, ) - theta_i = np.reshape(theta_i, (2, 3)) - theta_t = np.reshape(theta_t, (2, 3)) - np.testing.assert_allclose( + theta_i = xp_reshape(xp_as_array(theta_i, xp=xp), (2, 3), xp=xp) + theta_t = xp_reshape(xp_as_array(theta_t, xp=xp), (2, 3), xp=xp) + xp_assert_close( snell_law(n_1, n_2, theta_i), theta_t, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -112,23 +123,29 @@ class TestPolarisedLightMagnitudeElements: definition unit tests methods. """ - def test_polarised_light_magnitude_elements(self) -> None: + def test_polarised_light_magnitude_elements(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_magnitude_elements` definition. """ - result = polarised_light_magnitude_elements(1.0, 1.5, 0.0, 0.0) - np.testing.assert_allclose(result[0], 1.0 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(result[1], 1.0 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(result[2], 1.5 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(result[3], 1.5 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) + result = polarised_light_magnitude_elements( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ) + xp_assert_close(result[0], 1.0 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(result[1], 1.0 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(result[2], 1.5 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(result[3], 1.5 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) # Test at 45 degrees - result_45 = polarised_light_magnitude_elements(1.0, 1.5, 45.0, 30.0) + result_45 = polarised_light_magnitude_elements( + 1.0, 1.5, xp_as_array([45.0], xp=xp), xp_as_array([30.0], xp=xp) + ) assert len(result_45) == 4 - def test_n_dimensional_polarised_light_magnitude_elements(self) -> None: + def test_n_dimensional_polarised_light_magnitude_elements( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_magnitude_elements` definition n-dimensional arrays support. @@ -140,40 +157,56 @@ def test_n_dimensional_polarised_light_magnitude_elements(self) -> None: theta_t = 0.0 m0, m1, m2, m3 = polarised_light_magnitude_elements(n_1, n_2, theta_i, theta_t) - theta_i_array = np.tile(theta_i, 6) - theta_t_array = np.tile(theta_t, 6) + theta_i_array = xp.tile(xp_as_array(theta_i, xp=xp), (6,)) + theta_t_array = xp.tile(xp_as_array(theta_t, xp=xp), (6,)) m0_array, m1_array, m2_array, m3_array = polarised_light_magnitude_elements( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( - m0_array, np.tile(m0, 6), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m0_array, + np.tile(as_ndarray(m0), (6,)), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - m1_array, np.tile(m1, 6), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m1_array, + np.tile(as_ndarray(m1), (6,)), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - m2_array, np.tile(m2, 6), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m2_array, + np.tile(as_ndarray(m2), (6,)), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - m3_array, np.tile(m3, 6), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m3_array, + np.tile(as_ndarray(m3), (6,)), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - theta_i_array = np.reshape(theta_i_array, (2, 3)) - theta_t_array = np.reshape(theta_t_array, (2, 3)) + theta_i_array = xp_reshape(xp_as_array(theta_i_array, xp=xp), (2, 3), xp=xp) + theta_t_array = xp_reshape(xp_as_array(theta_t_array, xp=xp), (2, 3), xp=xp) m0_array, m1_array, m2_array, m3_array = polarised_light_magnitude_elements( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( - m0_array, np.tile(m0, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m0_array, + np.tile(as_ndarray(m0), (6,)).reshape(2, 3), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - m1_array, np.tile(m1, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m1_array, + np.tile(as_ndarray(m1), (6,)).reshape(2, 3), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - m2_array, np.tile(m2, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m2_array, + np.tile(as_ndarray(m2), (6,)).reshape(2, 3), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - m3_array, np.tile(m3, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + m3_array, + np.tile(as_ndarray(m3), (6,)).reshape(2, 3), + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -197,25 +230,34 @@ class TestPolarisedLightReflectionAmplitude: definition unit tests methods. """ - def test_polarised_light_reflection_amplitude(self) -> None: + def test_polarised_light_reflection_amplitude(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_reflection_amplitude` definition. """ - np.testing.assert_allclose( - polarised_light_reflection_amplitude(1.0, 1.5, 0.0, 0.0), - np.array([-0.2 + 0j, -0.2 + 0j]), + xp_assert_close( + polarised_light_reflection_amplitude( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ), + [[-0.2 + 0j, -0.2 + 0j]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - polarised_light_reflection_amplitude(1.0, 1.5, 30.0, 19.47), - np.array([-0.24041175 + 0j, -0.15889613 + 0j]), + xp_assert_close( + polarised_light_reflection_amplitude( + 1.0, + 1.5, + xp_as_array([30.0], xp=xp), + xp_as_array([19.47], xp=xp), + ), + [[-0.24041175 + 0j, -0.15889613 + 0j]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_polarised_light_reflection_amplitude(self) -> None: + def test_n_dimensional_polarised_light_reflection_amplitude( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_reflection_amplitude` definition n-dimensional arrays support. @@ -227,25 +269,25 @@ def test_n_dimensional_polarised_light_reflection_amplitude(self) -> None: theta_t = 0.0 r = polarised_light_reflection_amplitude(n_1, n_2, theta_i, theta_t) - theta_i_array = np.tile(theta_i, 6) - theta_t_array = np.tile(theta_t, 6) + theta_i_array = xp.tile(xp_as_array(theta_i, xp=xp), (6,)) + theta_t_array = xp.tile(xp_as_array(theta_t, xp=xp), (6,)) r_array = polarised_light_reflection_amplitude( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( r_array, - np.tile(r, (6, 1)), + np.tile(as_ndarray(r), (6, 1)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - theta_i_array = np.reshape(theta_i_array, (2, 3)) - theta_t_array = np.reshape(theta_t_array, (2, 3)) + theta_i_array = xp_reshape(xp_as_array(theta_i_array, xp=xp), (2, 3), xp=xp) + theta_t_array = xp_reshape(xp_as_array(theta_t_array, xp=xp), (2, 3), xp=xp) r_array = polarised_light_reflection_amplitude( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( r_array, - np.tile(r, (6, 1)).reshape(2, 3, 2), + np.tile(as_ndarray(r), (6, 1)).reshape(2, 3, 2), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -270,24 +312,35 @@ class TestPolarisedLightReflectionCoefficient: definition unit tests methods. """ - def test_polarised_light_reflection_coefficient(self) -> None: + def test_polarised_light_reflection_coefficient(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_reflection_coefficient` definition. """ - np.testing.assert_allclose( - polarised_light_reflection_coefficient(1.0, 1.5, 0.0, 0.0), - np.array([0.04 + 0j, 0.04 + 0j]), + xp_assert_close( + polarised_light_reflection_coefficient( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ), + [[0.04 + 0j, 0.04 + 0j]], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test that reflectance is always between 0 and 1 - R = polarised_light_reflection_coefficient(1.0, 1.5, 30.0, 19.47) + R = as_ndarray( + polarised_light_reflection_coefficient( + 1.0, + 1.5, + xp_as_array([30.0], xp=xp), + xp_as_array([19.47], xp=xp), + ) + ) assert np.all(np.real(R) >= 0) assert np.all(np.real(R) <= 1) - def test_n_dimensional_polarised_light_reflection_coefficient(self) -> None: + def test_n_dimensional_polarised_light_reflection_coefficient( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_reflection_coefficient` definition n-dimensional arrays support. @@ -299,25 +352,25 @@ def test_n_dimensional_polarised_light_reflection_coefficient(self) -> None: theta_t = 0.0 R = polarised_light_reflection_coefficient(n_1, n_2, theta_i, theta_t) - theta_i_array = np.tile(theta_i, 6) - theta_t_array = np.tile(theta_t, 6) + theta_i_array = xp.tile(xp_as_array(theta_i, xp=xp), (6,)) + theta_t_array = xp.tile(xp_as_array(theta_t, xp=xp), (6,)) R_array = polarised_light_reflection_coefficient( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( R_array, - np.tile(R, (6, 1)), + np.tile(as_ndarray(R), (6, 1)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - theta_i_array = np.reshape(theta_i_array, (2, 3)) - theta_t_array = np.reshape(theta_t_array, (2, 3)) + theta_i_array = xp_reshape(xp_as_array(theta_i_array, xp=xp), (2, 3), xp=xp) + theta_t_array = xp_reshape(xp_as_array(theta_t_array, xp=xp), (2, 3), xp=xp) R_array = polarised_light_reflection_coefficient( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( R_array, - np.tile(R, (6, 1)).reshape(2, 3, 2), + np.tile(as_ndarray(R), (6, 1)).reshape(2, 3, 2), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -342,19 +395,23 @@ class TestPolarisedLightTransmissionAmplitude: definition unit tests methods. """ - def test_polarised_light_transmission_amplitude(self) -> None: + def test_polarised_light_transmission_amplitude(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_transmission_amplitude` definition. """ - np.testing.assert_allclose( - polarised_light_transmission_amplitude(1.0, 1.5, 0.0, 0.0), - np.array([0.8 + 0j, 0.8 + 0j]), + xp_assert_close( + polarised_light_transmission_amplitude( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ), + [[0.8 + 0j, 0.8 + 0j]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_polarised_light_transmission_amplitude(self) -> None: + def test_n_dimensional_polarised_light_transmission_amplitude( + self, xp: ModuleType + ) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_transmission_amplitude` definition n-dimensional arrays support. @@ -366,25 +423,25 @@ def test_n_dimensional_polarised_light_transmission_amplitude(self) -> None: theta_t = 0.0 t = polarised_light_transmission_amplitude(n_1, n_2, theta_i, theta_t) - theta_i_array = np.tile(theta_i, 6) - theta_t_array = np.tile(theta_t, 6) + theta_i_array = xp.tile(xp_as_array(theta_i, xp=xp), (6,)) + theta_t_array = xp.tile(xp_as_array(theta_t, xp=xp), (6,)) t_array = polarised_light_transmission_amplitude( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( t_array, - np.tile(t, (6, 1)), + np.tile(as_ndarray(t), (6, 1)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - theta_i_array = np.reshape(theta_i_array, (2, 3)) - theta_t_array = np.reshape(theta_t_array, (2, 3)) + theta_i_array = xp_reshape(xp_as_array(theta_i_array, xp=xp), (2, 3), xp=xp) + theta_t_array = xp_reshape(xp_as_array(theta_t_array, xp=xp), (2, 3), xp=xp) t_array = polarised_light_transmission_amplitude( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( t_array, - np.tile(t, (6, 1)).reshape(2, 3, 2), + np.tile(as_ndarray(t), (6, 1)).reshape(2, 3, 2), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -409,27 +466,35 @@ class TestPolarisedLightTransmissionCoefficient: definition unit tests methods. """ - def test_polarised_light_transmission_coefficient(self) -> None: + def test_polarised_light_transmission_coefficient(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_transmission_coefficient` definition. """ - np.testing.assert_allclose( - polarised_light_transmission_coefficient(1.0, 1.5, 0.0, 0.0), - np.array([0.96 + 0j, 0.96 + 0j]), + xp_assert_close( + polarised_light_transmission_coefficient( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ), + [[0.96 + 0j, 0.96 + 0j]], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test energy conservation: R + T = 1 - R = polarised_light_reflection_coefficient(1.0, 1.5, 0.0, 0.0) - T = polarised_light_transmission_coefficient(1.0, 1.5, 0.0, 0.0) - np.testing.assert_allclose( - np.real(R + T), np.array([1.0, 1.0]), atol=TOLERANCE_ABSOLUTE_TESTS + R = as_ndarray( + polarised_light_reflection_coefficient( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ) ) + T = as_ndarray( + polarised_light_transmission_coefficient( + 1.0, 1.5, xp_as_array([0.0], xp=xp), xp_as_array([0.0], xp=xp) + ) + ) + xp_assert_close(np.real(R + T), [[1.0, 1.0]], atol=TOLERANCE_ABSOLUTE_TESTS) def test_n_dimensional_polarised_light_transmission_coefficient( - self, + self, xp: ModuleType ) -> None: """ Test :func:`colour.phenomena.tmm.polarised_light_transmission_coefficient` @@ -442,25 +507,25 @@ def test_n_dimensional_polarised_light_transmission_coefficient( theta_t = 0.0 T = polarised_light_transmission_coefficient(n_1, n_2, theta_i, theta_t) - theta_i_array = np.tile(theta_i, 6) - theta_t_array = np.tile(theta_t, 6) + theta_i_array = xp.tile(xp_as_array(theta_i, xp=xp), (6,)) + theta_t_array = xp.tile(xp_as_array(theta_t, xp=xp), (6,)) T_array = polarised_light_transmission_coefficient( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( T_array, - np.tile(T, (6, 1)), + np.tile(as_ndarray(T), (6, 1)), atol=TOLERANCE_ABSOLUTE_TESTS, ) - theta_i_array = np.reshape(theta_i_array, (2, 3)) - theta_t_array = np.reshape(theta_t_array, (2, 3)) + theta_i_array = xp_reshape(xp_as_array(theta_i_array, xp=xp), (2, 3), xp=xp) + theta_t_array = xp_reshape(xp_as_array(theta_t_array, xp=xp), (2, 3), xp=xp) T_array = polarised_light_transmission_coefficient( n_1, n_2, theta_i_array, theta_t_array ) - np.testing.assert_allclose( + xp_assert_close( T_array, - np.tile(T, (6, 1)).reshape(2, 3, 2), + np.tile(as_ndarray(T), (6, 1)).reshape(2, 3, 2), atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -485,7 +550,7 @@ class TestMatrixTransferTmm: definition unit tests methods. """ - def test_matrix_transfer_tmm(self) -> None: + def test_matrix_transfer_tmm(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` definition. @@ -493,7 +558,10 @@ def test_matrix_transfer_tmm(self) -> None: # Single layer structure result = matrix_transfer_tmm( - n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=550 + n=[1.0, 1.5, 1.0], + t=[250], + theta=0, + wavelength=xp_as_array([550], xp=xp), ) # Check shapes - (W, A, T, 2, 2) @@ -512,19 +580,17 @@ def test_matrix_transfer_tmm(self) -> None: # Check refractive indices # n has shape (media_count, wavelengths_count) assert result.n.shape == (3, 1) - np.testing.assert_allclose( - result.n[:, 0], [1.0, 1.5, 1.0], atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(result.n[:, 0], [1.0, 1.5, 1.0], atol=TOLERANCE_ABSOLUTE_TESTS) # Check angles (normal incidence) assert result.theta[0, 0] == 0.0 # incident assert result.theta[0, -1] == 0.0 # substrate (by Snell's law) # Check transfer matrix properties (should be 2x2 complex) - assert result.M_s.dtype in [np.complex64, np.complex128] - assert result.M_p.dtype in [np.complex64, np.complex128] + assert "complex" in str(result.M_s.dtype) + assert "complex" in str(result.M_p.dtype) - def test_matrix_transfer_tmm_multilayer(self) -> None: + def test_matrix_transfer_tmm_multilayer(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` with multiple layers. @@ -534,8 +600,8 @@ def test_matrix_transfer_tmm_multilayer(self) -> None: result = matrix_transfer_tmm( n=[1.0, 1.5, 2.0, 1.5], t=[250, 150], - theta=0, - wavelength=550, + theta=xp_as_array([0.0], xp=xp), + wavelength=xp_as_array([550.0], xp=xp), ) # Check shapes - (W, A, T, 2, 2) @@ -554,19 +620,24 @@ def test_matrix_transfer_tmm_multilayer(self) -> None: # Check refractive indices # n has shape (media_count, wavelengths_count) assert result.n.shape == (4, 1) - np.testing.assert_allclose( - result.n[:, 0], [1.0, 1.5, 2.0, 1.5], atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + result.n[:, 0], + [1.0, 1.5, 2.0, 1.5], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_matrix_transfer_tmm_multiple_wavelengths(self) -> None: + def test_matrix_transfer_tmm_multiple_wavelengths(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` with multiple wavelengths. """ - wavelengths = [400, 500, 600] + wavelengths = xp_as_array([400.0, 500.0, 600.0], xp=xp) result = matrix_transfer_tmm( - n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=wavelengths + n=[1.0, 1.5, 1.0], + t=[250], + theta=xp_as_array([0.0], xp=xp), + wavelength=wavelengths, ) # Check shapes - (W, A, T, 2, 2) @@ -584,7 +655,7 @@ def test_matrix_transfer_tmm_multiple_wavelengths(self) -> None: # n has shape (media_count, wavelengths_count) assert result.n.shape == (3, 3) - def test_matrix_transfer_tmm_complex_n(self) -> None: + def test_matrix_transfer_tmm_complex_n(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` with complex refractive indices. @@ -593,21 +664,22 @@ def test_matrix_transfer_tmm_complex_n(self) -> None: # Absorbing layer n_absorbing = 2.0 + 0.5j result = matrix_transfer_tmm( - n=[1.0, n_absorbing, 1.0], t=[250], theta=0, wavelength=550 + n=[1.0, n_absorbing, 1.0], + t=[250], + theta=xp_as_array([0.0], xp=xp), + wavelength=xp_as_array([550.0], xp=xp), ) # Check that complex n is preserved # n has shape (media_count, wavelengths_count) - assert np.iscomplex(result.n[1, 0]) - np.testing.assert_allclose( - result.n[1, 0], n_absorbing, atol=TOLERANCE_ABSOLUTE_TESTS - ) + assert np.iscomplex(as_ndarray(result.n)[1, 0]) + xp_assert_close(result.n[1, 0], n_absorbing, atol=TOLERANCE_ABSOLUTE_TESTS) # Transfer matrices should be complex - assert np.iscomplexobj(result.M_s) - assert np.iscomplexobj(result.M_p) + assert np.iscomplexobj(as_ndarray(result.M_s)) + assert np.iscomplexobj(as_ndarray(result.M_p)) - def test_matrix_transfer_tmm_oblique_incidence(self) -> None: + def test_matrix_transfer_tmm_oblique_incidence(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` with oblique incidence. @@ -615,21 +687,26 @@ def test_matrix_transfer_tmm_oblique_incidence(self) -> None: theta_i = 30.0 # 30 degrees result = matrix_transfer_tmm( - n=[1.0, 1.5, 1.0], t=[250], theta=theta_i, wavelength=550 + n=[1.0, 1.5, 1.0], + t=[250], + theta=xp_as_array([theta_i], xp=xp), + wavelength=xp_as_array([550.0], xp=xp), ) # Check incident angle - theta has shape (angles, media) - np.testing.assert_allclose( - result.theta[0, 0], theta_i, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(result.theta[0, 0], theta_i, atol=TOLERANCE_ABSOLUTE_TESTS) # Check that angle changes in layer (Snell's law) - assert result.theta[0, 1] != theta_i # Should be refracted + assert as_ndarray(result.theta)[0, 1] != theta_i # Should be refracted # s and p matrices should differ at oblique incidence - assert not np.allclose(result.M_s, result.M_p, atol=TOLERANCE_ABSOLUTE_TESTS) + assert not np.allclose( + as_ndarray(result.M_s), + as_ndarray(result.M_p), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - def test_matrix_transfer_tmm_energy_consistency(self) -> None: + def test_matrix_transfer_tmm_energy_consistency(self, xp: ModuleType) -> None: """ Test that transfer matrices from transfer_matrix_tmm give consistent R and T values. @@ -637,20 +714,24 @@ def test_matrix_transfer_tmm_energy_consistency(self) -> None: # Build transfer matrix result = matrix_transfer_tmm( - n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=550 + n=[1.0, 1.5, 1.0], + t=[250], + theta=xp_as_array([0.0], xp=xp), + wavelength=xp_as_array([550.0], xp=xp), ) # Extract R and T manually - M_s has shape (W, A, T, 2, 2) - r_s = result.M_s[0, 0, 0, 1, 0] / result.M_s[0, 0, 0, 0, 0] + M_s = as_ndarray(result.M_s) + r_s = M_s[0, 0, 0, 1, 0] / M_s[0, 0, 0, 0, 0] R_s = np.abs(r_s) ** 2 - t_s = 1.0 / result.M_s[0, 0, 0, 0, 0] + t_s = 1.0 / M_s[0, 0, 0, 0, 0] theta_i_rad = np.radians(0.0) - theta_f_rad = np.radians(result.theta[0, -1]) + theta_f_rad = np.radians(as_ndarray(result.theta)[0, -1]) # Extract incident and substrate from result.n - n_incident = result.n[0, 0] - n_substrate = result.n[-1, 0] + n_incident = as_ndarray(result.n)[0, 0] + n_substrate = as_ndarray(result.n)[-1, 0] angle_factor = np.real(n_substrate * np.cos(theta_f_rad)) / np.real( n_incident * np.cos(theta_i_rad) @@ -658,9 +739,9 @@ def test_matrix_transfer_tmm_energy_consistency(self) -> None: T_s = np.abs(t_s) ** 2 * angle_factor # Energy conservation for lossless media: R + T = 1 - np.testing.assert_allclose(R_s + T_s, 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(R_s + T_s, 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) - def test_n_dimensional_matrix_transfer_tmm(self) -> None: + def test_n_dimensional_matrix_transfer_tmm(self, xp: ModuleType) -> None: """ Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` definition n-dimensional arrays support. @@ -669,28 +750,28 @@ def test_n_dimensional_matrix_transfer_tmm(self) -> None: wl = 555 result = matrix_transfer_tmm(n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=wl) - wl_array = np.tile(wl, 6) + wl_array = xp.tile(xp_as_array(wl, xp=xp), (6,)) result_array = matrix_transfer_tmm( n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=wl_array ) # Check shape - (W, A, T, 2, 2) - assert result_array.M_s.shape == ( + assert as_ndarray(result_array.M_s).shape == ( 6, 1, 1, 2, 2, ) # (wavelengths=6, angles=1, thickness=1, 2, 2) - assert result_array.M_p.shape == (6, 1, 1, 2, 2) + assert as_ndarray(result_array.M_p).shape == (6, 1, 1, 2, 2) # theta shapes: result has (1, 3), result_array has (1, 3) - assert result_array.theta.shape == result.theta.shape + assert as_ndarray(result_array.theta).shape == result.theta.shape # n shapes: result has (3, 1), result_array has (3, 6) # For constant n, all wavelength columns should match - assert result_array.n.shape == (3, 6) + assert as_ndarray(result_array.n).shape == (3, 6) assert result.n.shape == (3, 1) - np.testing.assert_allclose( + xp_assert_close( result_array.n[:, 0], result.n[:, 0], atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/phenomena/tmm.py b/colour/phenomena/tmm.py index e88068d028..a9dabf75b4 100644 --- a/colour/phenomena/tmm.py +++ b/colour/phenomena/tmm.py @@ -44,13 +44,21 @@ import numpy as np -from colour.constants import DTYPE_COMPLEX_DEFAULT from colour.utilities import ( MixinDataclassArithmetic, + array_namespace, as_complex_array, as_float_array, - tsplit, - tstack, + as_ndarray, + xp_as_array, + xp_as_float_array, + xp_astype, + xp_atleast_1d, + xp_atleast_2d, + xp_broadcast_to, + xp_degrees, + xp_matrix_transpose, + xp_radians, ) if TYPE_CHECKING: @@ -80,9 +88,9 @@ def _tsplit_complex(a: ArrayLike) -> NDArrayComplex: Split the specified stacked array along the last axis (tail) to produce an array of complex arrays. - Convenience wrapper around :func:`colour.utilities.tsplit` that - automatically uses ``DTYPE_COMPLEX_DEFAULT`` for complex number - operations in *Transfer Matrix Method* calculations. + Convenience wrapper that ensures complex dtype via + :func:`colour.utilities.as_complex_array` (which handles backends + that do not support ``complex128``, e.g. *MPS*) before splitting. Parameters ---------- @@ -95,7 +103,11 @@ def _tsplit_complex(a: ArrayLike) -> NDArrayComplex: Array of complex arrays. """ - return tsplit(a, dtype=DTYPE_COMPLEX_DEFAULT) # type: ignore[arg-type] + a = as_complex_array(a) + + xp = array_namespace(a) + + return xp.stack([a[..., x] for x in range(a.shape[-1])]) def _tstack_complex(a: ArrayLike) -> NDArrayComplex: @@ -103,9 +115,9 @@ def _tstack_complex(a: ArrayLike) -> NDArrayComplex: Stack the specified array of arrays along the last axis (tail) to produce a stacked complex array. - Convenience wrapper around :func:`colour.utilities.tstack` that - automatically uses ``DTYPE_COMPLEX_DEFAULT`` for complex number - operations in *Transfer Matrix Method* calculations. + Convenience wrapper that ensures complex dtype via + :func:`colour.utilities.as_complex_array` (which handles backends + that do not support ``complex128``, e.g. *MPS*) before stacking. Parameters ---------- @@ -122,7 +134,11 @@ def _tstack_complex(a: ArrayLike) -> NDArrayComplex: :cite:`Byrnes2016` """ - return tstack(a, dtype=DTYPE_COMPLEX_DEFAULT) # type: ignore[arg-type] + a = [as_complex_array(x) for x in a] # pyright: ignore + + xp = array_namespace(a[0]) + + return xp.stack(a, axis=-1) def snell_law( @@ -172,12 +188,15 @@ def snell_law( np.float64(19.4712206...) """ - n_1 = np.real(as_complex_array(n_1)) - n_2 = np.real(as_complex_array(n_2)) - theta_i = np.radians(as_float_array(theta_i)) + theta_i = xp_radians(as_float_array(theta_i)) + + xp = array_namespace(theta_i) + + n_1 = xp_as_float_array(np.real(as_ndarray(n_1)), xp=xp, like=theta_i) + n_2 = xp_as_float_array(np.real(as_ndarray(n_2)), xp=xp, like=theta_i) # Apply Snell's law: n_i * sin(theta_i) = n_j * sin(theta_j) (Byrnes Eq. 3) - return np.degrees(np.arcsin(n_1 * np.sin(theta_i) / n_2)) + return xp_degrees(xp.asin(n_1 * xp.sin(theta_i) / n_2)) def polarised_light_magnitude_elements( @@ -233,8 +252,24 @@ def polarised_light_magnitude_elements( n_1 = as_complex_array(n_1) n_2 = as_complex_array(n_2) - cos_theta_i = np.cos(np.radians(as_float_array(theta_i))) - cos_theta_t = np.cos(np.radians(as_float_array(theta_t))) + theta_i_rad = xp_radians(as_float_array(theta_i)) + theta_t_rad = xp_radians(as_float_array(theta_t)) + + xp = array_namespace(theta_i_rad) + + n_1 = ( + xp_as_array(n_1, xp=xp, like=theta_i_rad) + if isinstance(n_1, np.ndarray) + else n_1 + ) + n_2 = ( + xp_as_array(n_2, xp=xp, like=theta_i_rad) + if isinstance(n_2, np.ndarray) + else n_2 + ) + + cos_theta_i = xp_astype(xp.cos(theta_i_rad), n_1.dtype, xp=xp) + cos_theta_t = xp_astype(xp.cos(theta_t_rad), n_1.dtype, xp=xp) n_1_cos_theta_i = n_1 * cos_theta_i n_1_cos_theta_t = n_1 * cos_theta_t @@ -377,7 +412,11 @@ def polarised_light_reflection_coefficient( """ # Reflectance: R = |r|^2 (Byrnes Eq. 23) - R = np.abs(polarised_light_reflection_amplitude(n_1, n_2, theta_i, theta_t)) ** 2 + r = polarised_light_reflection_amplitude(n_1, n_2, theta_i, theta_t) + + xp = array_namespace(r) + + R = xp.abs(r) ** 2 return as_complex_array(R) @@ -535,9 +574,11 @@ def polarised_light_transmission_coefficient( ) # Transmittance with beam cross-section correction (Byrnes Eq. 21-22) - T = (n_2_cos_theta_t / n_1_cos_theta_i)[..., None] * np.abs( - polarised_light_transmission_amplitude(n_1, n_2, theta_i, theta_t) - ) ** 2 + t_amp = polarised_light_transmission_amplitude(n_1, n_2, theta_i, theta_t) + + xp = array_namespace(t_amp) + + T = (n_2_cos_theta_t / n_1_cos_theta_i)[..., None] * xp.abs(t_amp) ** 2 return as_complex_array(T) @@ -726,20 +767,29 @@ def matrix_transfer_tmm( n = as_complex_array(n) t = as_float_array(t) - theta = np.atleast_1d(as_float_array(theta)) - wavelength = np.atleast_1d(as_float_array(wavelength)) + theta = as_float_array(theta) + wavelength = as_float_array(wavelength) + + xp = array_namespace(wavelength, theta) + + n = xp_as_array(n, xp=xp, like=wavelength) if isinstance(n, np.ndarray) else n + t = xp_as_array(t, xp=xp, like=wavelength) + theta = xp_atleast_1d(xp_as_array(theta, xp=xp, like=wavelength), xp=xp) + wavelength = xp_atleast_1d(wavelength, xp=xp) wavelengths_count = wavelength.shape[0] - # Convert 1D n to column vector and tile across wavelengths - # (M,) -> (M, 1) -> (M, W) if n.ndim == 1: - n = np.transpose(np.atleast_2d(n)) - n = np.tile(n, (1, wavelengths_count)) + n = xp_matrix_transpose(xp_atleast_2d(n, xp=xp), xp=xp) + # ``broadcast_to`` is equivalent to ``tile`` for an ``(M, 1)`` -> + # ``(M, W)`` expansion, works on every backend (*MPS* does not + # support ``tile`` on complex tensors) and avoids the copy; ``n`` + # is only ever read downstream. + n = xp_broadcast_to(n, (n.shape[0], wavelengths_count), xp=xp) # (1, layers_count) if t.ndim == 1: - t = t[np.newaxis, :] + t = t[None, :] media_count = n.shape[0] layers_count = media_count - 2 @@ -748,9 +798,14 @@ def matrix_transfer_tmm( # Snell's law: n_i * sin(theta_i) = n_j * sin(theta_j) (Byrnes Eq. 3) # Broadcasting: theta (A,) → theta_media (A, M) - theta_media = snell_law( - n_0, (n[:, 0] if n.ndim == 2 else n)[:, None], theta[None, :] - ).T + theta_media = xp_matrix_transpose( + snell_law( + n_0, + (n[:, 0] if n.ndim == 2 else n)[:, None], + theta[None, :], + ), + xp=xp, + ) # Fresnel coefficients (Byrnes Eq. 6) # Broadcasting: n (M, W), theta_media (A, M) → coefficients (A, M-1, W) @@ -783,19 +838,19 @@ def matrix_transfer_tmm( n_layer = n[1 : layers_count + 1, :] # (L, W) - Each layer's refractive index theta_layer = theta_media[:, 0:layers_count] # (A, L) - theta_radians = np.radians(theta_layer)[:, :, None] # (A, L, 1) - k_z_layers = np.sqrt( + theta_radians = xp_radians(theta_layer)[:, :, None] # (A, L, 1) + k_z_layers = xp.sqrt( n_layer[None, :, :] ** 2 - - n_previous[None, :, :] ** 2 * np.sin(theta_radians) ** 2 + - n_previous[None, :, :] ** 2 * xp.sin(theta_radians) ** 2 ) # (A, L, W) # Compute phase: delta = (2π/λ) * d * k_z phase_factor = 2 * np.pi / wavelength[:, None, None, None] # (W, 1, 1, 1) # Reshape k_z from (A, L, W) to (W, A, 1, L) for broadcasting with thickness - k_z = np.transpose(k_z_layers, (2, 0, 1))[:, :, None, :] # (W, A, 1, L) + k_z = xp.moveaxis(k_z_layers, (0, 1, 2), (1, 2, 0))[:, :, None, :] # (W, A, 1, L) delta = phase_factor * t[None, None, :, :] * k_z # (W, A, T, L) - A = np.exp(1j * delta) # (W, A, T, L) + A = xp.exp(1j * delta) # (W, A, T, L) # Layer matrices: M_n = L_n * I_{n,n+1} (Byrnes Eq. 10-11) # (W, A, T, L, 2, 2, 2) for [wavelengths, angles, thickness, layers, 2x2, pol] @@ -806,59 +861,58 @@ def matrix_transfer_tmm( # Broadcast Fresnel coefficients from (A, L, W) to (W, A, 1, L) # (A,L,W) -> (W,A,L) -> (W,A,1,L) - r_s_b = np.transpose(r_s, (2, 0, 1))[:, :, None, :] - r_p_b = np.transpose(r_p, (2, 0, 1))[:, :, None, :] - t_s_b = np.transpose(t_s, (2, 0, 1))[:, :, None, :] - t_p_b = np.transpose(t_p, (2, 0, 1))[:, :, None, :] + r_s_b = xp.moveaxis(r_s, (0, 1, 2), (1, 2, 0))[:, :, None, :] + r_p_b = xp.moveaxis(r_p, (0, 1, 2), (1, 2, 0))[:, :, None, :] + t_s_b = xp.moveaxis(t_s, (0, 1, 2), (1, 2, 0))[:, :, None, :] + t_p_b = xp.moveaxis(t_p, (0, 1, 2), (1, 2, 0))[:, :, None, :] - # Build 2x2 matrices for s and p polarizations using np.stack - M_s_layer = np.stack( + M_s_layer = xp.stack( [ - np.stack([1 / (A * t_s_b), r_s_b / (A * t_s_b)], axis=-1), - np.stack([A * r_s_b / t_s_b, A / t_s_b], axis=-1), + xp.stack([1 / (A * t_s_b), r_s_b / (A * t_s_b)], axis=-1), + xp.stack([A * r_s_b / t_s_b, A / t_s_b], axis=-1), ], axis=-2, ) - M_p_layer = np.stack( + M_p_layer = xp.stack( [ - np.stack([1 / (A * t_p_b), r_p_b / (A * t_p_b)], axis=-1), - np.stack([A * r_p_b / t_p_b, A / t_p_b], axis=-1), + xp.stack([1 / (A * t_p_b), r_p_b / (A * t_p_b)], axis=-1), + xp.stack([A * r_p_b / t_p_b, A / t_p_b], axis=-1), ], axis=-2, ) - M = np.stack([M_s_layer, M_p_layer], axis=-1) + M = xp.stack([M_s_layer, M_p_layer], axis=-1) # Initial interface matrix (Byrnes Eq. 11) # Shape: (W, A, T, 2, 2) # Fresnel coefficients at incident → first layer interface t_s_01 = t_media_s[:, 0, :] # (A, W) r_s_01 = r_media_s[:, 0, :] # (A, W) - inv_t_s = (1 / t_s_01).T[:, :, None] # (W, A, 1) - r_over_t_s = (r_s_01 / t_s_01).T[:, :, None] - M_s = np.stack( + inv_t_s = xp_matrix_transpose(1 / t_s_01, xp=xp)[:, :, None] # (W, A, 1) + r_over_t_s = xp_matrix_transpose(r_s_01 / t_s_01, xp=xp)[:, :, None] + M_s = xp.stack( [ - np.stack([inv_t_s, r_over_t_s], axis=-1), - np.stack([r_over_t_s, inv_t_s], axis=-1), + xp.stack([inv_t_s, r_over_t_s], axis=-1), + xp.stack([r_over_t_s, inv_t_s], axis=-1), ], axis=-2, ) t_p_01 = t_media_p[:, 0, :] # (A, W) r_p_01 = r_media_p[:, 0, :] # (A, W) - inv_t_p = (1 / t_p_01).T[:, :, None] - r_over_t_p = (r_p_01 / t_p_01).T[:, :, None] - M_p = np.stack( + inv_t_p = xp_matrix_transpose(1 / t_p_01, xp=xp)[:, :, None] + r_over_t_p = xp_matrix_transpose(r_p_01 / t_p_01, xp=xp)[:, :, None] + M_p = xp.stack( [ - np.stack([inv_t_p, r_over_t_p], axis=-1), - np.stack([r_over_t_p, inv_t_p], axis=-1), + xp.stack([inv_t_p, r_over_t_p], axis=-1), + xp.stack([r_over_t_p, inv_t_p], axis=-1), ], axis=-2, ) # Overall transfer matrix: M_tilde = I_01 @ M_1 @ M_2 @ ... (Byrnes Eq. 12) for i in range(layers_count): - M_s = np.matmul(M_s, M[:, :, :, i, :, :, 0]) - M_p = np.matmul(M_p, M[:, :, :, i, :, :, 1]) + M_s = xp.matmul(M_s, M[:, :, :, i, :, :, 0]) + M_p = xp.matmul(M_p, M[:, :, :, i, :, :, 1]) return TransferMatrixResult( M_s=M_s, diff --git a/colour/plotting/tests/test_common.py b/colour/plotting/tests/test_common.py index 8280d14bc3..32e9c091bb 100644 --- a/colour/plotting/tests/test_common.py +++ b/colour/plotting/tests/test_common.py @@ -44,7 +44,7 @@ uniform_axes3d, update_settings_collection, ) -from colour.utilities import attest +from colour.utilities import attest, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -142,7 +142,7 @@ def test_XYZ_to_plotting_colourspace(self) -> None: """ XYZ = np.random.random(3) - np.testing.assert_allclose( + xp_assert_close( XYZ_to_sRGB(XYZ), XYZ_to_plotting_colourspace(XYZ), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -160,29 +160,29 @@ def test_colour_cycle(self) -> None: cycler = colour_cycle() - np.testing.assert_allclose( + xp_assert_close( next(cycler), - np.array([0.95686275, 0.26274510, 0.21176471, 1.00000000]), + [0.95686275, 0.26274510, 0.21176471, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( next(cycler), - np.array([0.61582468, 0.15423299, 0.68456747, 1.00000000]), + [0.61582468, 0.15423299, 0.68456747, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( next(cycler), - np.array([0.25564014, 0.31377163, 0.70934256, 1.00000000]), + [0.25564014, 0.31377163, 0.70934256, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) cycler = colour_cycle(colour_cycle_map="viridis") - np.testing.assert_allclose( + xp_assert_close( next(cycler), - np.array([0.26700400, 0.00487400, 0.32941500, 1.00000000]), + [0.26700400, 0.00487400, 0.32941500, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/plotting/tests/test_diagrams.py b/colour/plotting/tests/test_diagrams.py index 99b36f1e75..ce8a8efdde 100644 --- a/colour/plotting/tests/test_diagrams.py +++ b/colour/plotting/tests/test_diagrams.py @@ -105,7 +105,8 @@ def test_plot_spectral_locus(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises(ValueError, lambda: plot_spectral_locus(method="Undefined")) + with pytest.raises(ValueError): + plot_spectral_locus(method="Undefined") class TestPlotChromaticityDiagramColours: @@ -125,10 +126,8 @@ def test_plot_chromaticity_diagram_colours(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises( - ValueError, - lambda: plot_chromaticity_diagram_colours(method="Undefined"), - ) + with pytest.raises(ValueError): + plot_chromaticity_diagram_colours(method="Undefined") figure, axes = plot_chromaticity_diagram_colours(diagram_colours="RGB") @@ -163,14 +162,12 @@ def test_plot_chromaticity_diagram(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises( - ValueError, - lambda: plot_chromaticity_diagram( + with pytest.raises(ValueError): + plot_chromaticity_diagram( method="Undefined", show_diagram_colours=False, show_spectral_locus=False, - ), - ) + ) class TestPlotChromaticityDiagramCIE1931: @@ -257,14 +254,12 @@ def test_plot_sds_in_chromaticity_diagram(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises( - ValueError, - lambda: plot_sds_in_chromaticity_diagram( + with pytest.raises(ValueError): + plot_sds_in_chromaticity_diagram( [SDS_ILLUMINANTS["A"], SDS_ILLUMINANTS["D65"]], chromaticity_diagram_callable=lambda **x: x, method="Undefined", - ), - ) + ) class TestPlotSdsInChromaticityDiagramCIE1931: diff --git a/colour/plotting/tests/test_models.py b/colour/plotting/tests/test_models.py index f90854e629..5e2d275c34 100644 --- a/colour/plotting/tests/test_models.py +++ b/colour/plotting/tests/test_models.py @@ -31,7 +31,7 @@ plot_RGB_chromaticities_in_chromaticity_diagram, plot_RGB_colourspaces_in_chromaticity_diagram, ) -from colour.utilities import is_scipy_installed +from colour.utilities import is_scipy_installed, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -76,31 +76,31 @@ def test_colourspace_model_axis_reorder(self) -> None: a = np.array([0, 1, 2]) - np.testing.assert_allclose( + xp_assert_close( colourspace_model_axis_reorder(a, "CIE Lab"), - np.array([1, 2, 0]), + [1, 2, 0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colourspace_model_axis_reorder(a, "IPT"), - np.array([1, 2, 0]), + [1, 2, 0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colourspace_model_axis_reorder(a, "OSA UCS"), - np.array([1, 2, 0]), + [1, 2, 0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colourspace_model_axis_reorder( colourspace_model_axis_reorder(a, "OSA UCS"), "OSA UCS", "Inverse", ), - np.array([0, 1, 2]), + [0, 1, 2], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -143,7 +143,8 @@ def test_plot_pointer_gamut(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises(ValueError, lambda: plot_pointer_gamut(method="Undefined")) + with pytest.raises(ValueError): + plot_pointer_gamut(method="Undefined") class TestPlotRGBColourspacesInChromaticityDiagram: @@ -176,14 +177,12 @@ def test_plot_RGB_colourspaces_in_chromaticity_diagram(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises( - ValueError, - lambda: plot_RGB_colourspaces_in_chromaticity_diagram( + with pytest.raises(ValueError): + plot_RGB_colourspaces_in_chromaticity_diagram( ["ITU-R BT.709", "ACEScg", "S-Gamut"], chromaticity_diagram_callable=lambda **x: x, method="Undefined", - ), - ) + ) class TestPlotRGBColourspacesInChromaticityDiagramCIE1931: @@ -355,7 +354,8 @@ def test_ellipses_MacAdam1942(self) -> None: assert len(ellipses_MacAdam1942()) == 25 - pytest.raises(ValueError, lambda: ellipses_MacAdam1942(method="Undefined")) + with pytest.raises(ValueError): + ellipses_MacAdam1942(method="Undefined") class TestPlotEllipsesMacAdam1942InChromaticityDiagram: diff --git a/colour/plotting/tests/test_temperature.py b/colour/plotting/tests/test_temperature.py index 57783a75eb..09708db5c2 100644 --- a/colour/plotting/tests/test_temperature.py +++ b/colour/plotting/tests/test_temperature.py @@ -69,7 +69,8 @@ def test_plot_daylight_locus(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises(ValueError, lambda: plot_daylight_locus(method="Undefined")) + with pytest.raises(ValueError): + plot_daylight_locus(method="Undefined") figure, axes = plot_daylight_locus(method="CIE 1976 UCS") @@ -113,7 +114,8 @@ def test_plot_planckian_locus(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises(ValueError, lambda: plot_planckian_locus(method="Undefined")) + with pytest.raises(ValueError): + plot_planckian_locus(method="Undefined") figure, axes = plot_planckian_locus(method="CIE 1976 UCS") @@ -168,15 +170,13 @@ def test_plot_planckian_locus_in_chromaticity_diagram(self) -> None: assert isinstance(figure, Figure) assert isinstance(axes, Axes) - pytest.raises( - ValueError, - lambda: plot_planckian_locus_in_chromaticity_diagram( + with pytest.raises(ValueError): + plot_planckian_locus_in_chromaticity_diagram( ["A", "B", "C"], chromaticity_diagram_callable=lambda **x: x, planckian_locus_callable=lambda **x: x, method="Undefined", - ), - ) + ) class TestPlotPlanckianLocusInChromaticityDiagramCIE1931: diff --git a/colour/plotting/tests/test_volume.py b/colour/plotting/tests/test_volume.py index 6b0540456c..342302909e 100644 --- a/colour/plotting/tests/test_volume.py +++ b/colour/plotting/tests/test_volume.py @@ -9,6 +9,7 @@ from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.plotting import plot_RGB_colourspaces_gamuts, plot_RGB_scatter from colour.plotting.volume import RGB_identity_cube, nadir_grid +from colour.utilities import xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -36,86 +37,80 @@ def test_nadir_grid(self) -> None: quads, faces_colours, edges_colours = nadir_grid(segments=1) - np.testing.assert_allclose( + xp_assert_close( quads, - np.array( + [ [ - [ - [-1.00000000, -1.00000000, 0.00000000], - [1.00000000, -1.00000000, 0.00000000], - [1.00000000, 1.00000000, 0.00000000], - [-1.00000000, 1.00000000, 0.00000000], - ], - [ - [-1.00000000, -1.00000000, 0.00000000], - [0.00000000, -1.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000], - [-1.00000000, 0.00000000, 0.00000000], - ], - [ - [-1.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - [-1.00000000, 1.00000000, 0.00000000], - ], - [ - [0.00000000, -1.00000000, 0.00000000], - [1.00000000, -1.00000000, 0.00000000], - [1.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000], - ], - [ - [0.00000000, 0.00000000, 0.00000000], - [1.00000000, 0.00000000, 0.00000000], - [1.00000000, 1.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - ], - [ - [-1.00000000, -0.00100000, 0.00000000], - [1.00000000, -0.00100000, 0.00000000], - [1.00000000, 0.00100000, 0.00000000], - [-1.00000000, 0.00100000, 0.00000000], - ], - [ - [-0.00100000, -1.00000000, 0.00000000], - [0.00100000, -1.00000000, 0.00000000], - [0.00100000, 1.00000000, 0.00000000], - [-0.00100000, 1.00000000, 0.00000000], - ], - ] - ), + [-1.00000000, -1.00000000, 0.00000000], + [1.00000000, -1.00000000, 0.00000000], + [1.00000000, 1.00000000, 0.00000000], + [-1.00000000, 1.00000000, 0.00000000], + ], + [ + [-1.00000000, -1.00000000, 0.00000000], + [0.00000000, -1.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000], + [-1.00000000, 0.00000000, 0.00000000], + ], + [ + [-1.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + [-1.00000000, 1.00000000, 0.00000000], + ], + [ + [0.00000000, -1.00000000, 0.00000000], + [1.00000000, -1.00000000, 0.00000000], + [1.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000], + ], + [ + [0.00000000, 0.00000000, 0.00000000], + [1.00000000, 0.00000000, 0.00000000], + [1.00000000, 1.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + ], + [ + [-1.00000000, -0.00100000, 0.00000000], + [1.00000000, -0.00100000, 0.00000000], + [1.00000000, 0.00100000, 0.00000000], + [-1.00000000, 0.00100000, 0.00000000], + ], + [ + [-0.00100000, -1.00000000, 0.00000000], + [0.00100000, -1.00000000, 0.00000000], + [0.00100000, 1.00000000, 0.00000000], + [-0.00100000, 1.00000000, 0.00000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( faces_colours, - np.array( - [ - [0.25000000, 0.25000000, 0.25000000, 0.10000000], - [0.00000000, 0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.00000000, 0.00000000, 1.00000000], - [0.00000000, 0.00000000, 0.00000000, 1.00000000], - ] - ), + [ + [0.25000000, 0.25000000, 0.25000000, 0.10000000], + [0.00000000, 0.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000, 0.00000000], + [0.00000000, 0.00000000, 0.00000000, 1.00000000], + [0.00000000, 0.00000000, 0.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( edges_colours, - np.array( - [ - [0.50000000, 0.50000000, 0.50000000, 0.50000000], - [0.75000000, 0.75000000, 0.75000000, 0.25000000], - [0.75000000, 0.75000000, 0.75000000, 0.25000000], - [0.75000000, 0.75000000, 0.75000000, 0.25000000], - [0.75000000, 0.75000000, 0.75000000, 0.25000000], - [0.00000000, 0.00000000, 0.00000000, 1.00000000], - [0.00000000, 0.00000000, 0.00000000, 1.00000000], - ] - ), + [ + [0.50000000, 0.50000000, 0.50000000, 0.50000000], + [0.75000000, 0.75000000, 0.75000000, 0.25000000], + [0.75000000, 0.75000000, 0.75000000, 0.25000000], + [0.75000000, 0.75000000, 0.75000000, 0.25000000], + [0.75000000, 0.75000000, 0.75000000, 0.25000000], + [0.00000000, 0.00000000, 0.00000000, 1.00000000], + [0.00000000, 0.00000000, 0.00000000, 1.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -131,63 +126,59 @@ def test_RGB_identity_cube(self) -> None: vertices, RGB = RGB_identity_cube(1, 1, 1) - np.testing.assert_allclose( + xp_assert_close( vertices, - np.array( + [ + [ + [0.00000000, 0.00000000, 0.00000000], + [1.00000000, 0.00000000, 0.00000000], + [1.00000000, 1.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + ], + [ + [0.00000000, 0.00000000, 1.00000000], + [1.00000000, 0.00000000, 1.00000000], + [1.00000000, 1.00000000, 1.00000000], + [0.00000000, 1.00000000, 1.00000000], + ], + [ + [0.00000000, 0.00000000, 0.00000000], + [1.00000000, 0.00000000, 0.00000000], + [1.00000000, 0.00000000, 1.00000000], + [0.00000000, 0.00000000, 1.00000000], + ], [ - [ - [0.00000000, 0.00000000, 0.00000000], - [1.00000000, 0.00000000, 0.00000000], - [1.00000000, 1.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - ], - [ - [0.00000000, 0.00000000, 1.00000000], - [1.00000000, 0.00000000, 1.00000000], - [1.00000000, 1.00000000, 1.00000000], - [0.00000000, 1.00000000, 1.00000000], - ], - [ - [0.00000000, 0.00000000, 0.00000000], - [1.00000000, 0.00000000, 0.00000000], - [1.00000000, 0.00000000, 1.00000000], - [0.00000000, 0.00000000, 1.00000000], - ], - [ - [0.00000000, 1.00000000, 0.00000000], - [1.00000000, 1.00000000, 0.00000000], - [1.00000000, 1.00000000, 1.00000000], - [0.00000000, 1.00000000, 1.00000000], - ], - [ - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 1.00000000, 0.00000000], - [0.00000000, 1.00000000, 1.00000000], - [0.00000000, 0.00000000, 1.00000000], - ], - [ - [1.00000000, 0.00000000, 0.00000000], - [1.00000000, 1.00000000, 0.00000000], - [1.00000000, 1.00000000, 1.00000000], - [1.00000000, 0.00000000, 1.00000000], - ], - ] - ), + [0.00000000, 1.00000000, 0.00000000], + [1.00000000, 1.00000000, 0.00000000], + [1.00000000, 1.00000000, 1.00000000], + [0.00000000, 1.00000000, 1.00000000], + ], + [ + [0.00000000, 0.00000000, 0.00000000], + [0.00000000, 1.00000000, 0.00000000], + [0.00000000, 1.00000000, 1.00000000], + [0.00000000, 0.00000000, 1.00000000], + ], + [ + [1.00000000, 0.00000000, 0.00000000], + [1.00000000, 1.00000000, 0.00000000], + [1.00000000, 1.00000000, 1.00000000], + [1.00000000, 0.00000000, 1.00000000], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB, - np.array( - [ - [0.50000000, 0.50000000, 0.00000000], - [0.50000000, 0.50000000, 1.00000000], - [0.50000000, 0.00000000, 0.50000000], - [0.50000000, 1.00000000, 0.50000000], - [0.00000000, 0.50000000, 0.50000000], - [1.00000000, 0.50000000, 0.50000000], - ] - ), + [ + [0.50000000, 0.50000000, 0.00000000], + [0.50000000, 0.50000000, 1.00000000], + [0.50000000, 0.00000000, 0.50000000], + [0.50000000, 1.00000000, 0.50000000], + [0.00000000, 0.50000000, 0.50000000], + [1.00000000, 0.50000000, 0.50000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/quality/cfi2017.py b/colour/quality/cfi2017.py index f2e4156de0..992b5049e8 100644 --- a/colour/quality/cfi2017.py +++ b/colour/quality/cfi2017.py @@ -21,7 +21,7 @@ import numpy as np -from colour.algebra import Extrapolator, euclidean_distance, linstep_function +from colour.algebra import Extrapolator, euclidean_distance from colour.appearance import ( VIEWING_CONDITIONS_CIECAM02, CAM_Specification_CIECAM02, @@ -29,14 +29,13 @@ ) from colour.colorimetry import ( MSDS_CMFS, + CIE_illuminant_D_series, MultiSpectralDistributions, SpectralDistribution, SpectralShape, msds_to_XYZ, + planck_law, reshape_msds, - sd_blackbody, - sd_CIE_illuminant_D_series, - sd_to_XYZ, ) if typing.TYPE_CHECKING: @@ -47,15 +46,20 @@ from colour.temperature import CCT_to_xy_CIE_D, uv_to_CCT_Ohno2013 from colour.utilities import ( CACHE_REGISTRY, + array_namespace, as_float, as_float_array, as_float_scalar, as_int_scalar, + as_ndarray, attest, is_caching_enabled, - tsplit, + suppress_warnings, tstack, usage_warning, + xp_as_float_array, + xp_average, + xp_matrix_transpose, ) __author__ = "Colour Developers" @@ -72,8 +76,6 @@ "ColourRendering_Specification_CIE2017", "colour_fidelity_index_CIE2017", "load_TCS_CIE2017", - "CCT_reference_illuminant", - "sd_reference_illuminant", "tcs_colorimetry_data", "delta_E_to_R_f", ] @@ -165,25 +167,27 @@ class ColourRendering_Specification_CIE2017: @typing.overload def colour_fidelity_index_CIE2017( - sd_test: SpectralDistribution, additional_data: Literal[True] = True -) -> ColourRendering_Specification_CIE2017: ... + sd_test: SpectralDistribution, additional_data: Literal[False] = False +) -> float: ... @typing.overload def colour_fidelity_index_CIE2017( - sd_test: SpectralDistribution, *, additional_data: Literal[False] -) -> float: ... + sd_test: SpectralDistribution, additional_data: Literal[True] +) -> ColourRendering_Specification_CIE2017: ... @typing.overload def colour_fidelity_index_CIE2017( - sd_test: SpectralDistribution, additional_data: Literal[False] -) -> float: ... + sd_test: MultiSpectralDistributions, + additional_data: Literal[False] = False, +) -> NDArrayFloat: ... def colour_fidelity_index_CIE2017( - sd_test: SpectralDistribution, additional_data: bool = False -) -> float | ColourRendering_Specification_CIE2017: + sd_test: SpectralDistribution | MultiSpectralDistributions, + additional_data: bool = False, +) -> float | NDArrayFloat | ColourRendering_Specification_CIE2017: """ Compute the *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` of the specified spectral distribution. @@ -191,13 +195,17 @@ def colour_fidelity_index_CIE2017( Parameters ---------- sd_test - Test spectral distribution. + Test spectral distribution. A + :class:`colour.MultiSpectralDistributions` of ``N`` test + illuminants is also accepted, in which case ``additional_data`` + must be ``False`` and the return value is a :class:`numpy.ndarray` + of ``N`` :math:`R_f` values. additional_data Whether to output additional data. Returns ------- - :class:`float` or \ + :class:`float`, :class:`numpy.ndarray` or \ :class:`colour.quality.ColourRendering_Specification_CIE2017` *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`. @@ -213,6 +221,15 @@ def colour_fidelity_index_CIE2017( np.float64(70.1208244...) """ + is_msds = isinstance(sd_test, MultiSpectralDistributions) + + if is_msds and additional_data: + error = ( + '"additional_data=True" is not supported when "sd_test" is a ' + '"MultiSpectralDistributions" instance.' + ) + raise NotImplementedError(error) + if sd_test.shape.interval > 5: error = ( "Test spectral distribution interval is greater than " @@ -249,9 +266,6 @@ def colour_fidelity_index_CIE2017( if sd_test.shape.boundaries != shape.boundaries: sd_test.trim(shape) - CCT, D_uv = tsplit(CCT_reference_illuminant(sd_test)) - sd_reference = sd_reference_illuminant(CCT, shape) - # NOTE: All computations except CCT calculation use the # "CIE 1964 10 Degree Standard Observer". cmfs_10 = reshape_msds( @@ -260,32 +274,127 @@ def colour_fidelity_index_CIE2017( sds_tcs = load_TCS_CIE2017(shape) - ( - test_tcs_colorimetry_data, - reference_tcs_colorimetry_data, - ) = tcs_colorimetry_data([sd_test, sd_reference], sds_tcs, cmfs_10) + sd_test_values = sd_test.values + xp = array_namespace(sds_tcs.values, sd_test_values) + + if is_msds: + test_values = xp_matrix_transpose( + xp_as_float_array(sd_test_values, xp=xp), xp=xp + ) + else: + test_values = xp_as_float_array(sd_test_values, xp=xp)[None, :] + + XYZ_test_2deg = msds_to_XYZ(test_values, method="Integration", shape=shape) + CCT_Duv = uv_to_CCT_Ohno2013( + UCS_to_uv(XYZ_to_UCS(XYZ_test_2deg)), start=1000, end=25000 + ) + CCT = CCT_Duv[..., 0] + D_uv = CCT_Duv[..., 1] + + # ``CIE 2017 CFI`` 3-way reference: Y-normalised Planckian / daylight + # mixture clipped over the ``[4000, 5000]`` K transition window + # (:cite:`CIETC1-902017`, Section 4.2). ``planck_law`` squeezes its + # output, so a single-CCT batch collapses to 1-D; the sample axis is + # reinstated below. This vectorised mixture is the single source of the + # reference illuminant; the ``additional_data`` branch wraps the + # corresponding ``ref_values`` row in a spectral distribution. + planckian = planck_law(shape.wavelengths * 1e-9, CCT) * 1e-9 + planckian_values = ( + planckian[None, :] + if planckian.ndim == 1 + else xp_matrix_transpose(planckian, xp=xp) + ) + # ``CCT_to_xy_CIE_D`` warns for any sample outside ``[4000, 25000]`` K + # (matching the suppression in :mod:`colour.quality.cri` / + # :mod:`colour.quality.cqs`); ``m = 0`` nulls the extrapolated daylight + # so the leaked warning is spurious here. + with suppress_warnings(colour_usage_warnings=True): + daylight = CIE_illuminant_D_series(CCT_to_xy_CIE_D(CCT), shape=shape) + daylight_values = ( + daylight[None, :] + if daylight.ndim == 1 + else xp_matrix_transpose(daylight, xp=xp) + ) + Y_planckian = msds_to_XYZ(planckian_values, method="Integration", shape=shape)[ + ..., 1:2 + ] + Y_daylight = msds_to_XYZ(daylight_values, method="Integration", shape=shape)[ + ..., 1:2 + ] + m = xp.clip((CCT - 4000) / 1000, 0, 1)[..., None] + ref_values = (1 - m) * (planckian_values / Y_planckian) + m * ( + daylight_values / Y_daylight + ) + + irradiance_values = xp.stack([test_values, ref_values]) + XYZ_t = msds_to_XYZ(irradiance_values, cmfs_10, method="Integration", shape=shape) + k = 100 / XYZ_t[..., 1:2] + XYZ_w = k * XYZ_t + irradiance_values = irradiance_values * k - delta_E_s = euclidean_distance( - test_tcs_colorimetry_data.Jpapbp, - reference_tcs_colorimetry_data.Jpapbp, + XYZ, specification, JMh, Jpapbp = _tcs_colorimetry_data( + irradiance_values, XYZ_w, sds_tcs, cmfs_10 ) + delta_E_s = euclidean_distance(Jpapbp[0], Jpapbp[1]) + R_s = delta_E_to_R_f(delta_E_s) - R_f = cast("float", delta_E_to_R_f(np.average(delta_E_s))) - - if additional_data: - return ColourRendering_Specification_CIE2017( - sd_test.name, - sd_reference, - R_f, - R_s, - CCT, - D_uv, - (test_tcs_colorimetry_data, reference_tcs_colorimetry_data), - delta_E_s, - ) + R_f = delta_E_to_R_f(xp_average(delta_E_s, axis=-1, xp=xp)) + + if is_msds: + return R_f + + R_f_scalar = as_float_scalar(R_f[0]) + + if not additional_data: + return R_f_scalar + + sd_reference = SpectralDistribution( + as_ndarray(ref_values[0]), + shape.wavelengths, + name=f"{int(CCT[0])}K CIE 2017 Reference Illuminant", + ) + + # Drop the size-1 batch axis from the rank-3 outputs to recover the + # original (n_irradiances, n_TCS, *) layout. + XYZ = XYZ[:, 0] + JMh = JMh[:, 0] + Jpapbp = Jpapbp[:, 0] + specification = CAM_Specification_CIECAM02( + **{ + name: (value[:, 0] if value is not None else None) + for name, value in specification + } + ) - return R_f + # ``as_float_array`` materialises the dataclass to *NumPy* via + # ``__array__``, so the transpose stays on *NumPy* regardless of ``xp``. + specification = np.transpose(as_float_array(specification), (0, 2, 1)) + specifications = [CAM_Specification_CIECAM02(*t) for t in specification] + test_tcs_colorimetry_data = DataColorimetry_TCS_CIE2017( + sds_tcs.display_labels, + XYZ[0], + specifications[0], + JMh[0], + Jpapbp[0], + ) + reference_tcs_colorimetry_data = DataColorimetry_TCS_CIE2017( + sds_tcs.display_labels, + XYZ[1], + specifications[1], + JMh[1], + Jpapbp[1], + ) + return ColourRendering_Specification_CIE2017( + sd_test.name, + sd_reference, + R_f_scalar, + R_s[0], + as_float_scalar(CCT[0]), + as_float_scalar(D_uv[0]), + (test_tcs_colorimetry_data, reference_tcs_colorimetry_data), + delta_E_s[0], + ) def load_TCS_CIE2017(shape: SpectralShape) -> MultiSpectralDistributions: @@ -339,124 +448,72 @@ def load_TCS_CIE2017(shape: SpectralShape) -> MultiSpectralDistributions: return tcs -def CCT_reference_illuminant(sd: SpectralDistribution) -> NDArrayFloat: +def _tcs_colorimetry_data( + irradiance_values: NDArrayFloat, + XYZ_w: NDArrayFloat, + sds_tcs: MultiSpectralDistributions, + cmfs: MultiSpectralDistributions, +) -> Tuple[NDArrayFloat, CAM_Specification_CIECAM02, NDArrayFloat, NDArrayFloat]: """ - Compute the reference illuminant correlated colour temperature - :math:`T_{cp}` and :math:`\\Delta_{uv}` for the specified test spectral - distribution using the *Ohno (2013)* method. + Compute the *test colour samples* colorimetry arrays under the specified + irradiance(s) and reference white point(s) for the *CIE 2017 Colour + Fidelity Index* (CFI) computations. Parameters ---------- - sd - Test spectral distribution. + irradiance_values + Per-illuminant normalised irradiance spectral values of shape + ``(..., n_wavelengths)``. + XYZ_w + Per-illuminant *CIE XYZ* tristimulus values of shape ``(..., 3)``. + sds_tcs + *Test colour samples* spectral reflectance distributions. + cmfs + Standard observer colour matching functions. Returns ------- - :class:`numpy.ndarray` - Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. - - Examples - -------- - >>> from colour import SDS_ILLUMINANTS - >>> sd = SDS_ILLUMINANTS["FL2"] - >>> CCT_reference_illuminant(sd) # doctest: +ELLIPSIS - array([4.2244776...e+03, 1.7885608...e-03]) - """ - - XYZ = sd_to_XYZ(sd.values, shape=sd.shape, method="Integration") - - # NOTE: Use "CFI2017" and "TM30" recommended temperature range of 1,000K to - # 25,000K for performance. - return uv_to_CCT_Ohno2013(UCS_to_uv(XYZ_to_UCS(XYZ)), start=1000, end=25000) - - -def sd_reference_illuminant(CCT: float, shape: SpectralShape) -> SpectralDistribution: + :class:`tuple` + ``(XYZ, specification, JMh, Jpapbp)`` arrays, each with leading + shape ``(..., n_test_colour_samples)``. """ - Compute the reference illuminant for the specified correlated colour - temperature :math:`T_{cp}` for use in *CIE 2017 Colour Fidelity Index* - (CFI) computation. - - Parameters - ---------- - CCT - Correlated colour temperature :math:`T_{cp}`. - shape - Desired shape of the returned spectral distribution. - Returns - ------- - :class:`colour.SpectralDistribution` - Reference illuminant for *CIE 2017 Colour Fidelity Index* (CFI) - computation. + sds_tcs_values_raw = sds_tcs.values + xp = array_namespace(irradiance_values, sds_tcs_values_raw) - Examples - -------- - >>> from colour.utilities import numpy_print_options - >>> with numpy_print_options(suppress=True): - ... sd_reference_illuminant( # doctest: +ELLIPSIS - ... 4224.469705295263300, SpectralShape(380, 780, 20) - ... ) - SpectralDistribution([[380. , 0.0034089...], - [400. , 0.0044208...], - [420. , 0.0053260...], - [440. , 0.0062857...], - [460. , 0.0072767...], - [480. , 0.0080207...], - [500. , 0.0086590...], - [520. , 0.0092242...], - [540. , 0.0097686...], - [560. , 0.0101444...], - [580. , 0.0104475...], - [600. , 0.0107642...], - [620. , 0.0110439...], - [640. , 0.0112535...], - [660. , 0.0113922...], - [680. , 0.0115185...], - [700. , 0.0113155...], - [720. , 0.0108192...], - [740. , 0.0111582...], - [760. , 0.0101299...], - [780. , 0.0105638...]], - SpragueInterpolator, - {}, - Extrapolator, - {'method': 'Constant', 'left': None, 'right': None}) - """ + sds_tcs_values = xp_as_float_array( + sds_tcs_values_raw, xp=xp, like=irradiance_values + ) + sds_tcs_t = ( + xp_matrix_transpose(sds_tcs_values, xp=xp) * irradiance_values[..., None, :] + ) - if CCT <= 5000: - sd_planckian = sd_blackbody(CCT, shape) - - if CCT >= 4000: - xy = CCT_to_xy_CIE_D(CCT) - sd_daylight = sd_CIE_illuminant_D_series(xy, shape=shape) - - if CCT < 4000: - sd_reference = sd_planckian - elif 4000 <= CCT <= 5000: - # Planckian and daylight illuminant must be normalised so that the - # mixture isn't biased. - sd_planckian = ( - sd_planckian - / sd_to_XYZ(sd_planckian.values, shape=shape, method="Integration")[1] - ) - sd_daylight = ( - sd_daylight - / sd_to_XYZ(sd_daylight.values, shape=shape, method="Integration")[1] - ) + XYZ = msds_to_XYZ( + sds_tcs_t, + cmfs, + method="Integration", + shape=sds_tcs.shape, + ) + specification = XYZ_to_CIECAM02( + XYZ, + XYZ_w[..., None, :], + 100, # L_A + 20, # Y_b + VIEWING_CONDITIONS_CIECAM02["Average"], + discount_illuminant=True, + compute_H=False, + ) - # Mixture: 4200K should be 80% Planckian, 20% CIE Illuminant D Series. - m = (CCT - 4000) / 1000 - values = linstep_function(m, sd_planckian.values, sd_daylight.values) - name = ( - f"{as_int_scalar(CCT)}K " - f"Blackbody & CIE Illuminant D Series Mixture - " - f"{as_float_scalar(100 * m):.1f}%" - ) - sd_reference = SpectralDistribution(values, shape.wavelengths, name=name) - elif CCT > 5000: - sd_reference = sd_daylight + JMh = tstack( + [ + cast("NDArrayFloat", specification.J), + cast("NDArrayFloat", specification.M), + cast("NDArrayFloat", specification.h), + ] + ) + Jpapbp = JMh_CIECAM02_to_CAM02UCS(JMh) - return sd_reference + return XYZ, specification, JMh, Jpapbp def tcs_colorimetry_data( @@ -501,58 +558,36 @@ def tcs_colorimetry_data( if isinstance(sd_irradiance, SpectralDistribution): sd_irradiance = [sd_irradiance] - XYZ_t_s = [ - sd_to_XYZ(sd.values, cmfs, shape=sd.shape, method="Integration") - for sd in sd_irradiance - ] - k_s = [100 / XYZ_t[1] for XYZ_t in XYZ_t_s] - XYZ_w = as_float_array([k * XYZ_t for k, XYZ_t in zip(k_s, XYZ_t_s, strict=False)]) - sd_irradiance = [sd.copy() * k for sd, k in zip(sd_irradiance, k_s, strict=False)] - - Y_b = 20 - L_A = 100 - surround = VIEWING_CONDITIONS_CIECAM02["Average"] - - sds_tcs_t = np.tile(np.transpose(sds_tcs.values), (len(sd_irradiance), 1, 1)) - sds_tcs_t = sds_tcs_t * np.reshape( - as_float_array([sd.values for sd in sd_irradiance]), - (len(sd_irradiance), 1, len(sd_irradiance[0])), - ) + xp = array_namespace(sds_tcs.values, sd_irradiance[0].values) - XYZ = msds_to_XYZ( - sds_tcs_t, + irradiance_values = xp.stack( + [xp_as_float_array(sd.values, xp=xp) for sd in sd_irradiance] + ) + XYZ_t = msds_to_XYZ( + irradiance_values, cmfs, method="Integration", - shape=sds_tcs.shape, - ) - specification = XYZ_to_CIECAM02( - XYZ, - np.reshape(XYZ_w, (len(sd_irradiance), 1, 3)), - L_A, - Y_b, - surround, - discount_illuminant=True, - compute_H=False, + shape=sd_irradiance[0].shape, ) + k = 100 / XYZ_t[..., 1:2] + XYZ_w = k * XYZ_t + irradiance_values = irradiance_values * k - JMh = tstack( - [ - cast("NDArrayFloat", specification.J), - cast("NDArrayFloat", specification.M), - cast("NDArrayFloat", specification.h), - ] + XYZ, specification, JMh, Jpapbp = _tcs_colorimetry_data( + irradiance_values, XYZ_w, sds_tcs, cmfs ) - Jpapbp = JMh_CIECAM02_to_CAM02UCS(JMh) - specification = as_float_array(specification).transpose((0, 2, 1)) - specification = [CAM_Specification_CIECAM02(*t) for t in specification] + # ``as_float_array`` materialises the dataclass to *NumPy* via + # ``__array__``, so the transpose stays on *NumPy* regardless of ``xp``. + specification = np.transpose(as_float_array(specification), (0, 2, 1)) + specifications = [CAM_Specification_CIECAM02(*t) for t in specification] return tuple( [ DataColorimetry_TCS_CIE2017( sds_tcs.display_labels, XYZ[sd_idx], - specification[sd_idx], + specifications[sd_idx], JMh[sd_idx], Jpapbp[sd_idx], ) @@ -580,6 +615,8 @@ def delta_E_to_R_f(delta_E: ArrayLike) -> NDArrayFloat: delta_E = as_float_array(delta_E) + xp = array_namespace(delta_E) + c_f = 6.73 - return as_float(10 * np.log1p(np.exp((100 - c_f * delta_E) / 10))) + return as_float(10 * xp.log1p(xp.exp((100 - c_f * delta_E) / 10))) diff --git a/colour/quality/cqs.py b/colour/quality/cqs.py index 70ad2dfa1c..16e46821d1 100644 --- a/colour/quality/cqs.py +++ b/colour/quality/cqs.py @@ -26,20 +26,19 @@ import typing from dataclasses import dataclass -import numpy as np - from colour.adaptation import chromatic_adaptation_VonKries from colour.algebra import euclidean_distance, sdiv, sdiv_mode from colour.colorimetry import ( CCS_ILLUMINANTS, MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT, + CIE_illuminant_D_series, MultiSpectralDistributions, SpectralDistribution, + msds_to_XYZ, + planck_law, reshape_msds, reshape_sd, - sd_blackbody, - sd_CIE_illuminant_D_series, sd_to_XYZ, ) @@ -52,12 +51,22 @@ Tuple, ) -from colour.hints import cast from colour.models import Lab_to_LCHab # pyright: ignore from colour.models import UCS_to_uv, XYZ_to_Lab, XYZ_to_UCS, XYZ_to_xy, xy_to_XYZ from colour.quality.datasets.vs import INDEXES_TO_NAMES_VS, SDS_VS from colour.temperature import CCT_to_xy_CIE_D, uv_to_CCT_Ohno2013 -from colour.utilities import as_float_array, domain_range_scale, tsplit, validate_method +from colour.utilities import ( + array_namespace, + as_float_array, + as_float_scalar, + domain_range_scale, + suppress_warnings, + tsplit, + validate_method, + xp_as_float_array, + xp_average, + xp_matrix_transpose, +) from colour.utilities.documentation import DocstringTuple, is_documentation_building __author__ = "Colour Developers" @@ -209,8 +218,7 @@ class ColourRendering_Specification_CQS: @typing.overload def colour_quality_scale( sd_test: SpectralDistribution, - *, - additional_data: Literal[False], + additional_data: Literal[False] = False, method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = ..., ) -> float: ... @@ -218,24 +226,24 @@ def colour_quality_scale( @typing.overload def colour_quality_scale( sd_test: SpectralDistribution, - additional_data: Literal[True] = True, + additional_data: Literal[True], method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = ..., ) -> ColourRendering_Specification_CQS: ... @typing.overload def colour_quality_scale( - sd_test: SpectralDistribution, - additional_data: Literal[False], + sd_test: MultiSpectralDistributions, + additional_data: Literal[False] = False, method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = ..., -) -> float: ... +) -> NDArrayFloat: ... def colour_quality_scale( - sd_test: SpectralDistribution, + sd_test: SpectralDistribution | MultiSpectralDistributions, additional_data: bool = False, method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = "NIST CQS 9.0", -) -> float | ColourRendering_Specification_CQS: +) -> float | NDArrayFloat | ColourRendering_Specification_CQS: """ Compute the *Colour Quality Scale* (CQS) of the specified spectral distribution using the specified method. @@ -243,7 +251,11 @@ def colour_quality_scale( Parameters ---------- sd_test - Test spectral distribution. + Test spectral distribution. A + :class:`colour.MultiSpectralDistributions` of ``N`` test + illuminants is also accepted, in which case ``additional_data`` + must be ``False`` and the return value is a :class:`numpy.ndarray` + of ``N`` :math:`Q_a` values. additional_data Whether to output additional data. method @@ -251,7 +263,8 @@ def colour_quality_scale( Returns ------- - :class:`float` or :class:`colour.quality.ColourRendering_Specification_CQS` + :class:`float`, :class:`numpy.ndarray` or \ +:class:`colour.quality.ColourRendering_Specification_CQS` *Colour Quality Scale* (CQS). References @@ -275,89 +288,161 @@ def colour_quality_scale( ) shape = cmfs.shape - sd_test = reshape_sd(sd_test, shape, copy=False) vs_sds = { sd.name: reshape_sd(sd, shape, copy=False) for sd in SDS_VS[method].values() } + is_msds = isinstance(sd_test, MultiSpectralDistributions) + if is_msds and additional_data: + error = ( + '"additional_data=True" is not supported when "sd_test" is a ' + '"MultiSpectralDistributions" instance.' + ) + raise NotImplementedError(error) + + if is_msds: + sd_test = reshape_msds(sd_test, shape, copy=False) + sd_test_values = sd_test.values + xp = array_namespace(sd_test_values) + test_values = xp_matrix_transpose( + xp_as_float_array(sd_test_values, xp=xp), xp=xp + ) + else: + sd_test = reshape_sd(sd_test, shape, copy=False) + sd_test_values = sd_test.values + xp = array_namespace(sd_test_values) + test_values = xp_as_float_array(sd_test_values, xp=xp)[None, :] + with domain_range_scale("1"): - XYZ = sd_to_XYZ(sd_test, cmfs) + XYZ = ( + msds_to_XYZ(test_values, cmfs, method="Integration", shape=shape) + if is_msds + else sd_to_XYZ(sd_test, cmfs)[None, :] + ) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) - CCT, _D_uv = uv_to_CCT_Ohno2013(uv) - - if CCT < 5000: - sd_reference = sd_blackbody(CCT, shape) - else: - xy = CCT_to_xy_CIE_D(CCT) - sd_reference = sd_CIE_illuminant_D_series(xy) - sd_reference.align(shape) + CCT = uv_to_CCT_Ohno2013(uv)[..., 0] + + # ``planck_law`` squeezes its output, so a single-CCT batch collapses + # to 1-D; the sample axis is reinstated below. + planckian = planck_law(shape.wavelengths * 1e-9, CCT) * 1e-9 + planckian_values = ( + planckian[None, :] + if planckian.ndim == 1 + else xp_matrix_transpose(planckian, xp=xp) + ) + # See :mod:`colour.quality.cri` for the warning-suppression rationale. + with suppress_warnings(colour_usage_warnings=True): + daylight = CIE_illuminant_D_series(CCT_to_xy_CIE_D(CCT), shape=shape) + daylight_values = ( + daylight[None, :] + if daylight.ndim == 1 + else xp_matrix_transpose(daylight, xp=xp) + ) + ref_values = xp.where(CCT[..., None] < 5000, planckian_values, daylight_values) - test_vs_colorimetry_data = vs_colorimetry_data( - sd_test, sd_reference, vs_sds, cmfs, chromatic_adaptation=True + test_names, test_XYZ, test_Lab, test_C = _vs_colorimetry_data( + test_values, ref_values, vs_sds, cmfs, chromatic_adaptation=True + ) + ref_names, ref_XYZ, ref_Lab, ref_C = _vs_colorimetry_data( + ref_values, ref_values, vs_sds, cmfs ) - reference_vs_colorimetry_data = vs_colorimetry_data( - sd_reference, sd_reference, vs_sds, cmfs + D_C_ab = test_C - ref_C + D_E_ab = euclidean_distance(test_Lab, ref_Lab) + # ``D_E_ab ** 2 >= D_C_ab ** 2`` by Pythagoras; the inner ``xp.where`` + # guards ``xp.sqrt`` against floating-point cancellation, not a colour + # data clamp. + D_Ep_squared = D_E_ab**2 - D_C_ab**2 + D_Ep_ab = xp.where( + D_C_ab > 0, + xp.sqrt(xp.where(D_Ep_squared > 0, D_Ep_squared, xp.zeros_like(D_Ep_squared))), + D_E_ab, ) - CCT_f: float if method == "nist cqs 9.0": - CCT_f = 1 + CCT_f = xp.ones_like(D_C_ab[..., 0]) scaling_f = 3.2 else: - XYZ_r = sd_to_XYZ(sd_reference, cmfs) - XYZ_r = XYZ_r / XYZ_r[1] - CCT_f = CCT_factor(reference_vs_colorimetry_data, XYZ_r) + ref_XYZ_white = msds_to_XYZ( + ref_values, cmfs, method="Integration", shape=cmfs.shape + ) + with sdiv_mode(): + ref_XYZ_white_n = sdiv(ref_XYZ_white, ref_XYZ_white[..., 1:2]) + CCT_f = _CCT_factor(ref_XYZ, ref_XYZ_white_n) scaling_f = 3.104 - Q_as = colour_quality_scales( - test_vs_colorimetry_data, - reference_vs_colorimetry_data, - scaling_f, - CCT_f, - ) - - D_E_RMS = delta_E_RMS(Q_as, "D_E_ab") - D_Ep_RMS = delta_E_RMS(Q_as, "D_Ep_ab") + D_E_RMS = xp.sqrt(xp_average(D_E_ab**2, axis=-1, xp=xp)) + D_Ep_RMS = xp.sqrt(xp_average(D_Ep_ab**2, axis=-1, xp=xp)) Q_a = scale_conversion(D_Ep_RMS, CCT_f, scaling_f) - scaling_f = 2.93 * 1.0343 if method == "nist cqs 9.0" else 2.928 - - Q_f = scale_conversion(D_E_RMS, CCT_f, scaling_f) - - G_t = gamut_area([vs_CQS_data.Lab for vs_CQS_data in test_vs_colorimetry_data]) - G_r = gamut_area([vs_CQS_data.Lab for vs_CQS_data in reference_vs_colorimetry_data]) + scaling_f_Q_f = 2.93 * 1.0343 if method == "nist cqs 9.0" else 2.928 + Q_f = scale_conversion(D_E_RMS, CCT_f, scaling_f_Q_f) + G_t = gamut_area(test_Lab) + G_r = gamut_area(ref_Lab) Q_g = G_t / GAMUT_AREA_D65 * 100 + Q_p: NDArrayFloat | None + Q_d: NDArrayFloat | None if method == "nist cqs 9.0": - Q_p = Q_d = None + Q_p = None + Q_d = None else: - p_delta_C = cast( - "float", - np.average([max(0, sample_data.D_C_ab) for sample_data in Q_as.values()]), + p_delta_C = xp_average( + xp.where(D_C_ab > 0, D_C_ab, xp.zeros_like(D_C_ab)), + axis=-1, + xp=xp, ) Q_p = 100 - 3.6 * (D_Ep_RMS - p_delta_C) Q_d = G_t / G_r * CCT_f * 100 + if is_msds: + return Q_a + + Q_a_scalar = as_float_scalar(Q_a[0]) + if additional_data: + Q_f_scalar = as_float_scalar(Q_f[0]) + Q_p_scalar = as_float_scalar(Q_p[0]) if Q_p is not None else None + Q_g_scalar = as_float_scalar(Q_g[0]) + Q_d_scalar = as_float_scalar(Q_d[0]) if Q_d is not None else None + + CCT_f_scalar = as_float_scalar(CCT_f[0]) + + Q_as: Dict[int, DataColourQualityScale_VS] = {} + for i, name in enumerate(test_names): + D_C_i = as_float_scalar(D_C_ab[0, i]) + D_E_i = as_float_scalar(D_E_ab[0, i]) + D_Ep_i = as_float_scalar(D_Ep_ab[0, i]) + Q_a_i = float(scale_conversion(D_Ep_i, CCT_f_scalar, scaling_f)) + Q_as[i + 1] = DataColourQualityScale_VS(name, Q_a_i, D_C_i, D_E_i, D_Ep_i) + + test_data = tuple( + DataColorimetry_VS(name, test_XYZ[0, i], test_Lab[0, i], test_C[0, i]) + for i, name in enumerate(test_names) + ) + ref_data = tuple( + DataColorimetry_VS(name, ref_XYZ[0, i], ref_Lab[0, i], ref_C[0, i]) + for i, name in enumerate(ref_names) + ) + return ColourRendering_Specification_CQS( sd_test.name, - Q_a, - Q_f, - Q_p, - Q_g, - Q_d, + Q_a_scalar, + Q_f_scalar, + Q_p_scalar, + Q_g_scalar, + Q_d_scalar, Q_as, - (test_vs_colorimetry_data, reference_vs_colorimetry_data), + (test_data, ref_data), ) - return Q_a + return Q_a_scalar -def gamut_area(Lab: ArrayLike) -> float: +def gamut_area(Lab: ArrayLike) -> NDArrayFloat: """ Compute the gamut area :math:`G` covered by the specified *CIE L\\*a\\*b\\** colourspace matrices. @@ -374,6 +459,7 @@ def gamut_area(Lab: ArrayLike) -> float: Examples -------- + >>> import numpy as np >>> Lab = [ ... np.array([39.94996006, 34.59018231, -19.86046321]), ... np.array([38.88395498, 21.44348519, -34.87805301]), @@ -396,18 +482,96 @@ def gamut_area(Lab: ArrayLike) -> float: """ Lab = as_float_array(Lab) - Lab_s = np.roll(np.copy(Lab), -3) - _L, a, b = tsplit(Lab) - _L_s, a_s, b_s = tsplit(Lab_s) + xp = array_namespace(Lab) + + Lab_s = xp.roll(Lab, shift=-1, axis=-2) - A = np.linalg.norm(Lab[..., 1:3], axis=-1) - B = np.linalg.norm(Lab_s[..., 1:3], axis=-1) - C = np.linalg.norm(np.dstack([a_s - a, b_s - b]), axis=-1) + A = xp.linalg.vector_norm(Lab[..., 1:3], axis=-1) + B = xp.linalg.vector_norm(Lab_s[..., 1:3], axis=-1) + C = xp.linalg.vector_norm(Lab_s[..., 1:3] - Lab[..., 1:3], axis=-1) t = (A + B + C) / 2 - S = np.sqrt(t * (t - A) * (t - B) * (t - C)) + S = xp.sqrt(t * (t - A) * (t - B) * (t - C)) + + return xp.sum(S, axis=-1) + + +def _vs_colorimetry_data( + t_values: NDArrayFloat, + r_values: NDArrayFloat, + sds_vs: Dict[str, SpectralDistribution], + cmfs: MultiSpectralDistributions, + chromatic_adaptation: bool = False, +) -> Tuple[list[str], NDArrayFloat, NDArrayFloat, NDArrayFloat]: + """ + Compute the *VS test colour samples* colorimetry arrays in a single + vectorised pass over an arbitrary leading shape of test/reference + irradiance pairs. + + Parameters + ---------- + t_values, r_values + Test and reference irradiance values of shape + ``(..., n_wavelengths)``. + + Returns + ------- + :class:`tuple` + ``(names, XYZ_vs, Lab_vs, C_vs)`` with leading shape + ``(..., n_test_colour_samples)``. + """ - return np.sum(S) + XYZ_t = msds_to_XYZ(t_values, cmfs, method="Integration", shape=cmfs.shape) + + with sdiv_mode(): + XYZ_t_n = sdiv(XYZ_t, XYZ_t[..., 1:2]) + + XYZ_r = msds_to_XYZ(r_values, cmfs, method="Integration", shape=cmfs.shape) + + with sdiv_mode(): + XYZ_r_n = sdiv(XYZ_r, XYZ_r[..., 1:2]) + + xy_r = XYZ_to_xy(XYZ_r_n) + + names: list[str] = [] + vs_values_list = [] + for _key, value in sorted(INDEXES_TO_NAMES_VS.items()): + if value not in sds_vs: + continue + names.append(sds_vs[value].name) + vs_values_list.append(sds_vs[value].values) + + xp = array_namespace(XYZ_t, t_values) + vs_values = xp.stack( + [xp_as_float_array(values, xp=xp, like=XYZ_t) for values in vs_values_list] + ) + + # Vectorised :math:`XYZ_{vs}` across the VS test colour samples; the + # ``1 / Y_t`` factor recovers the ``domain_range_scale("1")`` + # reflectance-under-illuminant scale. + sds_vs_t = vs_values * t_values[..., None, :] + XYZ_vs = ( + msds_to_XYZ( + sds_vs_t, + cmfs, + method="Integration", + shape=cmfs.shape, + ) + / XYZ_t[..., 1:2, None] + ) + + if chromatic_adaptation: + XYZ_vs = chromatic_adaptation_VonKries( + XYZ_vs, + XYZ_t_n[..., None, :], + XYZ_r_n[..., None, :], + transform="CMCCAT2000", + ) + + Lab_vs = XYZ_to_Lab(XYZ_vs, illuminant=xy_r[..., None, :]) + _L_vs, C_vs, _Hab = tsplit(Lab_to_LCHab(Lab_vs)) + + return names, XYZ_vs, Lab_vs, C_vs def vs_colorimetry_data( @@ -439,36 +603,19 @@ def vs_colorimetry_data( *VS test colour samples* colorimetry data. """ - XYZ_t = sd_to_XYZ(sd_test, cmfs) - - with sdiv_mode(): - XYZ_t = sdiv(XYZ_t, XYZ_t[1]) - - XYZ_r = sd_to_XYZ(sd_reference, cmfs) - - with sdiv_mode(): - XYZ_r = sdiv(XYZ_r, XYZ_r[1]) - - xy_r = XYZ_to_xy(XYZ_r) - - vs_data = [] - for _key, value in sorted(INDEXES_TO_NAMES_VS.items()): - sd_vs = sds_vs[value] - - with domain_range_scale("1"): - XYZ_vs = sd_to_XYZ(sd_vs, cmfs, sd_test) - - if chromatic_adaptation: - XYZ_vs = chromatic_adaptation_VonKries( - XYZ_vs, XYZ_t, XYZ_r, transform="CMCCAT2000" - ) - - Lab_vs = XYZ_to_Lab(XYZ_vs, illuminant=xy_r) - _L_vs, C_vs, _Hab = Lab_to_LCHab(Lab_vs) - - vs_data.append(DataColorimetry_VS(sd_vs.name, XYZ_vs, Lab_vs, C_vs)) + xp = array_namespace(sd_test.values) + names, XYZ_vs, Lab_vs, C_vs = _vs_colorimetry_data( + xp_as_float_array(sd_test.values, xp=xp), + xp_as_float_array(sd_reference.values, xp=xp), + sds_vs, + cmfs, + chromatic_adaptation, + ) - return tuple(vs_data) + return tuple( + DataColorimetry_VS(name, XYZ_vs[i], Lab_vs[i], C_vs[i]) + for i, name in enumerate(names) + ) def CCT_factor( @@ -491,13 +638,34 @@ def CCT_factor( Correlated colour temperature factor. """ + XYZ_vs = as_float_array( + [colorimetry_data.XYZ for colorimetry_data in reference_data] + ) + + return as_float_scalar(_CCT_factor(XYZ_vs, as_float_array(XYZ_r))) + + +def _CCT_factor(XYZ_vs: NDArrayFloat, XYZ_r: NDArrayFloat) -> NDArrayFloat: + """ + Compute the correlated colour temperature factor for arbitrary leading + batch shape. + + Parameters + ---------- + XYZ_vs + Reference VS sample tristimulus values of shape + ``(..., n_test_colour_samples, 3)``. + XYZ_r + Reference white tristimulus values of shape ``(..., 3)``. + """ + xy_w = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] XYZ_w = xy_to_XYZ(xy_w) Lab = XYZ_to_Lab( chromatic_adaptation_VonKries( - [colorimetry_data.XYZ for colorimetry_data in reference_data], - XYZ_r, + XYZ_vs, + XYZ_r[..., None, :], XYZ_w, transform="CMCCAT2000", ), @@ -506,10 +674,14 @@ def CCT_factor( G_r = gamut_area(Lab) / GAMUT_AREA_D65 - return min(G_r, 1) + xp = array_namespace(G_r) + return xp.minimum(G_r, xp.ones_like(G_r)) -def scale_conversion(D_E_ab: float, CCT_f: float, scaling_f: float) -> float: + +def scale_conversion( + D_E_ab: ArrayLike, CCT_f: ArrayLike, scaling_f: float +) -> NDArrayFloat: """ Compute the *Colour Quality Scale* (CQS) for the specified :math:`\\Delta E_{ab}` value and correlated colour temperature @@ -526,11 +698,15 @@ def scale_conversion(D_E_ab: float, CCT_f: float, scaling_f: float) -> float: Returns ------- - :class:`float` + :class:`numpy.ndarray` *Colour Quality Scale* (CQS). """ - return 10 * np.log1p(np.exp((100 - scaling_f * D_E_ab) / 10)) * CCT_f + D_E_ab = as_float_array(D_E_ab) + + xp = array_namespace(D_E_ab) + + return 10 * xp.log1p(xp.exp((100 - scaling_f * D_E_ab) / 10)) * CCT_f def delta_E_RMS( @@ -554,14 +730,14 @@ def delta_E_RMS( Root-mean-square average. """ - return np.sqrt( - 1 - / len(CQS_data) - * np.sum( - [getattr(sample_data, attribute) ** 2 for sample_data in CQS_data.values()] - ) + values = as_float_array( + [getattr(sample_data, attribute) ** 2 for sample_data in CQS_data.values()] ) + xp = array_namespace(values) + + return xp.sqrt(1 / len(CQS_data) * xp.sum(values)) + def colour_quality_scales( test_data: Tuple[DataColorimetry_VS, ...], @@ -592,16 +768,19 @@ def colour_quality_scales( """ Q_as = {} + + xp = array_namespace(test_data[0].Lab) + for i in range(len(test_data)): - D_C_ab = cast("float", test_data[i].C - reference_data[i].C) - D_E_ab = cast( - "float", euclidean_distance(test_data[i].Lab, reference_data[i].Lab) - ) - D_Ep_ab = cast( - "float", np.sqrt(D_E_ab**2 - D_C_ab**2) if D_C_ab > 0 else D_E_ab + D_C_ab = as_float_scalar(test_data[i].C - reference_data[i].C) + D_E_ab = as_float_scalar( + euclidean_distance(test_data[i].Lab, reference_data[i].Lab) ) + D_Ep_ab_arr = as_float_array(D_E_ab**2 - D_C_ab**2) + + D_Ep_ab = as_float_scalar(xp.sqrt(D_Ep_ab_arr) if D_C_ab > 0 else D_E_ab) - Q_a = scale_conversion(D_Ep_ab, CCT_f, scaling_f) + Q_a = float(scale_conversion(D_Ep_ab, CCT_f, scaling_f)) Q_as[i + 1] = DataColourQualityScale_VS( test_data[i].name, Q_a, D_C_ab, D_E_ab, D_Ep_ab ) diff --git a/colour/quality/cri.py b/colour/quality/cri.py index 972b22e008..d9ea8252c5 100644 --- a/colour/quality/cri.py +++ b/colour/quality/cri.py @@ -20,29 +20,37 @@ import typing from dataclasses import dataclass -import numpy as np - from colour.algebra import euclidean_distance, sdiv, sdiv_mode, spow from colour.colorimetry import ( MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT, + CIE_illuminant_D_series, MultiSpectralDistributions, SpectralDistribution, + msds_to_XYZ, + planck_law, reshape_msds, reshape_sd, - sd_blackbody, - sd_CIE_illuminant_D_series, sd_to_XYZ, ) if typing.TYPE_CHECKING: - from colour.hints import Dict, Literal, NDArrayFloat, Tuple + from colour.hints import Dict, List, Literal, NDArrayFloat, Tuple -from colour.hints import cast from colour.models import UCS_to_uv, XYZ_to_UCS, XYZ_to_xyY from colour.quality.datasets.tcs import INDEXES_TO_NAMES_TCS, SDS_TCS from colour.temperature import CCT_to_xy_CIE_D, uv_to_CCT_Robertson1968 -from colour.utilities import domain_range_scale, validate_method +from colour.utilities import ( + array_namespace, + as_float_scalar, + domain_range_scale, + suppress_warnings, + tstack, + validate_method, + xp_as_float_array, + xp_average, + xp_matrix_transpose, +) from colour.utilities.documentation import DocstringTuple, is_documentation_building __author__ = "Colour Developers" @@ -161,33 +169,32 @@ class ColourRendering_Specification_CRI: @typing.overload def colour_rendering_index( sd_test: SpectralDistribution, - additional_data: Literal[True] = True, + additional_data: Literal[False] = False, method: Literal["CIE 1995", "CIE 2024"] | str = ..., -) -> ColourRendering_Specification_CRI: ... +) -> float: ... @typing.overload def colour_rendering_index( sd_test: SpectralDistribution, - *, - additional_data: Literal[False], + additional_data: Literal[True], method: Literal["CIE 1995", "CIE 2024"] | str = ..., -) -> float: ... +) -> ColourRendering_Specification_CRI: ... @typing.overload def colour_rendering_index( - sd_test: SpectralDistribution, - additional_data: Literal[False], + sd_test: MultiSpectralDistributions, + additional_data: Literal[False] = False, method: Literal["CIE 1995", "CIE 2024"] | str = ..., -) -> float: ... +) -> NDArrayFloat: ... def colour_rendering_index( - sd_test: SpectralDistribution, + sd_test: SpectralDistribution | MultiSpectralDistributions, additional_data: bool = False, method: Literal["CIE 1995", "CIE 2024"] | str = "CIE 1995", -) -> float | ColourRendering_Specification_CRI: +) -> float | NDArrayFloat | ColourRendering_Specification_CRI: """ Compute the *Colour Rendering Index* (CRI) :math:`Q_a` of the specified spectral distribution. @@ -195,7 +202,11 @@ def colour_rendering_index( Parameters ---------- sd_test - Test spectral distribution. + Test spectral distribution. A + :class:`colour.MultiSpectralDistributions` of ``N`` test + illuminants is also accepted, in which case ``additional_data`` + must be ``False`` and the return value is a :class:`numpy.ndarray` + of ``N`` :math:`Q_a` values. additional_data Whether to output additional data. method @@ -203,7 +214,8 @@ def colour_rendering_index( Returns ------- - :class:`float` or :class:`colour.quality.ColourRendering_Specification_CRI` + :class:`float`, :class:`numpy.ndarray` or \ +:class:`colour.quality.ColourRendering_Specification_CRI` *Colour Rendering Index* (CRI). References @@ -227,140 +239,245 @@ def colour_rendering_index( ) shape = cmfs.shape - sd_test = reshape_sd(sd_test, shape, copy=False) sds_tcs = SDS_TCS[method] tcs_sds = {sd.name: reshape_sd(sd, shape, copy=False) for sd in sds_tcs.values()} + is_msds = isinstance(sd_test, MultiSpectralDistributions) + if is_msds and additional_data: + error = ( + '"additional_data=True" is not supported when "sd_test" is a ' + '"MultiSpectralDistributions" instance.' + ) + raise NotImplementedError(error) + + if is_msds: + sd_test = reshape_msds(sd_test, shape, copy=False) + sd_test_values = sd_test.values + xp = array_namespace(sd_test_values) + test_values = xp_matrix_transpose( + xp_as_float_array(sd_test_values, xp=xp), xp=xp + ) + else: + sd_test = reshape_sd(sd_test, shape, copy=False) + sd_test_values = sd_test.values + xp = array_namespace(sd_test_values) + test_values = xp_as_float_array(sd_test_values, xp=xp)[None, :] + with domain_range_scale("1"): - XYZ = sd_to_XYZ(sd_test, cmfs) + XYZ = ( + msds_to_XYZ(test_values, cmfs, method="Integration", shape=shape) + if is_msds + else sd_to_XYZ(sd_test, cmfs)[None, :] + ) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) - CCT, _D_uv = uv_to_CCT_Robertson1968(uv) - - if CCT < 5000: - sd_reference = sd_blackbody(CCT, shape) - else: - xy = CCT_to_xy_CIE_D(CCT) - sd_reference = sd_CIE_illuminant_D_series(xy) - sd_reference.align(shape) - - test_tcs_colorimetry_data = tcs_colorimetry_data( - sd_test, sd_reference, tcs_sds, cmfs, chromatic_adaptation=True, method=method + CCT = uv_to_CCT_Robertson1968(uv)[..., 0] + + # ``planck_law`` squeezes its output, so a single-CCT batch collapses + # to 1-D; the sample axis is reinstated below. + planckian = planck_law(shape.wavelengths * 1e-9, CCT) * 1e-9 + planckian_values = ( + planckian[None, :] + if planckian.ndim == 1 + else xp_matrix_transpose(planckian, xp=xp) ) - - reference_tcs_colorimetry_data = tcs_colorimetry_data( - sd_reference, sd_reference, tcs_sds, cmfs, method=method + # ``CCT_to_xy_CIE_D`` warns for any sample outside ``[4000, 25000]`` K + # even when the ``xp.where`` below will discard those values. + with suppress_warnings(colour_usage_warnings=True): + daylight = CIE_illuminant_D_series(CCT_to_xy_CIE_D(CCT), shape=shape) + daylight_values = ( + daylight[None, :] + if daylight.ndim == 1 + else xp_matrix_transpose(daylight, xp=xp) ) + ref_values = xp.where(CCT[..., None] < 5000, planckian_values, daylight_values) - Q_as = colour_rendering_indexes( - test_tcs_colorimetry_data, reference_tcs_colorimetry_data + test_names, test_XYZ, test_uv, test_UVW = _tcs_colorimetry_data( + test_values, ref_values, tcs_sds, cmfs, chromatic_adaptation=True, method=method ) - - Q_a = cast( - "float", - np.average([v.Q_a for k, v in Q_as.items() if k in (1, 2, 3, 4, 5, 6, 7, 8)]), + ref_names, ref_XYZ, ref_uv, ref_UVW = _tcs_colorimetry_data( + ref_values, ref_values, tcs_sds, cmfs, method=method ) + delta_E = euclidean_distance(test_UVW, ref_UVW) + # The general *Colour Rendering Index* (CRI) :math:`R_a` is defined over + # the first 8 test colour samples only, the remaining samples yield + # special indexes. + delta_E_8 = delta_E[..., :8] + Q_a = xp_average(100 - 4.6 * delta_E_8, axis=-1, xp=xp) + + if is_msds: + return Q_a + + Q_a_scalar = as_float_scalar(Q_a[0]) + if additional_data: + Q_as = { + i + 1: DataColourQualityScale_TCS( + test_names[i], as_float_scalar(100 - 4.6 * delta_E[0, i]) + ) + for i in range(len(test_names)) + } + test_data = tuple( + DataColorimetry_TCS(name, test_XYZ[0, i], test_uv[0, i], test_UVW[0, i]) + for i, name in enumerate(test_names) + ) + ref_data = tuple( + DataColorimetry_TCS(name, ref_XYZ[0, i], ref_uv[0, i], ref_UVW[0, i]) + for i, name in enumerate(ref_names) + ) return ColourRendering_Specification_CRI( - sd_test.name, - Q_a, - Q_as, - (test_tcs_colorimetry_data, reference_tcs_colorimetry_data), + sd_test.name, Q_a_scalar, Q_as, (test_data, ref_data) ) - return Q_a + return Q_a_scalar -def tcs_colorimetry_data( - sd_t: SpectralDistribution, - sd_r: SpectralDistribution, +def _tcs_colorimetry_data( + t_values: NDArrayFloat, + r_values: NDArrayFloat, sds_tcs: Dict[str, SpectralDistribution], cmfs: MultiSpectralDistributions, chromatic_adaptation: bool = False, method: Literal["CIE 1995", "CIE 2024"] | str = "CIE 1995", -) -> Tuple[DataColorimetry_TCS, ...]: +) -> Tuple[List[str], NDArrayFloat, NDArrayFloat, NDArrayFloat]: """ - Compute the *test colour samples* colorimetry data. + Compute the *test colour samples* colorimetry arrays in a single + vectorised pass over an arbitrary leading shape of test/reference + irradiance pairs. Parameters ---------- - sd_t - Test spectral distribution. - sd_r - Reference spectral distribution. - sds_tcs - *Test colour samples* spectral reflectance distributions. - cmfs - Standard observer colour matching functions. - chromatic_adaptation - Perform chromatic adaptation. + t_values, r_values + Test and reference irradiance values of shape + ``(..., n_wavelengths)``. Returns ------- :class:`tuple` - *Test colour samples* colorimetry data. + ``(names, XYZ_tcs, uv_tcs, UVW_tcs)`` with leading shape + ``(..., n_test_colour_samples)``. """ method = validate_method(method, tuple(COLOUR_RENDERING_INDEX_METHODS)) - XYZ_t = sd_to_XYZ(sd_t, cmfs) + XYZ_t = msds_to_XYZ(t_values, cmfs, method="Integration", shape=cmfs.shape) uv_t = UCS_to_uv(XYZ_to_UCS(XYZ_t)) - u_t, v_t = uv_t[0], uv_t[1] + u_t, v_t = uv_t[..., 0], uv_t[..., 1] - XYZ_r = sd_to_XYZ(sd_r, cmfs) + XYZ_r = msds_to_XYZ(r_values, cmfs, method="Integration", shape=cmfs.shape) uv_r = UCS_to_uv(XYZ_to_UCS(XYZ_r)) - u_r, v_r = uv_r[0], uv_r[1] + u_r, v_r = uv_r[..., 0], uv_r[..., 1] - tcs_data = [] + names: List[str] = [] + tcs_values_list = [] for _key, value in sorted(INDEXES_TO_NAMES_TCS[method].items()): if value not in sds_tcs: continue + names.append(sds_tcs[value].name) + tcs_values_list.append(sds_tcs[value].values) - sd_tcs = sds_tcs[value] - XYZ_tcs = sd_to_XYZ(sd_tcs, cmfs, sd_t) - xyY_tcs = XYZ_to_xyY(XYZ_tcs) - uv_tcs = UCS_to_uv(XYZ_to_UCS(XYZ_tcs)) - u_tcs, v_tcs = uv_tcs[0], uv_tcs[1] + xp = array_namespace(XYZ_t, t_values) + tcs_values = xp.stack( + [xp_as_float_array(values, xp=xp, like=XYZ_t) for values in tcs_values_list] + ) - if chromatic_adaptation: + # Vectorised :math:`XYZ_{tcs}` across the test colour samples; the + # ``100 / Y_t`` factor recovers the reflectance-under-illuminant scale + # of :func:`sd_to_XYZ(sd_tcs, cmfs, sd_t)`. + sds_tcs_t = tcs_values * t_values[..., None, :] + XYZ_tcs = msds_to_XYZ( + sds_tcs_t, + cmfs, + method="Integration", + shape=cmfs.shape, + ) * (100 / XYZ_t[..., 1:2, None]) - def c(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat: - """Compute the :math:`c` term.""" + xyY_tcs = XYZ_to_xyY(XYZ_tcs) + uv_tcs = UCS_to_uv(XYZ_to_UCS(XYZ_tcs)) + u_tcs, v_tcs = uv_tcs[..., 0], uv_tcs[..., 1] - with sdiv_mode(): - return sdiv(4 - x - 10 * y, y) + if chromatic_adaptation: - def d(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat: - """Compute the :math:`d` term.""" + def c(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat: + """Compute the :math:`c` term.""" - with sdiv_mode(): - return sdiv(1.708 * y + 0.404 - 1.481 * x, y) + with sdiv_mode(): + return sdiv(4 - x - 10 * y, y) - c_t, d_t = c(u_t, v_t), d(u_t, v_t) - c_r, d_r = c(u_r, v_r), d(u_r, v_r) - tcs_c, tcs_d = c(u_tcs, v_tcs), d(u_tcs, v_tcs) + def d(x: NDArrayFloat, y: NDArrayFloat) -> NDArrayFloat: + """Compute the :math:`d` term.""" with sdiv_mode(): - c_r_c_t = sdiv(c_r, c_t) - d_r_d_t = sdiv(d_r, d_t) + return sdiv(1.708 * y + 0.404 - 1.481 * x, y) - u_tcs = (10.872 + 0.404 * c_r_c_t * tcs_c - 4 * d_r_d_t * tcs_d) / ( - 16.518 + 1.481 * c_r_c_t * tcs_c - d_r_d_t * tcs_d - ) - v_tcs = 5.52 / (16.518 + 1.481 * c_r_c_t * tcs_c - d_r_d_t * tcs_d) + c_t, d_t = c(u_t, v_t), d(u_t, v_t) + c_r, d_r = c(u_r, v_r), d(u_r, v_r) + tcs_c, tcs_d = c(u_tcs, v_tcs), d(u_tcs, v_tcs) - W_tcs = 25 * spow(xyY_tcs[-1], 1 / 3) - 17 - U_tcs = 13 * W_tcs * (u_tcs - u_r) - V_tcs = 13 * W_tcs * (v_tcs - v_r) + with sdiv_mode(): + c_r_c_t = sdiv(c_r, c_t)[..., None] + d_r_d_t = sdiv(d_r, d_t)[..., None] - tcs_data.append( - DataColorimetry_TCS( - sd_tcs.name, XYZ_tcs, uv_tcs, np.array([U_tcs, V_tcs, W_tcs]) - ) + # NOTE: ``uv_tcs`` keeps the pre-adaptation value; the adapted + # ``u``, ``v`` only feed the ``U``, ``V`` derivation below. + u_tcs = (10.872 + 0.404 * c_r_c_t * tcs_c - 4 * d_r_d_t * tcs_d) / ( + 16.518 + 1.481 * c_r_c_t * tcs_c - d_r_d_t * tcs_d ) + v_tcs = 5.52 / (16.518 + 1.481 * c_r_c_t * tcs_c - d_r_d_t * tcs_d) + + W_tcs = 25 * spow(xyY_tcs[..., -1], 1 / 3) - 17 + U_tcs = 13 * W_tcs * (u_tcs - u_r[..., None]) + V_tcs = 13 * W_tcs * (v_tcs - v_r[..., None]) + UVW_tcs = tstack([U_tcs, V_tcs, W_tcs]) - return tuple(tcs_data) + return names, XYZ_tcs, uv_tcs, UVW_tcs + + +def tcs_colorimetry_data( + sd_t: SpectralDistribution, + sd_r: SpectralDistribution, + sds_tcs: Dict[str, SpectralDistribution], + cmfs: MultiSpectralDistributions, + chromatic_adaptation: bool = False, + method: Literal["CIE 1995", "CIE 2024"] | str = "CIE 1995", +) -> Tuple[DataColorimetry_TCS, ...]: + """ + Compute the *test colour samples* colorimetry data. + + Parameters + ---------- + sd_t + Test spectral distribution. + sd_r + Reference spectral distribution. + sds_tcs + *Test colour samples* spectral reflectance distributions. + cmfs + Standard observer colour matching functions. + chromatic_adaptation + Perform chromatic adaptation. + + Returns + ------- + :class:`tuple` + *Test colour samples* colorimetry data. + """ + + xp = array_namespace(sd_t.values) + names, XYZ_tcs, uv_tcs, UVW_tcs = _tcs_colorimetry_data( + xp_as_float_array(sd_t.values, xp=xp), + xp_as_float_array(sd_r.values, xp=xp), + sds_tcs, + cmfs, + chromatic_adaptation, + method, + ) + + return tuple( + DataColorimetry_TCS(name, XYZ_tcs[i], uv_tcs[i], UVW_tcs[i]) + for i, name in enumerate(names) + ) def colour_rendering_indexes( @@ -390,8 +507,7 @@ def colour_rendering_indexes( test_data[i].name, 100 - 4.6 - * cast( - "float", + * as_float_scalar( euclidean_distance(reference_data[i].UVW, test_data[i].UVW), ), ) diff --git a/colour/quality/ssi.py b/colour/quality/ssi.py index d1ae9b36db..902affd1bb 100644 --- a/colour/quality/ssi.py +++ b/colour/quality/ssi.py @@ -30,10 +30,15 @@ ) if typing.TYPE_CHECKING: - from colour.hints import NDArrayFloat - - -from colour.utilities import required + from colour.hints import ModuleType, NDArrayFloat + +from colour.utilities import ( + array_namespace, + as_ndarray, + required, + xp_as_float_array, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -44,6 +49,7 @@ __all__ = [ "SPECTRAL_SHAPE_SSI", + "matrix_integration_SSI", "spectral_similarity_index", ] @@ -55,6 +61,44 @@ _MATRIX_INTEGRATION: NDArrayFloat | None = None +def matrix_integration_SSI(*, xp: ModuleType = np) -> NDArrayFloat: + """ + Build the *SSI* sparse integration matrix in the specified namespace. + + The matrix maps the 1 nm *SSI* working shape to the 10 nm reference + bands by convolving each band with a unit-area triangular kernel. It + is cached at module level via :data:`_MATRIX_INTEGRATION`; callers + promote it to the per-input backend at use time via + :func:`xp_as_float_array`. + """ + + n_rows = len(_SPECTRAL_SHAPE_SSI_LARGE.wavelengths) + n_cols = len(SPECTRAL_SHAPE_SSI.wavelengths) + weights = xp_as_float_array([0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5], xp=xp) + + return xp.concat( + [ + xp_reshape( + xp.concat( + [ + xp_as_float_array(xp.zeros(10 * i), xp=xp, like=weights), + weights, + xp_as_float_array( + xp.zeros(max(0, n_cols - 10 * i - 11)), + xp=xp, + like=weights, + ), + ] + )[:n_cols], + (1, -1), + xp=xp, + ) + for i in range(n_rows) + ], + axis=0, + ) + + @required("SciPy") def spectral_similarity_index( sd_test: SpectralDistribution | MultiSpectralDistributions, @@ -113,22 +157,7 @@ def spectral_similarity_index( global _MATRIX_INTEGRATION # noqa: PLW0603 if _MATRIX_INTEGRATION is None: - n_rows = len(_SPECTRAL_SHAPE_SSI_LARGE.wavelengths) - n_cols = len(SPECTRAL_SHAPE_SSI.wavelengths) - weights = np.array([0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5]) - - _MATRIX_INTEGRATION = np.vstack( - [ - np.concatenate( - [ - np.zeros(10 * i), - weights, - np.zeros(max(0, n_cols - 10 * i - 11)), - ] - )[:n_cols] - for i in range(n_rows) - ] - ) + _MATRIX_INTEGRATION = matrix_integration_SSI() settings = { "interpolator": LinearInterpolator, @@ -148,20 +177,28 @@ def spectral_similarity_index( ) ) - test_i = np.dot(_MATRIX_INTEGRATION, sd_test.values) - reference_i = np.dot(_MATRIX_INTEGRATION, sd_reference.values) + xp = array_namespace(sd_test.values, sd_reference.values) + + sd_test_values = xp_as_float_array(sd_test.values, xp=xp) + sd_reference_values = xp_as_float_array( + sd_reference.values, xp=xp, like=sd_test_values + ) + matrix = xp_as_float_array(_MATRIX_INTEGRATION, xp=xp, like=sd_test_values) + + test_i = xp.matmul(matrix, sd_test_values) + reference_i = xp.matmul(matrix, sd_reference_values) if test_i.ndim == 1 and reference_i.ndim == 2: - test_i = np.tile(test_i[:, np.newaxis], (1, reference_i.shape[1])) + test_i = xp.tile(test_i[:, None], (1, reference_i.shape[1])) elif test_i.ndim == 2 and reference_i.ndim == 1: - reference_i = np.tile(reference_i[:, np.newaxis], (1, test_i.shape[1])) + reference_i = xp.tile(reference_i[:, None], (1, test_i.shape[1])) with sdiv_mode(): - test_i = sdiv(test_i, np.sum(test_i, axis=0, keepdims=True)) - reference_i = sdiv(reference_i, np.sum(reference_i, axis=0, keepdims=True)) + test_i = sdiv(test_i, xp.sum(test_i, axis=0, keepdims=True)) + reference_i = sdiv(reference_i, xp.sum(reference_i, axis=0, keepdims=True)) dr_i = sdiv(test_i - reference_i, reference_i + 1 / 30) - weights = np.array( + weights = xp_as_float_array( [ 4 / 15, 22 / 45, @@ -193,16 +230,21 @@ def spectral_similarity_index( 1, 11 / 15, 3 / 15, - ] + ], + xp=xp, + like=sd_test_values, ) if dr_i.ndim == 2: - weights = weights[:, np.newaxis] + weights = weights[:, None] wdr_i = dr_i * weights - c_wdr_i = convolve1d(wdr_i, [0.22, 0.56, 0.22], axis=0, mode="constant", cval=0) - m_v = np.sum(np.square(c_wdr_i), axis=0) + c_wdr_i = convolve1d( + as_ndarray(wdr_i), [0.22, 0.56, 0.22], axis=0, mode="constant", cval=0 + ) + c_wdr_i = xp_as_float_array(c_wdr_i, xp=xp, like=sd_test_values) + m_v = xp.sum(xp.square(c_wdr_i), axis=0) - SSI = 100 - 32 * np.sqrt(m_v) + SSI = 100 - 32 * xp.sqrt(m_v) - return np.around(SSI) if round_result else SSI + return xp.round(SSI) if round_result else SSI diff --git a/colour/quality/tests/test_cfi2017.py b/colour/quality/tests/test_cfi2017.py index 85d76bc02d..ff930831ea 100644 --- a/colour/quality/tests/test_cfi2017.py +++ b/colour/quality/tests/test_cfi2017.py @@ -9,25 +9,30 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np import pytest from colour import MSDS_CMFS from colour.colorimetry import ( SDS_ILLUMINANTS, + SDS_LIGHT_SOURCES, + MultiSpectralDistributions, SpectralDistribution, SpectralShape, reshape_sd, - sd_blackbody, ) +from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.quality.cfi2017 import ( - CCT_reference_illuminant, colour_fidelity_index_CIE2017, load_TCS_CIE2017, - sd_reference_illuminant, tcs_colorimetry_data, ) -from colour.utilities import ColourUsageWarning +from colour.utilities import ColourUsageWarning, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -42,8 +47,6 @@ "DATA_SD_SAMPLE_1NM", "SD_SAMPLE_1NM", "TestColourFidelityIndexCIE2017", - "TestCctReferenceIlluminant", - "TestSdReferenceIlluminant", "TestTcsColorimetryData", ] @@ -556,16 +559,19 @@ class TestColourFidelityIndexCIE2017: definition unit tests methods. """ - def test_colour_fidelity_index_CIE2017(self) -> None: + def test_colour_fidelity_index_CIE2017(self, xp: ModuleType) -> None: """ Test :func:`colour.quality.CIE2017.colour_fidelity_index_CIE2017` definition. """ for sd in [SD_SAMPLE_5NM, SD_SAMPLE_1NM]: - specification = colour_fidelity_index_CIE2017(sd, additional_data=True) - np.testing.assert_allclose(specification.R_f, 81.6, atol=0.1) - np.testing.assert_allclose( + sd_xp = sd.copy(xp=xp) + specification = colour_fidelity_index_CIE2017(sd_xp, additional_data=True) + xp_assert_close( + specification.R_f, 81.6, atol=TOLERANCE_ABSOLUTE_TESTS * 1e06 + ) + xp_assert_close( specification.R_s, [ 89.5, @@ -668,14 +674,13 @@ def test_colour_fidelity_index_CIE2017(self) -> None: 84.2, 77.4, ], - atol=0.1, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e06, ) - specification = colour_fidelity_index_CIE2017( - SDS_ILLUMINANTS["FL1"], additional_data=True - ) - np.testing.assert_allclose(specification.R_f, 80.6, atol=0.1) - np.testing.assert_allclose( + sd_fl1_xp = SDS_ILLUMINANTS["FL1"].copy(xp=xp) + specification = colour_fidelity_index_CIE2017(sd_fl1_xp, additional_data=True) + xp_assert_close(specification.R_f, 80.6, atol=TOLERANCE_ABSOLUTE_TESTS * 1e06) + xp_assert_close( specification.R_s, [ 85.1, @@ -778,14 +783,13 @@ def test_colour_fidelity_index_CIE2017(self) -> None: 75.2, 55.5, ], - atol=0.1, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e06, ) - specification = colour_fidelity_index_CIE2017( - SDS_ILLUMINANTS["FL2"], additional_data=True - ) - np.testing.assert_allclose(specification.R_f, 70.1, atol=0.1) - np.testing.assert_allclose( + sd_fl2_xp = SDS_ILLUMINANTS["FL2"].copy(xp=xp) + specification = colour_fidelity_index_CIE2017(sd_fl2_xp, additional_data=True) + xp_assert_close(specification.R_f, 70.1, atol=TOLERANCE_ABSOLUTE_TESTS * 1e06) + xp_assert_close( specification.R_s, [ 78.9, @@ -888,9 +892,36 @@ def test_colour_fidelity_index_CIE2017(self) -> None: 67.0, 45.0, ], - atol=0.1, + atol=TOLERANCE_ABSOLUTE_TESTS * 1e06, ) + shape = SpectralShape(380, 780, 5) + sds = [ + reshape_sd(sd, shape) + for sd in ( + SDS_ILLUMINANTS["FL1"], + SDS_ILLUMINANTS["FL2"], + SDS_LIGHT_SOURCES["Neodimium Incandescent"], + SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"], + ) + ] + msds = MultiSpectralDistributions( + xp_as_array(np.column_stack([sd.values for sd in sds]), xp=xp), + sds[0].wavelengths, + labels=[sd.name for sd in sds], + ) + xp_assert_close( + colour_fidelity_index_CIE2017(msds), + xp_as_array( + [colour_fidelity_index_CIE2017(sd.copy(xp=xp)) for sd in sds], + xp=xp, + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + with pytest.raises(NotImplementedError): + colour_fidelity_index_CIE2017(msds, additional_data=True) # pyright: ignore[reportCallIssue, reportArgumentType] + def test_raise_exception_colour_fidelity_index_CFI2017(self) -> None: """ Test :func:`colour.quality.CIE2017.colour_fidelity_index_CFI2017` @@ -898,54 +929,12 @@ def test_raise_exception_colour_fidelity_index_CFI2017(self) -> None: """ sd = reshape_sd(SDS_ILLUMINANTS["FL2"], SpectralShape(400, 700, 5)) - pytest.warns(ColourUsageWarning, colour_fidelity_index_CIE2017, sd) + with pytest.warns(ColourUsageWarning): + colour_fidelity_index_CIE2017(sd) sd = reshape_sd(SDS_ILLUMINANTS["FL2"], SpectralShape(380, 780, 10)) - pytest.raises(ValueError, colour_fidelity_index_CIE2017, sd) - - -class TestCctReferenceIlluminant: - """ - Define :func:`colour.quality.CIE2017.CCT_reference_illuminant` - definition unit tests methods. - """ - - def test_CCT_reference_illuminant(self) -> None: - """ - Test :func:`colour.quality.CIE2017.CCT_reference_illuminant` - definition. - """ - - for sd in [SD_SAMPLE_5NM, SD_SAMPLE_1NM]: - CCT, D_uv = CCT_reference_illuminant(sd) - np.testing.assert_allclose(CCT, 3287.5, atol=0.5) - np.testing.assert_allclose(D_uv, -0.000300000000000, atol=0.0005) - - -class TestSdReferenceIlluminant: - """ - Define :func:`colour.quality.CIE2017.sd_reference_illuminant` - definition unit tests methods. - """ - - def test_sd_reference_illuminant(self) -> None: - """ - Test :func:`colour.quality.CIE2017.sd_reference_illuminant` - definition. - """ - - for sd, shape in [ - (SD_SAMPLE_5NM, SD_SAMPLE_5NM.shape), - (SD_SAMPLE_1NM, SD_SAMPLE_1NM.shape), - ]: - CCT, _D_uv = CCT_reference_illuminant(sd) - sd_reference = sd_reference_illuminant(CCT, shape) - - np.testing.assert_allclose( - sd_reference.values, - sd_blackbody(3288, shape).values, - atol=1.75, - ) + with pytest.raises(ValueError): + colour_fidelity_index_CIE2017(sd) class TestTcsColorimetryData: @@ -954,7 +943,7 @@ class TestTcsColorimetryData: definition unit tests methods. """ - def test_tcs_colorimetry_data_single_sd(self) -> None: + def test_tcs_colorimetry_data_single_sd(self, xp: ModuleType) -> None: """ Test :func:`colour.quality.cfi2017.tcs_colorimetry_data` definition with a single spectral distribution (not a list). @@ -962,8 +951,9 @@ def test_tcs_colorimetry_data_single_sd(self) -> None: shape = SpectralShape(380, 780, 5) sd = SD_SAMPLE_5NM.copy().align(shape) + sd_xp = sd.copy(xp=xp) cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"].copy().align(shape) test_sds = load_TCS_CIE2017(shape) - result = tcs_colorimetry_data(sd, test_sds, cmfs) + result = tcs_colorimetry_data(sd_xp, test_sds, cmfs) assert len(result) == 1 diff --git a/colour/quality/tests/test_cqs.py b/colour/quality/tests/test_cqs.py index 0cfdab32e0..792033e587 100644 --- a/colour/quality/tests/test_cqs.py +++ b/colour/quality/tests/test_cqs.py @@ -2,12 +2,24 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np +import pytest -from colour.colorimetry import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES +from colour.colorimetry import ( + SDS_ILLUMINANTS, + SDS_LIGHT_SOURCES, + SPECTRAL_SHAPE_DEFAULT, + MultiSpectralDistributions, +) from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.quality import ColourRendering_Specification_CQS, colour_quality_scale from colour.quality.cqs import DataColorimetry_VS, DataColourQualityScale_VS +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -27,48 +39,54 @@ class TestColourQualityScale: tests methods. """ - def test_colour_quality_scale(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_colour_quality_scale(self, xp: ModuleType) -> None: """Test :func:`colour.quality.cqs.colour_quality_scale` definition.""" - np.testing.assert_allclose( - colour_quality_scale(SDS_ILLUMINANTS["FL1"], additional_data=False), + sd_fl1_xp = SDS_ILLUMINANTS["FL1"].copy(xp=xp) + + xp_assert_close( + colour_quality_scale(sd_fl1_xp, additional_data=False), 74.982585798279914, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_quality_scale( - SDS_ILLUMINANTS["FL1"], additional_data=False, method="NIST CQS 7.4" + sd_fl1_xp, additional_data=False, method="NIST CQS 7.4" ), 75.377089740493361, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - colour_quality_scale(SDS_ILLUMINANTS["FL2"], additional_data=False), + sd_fl2_xp = SDS_ILLUMINANTS["FL2"].copy(xp=xp) + + xp_assert_close( + colour_quality_scale(sd_fl2_xp, additional_data=False), 64.111822015662852, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_quality_scale( - SDS_ILLUMINANTS["FL2"], additional_data=False, method="NIST CQS 7.4" + sd_fl2_xp, additional_data=False, method="NIST CQS 7.4" ), 64.774586908581369, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - colour_quality_scale( - SDS_LIGHT_SOURCES["Neodimium Incandescent"], additional_data=False - ), + sd_neo = SDS_LIGHT_SOURCES["Neodimium Incandescent"] + sd_neo_xp = sd_neo.copy(xp=xp) + + xp_assert_close( + colour_quality_scale(sd_neo_xp, additional_data=False), 89.737456186836681, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_quality_scale( - SDS_LIGHT_SOURCES["Neodimium Incandescent"], + sd_neo_xp, additional_data=False, method="NIST CQS 7.4", ), @@ -76,17 +94,18 @@ def test_colour_quality_scale(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - colour_quality_scale( - SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"], additional_data=False - ), + sd_tri = SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"] + sd_tri_xp = sd_tri.copy(xp=xp) + + xp_assert_close( + colour_quality_scale(sd_tri_xp, additional_data=False), 84.934928463428903, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( colour_quality_scale( - SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"], + sd_tri_xp, additional_data=False, method="NIST CQS 7.4", ), @@ -400,7 +419,7 @@ def test_colour_quality_scale(self) -> None: SDS_ILLUMINANTS["FL1"], additional_data=True, method="NIST CQS 7.4" ) - np.testing.assert_allclose( + xp_assert_close( [data.Q_a for _index, data in sorted(specification_r.Q_as.items())], [data.Q_a for _index, data in sorted(specification_t.Q_as.items())], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -712,8 +731,36 @@ def test_colour_quality_scale(self) -> None: SDS_ILLUMINANTS["FL1"], additional_data=True, method="NIST CQS 9.0" ) - np.testing.assert_allclose( + xp_assert_close( [data.Q_a for _index, data in sorted(specification_r.Q_as.items())], [data.Q_a for _index, data in sorted(specification_t.Q_as.items())], atol=TOLERANCE_ABSOLUTE_TESTS, ) + + sds = [ + sd.copy().align(SPECTRAL_SHAPE_DEFAULT) + for sd in ( + SDS_ILLUMINANTS["FL1"], + SDS_ILLUMINANTS["FL2"], + SDS_LIGHT_SOURCES["Neodimium Incandescent"], + SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"], + ) + ] + msds = MultiSpectralDistributions( + xp_as_array(np.column_stack([sd.values for sd in sds]), xp=xp), + sds[0].wavelengths, + labels=[sd.name for sd in sds], + ) + + for method in ("NIST CQS 9.0", "NIST CQS 7.4"): + xp_assert_close( + colour_quality_scale(msds, method=method), + xp_as_array( + [colour_quality_scale(sd.copy(xp=xp), method=method) for sd in sds], + xp=xp, + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + with pytest.raises(NotImplementedError): + colour_quality_scale(msds, additional_data=True) # pyright: ignore[reportCallIssue, reportArgumentType] diff --git a/colour/quality/tests/test_cri.py b/colour/quality/tests/test_cri.py index 0fc7ce5f36..ddf53a9763 100644 --- a/colour/quality/tests/test_cri.py +++ b/colour/quality/tests/test_cri.py @@ -2,12 +2,20 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np +import pytest from colour.colorimetry import ( MSDS_CMFS, SDS_ILLUMINANTS, + SDS_LIGHT_SOURCES, SPECTRAL_SHAPE_DEFAULT, + MultiSpectralDistributions, SpectralDistribution, reshape_msds, reshape_sd, @@ -20,6 +28,7 @@ tcs_colorimetry_data, ) from colour.quality.datasets.tcs import SDS_TCS +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -123,31 +132,39 @@ class TestColourRenderingIndex: definition unit tests methods. """ - def test_colour_rendering_index(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_colour_rendering_index(self, xp: ModuleType) -> None: """Test :func:`colour.quality.cri.colour_rendering_index` definition.""" - np.testing.assert_allclose( - colour_rendering_index(SDS_ILLUMINANTS["FL1"], additional_data=False), + sd_fl1_xp = SDS_ILLUMINANTS["FL1"].copy(xp=xp) + + xp_assert_close( + colour_rendering_index(sd_fl1_xp, additional_data=False), 75.852827992149358, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - colour_rendering_index(SDS_ILLUMINANTS["FL2"], additional_data=False), + sd_fl2_xp = SDS_ILLUMINANTS["FL2"].copy(xp=xp) + + xp_assert_close( + colour_rendering_index(sd_fl2_xp, additional_data=False), 64.233724121664778, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - colour_rendering_index(SDS_ILLUMINANTS["A"], additional_data=False), + sd_a_xp = SDS_ILLUMINANTS["A"].copy(xp=xp) + + xp_assert_close( + colour_rendering_index(sd_a_xp, additional_data=False), 99.996230290506887, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - colour_rendering_index( - SpectralDistribution(DATA_SAMPLE), additional_data=False - ), + sd_sample = SpectralDistribution(DATA_SAMPLE) + sd_sample_xp = sd_sample.copy(xp=xp) + + xp_assert_close( + colour_rendering_index(sd_sample_xp, additional_data=False), 70.815265381660197, atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -364,7 +381,11 @@ def test_colour_rendering_index(self) -> None: SDS_ILLUMINANTS["FL1"], additional_data=True, method="CIE 2024" ) - np.testing.assert_allclose( + xp_assert_close( + specification_r.Q_a, specification_t.Q_a, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + xp_assert_close( [data.Q_a for _index, data in sorted(specification_r.Q_as.items())], [data.Q_a for _index, data in sorted(specification_t.Q_as.items())], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -379,7 +400,11 @@ def test_colour_rendering_index(self) -> None: SDS_ILLUMINANTS["FL1"], additional_data=True, method="CIE 1995" ) - np.testing.assert_allclose( + xp_assert_close( + specification_r.Q_a, specification_t.Q_a, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + xp_assert_close( [data.Q_a for _index, data in sorted(specification_r.Q_as.items())], [data.Q_a for _index, data in sorted(specification_t.Q_as.items())], atol=TOLERANCE_ABSOLUTE_TESTS, @@ -389,18 +414,18 @@ def test_colour_rendering_index(self) -> None: SDS_ILLUMINANTS["FL1"], additional_data=True ) - np.testing.assert_allclose( + xp_assert_close( specification_r.Q_a, specification_t.Q_a, atol=TOLERANCE_ABSOLUTE_TESTS ) - np.testing.assert_allclose( + xp_assert_close( [data.Q_a for _index, data in sorted(specification_r.Q_as.items())], [data.Q_a for _index, data in sorted(specification_t.Q_as.items())], atol=TOLERANCE_ABSOLUTE_TESTS, ) for data in ["XYZ", "uv", "UVW"]: - np.testing.assert_allclose( + xp_assert_close( [ getattr(tcs, data) for colorimetry_data in specification_r.colorimetry_data @@ -434,3 +459,29 @@ def test_colour_rendering_index(self) -> None: sd_test, sd_test, tcs_dict_partial, cmfs, method="CIE 1995" ) assert len(result) == 13 + + sds = [ + sd.copy().align(SPECTRAL_SHAPE_DEFAULT) + for sd in ( + SDS_ILLUMINANTS["FL1"], + SDS_ILLUMINANTS["FL2"], + SDS_LIGHT_SOURCES["Neodimium Incandescent"], + SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"], + ) + ] + msds = MultiSpectralDistributions( + xp_as_array(np.column_stack([sd.values for sd in sds]), xp=xp), + sds[0].wavelengths, + labels=[sd.name for sd in sds], + ) + xp_assert_close( + colour_rendering_index(msds), + xp_as_array( + [colour_rendering_index(sd.copy(xp=xp)) for sd in sds], + xp=xp, + ), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + with pytest.raises(NotImplementedError): + colour_rendering_index(msds, additional_data=True) # pyright: ignore[reportCallIssue, reportArgumentType] diff --git a/colour/quality/tests/test_ssi.py b/colour/quality/tests/test_ssi.py index fb72e8dc9c..3b4fe94d43 100644 --- a/colour/quality/tests/test_ssi.py +++ b/colour/quality/tests/test_ssi.py @@ -2,7 +2,11 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.colorimetry import ( SDS_ILLUMINANTS, @@ -12,7 +16,7 @@ ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.quality import spectral_similarity_index -from colour.utilities import is_scipy_installed +from colour.utilities import is_scipy_installed, xp_assert_close, xp_assert_equal __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -566,37 +570,37 @@ class TestSpectralSimilarityIndex: definition unit tests methods. """ - def test_spectral_similarity_index(self) -> None: + def test_spectral_similarity_index(self, xp: ModuleType) -> None: """Test :func:`colour.quality.ssi.spectral_similarity_index` definition.""" if not is_scipy_installed(): # pragma: no cover return - assert ( - spectral_similarity_index(SDS_ILLUMINANTS["C"], SDS_ILLUMINANTS["D65"]) - == 94.0 - ) - assert ( - spectral_similarity_index( - SpectralDistribution(DATA_HMI), SDS_ILLUMINANTS["D50"] - ) - == 72.0 - ) + sd_c_xp = SDS_ILLUMINANTS["C"].copy(xp=xp) + sd_d65_xp = SDS_ILLUMINANTS["D65"].copy(xp=xp) - np.testing.assert_allclose( + assert spectral_similarity_index(sd_c_xp, sd_d65_xp) == 94.0 + + sd_hmi = SpectralDistribution(DATA_HMI) + sd_hmi_xp = sd_hmi.copy(xp=xp) + sd_d50_xp = SDS_ILLUMINANTS["D50"].copy(xp=xp) + + assert spectral_similarity_index(sd_hmi_xp, sd_d50_xp) == 72.0 + + xp_assert_close( spectral_similarity_index( - SDS_ILLUMINANTS["C"], - SDS_ILLUMINANTS["D65"], + sd_c_xp, + sd_d65_xp, round_result=False, ), 94.182971057336000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spectral_similarity_index( - SpectralDistribution(DATA_HMI), - SDS_ILLUMINANTS["D50"], + sd_hmi_xp, + sd_d50_xp, round_result=False, ), 71.775054824255550, @@ -610,23 +614,21 @@ def test_spectral_similarity_index(self) -> None: msds = sds_and_msds_to_msds([sd_led_1, sd_led_2, sd_led_3]) sd_reference = sd_single_led(535, half_spectral_width=48) - np.testing.assert_array_equal( - spectral_similarity_index(msds, msds), [100.0, 100.0, 100.0] - ) + xp_assert_equal(spectral_similarity_index(msds, msds), [100.0, 100.0, 100.0]) - np.testing.assert_allclose( + xp_assert_close( spectral_similarity_index(msds, msds, round_result=False), [100.0, 100.0, 100.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spectral_similarity_index(msds, sd_reference), [52.0, 82.0, 18.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( spectral_similarity_index(sd_reference, msds), [50.0, 84.0, 20.0], atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/quality/tests/test_tm3018.py b/colour/quality/tests/test_tm3018.py index 16d5183742..38d74d0c30 100644 --- a/colour/quality/tests/test_tm3018.py +++ b/colour/quality/tests/test_tm3018.py @@ -10,14 +10,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np +import pytest -from colour.colorimetry import SDS_ILLUMINANTS +from colour.colorimetry import ( + SDS_ILLUMINANTS, + SDS_LIGHT_SOURCES, + SPECTRAL_SHAPE_DEFAULT, + MultiSpectralDistributions, +) +from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.quality.tm3018 import ( averages_area, colour_fidelity_index_ANSIIESTM3018, ) -from colour.utilities import as_float_array +from colour.utilities import as_float_array, as_ndarray, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -38,29 +50,33 @@ class TestColourFidelityIndexANSIIESTM3018: definition unit tests methods. """ - def test_colour_fidelity_index_ANSIIESTM3018(self) -> None: + def test_colour_fidelity_index_ANSIIESTM3018(self, xp: ModuleType) -> None: """ Test :func:`colour.quality.tm3018.colour_fidelity_index_ANSIIESTM3018` definition. """ + sd_fl2_xp = SDS_ILLUMINANTS["FL2"].copy(xp=xp) + # Test without additional data (returns R_f only) - R_f = colour_fidelity_index_ANSIIESTM3018( - SDS_ILLUMINANTS["FL2"], additional_data=False - ) - np.testing.assert_allclose(R_f, 70, atol=2e-1) + R_f = colour_fidelity_index_ANSIIESTM3018(sd_fl2_xp, additional_data=False) + xp_assert_close(R_f, 70, atol=TOLERANCE_ABSOLUTE_TESTS * 2e06) # Test with additional data (returns full specification) specification = colour_fidelity_index_ANSIIESTM3018( - SDS_ILLUMINANTS["FL2"], additional_data=True + sd_fl2_xp, additional_data=True ) - np.testing.assert_allclose(specification.R_f, 70, atol=2e-1) - np.testing.assert_allclose(specification.R_g, 86, atol=5e-1) - np.testing.assert_allclose(specification.CCT, 4225, atol=1) - np.testing.assert_allclose(specification.D_uv, 0.0019, atol=1e-3) + xp_assert_close(specification.R_f, 70, atol=TOLERANCE_ABSOLUTE_TESTS * 2e06) + xp_assert_close(specification.R_g, 86, atol=TOLERANCE_ABSOLUTE_TESTS * 5000000) + xp_assert_close( + specification.CCT, 4225, atol=TOLERANCE_ABSOLUTE_TESTS * 10000000 + ) + xp_assert_close( + specification.D_uv, 0.0019, atol=TOLERANCE_ABSOLUTE_TESTS * 10000 + ) - np.testing.assert_allclose( + xp_assert_close( specification.R_s, [ 79, @@ -163,20 +179,20 @@ def test_colour_fidelity_index_ANSIIESTM3018(self) -> None: 67, 45, ], - atol=0.75, + atol=TOLERANCE_ABSOLUTE_TESTS * 7500000, ) - np.testing.assert_allclose( + xp_assert_close( specification.R_fs, [60, 61, 53, 68, 80, 88, 77, 73, 76, 62, 70, 77, 81, 71, 64, 65], - atol=0.75, + atol=TOLERANCE_ABSOLUTE_TESTS * 7500000, ) - np.testing.assert_allclose( + xp_assert_close( specification.R_cs, [-25, -18, -9, 5, 11, 4, -8, -15, -17, -15, -4, 5, 11, 7, -6, -16], - atol=0.75, + atol=TOLERANCE_ABSOLUTE_TESTS * 7500000, ) - np.testing.assert_allclose( + xp_assert_close( specification.R_hs, [ -0.02, @@ -196,9 +212,54 @@ def test_colour_fidelity_index_ANSIIESTM3018(self) -> None: -0.26, -0.17, ], - atol=0.75, + atol=TOLERANCE_ABSOLUTE_TESTS * 7500000, + ) + + # A :class:`MultiSpectralDistributions` batch returns the same + # per-column results as evaluating each distribution individually. + sds = [ + sd.copy().align(SPECTRAL_SHAPE_DEFAULT) + for sd in ( + SDS_ILLUMINANTS["FL1"], + SDS_ILLUMINANTS["FL2"], + SDS_LIGHT_SOURCES["Neodimium Incandescent"], + SDS_LIGHT_SOURCES["F32T8/TL841 (Triphosphor)"], + ) + ] + msds = MultiSpectralDistributions( + xp_as_array(np.column_stack([sd.values for sd in sds]), xp=xp), + sds[0].wavelengths, + labels=[sd.name for sd in sds], + ) + xp_assert_close( + colour_fidelity_index_ANSIIESTM3018(msds), + xp_as_array( + [colour_fidelity_index_ANSIIESTM3018(sd.copy(xp=xp)) for sd in sds], + xp=xp, + ), + atol=TOLERANCE_ABSOLUTE_TESTS, ) + def test_raise_exception_colour_fidelity_index_ANSIIESTM3018_msds( + self, xp: ModuleType + ) -> None: + """ + Test :func:`colour.quality.tm3018.colour_fidelity_index_ANSIIESTM3018` + raises :class:`NotImplementedError` for + :class:`MultiSpectralDistributions` input combined with + ``additional_data=True``. + """ + + sd_fl2_xp = SDS_ILLUMINANTS["FL2"].copy(xp=xp) + msds = MultiSpectralDistributions( + xp_as_array(sd_fl2_xp.values[:, None], xp=xp), + sd_fl2_xp.wavelengths, + labels=["FL2"], + ) + + with pytest.raises(NotImplementedError): + colour_fidelity_index_ANSIIESTM3018(msds, additional_data=True) # pyright: ignore[reportCallIssue, reportArgumentType] + class TestAveragesArea: """ @@ -206,7 +267,7 @@ class TestAveragesArea: methods. """ - def test_averages_area(self) -> None: + def test_averages_area(self, xp: ModuleType) -> None: """Test :func:`colour.quality.tm3018.averages_area` definition.""" # Simple 3 * sqrt(2) by sqrt(2) rectangle. @@ -214,5 +275,7 @@ def test_averages_area(self) -> None: np.allclose(averages_area(rectangle), 6) # Concave polygon. - poly = np.array([[1.0, -1], [1, 1], [3, 1], [3, 3], [-1, 3], [-1, -1]]) - np.allclose(averages_area(poly), 12) + poly = xp_as_array( + [[1.0, -1], [1, 1], [3, 1], [3, 3], [-1, 3], [-1, -1]], xp=xp + ) + np.allclose(as_ndarray(averages_area(poly)), 12) diff --git a/colour/quality/tm3018.py b/colour/quality/tm3018.py index 1c5370b0b4..ac611f379d 100644 --- a/colour/quality/tm3018.py +++ b/colour/quality/tm3018.py @@ -22,18 +22,24 @@ import typing from dataclasses import dataclass -import numpy as np - if typing.TYPE_CHECKING: - from colour.colorimetry import SpectralDistribution from colour.hints import ArrayLike, Literal, NDArrayFloat, NDArrayInt, Tuple +from colour.colorimetry import MultiSpectralDistributions, SpectralDistribution from colour.quality import colour_fidelity_index_CIE2017 from colour.quality.cfi2017 import ( DataColorimetry_TCS_CIE2017, delta_E_to_R_f, ) -from colour.utilities import as_float_array, as_float_scalar, as_int_array +from colour.utilities import ( + array_namespace, + as_float_array, + as_float_scalar, + as_int_array, + xp_matrix_transpose, + xp_nanmean, + xp_reshape, +) @dataclass @@ -116,9 +122,17 @@ def colour_fidelity_index_ANSIIESTM3018( ) -> float: ... +@typing.overload def colour_fidelity_index_ANSIIESTM3018( - sd_test: SpectralDistribution, additional_data: bool = False -) -> float | ColourQuality_Specification_ANSIIESTM3018: + sd_test: MultiSpectralDistributions, + additional_data: Literal[False] = False, +) -> NDArrayFloat: ... + + +def colour_fidelity_index_ANSIIESTM3018( + sd_test: SpectralDistribution | MultiSpectralDistributions, + additional_data: bool = False, +) -> float | NDArrayFloat | ColourQuality_Specification_ANSIIESTM3018: """ Compute the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) :math:`R_f` for the specified test spectral distribution. @@ -126,13 +140,17 @@ def colour_fidelity_index_ANSIIESTM3018( Parameters ---------- sd_test - Test spectral distribution. + Test spectral distribution. A + :class:`colour.MultiSpectralDistributions` of ``N`` test + illuminants is also accepted, in which case ``additional_data`` + must be ``False`` and the return value is a :class:`numpy.ndarray` + of ``N`` :math:`R_f` values. additional_data Whether to output additional data. Returns ------- - :class:`float` or \ + :class:`float`, :class:`numpy.ndarray` or \ :class:`colour.quality.ColourQuality_Specification_ANSIIESTM3018` *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI). @@ -151,58 +169,75 @@ def colour_fidelity_index_ANSIIESTM3018( if not additional_data: return colour_fidelity_index_CIE2017(sd_test, False) + if not isinstance(sd_test, SpectralDistribution): + error = ( + '"additional_data=True" is not supported when "sd_test" is a ' + '"MultiSpectralDistributions" instance.' + ) + raise NotImplementedError(error) + specification = colour_fidelity_index_CIE2017(sd_test, True) # Setup bins based on where the reference a'b' points are located. - bins = as_int_array(np.floor(specification.colorimetry_data[1].JMh[:, 2] / 22.5)) + JMh = specification.colorimetry_data[1].JMh + + xp = array_namespace(JMh) + + bins = as_int_array(xp.floor(JMh[:, 2] / 22.5)) - bin_mask = bins == np.reshape(np.arange(16), (-1, 1)) + arange_16 = xp.arange(16) + bin_mask = bins == xp_reshape(arange_16, (-1, 1), xp=xp) - # "bin_mask" is used later with Numpy broadcasting and "np.nanmean" - # to skip a list comprehension and keep all the mean calculation vectorised - # as per :cite:`VincentJ2017`. - bin_mask = np.choose(bin_mask, [np.nan, 1]) + # "bin_mask" is used later with broadcasting and "nanmean" to skip a list + # comprehension and keep all the mean calculation vectorised as per + # :cite:`VincentJ2017`. + bin_mask = xp.where(bin_mask == 0, float("nan"), 1.0) # Per-bin a'b' averages. - test_apbp = as_float_array(specification.colorimetry_data[0].Jpapbp[:, 1:]) - ref_apbp = as_float_array(specification.colorimetry_data[1].Jpapbp[:, 1:]) + test_apbp = specification.colorimetry_data[0].Jpapbp[:, 1:] + ref_apbp = specification.colorimetry_data[1].Jpapbp[:, 1:] # Tile the "apbp" data in the third dimension and use broadcasting to place # each bin mask along the third dimension. By multiplying these matrices - # together, Numpy automatically expands the apbp data in the third + # together, the backend automatically expands the apbp data in the third # dimension and multiplies by the nan-filled bin mask. Finally, - # "np.nanmean" can compute the bin mean apbp positions with the appropriate + # "nanmean" can compute the bin mean apbp positions with the appropriate # axis argument. - averages_test = np.transpose( - np.nanmean( - np.reshape(np.transpose(bin_mask), (99, 1, 16)) - * np.reshape(test_apbp, (*ref_apbp.shape, 1)), + averages_test = xp_matrix_transpose( + xp_nanmean( + xp_reshape(xp_matrix_transpose(bin_mask, xp=xp), (99, 1, 16), xp=xp) + * xp_reshape(test_apbp, (*ref_apbp.shape, 1), xp=xp), axis=0, - ) + xp=xp, + ), + xp=xp, ) - averages_reference = np.transpose( - np.nanmean( - np.reshape(np.transpose(bin_mask), (99, 1, 16)) - * np.reshape(ref_apbp, (*ref_apbp.shape, 1)), + averages_reference = xp_matrix_transpose( + xp_nanmean( + xp_reshape(xp_matrix_transpose(bin_mask, xp=xp), (99, 1, 16), xp=xp) + * xp_reshape(ref_apbp, (*ref_apbp.shape, 1), xp=xp), axis=0, - ) + xp=xp, + ), + xp=xp, ) # Gamut Index. R_g = 100 * (averages_area(averages_test) / averages_area(averages_reference)) # Local colour fidelity indexes, i.e., 16 CFIs for each bin. - bin_delta_E_s = np.nanmean( - np.reshape(specification.delta_E_s, (1, -1)) * bin_mask, axis=1 + bin_delta_E_s = xp_nanmean( + xp_reshape(specification.delta_E_s, (1, -1), xp=xp) * bin_mask, axis=1, xp=xp ) - R_fs = as_float_array(delta_E_to_R_f(bin_delta_E_s)) + R_fs = delta_E_to_R_f(bin_delta_E_s) - # Angles bisecting the hue bins. - angles = (22.5 * np.arange(16) + 11.25) / 180 * np.pi - cosines = np.cos(angles) - sines = np.sin(angles) + # Angles bisecting the 16 hue bins of width ``360 / 16 = 22.5`` degrees, + # offset by half a bin (*ANSI/IES TM-30-18*, Section 4.5). + angles = (22.5 * xp.arange(16) + 11.25) / 180 * xp.pi + cosines = xp.cos(angles) + sines = xp.sin(angles) - average_norms = np.linalg.norm(averages_reference, axis=1) + average_norms = xp.linalg.vector_norm(averages_reference, axis=1) a_deltas = averages_test[:, 0] - averages_reference[:, 0] b_deltas = averages_test[:, 1] - averages_reference[:, 1] @@ -249,9 +284,11 @@ def averages_area(averages: ArrayLike) -> float: averages = as_float_array(averages) + xp = array_namespace(averages) + # Vectorized shoelace formula u = averages - v = np.roll(averages, -1, axis=0) + v = xp.roll(averages, -1, axis=0) triangle_areas = (u[:, 0] * v[:, 1] - u[:, 1] * v[:, 0]) / 2 - return as_float_scalar(np.sum(triangle_areas)) + return as_float_scalar(xp.sum(triangle_areas)) diff --git a/colour/recovery/__init__.py b/colour/recovery/__init__.py index 15f66b05a3..0a1ab37550 100644 --- a/colour/recovery/__init__.py +++ b/colour/recovery/__init__.py @@ -23,7 +23,7 @@ import typing if typing.TYPE_CHECKING: - from colour.colorimetry import SpectralDistribution + from colour.colorimetry import MultiSpectralDistributions, SpectralDistribution from colour.hints import Any, ArrayLike, Literal, NDArrayFloat from colour.utilities import ( @@ -637,13 +637,33 @@ def XYZ_to_sd( """ +@typing.overload +def XYZ_to_msds( + XYZ: ArrayLike, + method: (Literal["Gaussian", "Smits 1999"] | str) = ..., + *, + as_array: Literal[False] = False, +) -> MultiSpectralDistributions: ... + + +@typing.overload +def XYZ_to_msds( + XYZ: ArrayLike, + method: (Literal["Gaussian", "Smits 1999"] | str) = ..., + *, + as_array: Literal[True], +) -> NDArrayFloat: ... + + def XYZ_to_msds( XYZ: ArrayLike, method: (Literal["Gaussian", "Smits 1999"] | str) = "Gaussian", -) -> NDArrayFloat: + *, + as_array: bool = False, +) -> MultiSpectralDistributions | NDArrayFloat: """ - Recover spectral values from the specified *CIE XYZ* tristimulus values - using the specified method. + Recover the multi-spectral distributions from the specified *CIE XYZ* + tristimulus values using the specified method. Parameters ---------- @@ -652,11 +672,17 @@ def XYZ_to_msds( The last dimension must be size 3. method Computation method. + as_array + Whether to return raw spectral values as a + :class:`numpy.ndarray` of shape + ``(*XYZ.shape[:-1], wavelengths)`` instead of a + :class:`MultiSpectralDistributions` instance. Defaults to *False*. Returns ------- - :class:`numpy.ndarray` - Recovered spectral values with shape ``(*XYZ.shape[:-1], wavelengths)``. + :class:`MultiSpectralDistributions` or :class:`numpy.ndarray` + Recovered multi-spectral distributions, or the underlying + spectral values when ``as_array=True``. Notes ----- @@ -686,16 +712,18 @@ def XYZ_to_msds( ... [0.07820260, 0.06157595, 0.28106183], ... ] ... ) - >>> XYZ_to_msds(XYZ, method="Gaussian").shape + >>> XYZ_to_msds(XYZ, method="Gaussian", as_array=True).shape (3, 421) - >>> float(XYZ_to_msds(XYZ, method="Gaussian")[0, 300]) # doctest: +ELLIPSIS + >>> float(XYZ_to_msds(XYZ, method="Gaussian", as_array=True)[0, 300]) + ... # doctest: +ELLIPSIS 0.3785... *Smits (1999)* reflectance recovery: - >>> XYZ_to_msds(XYZ, method="Smits 1999").shape + >>> XYZ_to_msds(XYZ, method="Smits 1999", as_array=True).shape (3, 10) - >>> float(XYZ_to_msds(XYZ, method="Smits 1999")[0, 6]) # doctest: +ELLIPSIS + >>> float(XYZ_to_msds(XYZ, method="Smits 1999", as_array=True)[0, 6]) + ... # doctest: +ELLIPSIS 0.3207... """ @@ -710,7 +738,7 @@ def XYZ_to_msds( a = XYZ_to_RGB_Smits1999(XYZ) - return function(a) + return function(a, as_array=as_array) __all__ += [ diff --git a/colour/recovery/gaussian.py b/colour/recovery/gaussian.py index 5863e6956f..ec4534e84b 100644 --- a/colour/recovery/gaussian.py +++ b/colour/recovery/gaussian.py @@ -7,10 +7,9 @@ from __future__ import annotations +import typing from typing import TYPE_CHECKING -import numpy as np - from colour.characterisation import SDS_COLOURCHECKERS from colour.colorimetry import ( CCS_ILLUMINANTS, @@ -30,10 +29,25 @@ from colour.difference import delta_E from colour.models import RGB_Colourspace, RGB_COLOURSPACE_sRGB, XYZ_to_Lab, XYZ_to_RGB from colour.recovery.smits1999 import RGB_to_msds_Smits1999, RGB_to_sd_Smits1999 -from colour.utilities import as_float, optional, required +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + optional, + required, + xp_as_float_array, + xp_matrix_transpose, +) if TYPE_CHECKING: - from colour.hints import ArrayLike, Domain1, DTypeFloat, NDArrayFloat, Range1 + from colour.hints import ( + ArrayLike, + Domain1, + DTypeFloat, + Literal, + NDArrayFloat, + Range1, + ) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -244,21 +258,29 @@ def optimise_gaussian_basis_parameters( # XYZ values for round-trip optimization (RGB, CMY, grey) M = colourspace.matrix_RGB_to_XYZ - XYZ_c = np.array( + + xp = array_namespace(M) + + primaries = xp_as_float_array( [ - np.dot(M, [1, 0, 0]), # Red - np.dot(M, [0, 1, 0]), # Green - np.dot(M, [0, 0, 1]), # Blue - np.dot(M, [0, 1, 1]), # Cyan - np.dot(M, [1, 0, 1]), # Magenta - np.dot(M, [1, 1, 0]), # Yellow - np.dot(M, [0.5, 0.5, 0.5]), # Grey - ] + [1, 0, 0], # Red + [0, 1, 0], # Green + [0, 0, 1], # Blue + [0, 1, 1], # Cyan + [1, 0, 1], # Magenta + [1, 1, 0], # Yellow + [0.5, 0.5, 0.5], # Grey + ], + xp=xp, + like=M, ) + XYZ_c = xp.matmul(primaries, xp_matrix_transpose(M, xp=xp)) sds_cc_r = list(SDS_COLOURCHECKERS["ISO 17321-1"].values()) - XYZ_cc_r = np.array( - [sd_to_XYZ(sd, cmfs=cmfs, illuminant=illuminant) / 100 for sd in sds_cc_r] + XYZ_cc_r = xp_as_float_array( + [sd_to_XYZ(sd, cmfs=cmfs, illuminant=illuminant) / 100 for sd in sds_cc_r], + xp=xp, + like=M, ) RGB_cc_r = XYZ_to_RGB(XYZ_cc_r, colourspace) Lab_cc_r = XYZ_to_Lab(XYZ_cc_r) @@ -325,30 +347,36 @@ def objective(parameters: NDArrayFloat) -> DTypeFloat: name="Gaussian Basis (Optimisation)", ) + spd_vals = as_float_array( + RGB_to_msds_Smits1999(XYZ_to_RGB(XYZ_c, colourspace), basis, as_array=True) + ) msds = MultiSpectralDistributions( - np.transpose(RGB_to_msds_Smits1999(XYZ_to_RGB(XYZ_c, colourspace), basis)), + xp_matrix_transpose(spd_vals, xp=xp), basis.wavelengths, labels=[str(i) for i in range(len(XYZ_c))], ) XYZ_t = msds_to_XYZ_integration(msds, cmfs, illuminant) / 100 # Colorimetric error for primaries/secondaries - colorimetric_error = np.sum((XYZ_t - XYZ_c) ** 2) + colorimetric_error = xp.sum((XYZ_t - XYZ_c) ** 2) # ColorChecker Delta E error + spd_vals_cc = as_float_array( + RGB_to_msds_Smits1999(RGB_cc_r, basis, as_array=True) + ) msds_cc_t = MultiSpectralDistributions( - np.transpose(RGB_to_msds_Smits1999(RGB_cc_r, basis)), + xp_matrix_transpose(spd_vals_cc, xp=xp), basis.wavelengths, labels=[str(i) for i in range(len(RGB_cc_r))], ) XYZ_cc_t = msds_to_XYZ_integration(msds_cc_t, cmfs, illuminant) / 100 Lab_cc_t = XYZ_to_Lab(XYZ_cc_t) delta_E_cc = delta_E(Lab_cc_r, Lab_cc_t, method="CIE 2000") - colorchecker_error = np.mean(delta_E_cc) + colorchecker_error = xp.mean(delta_E_cc) # Smoothness penalty: penalize deviation from standard Gaussian (exp=2) - exponents = np.array([R_exp, G_exp, B_exp, C_exp, M_exp, Y_exp]) - smoothness_penalty = np.sum((exponents - 2.0) ** 2) * smoothness_penalty_weight + exponents = xp_as_float_array([R_exp, G_exp, B_exp, C_exp, M_exp, Y_exp], xp=xp) + smoothness_penalty = xp.sum((exponents - 2.0) ** 2) * smoothness_penalty_weight # Combined loss: colorimetric error + ColorChecker Delta E + smoothness return as_float(colorimetric_error + colorchecker_error + smoothness_penalty) @@ -645,21 +673,42 @@ def generate_gaussian_basis( """ -def RGB_to_msds_Gaussian(RGB: ArrayLike) -> NDArrayFloat: +@typing.overload +def RGB_to_msds_Gaussian( + RGB: ArrayLike, *, as_array: Literal[False] = False +) -> MultiSpectralDistributions: ... + + +@typing.overload +def RGB_to_msds_Gaussian( + RGB: ArrayLike, *, as_array: Literal[True] +) -> NDArrayFloat: ... + + +def RGB_to_msds_Gaussian( + RGB: ArrayLike, *, as_array: bool = False +) -> MultiSpectralDistributions | NDArrayFloat: """ - Recover spectral values from *RGB* colourspace array using *Gaussian* - basis spectra and the *Smits (1999)* decomposition algorithm. + Recover the multi-spectral distributions from the specified *RGB* + colourspace array using *Gaussian* basis spectra and the + *Smits (1999)* decomposition algorithm. Parameters ---------- RGB *RGB* colourspace array to recover spectral values from. The last dimension must be size 3. + as_array + Whether to return raw spectral values as a + :class:`numpy.ndarray` of shape + ``(*RGB.shape[:-1], wavelengths)`` instead of a + :class:`MultiSpectralDistributions` instance. Defaults to *False*. Returns ------- - :class:`numpy.ndarray` - Recovered spectral values with shape ``(*RGB.shape[:-1], wavelengths)``. + :class:`MultiSpectralDistributions` or :class:`numpy.ndarray` + Recovered multi-spectral distributions, or the underlying + spectral values when ``as_array=True``. Notes ----- @@ -679,13 +728,14 @@ def RGB_to_msds_Gaussian(RGB: ArrayLike) -> NDArrayFloat: ... [0.01863137, 0.05139773, 0.28887675], ... ] ... ) - >>> RGB_to_msds_Gaussian(RGB).shape + >>> RGB_to_msds_Gaussian(RGB, as_array=True).shape (3, 421) - >>> float(RGB_to_msds_Gaussian(RGB)[0, 300]) # doctest: +ELLIPSIS + >>> float(RGB_to_msds_Gaussian(RGB, as_array=True)[0, 300]) + ... # doctest: +ELLIPSIS 0.4561... """ - return RGB_to_msds_Smits1999(RGB, MSDS_GAUSSIAN_BASIS) + return RGB_to_msds_Smits1999(RGB, MSDS_GAUSSIAN_BASIS, as_array=as_array) def RGB_to_sd_Gaussian(RGB: Domain1) -> SpectralDistribution: diff --git a/colour/recovery/jakob2019.py b/colour/recovery/jakob2019.py index bec69a6f23..8e82cbd651 100644 --- a/colour/recovery/jakob2019.py +++ b/colour/recovery/jakob2019.py @@ -19,6 +19,7 @@ from __future__ import annotations +import itertools import struct import typing @@ -49,8 +50,10 @@ from colour.hints import ArrayLike, Domain1, NDArrayFloat # noqa: TC001 from colour.models import RGB_Colourspace, RGB_to_XYZ, XYZ_to_Lab, XYZ_to_xy from colour.utilities import ( + array_namespace, as_float_array, as_float_scalar, + as_ndarray, domain_range_scale, full, index_along_last_axis, @@ -60,6 +63,10 @@ required, to_domain_1, tsplit, + xp_as_float_array, + xp_astype, + xp_matrix_transpose, + xp_reshape, zeros, ) @@ -192,10 +199,14 @@ def sd_Jakob2019( {'method': 'Constant', 'left': None, 'right': None}) """ - c_0, c_1, c_2 = as_float_array(coefficients) + coefficients = as_float_array(coefficients) + c_0, c_1, c_2 = coefficients + + xp = array_namespace(coefficients) + wl = shape.wavelengths U = c_0 * wl**2 + c_1 * wl + c_2 - R = 1 / 2 + U / (2 * np.sqrt(1 + U**2)) + R = 1 / 2 + U / (2 * xp.sqrt(1 + U**2)) name = f"{coefficients!r} (COEFF) - Jakob (2019)" @@ -283,18 +294,22 @@ def error_function( target = as_float_array(target) c_0, c_1, c_2 = as_float_array(coefficients) - wv = np.linspace(0, 1, len(cmfs.shape)) + + xp = array_namespace(target) + + wv = xp.linspace(0, 1, len(cmfs.shape)) U = c_0 * wv**2 + c_1 * wv + c_2 - t1 = np.sqrt(1 + U**2) + t1 = xp.sqrt(1 + U**2) R = 1 / 2 + U / (2 * t1) t2 = 1 / (2 * t1) - U**2 / (2 * t1**3) - dR = np.array([wv**2 * t2, wv * t2, t2]) + dR = xp_as_float_array([wv**2 * t2, wv * t2, t2], xp=xp) XYZ = sd_to_XYZ_integration(R, cmfs, illuminant, shape=cmfs.shape) / 100 - dXYZ = np.transpose( - sd_to_XYZ_integration(dR, cmfs, illuminant, shape=cmfs.shape) / 100 + dXYZ = xp_matrix_transpose( + sd_to_XYZ_integration(dR, cmfs, illuminant, shape=cmfs.shape) / 100, + xp=xp, ) XYZ_n = sd_to_XYZ_integration(illuminant, cmfs) @@ -302,7 +317,7 @@ def error_function( XYZ_XYZ_n = XYZ / XYZ_n XYZ_f = intermediate_lightness_function_CIE1976(XYZ, XYZ_n) - dXYZ_f = np.where( + dXYZ_f = xp.where( XYZ_XYZ_n[..., None] > (24 / 116) ** 3, 1 / (3 * spow(XYZ_n[..., None], 1 / 3) * spow(XYZ[..., None], 2 / 3)) * dXYZ, (841 / 108) * dXYZ / XYZ_n[..., None], @@ -316,22 +331,23 @@ def intermediate_XYZ_to_Lab( conversion. """ - return np.array( + return xp_as_float_array( [ 116 * XYZ_i[1] - offset, 500 * (XYZ_i[0] - XYZ_i[1]), 200 * (XYZ_i[1] - XYZ_i[2]), - ] + ], + xp=xp, ) Lab_i = intermediate_XYZ_to_Lab(XYZ_f) dLab_i = intermediate_XYZ_to_Lab(dXYZ_f, 0) - error = np.sqrt(np.sum((Lab_i - target) ** 2)) + error = xp.sqrt(xp.sum((Lab_i - target) ** 2)) if max_error is not None and error <= max_error: raise StopMinimizationEarlyError(coefficients, error) - derror = np.sum(dLab_i * (Lab_i[..., None] - target[..., None]), axis=0) / error + derror = xp.sum(dLab_i * (Lab_i[..., None] - target[..., None]), axis=0) / error if additional_data: return error, derror, R, XYZ, Lab_i @@ -367,14 +383,17 @@ def dimensionalise_coefficients( and 1, respectively. """ + xp = array_namespace(coefficients) + cp_0, cp_1, cp_2 = tsplit(coefficients) + span = shape.end - shape.start c_0 = cp_0 / span**2 c_1 = cp_1 / span - 2 * cp_0 * shape.start / span**2 c_2 = cp_0 * shape.start**2 / span**2 - cp_1 * shape.start / span + cp_2 - return np.array([c_0, c_1, c_2]) + return xp_as_float_array([c_0, c_1, c_2], xp=xp) def lightness_scale(steps: int) -> NDArrayFloat: @@ -402,7 +421,9 @@ def lightness_scale(steps: int) -> NDArrayFloat: array([0. , 0.0656127..., 0.5 , 0.9343872..., 1. ]) """ - linear = np.linspace(0, 1, steps) + xp = array_namespace() + + linear = xp.linspace(0, 1, steps) return smoothstep_function(smoothstep_function(linear)) @@ -677,6 +698,8 @@ def XYZ_to_sd_Jakob2019( XYZ = to_domain_1(XYZ) + XYZ = as_ndarray(XYZ) + cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019 ) @@ -880,7 +903,9 @@ def _create_interpolator(self) -> None: from scipy.interpolate import RegularGridInterpolator # noqa: PLC0415 - samples = np.linspace(0, 1, self._size) + xp = array_namespace() + + samples = xp.linspace(0, 1, self._size) axes = ([0, 1, 2], self._lightness_scale, samples, samples) self._interpolator = RegularGridInterpolator( @@ -961,26 +986,33 @@ def generate( lightness_steps = size chroma_steps = size + xp = array_namespace() + self._lightness_scale = lightness_scale(lightness_steps) - self._coefficients = np.empty( - [3, chroma_steps, chroma_steps, lightness_steps, 3] + self._coefficients = xp.zeros( + (3, chroma_steps, chroma_steps, lightness_steps, 3) ) - cube_indexes = np.ndindex(3, chroma_steps, chroma_steps) + cube_indexes = itertools.product( + range(3), range(chroma_steps), range(chroma_steps) + ) total_coefficients = chroma_steps**2 * 3 # First, create a list of all the fully bright colours with the order # matching cube_indexes. - samples = np.linspace(0, 1, chroma_steps) - ij = np.reshape( - np.transpose(np.meshgrid([1], samples, samples, indexing="ij")), - (-1, 3), + samples = xp.linspace(0, 1, chroma_steps) + mg = xp.meshgrid( + xp_as_float_array([1.0], xp=xp), samples, samples, indexing="ij" ) - chromas = np.concatenate( + # ``mg`` stacks three rank-3 axes; reversing all four axes mirrors + # the original ``np.transpose`` over a meshgrid stack. + mg_t = xp.permute_dims(xp.stack(mg), (3, 2, 1, 0)) + ij = xp_reshape(mg_t, (-1, 3), xp=xp) + chromas = xp.concat( [ ij, - np.roll(ij, 1, axis=1), - np.roll(ij, 2, axis=1), + xp.roll(ij, 1, axis=1), + xp.roll(ij, 2, axis=1), ] ) @@ -1024,21 +1056,27 @@ def optimize( # find_coefficients_Jakob2019" definition. L_middle = lightness_steps // 3 coefficients_middle = optimize( - np.hstack([ijk, L_middle]), zeros(3), chroma + xp.concat([ijk, xp_as_float_array([L_middle], xp=xp)], axis=0), + zeros(3), + chroma, ) # Down the lightness scale. coefficients_0 = coefficients_middle for L in reversed(range(L_middle)): coefficients_0 = optimize( - np.hstack([ijk, L]), coefficients_0, chroma + xp.concat([ijk, xp_as_float_array([L], xp=xp)], axis=0), + coefficients_0, + chroma, ) # Up the lightness scale. coefficients_0 = coefficients_middle for L in range(L_middle + 1, lightness_steps): coefficients_0 = optimize( - np.hstack([ijk, L]), coefficients_0, chroma + xp.concat([ijk, xp_as_float_array([L], xp=xp)], axis=0), + coefficients_0, + chroma, ) self._size = size @@ -1085,24 +1123,90 @@ def RGB_to_coefficients(self, RGB: ArrayLike) -> NDArrayFloat: array([ 1.5013448...e-04, -1.4679754...e-01, 3.4020219...e+01]) """ - if len(self._interpolator.grid) != 0: - RGB = as_float_array(RGB) + if len(self._interpolator.grid) == 0: + error = "The pre-computed lookup table has not been read or generated!" + + raise RuntimeError(error) + + RGB = as_float_array(RGB) - value_max = np.max(RGB, axis=-1) - chroma = RGB / (value_max[..., None] + 1e-10) + xp = array_namespace(RGB) - i_m = np.argmax(RGB, axis=-1) - i_1 = index_along_last_axis(RGB, i_m) - i_2 = index_along_last_axis(chroma, (i_m + 2) % 3) - i_3 = index_along_last_axis(chroma, (i_m + 1) % 3) + value_max = xp.max(RGB, axis=-1) + chroma = RGB / (value_max[..., None] + 1e-10) - indexes = np.stack([i_m, i_1, i_2, i_3], axis=-1) + i_m = xp.argmax(RGB, axis=-1) + i_1 = index_along_last_axis(RGB, i_m) + i_2 = index_along_last_axis(chroma, (i_m + 2) % 3) + i_3 = index_along_last_axis(chroma, (i_m + 1) % 3) - return self._interpolator(indexes).squeeze() + # Trilinear gather over the LUT grid, replacing + # ``scipy.interpolate.RegularGridInterpolator(bounds_error=False)`` + # to keep the path backend-agnostic. The dominant-channel axis + # is integer-indexed (``i_m``); the lightness axis uses the + # non-uniform ``_lightness_scale``; the two chroma axes are + # uniform on ``[0, 1]`` with ``size`` samples. Out-of-range + # queries are *NaN*-filled to match the *scipy* default. + coefficients = xp_as_float_array(self._coefficients, xp=xp, like=RGB) + lightness_scale = xp_as_float_array(self._lightness_scale, xp=xp, like=RGB) - error = "The pre-computed lookup table has not been read or generated!" + size = self._size + lightness_steps = lightness_scale.shape[0] - raise RuntimeError(error) + outside_lookup_domain = ( + (i_1 < lightness_scale[0]) + | (i_1 > lightness_scale[-1]) + | (i_2 < 0) + | (i_2 > 1) + | (i_3 < 0) + | (i_3 > 1) + ) + + j = xp.searchsorted(lightness_scale, i_1) - 1 + j = xp.clip(j, 0, lightness_steps - 2) + v_lo = xp.take(lightness_scale, j, axis=0) + v_hi = xp.take(lightness_scale, j + 1, axis=0) + f_v = (i_1 - v_lo) / (v_hi - v_lo) + + last = size - 1 + f_2 = i_2 * last + k_2 = xp.clip(xp_astype(xp.floor(f_2), DTYPE_INT_DEFAULT, xp=xp), 0, last - 1) + f_2 = f_2 - xp_astype(k_2, f_2.dtype, xp=xp) + + f_3 = i_3 * last + k_3 = xp.clip(xp_astype(xp.floor(f_3), DTYPE_INT_DEFAULT, xp=xp), 0, last - 1) + f_3 = f_3 - xp_astype(k_3, f_3.dtype, xp=xp) + + # 8 corners of the unit cube: ``coefficients[i_m, j+a, k_2+b, + # k_3+c, :]`` for ``a, b, c`` in ``{0, 1}``. Advanced indexing + # broadcasts the per-pixel index arrays into the leading + # ``(N,)`` shape of the result. + c000 = coefficients[i_m, j, k_2, k_3, :] + c100 = coefficients[i_m, j + 1, k_2, k_3, :] + c010 = coefficients[i_m, j, k_2 + 1, k_3, :] + c110 = coefficients[i_m, j + 1, k_2 + 1, k_3, :] + c001 = coefficients[i_m, j, k_2, k_3 + 1, :] + c101 = coefficients[i_m, j + 1, k_2, k_3 + 1, :] + c011 = coefficients[i_m, j, k_2 + 1, k_3 + 1, :] + c111 = coefficients[i_m, j + 1, k_2 + 1, k_3 + 1, :] + + f_v = f_v[..., None] + f_2 = f_2[..., None] + f_3 = f_3[..., None] + + c00 = c000 * (1 - f_v) + c100 * f_v + c01 = c001 * (1 - f_v) + c101 * f_v + c10 = c010 * (1 - f_v) + c110 * f_v + c11 = c011 * (1 - f_v) + c111 * f_v + + c0 = c00 * (1 - f_2) + c10 * f_2 + c1 = c01 * (1 - f_2) + c11 * f_2 + + return xp.where( + outside_lookup_domain[..., None], + float("nan"), + c0 * (1 - f_3) + c1 * f_3, + ) def RGB_to_sd( self, RGB: ArrayLike, shape: SpectralShape = SPECTRAL_SHAPE_JAKOB2019 @@ -1249,7 +1353,10 @@ def read(self, path: str | PathLike) -> LUT3D_Jakob2019: self._coefficients = np.fromfile( coeff_file, count=3 * (self._size**3) * 3, dtype=np.float32 ) - self._coefficients = np.reshape( + + xp = array_namespace(self._coefficients) + + self._coefficients = xp.reshape( self._coefficients, (3, self._size, self._size, self._size, 3) ) diff --git a/colour/recovery/jiang2013.py b/colour/recovery/jiang2013.py index a2ccaae389..b6fda546e6 100644 --- a/colour/recovery/jiang2013.py +++ b/colour/recovery/jiang2013.py @@ -21,8 +21,6 @@ import typing -import numpy as np - from colour.algebra import eigen_decomposition from colour.characterisation import RGB_CameraSensitivities from colour.colorimetry import ( @@ -43,9 +41,18 @@ Tuple, ) -from colour.hints import cast from colour.recovery import BASIS_FUNCTIONS_DYER2017 -from colour.utilities import as_float_array, optional, runtime_warning, tsplit +from colour.utilities import ( + array_namespace, + as_float_array, + optional, + runtime_warning, + tsplit, + xp_create_diagonal, + xp_lstsq, + xp_matrix_transpose, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -123,6 +130,7 @@ def PCA_Jiang2013( Examples -------- + >>> import numpy as np >>> from colour.colorimetry import SpectralShape >>> from colour.characterisation import MSDS_CAMERA_SENSITIVITIES >>> shape = SpectralShape(400, 700, 10) @@ -141,7 +149,7 @@ def normalised_sensitivity( ) -> NDArrayFloat: """Generate a normalised camera *RGB* sensitivity.""" - sensitivity = cast("SpectralDistribution", msds.signals[channel].copy()) + sensitivity = msds.signals[channel].copy() return sensitivity.normalise().values @@ -150,14 +158,22 @@ def normalised_sensitivity( G_sensitivities.append(normalised_sensitivity(msds, msds.labels[1])) B_sensitivities.append(normalised_sensitivity(msds, msds.labels[2])) + xp = array_namespace(R_sensitivities[0]) + R_w_v = eigen_decomposition( - np.vstack(R_sensitivities), eigen_w_v_count, covariance_matrix=True + xp.concat([xp_reshape(s, (1, -1), xp=xp) for s in R_sensitivities], axis=0), + eigen_w_v_count, + covariance_matrix=True, ) G_w_v = eigen_decomposition( - np.vstack(G_sensitivities), eigen_w_v_count, covariance_matrix=True + xp.concat([xp_reshape(s, (1, -1), xp=xp) for s in G_sensitivities], axis=0), + eigen_w_v_count, + covariance_matrix=True, ) B_w_v = eigen_decomposition( - np.vstack(B_sensitivities), eigen_w_v_count, covariance_matrix=True + xp.concat([xp_reshape(s, (1, -1), xp=xp) for s in B_sensitivities], axis=0), + eigen_w_v_count, + covariance_matrix=True, ) if additional_data: @@ -213,6 +229,7 @@ def RGB_to_sd_camera_sensitivity_Jiang2013( Examples -------- + >>> import numpy as np >>> from colour.colorimetry import ( ... SDS_ILLUMINANTS, ... msds_to_XYZ, @@ -289,6 +306,9 @@ def RGB_to_sd_camera_sensitivity_Jiang2013( """ RGB = as_float_array(RGB) + + xp = array_namespace(RGB) + shape = optional(shape, illuminant.shape) if illuminant.shape != shape: @@ -301,24 +321,50 @@ def RGB_to_sd_camera_sensitivity_Jiang2013( ) reflectances = reshape_msds(reflectances, shape, copy=False) - S = np.diag(illuminant.values) - R = np.transpose(reflectances.values) + S = xp_create_diagonal(illuminant.values, xp=xp) + R = xp_matrix_transpose(reflectances.values, xp=xp) - A = np.dot(np.dot(R, S), eigen_w) + A = xp.matmul(xp.matmul(R, S), eigen_w) - X = np.linalg.lstsq(A, RGB, rcond=None)[0] - X = np.dot(eigen_w, X) + X = xp_lstsq(A, RGB, xp=xp) + X = xp.matmul(eigen_w, X) return SpectralDistribution(X, shape.wavelengths) +@typing.overload +def RGB_to_msds_camera_sensitivities_Jiang2013( + RGB: Domain1, + illuminant: SpectralDistribution, + reflectances: MultiSpectralDistributions, + basis_functions: ArrayLike = ..., + shape: SpectralShape | None = ..., + *, + as_array: Literal[False] = False, +) -> MultiSpectralDistributions: ... + + +@typing.overload +def RGB_to_msds_camera_sensitivities_Jiang2013( + RGB: Domain1, + illuminant: SpectralDistribution, + reflectances: MultiSpectralDistributions, + basis_functions: ArrayLike = ..., + shape: SpectralShape | None = ..., + *, + as_array: Literal[True], +) -> NDArrayFloat: ... + + def RGB_to_msds_camera_sensitivities_Jiang2013( RGB: Domain1, illuminant: SpectralDistribution, reflectances: MultiSpectralDistributions, basis_functions: ArrayLike = BASIS_FUNCTIONS_DYER2017, shape: SpectralShape | None = None, -) -> MultiSpectralDistributions: + *, + as_array: bool = False, +) -> MultiSpectralDistributions | NDArrayFloat: """ Recover the camera *RGB* sensitivities for the specified camera *RGB* values using *Jiang et al. (2013)* method. @@ -341,11 +387,16 @@ def RGB_to_msds_camera_sensitivities_Jiang2013( Spectral shape of the recovered camera *RGB* sensitivities. The ``illuminant`` and ``reflectances`` will be aligned to it if passed, otherwise, the ``illuminant`` shape is used. + as_array + Whether to return raw spectral values as a + :class:`numpy.ndarray` of shape ``(n_wavelengths, 3)`` instead of + a :class:`RGB_CameraSensitivities` instance. Defaults to *False*. Returns ------- - :class:`colour.RGB_CameraSensitivities` - Recovered camera *RGB* sensitivities. + :class:`colour.RGB_CameraSensitivities` or :class:`numpy.ndarray` + Recovered camera *RGB* sensitivities, or the underlying spectral + values when ``as_array=True``. Notes ----- @@ -425,11 +476,15 @@ def RGB_to_msds_camera_sensitivities_Jiang2013( [-6.00395414e-03, 1.54678227e-03, 5.40394352e-04]]) """ - R, G, B = tsplit(np.reshape(RGB, [-1, 3])) + RGB = as_float_array(RGB) + + xp = array_namespace(RGB) + + R, G, B = tsplit(xp_reshape(RGB, [-1, 3], xp=xp)) basis_functions = as_float_array(basis_functions) shape = optional(shape, illuminant.shape) - R_w, G_w, B_w = tsplit(np.moveaxis(basis_functions, 0, 1)) + R_w, G_w, B_w = tsplit(xp.moveaxis(basis_functions, 0, 1)) if illuminant.shape != shape: runtime_warning(f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') @@ -453,6 +508,9 @@ def RGB_to_msds_camera_sensitivities_Jiang2013( msds_camera_sensitivities = RGB_CameraSensitivities([S_R, S_G, S_B]) - msds_camera_sensitivities /= np.max(msds_camera_sensitivities.values) + msds_camera_sensitivities /= xp.max(msds_camera_sensitivities.values) + + if as_array: + return msds_camera_sensitivities.values return msds_camera_sensitivities diff --git a/colour/recovery/mallett2019.py b/colour/recovery/mallett2019.py index 7a5f701593..50776c59bf 100644 --- a/colour/recovery/mallett2019.py +++ b/colour/recovery/mallett2019.py @@ -33,7 +33,13 @@ from colour.hints import Domain1 # noqa: TC001 from colour.recovery import MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019 -from colour.utilities import required, to_domain_1 +from colour.utilities import ( + array_namespace, + as_ndarray, + required, + to_domain_1, + xp_matrix_transpose, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -174,15 +180,24 @@ def spectral_primary_decomposition_Mallett2019( N = len(cmfs.shape) + # ``scipy.optimize.minimize``, ``scipy.optimize.LinearConstraint`` and + # ``scipy.linalg.block_diag`` only operate on *NumPy* arrays, so the whole + # decomposition is pinned to *NumPy* at this boundary rather than dressing + # scipy-bound values in array-namespace calls. The returned basis functions + # are a fixed property of the colourspace and observer, not of any caller + # backend. + cmfs_values = as_ndarray(cmfs.values) + illuminant_values = as_ndarray(illuminant.values) + R_to_XYZ = np.transpose( - illuminant.values[..., None] - * cmfs.values - / (np.sum(cmfs.values[:, 1] * illuminant.values)) + illuminant_values[..., None] + * cmfs_values + / np.sum(cmfs_values[:, 1] * illuminant_values) ) - R_to_RGB = np.dot(colourspace.matrix_XYZ_to_RGB, R_to_XYZ) + R_to_RGB = np.matmul(colourspace.matrix_XYZ_to_RGB, R_to_XYZ) basis_to_RGB = block_diag(R_to_RGB, R_to_RGB, R_to_RGB) - primaries = np.reshape(np.identity(3), 9) + primaries = np.reshape(np.eye(3), 9) # Ensure that the reflectances correspond to the correct RGB colours. colour_match = LinearConstraint(basis_to_RGB, primaries, primaries) @@ -191,7 +206,7 @@ def spectral_primary_decomposition_Mallett2019( energy_conservation = Bounds(np.zeros(3 * N), np.ones(3 * N)) # Ensure that the sum of the three bases is bounded by [0, 1]. - sum_matrix = np.transpose(np.tile(np.identity(N), (3, 1))) + sum_matrix = np.transpose(np.tile(np.eye(N), (3, 1))) sum_constraint = LinearConstraint(sum_matrix, np.zeros(N), np.ones(N)) optimisation_settings = { @@ -372,8 +387,10 @@ def RGB_to_sd_Mallett2019( RGB = to_domain_1(RGB) + xp = array_namespace(RGB) + sd = SpectralDistribution( - np.dot(RGB, np.transpose(basis_functions.values)), + xp.matmul(RGB, xp_matrix_transpose(basis_functions.values, xp=xp)), basis_functions.wavelengths, ) sd.name = f"{RGB} (RGB) - Mallett (2019)" diff --git a/colour/recovery/meng2015.py b/colour/recovery/meng2015.py index a67c0204ba..69b883dba7 100644 --- a/colour/recovery/meng2015.py +++ b/colour/recovery/meng2015.py @@ -33,7 +33,14 @@ from colour.hints import DTypeFloat from colour.hints import Domain1, NDArrayFloat # noqa: TC001 -from colour.utilities import from_range_100, required, to_domain_1 +from colour.utilities import ( + array_namespace, + as_float_array, + as_ndarray, + from_range_100, + required, + to_domain_1, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -177,7 +184,7 @@ def XYZ_to_sd_Meng2015( from scipy.optimize import minimize # noqa: PLC0415 - XYZ = to_domain_1(XYZ) + XYZ = as_ndarray(to_domain_1(XYZ)) cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_MENG2015 @@ -188,7 +195,11 @@ def XYZ_to_sd_Meng2015( def objective_function(a: NDArrayFloat) -> DTypeFloat: """Define the objective function.""" - return np.sum(np.square(np.diff(a))) + a = as_float_array(a) + + xp = array_namespace(a) + + return xp.sum(xp.square(xp.diff(a))) def constraint_function(a: NDArrayFloat) -> NDArrayFloat: """Define the constraint function.""" diff --git a/colour/recovery/otsu2018.py b/colour/recovery/otsu2018.py index 2bd6105234..789ad906b5 100644 --- a/colour/recovery/otsu2018.py +++ b/colour/recovery/otsu2018.py @@ -57,6 +57,7 @@ ) from colour.utilities import ( TreeNode, + array_namespace, as_float_array, as_float_scalar, domain_range_scale, @@ -64,6 +65,8 @@ message_box, optional, to_domain_1, + xp_as_float_array, + xp_matrix_transpose, zeros, ) @@ -557,38 +560,48 @@ def XYZ_to_sd_Otsu2018( if shape is not None: XYZ = to_domain_1(XYZ) + xp = array_namespace(XYZ) + cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_OTSU2018 ) xy = XYZ_to_xy(XYZ) + # ``dataset.cluster`` returns *NumPy* basis functions and mean; bring + # them (and the ``sd_to_XYZ`` integrations they feed) into the input's + # namespace and device before the ``xp`` linear algebra below. basis_functions, mean = dataset.cluster(xy) - with domain_range_scale("ignore"): - M = np.column_stack( - [ - sd_to_XYZ( - SpectralDistribution(basis_functions[i, :], shape.wavelengths), - cmfs, - illuminant, - ) - / 100 - for i in range(3) - ] + M_columns = [ + sd_to_XYZ( + SpectralDistribution(basis_functions[i, :], shape.wavelengths), + cmfs, + illuminant, + ) + / 100 + for i in range(3) + ] + M = xp.concat( + [xp_as_float_array(c, xp=xp, like=XYZ)[:, None] for c in M_columns], + axis=1, ) - M_inverse = np.linalg.inv(M) + M_inverse = xp.linalg.inv(M) sd = SpectralDistribution(mean, shape.wavelengths) with domain_range_scale("ignore"): - XYZ_mu = sd_to_XYZ(sd, cmfs, illuminant) / 100 + XYZ_mu = xp_as_float_array( + sd_to_XYZ(sd, cmfs, illuminant) / 100, xp=xp, like=XYZ + ) - weights = np.dot(M_inverse, XYZ - XYZ_mu) - recovered_sd = np.dot(weights, basis_functions) + mean + weights = xp.matmul(M_inverse, XYZ - XYZ_mu) + recovered_sd = xp.matmul( + weights, xp_as_float_array(basis_functions, xp=xp, like=XYZ) + ) + xp_as_float_array(mean, xp=xp, like=XYZ) - recovered_sd = np.clip(recovered_sd, 0, 1) if clip else recovered_sd + recovered_sd = xp.clip(recovered_sd, 0, 1) if clip else recovered_sd return SpectralDistribution(recovered_sd, shape.wavelengths) @@ -900,21 +913,27 @@ def PCA(self) -> None: "shape": self._cmfs.shape, } - self._mean = np.mean(self._reflectances, axis=0) + xp = array_namespace(self._reflectances) + + self._mean = xp.mean(self._reflectances, axis=0) self._XYZ_mu = ( msds_to_XYZ_integration(cast("NDArrayFloat", self._mean), **settings) / 100 ) _w, w = eigen_decomposition( - self._reflectances - self._mean, # pyright: ignore + self._reflectances - cast("NDArrayFloat", self._mean), descending_order=False, covariance_matrix=True, ) - self._basis_functions = np.transpose(w[:, -3:]) - self._M = np.transpose( - msds_to_XYZ_integration(self._basis_functions, **settings) / 100 + xp = array_namespace(w) + + self._basis_functions = xp_matrix_transpose(w[:, -3:], xp=xp) + + self._M = xp_matrix_transpose( + msds_to_XYZ_integration(self._basis_functions, **settings) / 100, + xp=xp, ) def reconstruct(self, XYZ: ArrayLike) -> SpectralDistribution: @@ -947,9 +966,16 @@ def reconstruct(self, XYZ: ArrayLike) -> SpectralDistribution: ): XYZ = as_float_array(XYZ) - weights = np.dot(np.linalg.inv(self._M), XYZ - self._XYZ_mu) - reflectance = np.dot(weights, self._basis_functions) + self._mean - reflectance = np.clip(reflectance, 0, 1) + xp = array_namespace(XYZ) + + weights = xp.matmul( + xp.linalg.inv(xp_as_float_array(self._M, xp=xp, like=XYZ)), + XYZ - xp_as_float_array(self._XYZ_mu, xp=xp, like=XYZ), + ) + reflectance = xp.matmul( + weights, xp_as_float_array(self._basis_functions, xp=xp, like=XYZ) + ) + xp_as_float_array(cast("NDArrayFloat", self._mean), xp=xp) + reflectance = xp.clip(reflectance, 0, 1) return SpectralDistribution(reflectance, self._cmfs.wavelengths) @@ -995,9 +1021,11 @@ def reconstruction_error(self) -> float: sd = self._reflectances[i, :] XYZ = self._XYZ[i, :] recovered_sd = self.reconstruct(XYZ) - reconstruction_error += cast( - "float", np.sum((sd - recovered_sd.values) ** 2) - ) + diff = as_float_array(sd - recovered_sd.values) + + xp = array_namespace(diff) + + reconstruction_error += xp.sum(diff**2) self._reconstruction_error = reconstruction_error @@ -1146,15 +1174,16 @@ def minimise( axis = PartitionAxis(self.data.origin(i, direction), direction) data_lesser, data_greater = self.data.partition(axis) - if np.any( - np.array( - [ - len(data_lesser), - len(data_greater), - ] - ) - < minimum_cluster_size - ): + sizes = as_float_array( + [ + len(data_lesser), + len(data_greater), + ] + ) + + xp = array_namespace(sizes) + + if xp.any(sizes < minimum_cluster_size): continue lesser = Node_Otsu2018(data=data_lesser) @@ -1221,10 +1250,14 @@ def branch_reconstruction_error(self) -> float: if self.is_leaf(): return self.leaf_reconstruction_error() - return as_float_scalar( - np.sum([child.branch_reconstruction_error() for child in self.children]) + errors = as_float_array( + [child.branch_reconstruction_error() for child in self.children] ) + xp = array_namespace(errors) + + return as_float_scalar(xp.sum(errors)) + class Tree_Otsu2018(Node_Otsu2018): """ @@ -1362,10 +1395,14 @@ def __init__( self._cmfs: MultiSpectralDistributions = cmfs self._illuminant: SpectralDistribution = illuminant - self._reflectances: NDArrayFloat = np.transpose( + values = as_float_array( reshape_msds(reflectances, self._cmfs.shape, copy=False).values ) + xp = array_namespace(values) + + self._reflectances: NDArrayFloat = xp_matrix_transpose(values, xp=xp) + self.data: Data_Otsu2018 = Data_Otsu2018( self._reflectances, self._cmfs, self._illuminant ) diff --git a/colour/recovery/smits1999.py b/colour/recovery/smits1999.py index d96b91fe1a..7344704e49 100644 --- a/colour/recovery/smits1999.py +++ b/colour/recovery/smits1999.py @@ -13,10 +13,9 @@ from __future__ import annotations +import typing from typing import TYPE_CHECKING -import numpy as np - from colour.colorimetry import ( CCS_ILLUMINANTS, MultiSpectralDistributions, @@ -25,14 +24,18 @@ from colour.models import RGB_Colourspace, RGB_COLOURSPACE_sRGB, XYZ_to_RGB from colour.recovery import MSDS_SMITS1999 from colour.utilities import ( + array_namespace, as_float_array, optional, to_domain_1, - tsplit, + xp_as_float_array, + xp_atleast_2d, + xp_matrix_transpose, + xp_reshape, ) if TYPE_CHECKING: - from colour.hints import ArrayLike, Domain1, NDArrayFloat, Range1 + from colour.hints import ArrayLike, Domain1, Literal, NDArrayFloat, Range1 __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -118,13 +121,33 @@ def XYZ_to_RGB_Smits1999(XYZ: Domain1) -> Range1: return XYZ_to_RGB(XYZ, RGB_COLOURSPACE_SMITS1999) +@typing.overload +def RGB_to_msds_Smits1999( + RGB: ArrayLike, + basis: MultiSpectralDistributions | None = None, + *, + as_array: Literal[False] = False, +) -> MultiSpectralDistributions: ... + + +@typing.overload def RGB_to_msds_Smits1999( RGB: ArrayLike, basis: MultiSpectralDistributions | None = None, -) -> NDArrayFloat: + *, + as_array: Literal[True], +) -> NDArrayFloat: ... + + +def RGB_to_msds_Smits1999( + RGB: ArrayLike, + basis: MultiSpectralDistributions | None = None, + *, + as_array: bool = False, +) -> MultiSpectralDistributions | NDArrayFloat: """ - Recover spectral values from *RGB* colourspace array using the - *Smits (1999)* decomposition algorithm. + Recover the multi-spectral distributions from the specified *RGB* + colourspace array using the *Smits (1999)* decomposition algorithm. This is a vectorised implementation supporting multi-dimensional arrays. @@ -136,11 +159,17 @@ def RGB_to_msds_Smits1999( basis Multi-spectral distributions basis with signals: white, cyan, magenta, yellow, red, green, blue. Defaults to :attr:`MSDS_SMITS1999`. + as_array + Whether to return raw spectral values as a + :class:`numpy.ndarray` of shape + ``(*RGB.shape[:-1], wavelengths)`` instead of a + :class:`MultiSpectralDistributions` instance. Defaults to *False*. Returns ------- - :class:`numpy.ndarray` - Recovered spectral values with shape ``(*RGB.shape[:-1], wavelengths)``. + :class:`MultiSpectralDistributions` or :class:`numpy.ndarray` + Recovered multi-spectral distributions, or the underlying + spectral values when ``as_array=True``. Notes ----- @@ -164,73 +193,82 @@ def RGB_to_msds_Smits1999( ... [0.01863137, 0.05139773, 0.28887675], ... ] ... ) - >>> RGB_to_msds_Smits1999(RGB).shape + >>> RGB_to_msds_Smits1999(RGB, as_array=True).shape (3, 10) - >>> float(RGB_to_msds_Smits1999(RGB)[0, 0]) # doctest: +ELLIPSIS + >>> float(RGB_to_msds_Smits1999(RGB, as_array=True)[0, 0]) + ... # doctest: +ELLIPSIS 0.0829... """ basis = optional(basis, MSDS_SMITS1999) RGB = to_domain_1(as_float_array(RGB)) + + xp = array_namespace(RGB) + shape = RGB.shape - RGB = np.atleast_2d(RGB.reshape(-1, 3)) - - R, G, B = tsplit(RGB) - - labels = list(basis.labels) - white = basis.values[:, labels.index("white")] - cyan = basis.values[:, labels.index("cyan")] - magenta = basis.values[:, labels.index("magenta")] - yellow = basis.values[:, labels.index("yellow")] - red = basis.values[:, labels.index("red")] - green = basis.values[:, labels.index("green")] - blue = basis.values[:, labels.index("blue")] - - R = R[..., np.newaxis] - G = G[..., np.newaxis] - B = B[..., np.newaxis] - - # if R <= G and R <= B: - # sd += white * R - # if G <= B: - # sd += cyan * (G - R) + blue * (B - G) - # else: - # sd += cyan * (B - R) + green * (G - B) - R_le_G_le_B = (R <= G) & (R <= B) & (G <= B) - R_le_B_lt_G = (R <= G) & (R <= B) & (G > B) - - # elif G <= R and G <= B: - # sd += white * G - # if R <= B: - # sd += magenta * (R - G) + blue * (B - R) - # else: - # sd += magenta * (B - G) + red * (R - B) - G_lt_R_le_B = (G < R) & (G <= B) & (R <= B) - G_le_B_lt_R = (G < R) & (G <= B) & (R > B) - - # else: # B < R and B < G - # sd += white * B - # if R <= G: - # sd += yellow * (R - B) + green * (G - R) - # else: - # sd += yellow * (G - B) + red * (R - G) - B_lt_R_le_G = (B < R) & (B < G) & (R <= G) - B_lt_G_lt_R = (B < R) & (B < G) & (R > G) - - spectra = np.select( - [R_le_G_le_B, R_le_B_lt_G, G_lt_R_le_B, G_le_B_lt_R, B_lt_R_le_G, B_lt_G_lt_R], + RGB = xp_atleast_2d(xp_reshape(RGB, (-1, 3), xp=xp), xp=xp) + + label_to_index = {label: index for index, label in enumerate(basis.labels)} + basis_values = xp_as_float_array(basis.values, xp=xp, like=RGB) + + # *Smits (1999)* decomposes any *RGB* into three basis spectra + # weighted by the sorted ``(min, mid, max)`` channel deltas. The + # paper expresses this as a six-branch ``if`` ladder over the + # orderings of *R*, *G*, *B*: + # + # min max | secondary primary | spectrum + # ---- ---- | ---------- -------- | --------------------------------- + # R B | cyan blue | white*R + cyan*(G - R) + blue*(B - G) + # R G | cyan green | white*R + cyan*(B - R) + green*(G - B) + # G B | magenta blue | white*G + magenta*(R - G) + blue*(B - R) + # G R | magenta red | white*G + magenta*(B - G) + red*(R - B) + # B G | yellow green | white*B + yellow*(R - B) + green*(G - R) + # B R | yellow red | white*B + yellow*(G - B) + red*(R - G) + # + # Every branch reduces to ``white*min + secondary*(mid - min) + + # primary*(max - mid)``; ``argmin`` / ``argmax`` over the stacked + # candidate bases collapse the six-way switch to a single gather. + white = basis_values[:, label_to_index["white"]] + secondary_bases = xp.stack( [ - white * R + cyan * (G - R) + blue * (B - G), - white * R + cyan * (B - R) + green * (G - B), - white * G + magenta * (R - G) + blue * (B - R), - white * G + magenta * (B - G) + red * (R - B), - white * B + yellow * (R - B) + green * (G - R), - white * B + yellow * (G - B) + red * (R - G), - ], + basis_values[:, label_to_index["cyan"]], + basis_values[:, label_to_index["magenta"]], + basis_values[:, label_to_index["yellow"]], + ] + ) + primary_bases = xp.stack( + [ + basis_values[:, label_to_index["red"]], + basis_values[:, label_to_index["green"]], + basis_values[:, label_to_index["blue"]], + ] + ) + + min_channel = xp.min(RGB, axis=-1) + max_channel = xp.max(RGB, axis=-1) + mid_channel = xp.sum(RGB, axis=-1) - min_channel - max_channel + secondary = secondary_bases[xp.argmin(RGB, axis=-1)] + primary = primary_bases[xp.argmax(RGB, axis=-1)] + + spectra = ( + white * min_channel[..., None] + + secondary * (mid_channel - min_channel)[..., None] + + primary * (max_channel - mid_channel)[..., None] + ) + + spectra = xp_reshape(spectra, [*list(shape[:-1]), len(white)], xp=xp) + + if as_array: + return spectra + + # ``MultiSpectralDistributions`` expects ``(n_wavelengths, + # n_samples)``; flatten any leading shape into a single sample axis. + msds_values = xp_matrix_transpose( + xp_reshape(spectra, (-1, len(white)), xp=xp), xp=xp ) - return np.reshape(spectra, [*list(shape[:-1]), len(white)]) + return MultiSpectralDistributions(msds_values, basis.wavelengths) def RGB_to_sd_Smits1999( @@ -307,7 +345,7 @@ def RGB_to_sd_Smits1999( basis = optional(basis, MSDS_SMITS1999) name = optional(name, f"Smits (1999) - {RGB!r}") - values = RGB_to_msds_Smits1999(RGB, basis) + values = RGB_to_msds_Smits1999(RGB, basis, as_array=True) return SpectralDistribution( values, diff --git a/colour/recovery/tests/test__init__.py b/colour/recovery/tests/test__init__.py index 8fee1f5693..728accd74b 100644 --- a/colour/recovery/tests/test__init__.py +++ b/colour/recovery/tests/test__init__.py @@ -2,7 +2,13 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np +import pytest from colour.colorimetry import ( MSDS_CMFS, @@ -17,7 +23,12 @@ from colour.recovery import MSDS_GAUSSIAN_BASIS, XYZ_to_msds, XYZ_to_sd from colour.recovery.gaussian import RGB_COLOURSPACE_GAUSSIAN from colour.recovery.smits1999 import RGB_to_msds_Smits1999 -from colour.utilities import domain_range_scale, is_scipy_installed +from colour.utilities import ( + domain_range_scale, + is_scipy_installed, + xp_as_array, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -48,7 +59,11 @@ def setup_method(self) -> None: self._sd_D65 = reshape_sd(SDS_ILLUMINANTS["D65"], self._cmfs.shape) - def test_domain_range_scale_XYZ_to_sd(self) -> None: + @pytest.mark.mps_xfail("MPS float32 singular matrix") + def test_domain_range_scale_XYZ_to_sd( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.recovery.XYZ_to_sd` definition domain and range scale support. @@ -78,7 +93,7 @@ def test_domain_range_scale_XYZ_to_sd(self) -> None: for method, value in zip(m, v, strict=True): for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( XYZ_to_sd( XYZ * factor_a, @@ -114,224 +129,220 @@ def test_XYZ_to_msds(self) -> None: ) # Gaussian method - msds_gaussian = XYZ_to_msds(XYZ, method="Gaussian") + msds_gaussian = XYZ_to_msds(XYZ, method="Gaussian", as_array=True) assert msds_gaussian.shape == (3, 421) # Test with interpolated basis at 10nm for full array comparison basis_10nm = MSDS_GAUSSIAN_BASIS.copy().align(SpectralShape(360, 780, 10)) RGB = XYZ_to_RGB(XYZ, RGB_COLOURSPACE_GAUSSIAN) - msds_gaussian_10nm = RGB_to_msds_Smits1999(RGB, basis_10nm) + msds_gaussian_10nm = RGB_to_msds_Smits1999(RGB, basis_10nm, as_array=True) assert msds_gaussian_10nm.shape == (3, 43) - np.testing.assert_allclose( + xp_assert_close( msds_gaussian_10nm, - np.array( + [ + [ + 0.04502009, + 0.04501988, + 0.04501914, + 0.04501680, + 0.04500998, + 0.04499185, + 0.04494770, + 0.04484930, + 0.04464874, + 0.04427526, + 0.04364072, + 0.04265916, + 0.04128144, + 0.03953613, + 0.03755869, + 0.03558836, + 0.03392457, + 0.03287194, + 0.03279225, + 0.03461775, + 0.04157706, + 0.06103056, + 0.10398511, + 0.17707942, + 0.26899683, + 0.34699756, + 0.37718472, + 0.37785039, + 0.37824584, + 0.37846014, + 0.37856623, + 0.37861426, + 0.37863415, + 0.37864170, + 0.37864431, + 0.37864515, + 0.37864539, + 0.37864545, + 0.37864547, + 0.37864547, + 0.37864547, + 0.37864547, + 0.37864547, + ], [ - [ - 0.04502009, - 0.04501988, - 0.04501914, - 0.04501680, - 0.04500998, - 0.04499185, - 0.04494770, - 0.04484930, - 0.04464874, - 0.04427526, - 0.04364072, - 0.04265916, - 0.04128144, - 0.03953613, - 0.03755869, - 0.03558836, - 0.03392457, - 0.03287194, - 0.03279225, - 0.03461775, - 0.04157706, - 0.06103056, - 0.10398511, - 0.17707942, - 0.26899683, - 0.34699756, - 0.37718472, - 0.37785039, - 0.37824584, - 0.37846014, - 0.37856623, - 0.37861426, - 0.37863415, - 0.37864170, - 0.37864431, - 0.37864515, - 0.37864539, - 0.37864545, - 0.37864547, - 0.37864547, - 0.37864547, - 0.37864547, - 0.37864547, - ], - [ - 0.07907249, - 0.07907301, - 0.07907546, - 0.07908565, - 0.07912360, - 0.07925012, - 0.07962779, - 0.08063705, - 0.08305097, - 0.08821555, - 0.09808908, - 0.11492450, - 0.14044675, - 0.17466072, - 0.21482897, - 0.25537051, - 0.28914692, - 0.30987195, - 0.31497706, - 0.30547331, - 0.28048697, - 0.24387836, - 0.19557999, - 0.14451545, - 0.10357764, - 0.07663160, - 0.06088546, - 0.05238953, - 0.04817010, - 0.04627048, - 0.04550213, - 0.04522370, - 0.04513336, - 0.04510712, - 0.04510030, - 0.04509871, - 0.04509838, - 0.04509832, - 0.04509831, - 0.04509831, - 0.04509831, - 0.04509831, - 0.04509831, - ], - [ - 0.31783443, - 0.31783443, - 0.31783443, - 0.31783443, - 0.31783443, - 0.31783443, - 0.31783443, - 0.31783443, - 0.31783443, - 0.31368888, - 0.28984943, - 0.25028264, - 0.20369873, - 0.15863895, - 0.12105230, - 0.09341064, - 0.07527351, - 0.06457872, - 0.05888491, - 0.05613922, - 0.05493718, - 0.05429902, - 0.04596035, - 0.03204021, - 0.02165872, - 0.01705202, - 0.01574487, - 0.01550084, - 0.01547045, - 0.01546791, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - 0.01546776, - ], - ] - ), + 0.07907249, + 0.07907301, + 0.07907546, + 0.07908565, + 0.07912360, + 0.07925012, + 0.07962779, + 0.08063705, + 0.08305097, + 0.08821555, + 0.09808908, + 0.11492450, + 0.14044675, + 0.17466072, + 0.21482897, + 0.25537051, + 0.28914692, + 0.30987195, + 0.31497706, + 0.30547331, + 0.28048697, + 0.24387836, + 0.19557999, + 0.14451545, + 0.10357764, + 0.07663160, + 0.06088546, + 0.05238953, + 0.04817010, + 0.04627048, + 0.04550213, + 0.04522370, + 0.04513336, + 0.04510712, + 0.04510030, + 0.04509871, + 0.04509838, + 0.04509832, + 0.04509831, + 0.04509831, + 0.04509831, + 0.04509831, + 0.04509831, + ], + [ + 0.31783443, + 0.31783443, + 0.31783443, + 0.31783443, + 0.31783443, + 0.31783443, + 0.31783443, + 0.31783443, + 0.31783443, + 0.31368888, + 0.28984943, + 0.25028264, + 0.20369873, + 0.15863895, + 0.12105230, + 0.09341064, + 0.07527351, + 0.06457872, + 0.05888491, + 0.05613922, + 0.05493718, + 0.05429902, + 0.04596035, + 0.03204021, + 0.02165872, + 0.01705202, + 0.01574487, + 0.01550084, + 0.01547045, + 0.01546791, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + 0.01546776, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Smits 1999 method - native 10 wavelengths - msds_smits = XYZ_to_msds(XYZ, method="Smits 1999") + msds_smits = XYZ_to_msds(XYZ, method="Smits 1999", as_array=True) assert msds_smits.shape == (3, 10) - np.testing.assert_allclose( + xp_assert_close( msds_smits, - np.array( + [ + [ + 0.07878305, + 0.06220187, + 0.04462067, + 0.03522208, + 0.03241491, + 0.03301050, + 0.32071155, + 0.38361649, + 0.38361649, + 0.38356492, + ], [ - [ - 0.07878305, - 0.06220187, - 0.04462067, - 0.03522208, - 0.03241491, - 0.03301050, - 0.32071155, - 0.38361649, - 0.38361649, - 0.38356492, - ], - [ - 0.07808712, - 0.07712225, - 0.08553484, - 0.26638946, - 0.31507478, - 0.30136579, - 0.09098278, - 0.04509831, - 0.04509831, - 0.04568835, - ], - [ - 0.31671107, - 0.31561096, - 0.28928249, - 0.14182485, - 0.05421900, - 0.05422828, - 0.02160523, - 0.02519571, - 0.02820109, - 0.02854381, - ], - ] - ), + 0.07808712, + 0.07712225, + 0.08553484, + 0.26638946, + 0.31507478, + 0.30136579, + 0.09098278, + 0.04509831, + 0.04509831, + 0.04568835, + ], + [ + 0.31671107, + 0.31561096, + 0.28928249, + 0.14182485, + 0.05421900, + 0.05422828, + 0.02160523, + 0.02519571, + 0.02820109, + 0.02854381, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_XYZ_to_msds(self) -> None: + def test_domain_range_scale_XYZ_to_msds(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.XYZ_to_msds` definition domain and range scale support. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) - msds_reference = XYZ_to_msds(XYZ, method="Gaussian") + msds_reference = XYZ_to_msds(XYZ, method="Gaussian", as_array=True) d_r = (("reference", 1), ("1", 1), ("100", 100)) for scale, factor in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - XYZ_to_msds(XYZ * factor, method="Gaussian"), + xp_assert_close( + XYZ_to_msds(XYZ * factor, method="Gaussian", as_array=True), msds_reference, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/recovery/tests/test_gaussian.py b/colour/recovery/tests/test_gaussian.py index 1307e2bf70..b1a7434599 100644 --- a/colour/recovery/tests/test_gaussian.py +++ b/colour/recovery/tests/test_gaussian.py @@ -2,7 +2,10 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType from colour.colorimetry import SpectralShape, sd_to_XYZ_integration from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -16,7 +19,12 @@ optimise_gaussian_basis_parameters, ) from colour.recovery.smits1999 import RGB_to_msds_Smits1999, RGB_to_sd_Smits1999 -from colour.utilities import domain_range_scale +from colour.utilities import ( + as_ndarray, + domain_range_scale, + xp_as_array, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -92,25 +100,26 @@ class TestRGB_to_msds_Gaussian: definition unit tests methods. """ - def test_RGB_to_msds_Gaussian(self) -> None: + def test_RGB_to_msds_Gaussian(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.gaussian.RGB_to_msds_Gaussian` definition. """ - RGB = np.array( + RGB = xp_as_array( [ [0.45623196, 0.03080455, 0.04093343], [0.05438271, 0.29877169, 0.07188444], [0.01863137, 0.05139773, 0.28887675], - ] + ], + xp=xp, ) - msds = RGB_to_msds_Gaussian(RGB) + msds = RGB_to_msds_Gaussian(RGB, as_array=True) assert msds.shape == (3, 421) - np.testing.assert_allclose( + xp_assert_close( msds[0, 0], 0.04093343, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -118,151 +127,149 @@ def test_RGB_to_msds_Gaussian(self) -> None: # Test with interpolated basis at 10nm for full array comparison basis_10nm = MSDS_GAUSSIAN_BASIS.copy().align(SpectralShape(360, 780, 10)) - msds_10nm = RGB_to_msds_Smits1999(RGB, basis_10nm) + msds_10nm = RGB_to_msds_Smits1999(RGB, basis_10nm, as_array=True) assert msds_10nm.shape == (3, 43) - np.testing.assert_allclose( + xp_assert_close( msds_10nm, - np.array( + [ + [ + 0.04093337, + 0.04093320, + 0.04093260, + 0.04093072, + 0.04092523, + 0.04091063, + 0.04087508, + 0.04079585, + 0.04063436, + 0.04033363, + 0.03982269, + 0.03903235, + 0.03792300, + 0.03651767, + 0.03492546, + 0.03333915, + 0.03200184, + 0.03117438, + 0.03124197, + 0.03337885, + 0.04158892, + 0.06509127, + 0.11770222, + 0.20781604, + 0.32145436, + 0.41792699, + 0.45505576, + 0.45559176, + 0.45591017, + 0.45608272, + 0.45616815, + 0.45620682, + 0.45622284, + 0.45622892, + 0.45623103, + 0.45623170, + 0.45623189, + 0.45623194, + 0.45623196, + 0.45623196, + 0.45623196, + 0.45623196, + 0.45623196, + ], [ - [ - 0.04093337, - 0.04093320, - 0.04093260, - 0.04093072, - 0.04092523, - 0.04091063, - 0.04087508, - 0.04079585, - 0.04063436, - 0.04033363, - 0.03982269, - 0.03903235, - 0.03792300, - 0.03651767, - 0.03492546, - 0.03333915, - 0.03200184, - 0.03117438, - 0.03124197, - 0.03337885, - 0.04158892, - 0.06509127, - 0.11770222, - 0.20781604, - 0.32145436, - 0.41792699, - 0.45505576, - 0.45559176, - 0.45591017, - 0.45608272, - 0.45616815, - 0.45620682, - 0.45622284, - 0.45622892, - 0.45623103, - 0.45623170, - 0.45623189, - 0.45623194, - 0.45623196, - 0.45623196, - 0.45623196, - 0.45623196, - 0.45623196, - ], - [ - 0.07188456, - 0.07188506, - 0.07188742, - 0.07189721, - 0.07193369, - 0.07205532, - 0.07241839, - 0.07338861, - 0.07570918, - 0.08067403, - 0.09016571, - 0.10635006, - 0.13088528, - 0.16377607, - 0.20239089, - 0.24136455, - 0.27383472, - 0.29375824, - 0.29866593, - 0.28952972, - 0.26550968, - 0.23037931, - 0.18714401, - 0.14347913, - 0.10818026, - 0.08407715, - 0.06945102, - 0.06137903, - 0.05733465, - 0.05550949, - 0.05477091, - 0.05450325, - 0.05441641, - 0.05439118, - 0.05438462, - 0.05438310, - 0.05438278, - 0.05438272, - 0.05438271, - 0.05438271, - 0.05438271, - 0.05438271, - 0.05438271, - ], - [ - 0.28887675, - 0.28887675, - 0.28887675, - 0.28887675, - 0.28887675, - 0.28887675, - 0.28887675, - 0.28887675, - 0.28887675, - 0.28514242, - 0.26366776, - 0.22802585, - 0.18606289, - 0.14547288, - 0.11161469, - 0.08671499, - 0.07037699, - 0.06074309, - 0.05561408, - 0.05314075, - 0.05205795, - 0.05149186, - 0.04442883, - 0.03265094, - 0.02386852, - 0.01997155, - 0.01886579, - 0.01865936, - 0.01863365, - 0.01863150, - 0.01863138, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - 0.01863137, - ], - ] - ), + 0.07188456, + 0.07188506, + 0.07188742, + 0.07189721, + 0.07193369, + 0.07205532, + 0.07241839, + 0.07338861, + 0.07570918, + 0.08067403, + 0.09016571, + 0.10635006, + 0.13088528, + 0.16377607, + 0.20239089, + 0.24136455, + 0.27383472, + 0.29375824, + 0.29866593, + 0.28952972, + 0.26550968, + 0.23037931, + 0.18714401, + 0.14347913, + 0.10818026, + 0.08407715, + 0.06945102, + 0.06137903, + 0.05733465, + 0.05550949, + 0.05477091, + 0.05450325, + 0.05441641, + 0.05439118, + 0.05438462, + 0.05438310, + 0.05438278, + 0.05438272, + 0.05438271, + 0.05438271, + 0.05438271, + 0.05438271, + 0.05438271, + ], + [ + 0.28887675, + 0.28887675, + 0.28887675, + 0.28887675, + 0.28887675, + 0.28887675, + 0.28887675, + 0.28887675, + 0.28887675, + 0.28514242, + 0.26366776, + 0.22802585, + 0.18606289, + 0.14547288, + 0.11161469, + 0.08671499, + 0.07037699, + 0.06074309, + 0.05561408, + 0.05314075, + 0.05205795, + 0.05149186, + 0.04442883, + 0.03265094, + 0.02386852, + 0.01997155, + 0.01886579, + 0.01865936, + 0.01863365, + 0.01863150, + 0.01863138, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + 0.01863137, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -273,14 +280,14 @@ class TestRGB_to_sd_Gaussian: definition unit tests methods. """ - def test_RGB_to_sd_Gaussian(self) -> None: + def test_RGB_to_sd_Gaussian(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.gaussian.RGB_to_sd_Gaussian` definition. """ - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - RGB = XYZ_to_RGB_Gaussian(XYZ) + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + RGB = as_ndarray(XYZ_to_RGB_Gaussian(XYZ)) sd = RGB_to_sd_Gaussian(RGB) @@ -292,73 +299,73 @@ def test_RGB_to_sd_Gaussian(self) -> None: assert sd_10nm.values.shape == (43,) - np.testing.assert_allclose( + xp_assert_close( sd_10nm.values, - np.array( - [ - 0.04502009, - 0.04501988, - 0.04501914, - 0.04501680, - 0.04500998, - 0.04499185, - 0.04494770, - 0.04484930, - 0.04464874, - 0.04427526, - 0.04364072, - 0.04265916, - 0.04128144, - 0.03953613, - 0.03755869, - 0.03558836, - 0.03392457, - 0.03287194, - 0.03279225, - 0.03461775, - 0.04157706, - 0.06103056, - 0.10398511, - 0.17707942, - 0.26899683, - 0.34699756, - 0.37718472, - 0.37785039, - 0.37824584, - 0.37846014, - 0.37856623, - 0.37861426, - 0.37863415, - 0.37864170, - 0.37864431, - 0.37864515, - 0.37864539, - 0.37864545, - 0.37864547, - 0.37864547, - 0.37864547, - 0.37864547, - 0.37864547, - ] - ), + [ + 0.04502009, + 0.04501988, + 0.04501914, + 0.04501680, + 0.04500998, + 0.04499185, + 0.04494770, + 0.04484930, + 0.04464874, + 0.04427526, + 0.04364072, + 0.04265916, + 0.04128144, + 0.03953613, + 0.03755869, + 0.03558836, + 0.03392457, + 0.03287194, + 0.03279225, + 0.03461775, + 0.04157706, + 0.06103056, + 0.10398511, + 0.17707942, + 0.26899683, + 0.34699756, + 0.37718472, + 0.37785039, + 0.37824584, + 0.37846014, + 0.37856623, + 0.37861426, + 0.37863415, + 0.37864170, + 0.37864431, + 0.37864515, + 0.37864539, + 0.37864545, + 0.37864547, + 0.37864547, + 0.37864547, + 0.37864547, + 0.37864547, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_RGB_to_sd_Gaussian(self) -> None: + def test_domain_range_scale_RGB_to_sd_Gaussian(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.gaussian.RGB_to_sd_Gaussian` definition domain and range scale support. """ - XYZ_i = np.array([0.20654008, 0.12197225, 0.05136952]) - RGB_i = XYZ_to_RGB_Gaussian(XYZ_i) + XYZ_i = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + RGB_i = xp_as_array(XYZ_to_RGB_Gaussian(XYZ_i), xp=xp) XYZ_o = sd_to_XYZ_integration(RGB_to_sd_Gaussian(RGB_i)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sd_to_XYZ_integration(RGB_to_sd_Gaussian(RGB_i * factor_a)), + xp_assert_close( + sd_to_XYZ_integration( + RGB_to_sd_Gaussian(RGB_i * xp_as_array(factor_a, xp=xp)) + ), XYZ_o * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/recovery/tests/test_jakob2019.py b/colour/recovery/tests/test_jakob2019.py index b89535ea07..9784ab47b6 100644 --- a/colour/recovery/tests/test_jakob2019.py +++ b/colour/recovery/tests/test_jakob2019.py @@ -16,7 +16,7 @@ from colour.difference import JND_CIE1976, delta_E_CIE1976 if typing.TYPE_CHECKING: - from colour.hints import Type + from colour.hints import Type, ModuleType from colour.models import RGB_COLOURSPACE_sRGB, RGB_to_XYZ, XYZ_to_Lab, XYZ_to_xy from colour.recovery.jakob2019 import ( @@ -27,7 +27,16 @@ error_function, sd_Jakob2019, ) -from colour.utilities import domain_range_scale, full, is_scipy_installed, ones, zeros +from colour.utilities import ( + as_ndarray, + domain_range_scale, + full, + is_scipy_installed, + ones, + xp_as_array, + xp_assert_close, + zeros, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -97,11 +106,11 @@ def test_intermediates(self) -> None: ) sd_XYZ = sd_to_XYZ(sd, self._cmfs, self._sd_D65) / 100 - sd_Lab = XYZ_to_Lab(XYZ, self._xy_D65) + sd_Lab = as_ndarray(XYZ_to_Lab(XYZ, self._xy_D65)) error_reference = delta_E_CIE1976(self._Lab_e, Lab) - np.testing.assert_allclose(sd.values, R, atol=TOLERANCE_ABSOLUTE_TESTS) - np.testing.assert_allclose(XYZ, sd_XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(sd.values, R, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(XYZ, sd_XYZ, atol=TOLERANCE_ABSOLUTE_TESTS) assert abs(error_reference - error) < JND_CIE1976 / 100 assert delta_E_CIE1976(Lab, sd_Lab) < JND_CIE1976 / 100 @@ -136,8 +145,10 @@ def test_derivatives(self) -> None: # The approximated derivatives aren't too accurate, so tolerances # have to be rather loose. - np.testing.assert_allclose( - staggered_derrors, approximate_derrors, atol=5e-3 + xp_assert_close( + staggered_derrors, + approximate_derrors, + atol=TOLERANCE_ABSOLUTE_TESTS * 50000, ) @@ -170,7 +181,8 @@ def test_XYZ_to_sd_Jakob2019(self) -> None: if error > JND_CIE1976 / 100: # pragma: no cover pytest.fail(f"Delta E for '{name}' is {error}!") - def test_domain_range_scale_XYZ_to_sd_Jakob2019(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_domain_range_scale_XYZ_to_sd_Jakob2019(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.jakob2019.XYZ_to_sd_Jakob2019` definition domain and range scale support. @@ -179,7 +191,7 @@ def test_domain_range_scale_XYZ_to_sd_Jakob2019(self) -> None: if not is_scipy_installed(): # pragma: no cover return - XYZ_i = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ_i = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) XYZ_o = sd_to_XYZ( XYZ_to_sd_Jakob2019(XYZ_i, self._cmfs, self._sd_D65, additional_data=False), self._cmfs, @@ -189,10 +201,10 @@ def test_domain_range_scale_XYZ_to_sd_Jakob2019(self) -> None: d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ( XYZ_to_sd_Jakob2019( - XYZ_i * factor_a, + XYZ_i * xp_as_array(factor_a, xp=xp), self._cmfs, self._sd_D65, additional_data=False, @@ -278,9 +290,9 @@ def test_lightness_scale(self) -> None: property. """ - np.testing.assert_allclose( + xp_assert_close( TestLUT3D_Jakob2019.generate_LUT().lightness_scale, - np.array([0.00000000, 0.06561279, 0.50000000, 0.93438721, 1.00000000]), + [0.00000000, 0.06561279, 0.50000000, 0.93438721, 1.00000000], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -324,15 +336,15 @@ def test_LUT3D_Jakob2019(self) -> None: LUT.write(path) LUT_t = LUT3D_Jakob2019().read(path) - np.testing.assert_allclose( + xp_assert_close( LUT.lightness_scale, LUT_t.lightness_scale, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( LUT.coefficients, LUT_t.coefficients, - atol=1e-6, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) finally: shutil.rmtree(temporary_directory) @@ -345,12 +357,12 @@ def test_LUT3D_Jakob2019(self) -> None: full(3, 0.5), ones(3), ]: - XYZ = RGB_to_XYZ(RGB, self._RGB_colourspace, self._xy_D65) - Lab = XYZ_to_Lab(XYZ, self._xy_D65) + XYZ = as_ndarray(RGB_to_XYZ(RGB, self._RGB_colourspace, self._xy_D65)) + Lab = as_ndarray(XYZ_to_Lab(XYZ, self._xy_D65)) recovered_sd = LUT.RGB_to_sd(RGB) recovered_XYZ = sd_to_XYZ(recovered_sd, self._cmfs, self._sd_D65) / 100 - recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65) + recovered_Lab = as_ndarray(XYZ_to_Lab(recovered_XYZ, self._xy_D65)) error = delta_E_CIE1976(Lab, recovered_Lab) @@ -368,7 +380,37 @@ def test_raise_exception_RGB_to_coefficients(self) -> None: LUT = LUT3D_Jakob2019() - pytest.raises(RuntimeError, LUT.RGB_to_coefficients, np.array([1, 2, 3, 4])) + with pytest.raises(RuntimeError): + LUT.RGB_to_coefficients(np.array([1, 2, 3, 4])) + + def test_RGB_to_coefficients_out_of_range(self) -> None: + """ + Test :func:`colour.recovery.jakob2019.LUT3D_Jakob2019.\ +RGB_to_coefficients` *NaN* fill for out-of-range *RGB* queries (negative + and wide-gamut values), matching + :class:`scipy.interpolate.RegularGridInterpolator` with + ``bounds_error=False`` and ``fill_value=np.nan``. + """ + + if not is_scipy_installed(): # pragma: no cover + return + + LUT = TestLUT3D_Jakob2019.generate_LUT() + + out_of_range_RGB = np.array( + [ + [-0.1, 0.4, 0.5], # negative dominant-channel candidate + [1.5, 0.2, 0.3], # wide-gamut > 1 lightness + [0.4, -0.2, 0.6], # negative non-dominant chroma + ] + ) + coefficients = LUT.RGB_to_coefficients(out_of_range_RGB) + + assert np.all(np.isnan(coefficients)) + + in_range_RGB = np.array([0.45623196, 0.03080455, 0.04093343]) + + assert not np.any(np.isnan(LUT.RGB_to_coefficients(in_range_RGB))) def test_raise_exception_read(self) -> None: """ @@ -377,4 +419,5 @@ def test_raise_exception_read(self) -> None: """ LUT = LUT3D_Jakob2019() - pytest.raises(ValueError, LUT.read, __file__) + with pytest.raises(ValueError): + LUT.read(__file__) diff --git a/colour/recovery/tests/test_jiang2013.py b/colour/recovery/tests/test_jiang2013.py index fd73f49aeb..47ab904711 100644 --- a/colour/recovery/tests/test_jiang2013.py +++ b/colour/recovery/tests/test_jiang2013.py @@ -27,7 +27,7 @@ RGB_to_msds_camera_sensitivities_Jiang2013, RGB_to_sd_camera_sensitivity_Jiang2013, ) -from colour.utilities import tsplit +from colour.utilities import tsplit, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -68,139 +68,135 @@ def test_PCA_Jiang2013(self) -> None: additional_data=True, ) - np.testing.assert_allclose( + xp_assert_close( np.abs(np.array(w)), - np.array( + [ [ - [ - [0.00137594, 0.00399416, 0.00074515], - [0.00214835, 0.00184422, 0.85753040], - [0.02757181, 0.00553587, 0.02033235], - [0.02510621, 0.04216468, 0.01860012], - [0.02011623, 0.03371162, 0.01474896], - [0.01392282, 0.03297985, 0.00645105], - [0.00944513, 0.03300938, 0.00649418], - [0.02019958, 0.01289400, 0.01138365], - [0.02394423, 0.00980934, 0.00348705], - [0.04196326, 0.04987050, 0.00462977], - [0.04988732, 0.06936603, 0.01299320], - [0.06527141, 0.09378614, 0.00320186], - [0.09412575, 0.12244081, 0.02936062], - [0.10915913, 0.13119983, 0.02403866], - [0.12314840, 0.24280936, 0.03433110], - [0.11673941, 0.27700737, 0.11678028], - [0.12534133, 0.29994127, 0.11683063], - [0.14599255, 0.25586532, 0.04332511], - [0.25249090, 0.11499750, 0.04112949], - [0.35163407, 0.45286818, 0.04259130], - [0.35805737, 0.40724252, 0.24276366], - [0.36927899, 0.18120838, 0.28042101], - [0.35374885, 0.03010008, 0.05772918], - [0.35340909, 0.16847527, 0.23388908], - [0.32696116, 0.29068981, 0.09224334], - [0.29067354, 0.32862702, 0.14708450], - [0.08964758, 0.09682656, 0.04541265], - [0.01891664, 0.08221113, 0.01157780], - [0.00521149, 0.01578907, 0.00133064], - [0.00232366, 0.00137751, 0.00139661], - [0.00153787, 0.00254398, 0.00042512], - ], - [ - [0.00119598, 0.00267792, 0.00026101], - [0.00200327, 0.00322983, 0.62788798], - [0.01247816, 0.03313976, 0.01072884], - [0.03207685, 0.05703294, 0.03730884], - [0.04715050, 0.05296451, 0.02043038], - [0.05794010, 0.05455737, 0.01279190], - [0.10745571, 0.00158911, 0.03200481], - [0.14178525, 0.03362764, 0.15020663], - [0.16811402, 0.05569833, 0.00844788], - [0.18463716, 0.04615404, 0.03103250], - [0.21531623, 0.09745078, 0.39693604], - [0.25442570, 0.18330481, 0.14940077], - [0.28168018, 0.25193267, 0.00347081], - [0.29237178, 0.28545428, 0.03254141], - [0.29693117, 0.23909467, 0.00779634], - [0.28631319, 0.19476441, 0.31698680], - [0.27195968, 0.12087420, 0.28305850], - [0.25988140, 0.01581316, 0.04130875], - [0.24222660, 0.07912972, 0.18481425], - [0.23069698, 0.18583667, 0.18113417], - [0.20831983, 0.26745561, 0.13793240], - [0.19437168, 0.32425009, 0.20908259], - [0.18470894, 0.34768079, 0.15013614], - [0.18056180, 0.35983221, 0.24060984], - [0.17141337, 0.35067306, 0.04478566], - [0.14712541, 0.30423172, 0.05583266], - [0.02897026, 0.04573993, 0.02137366], - [0.00190228, 0.00461591, 0.00240276], - [0.00069122, 0.00118817, 0.00011696], - [0.00045559, 0.00015286, 0.00075939], - [0.00039509, 0.00049719, 0.00104581], - ], - [ - [0.03283371, 0.04707162, 0.99591944], - [0.05932690, 0.07529740, 0.06152683], - [0.11947381, 0.07977219, 0.00156116], - [0.18492233, 0.26127374, 0.00717981], - [0.22091564, 0.29279976, 0.00487132], - [0.25377875, 0.30677709, 0.00614140], - [0.29969822, 0.26541777, 0.00429149], - [0.30232755, 0.25378622, 0.00354243], - [0.30031732, 0.19751184, 0.00199307], - [0.28072276, 0.11804285, 0.00591452], - [0.26005747, 0.01836333, 0.00698676], - [0.23839367, 0.07182421, 0.01904751], - [0.21721831, 0.14245410, 0.00452400], - [0.19828405, 0.17684950, 0.01371456], - [0.19018451, 0.20137781, 0.01184653], - [0.18196762, 0.22086321, 0.01434790], - [0.17168644, 0.22771873, 0.02205056], - [0.16977073, 0.23504018, 0.01730589], - [0.16277670, 0.22897797, 0.02229014], - [0.15880423, 0.22583675, 0.02123217], - [0.14966812, 0.21494312, 0.01417066], - [0.13480155, 0.19511162, 0.01901915], - [0.12541764, 0.18113238, 0.01413883], - [0.12355731, 0.17835150, 0.01809536], - [0.11175064, 0.15997651, 0.01436804], - [0.09440304, 0.13423453, 0.01316519], - [0.01670581, 0.02019670, 0.00202853], - [0.00045002, 0.00147362, 0.00007713], - [0.00102919, 0.00095904, 0.00008866], - [0.00097397, 0.00123434, 0.00011166], - [0.00097116, 0.00124835, 0.00014463], - ], - ] - ), + [0.00137594, 0.00399416, 0.00069230], + [0.00214835, 0.00184422, 0.77773975], + [0.02757181, 0.00553587, 0.01405320], + [0.02510621, 0.04216468, 0.01296328], + [0.02011623, 0.03371162, 0.00006511], + [0.01392282, 0.03297985, 0.00076254], + [0.00944513, 0.03300938, 0.00272538], + [0.02019958, 0.01289400, 0.00733623], + [0.02394423, 0.00980934, 0.00116771], + [0.04196326, 0.04987050, 0.01214103], + [0.04988732, 0.06936603, 0.06276533], + [0.06527141, 0.09378614, 0.00531553], + [0.09412575, 0.12244081, 0.04024341], + [0.10915913, 0.13119983, 0.01812631], + [0.12314840, 0.24280936, 0.06691737], + [0.11673941, 0.27700737, 0.13336210], + [0.12534133, 0.29994127, 0.09242404], + [0.14599255, 0.25586532, 0.02807276], + [0.25249090, 0.11499750, 0.22445012], + [0.35163407, 0.45286818, 0.08876308], + [0.35805737, 0.40724252, 0.25003734], + [0.36927899, 0.18120838, 0.32590277], + [0.35374885, 0.03010008, 0.02424606], + [0.35340909, 0.16847527, 0.27657455], + [0.32696116, 0.29068981, 0.17751100], + [0.29067354, 0.32862702, 0.14423389], + [0.08964758, 0.09682656, 0.02477972], + [0.01891664, 0.08221113, 0.00586036], + [0.00521149, 0.01578907, 0.00094325], + [0.00232366, 0.00137751, 0.00072684], + [0.00153787, 0.00254398, 0.00141550], + ], + [ + [0.00119598, 0.00267792, 0.00023837], + [0.00200327, 0.00322983, 0.65812760], + [0.01247816, 0.03313976, 0.00904730], + [0.03207685, 0.05703294, 0.03496281], + [0.04715050, 0.05296451, 0.00746348], + [0.05794010, 0.05455737, 0.00797326], + [0.10745571, 0.00158911, 0.03618865], + [0.14178525, 0.03362764, 0.13230117], + [0.16811402, 0.05569833, 0.00126897], + [0.18463716, 0.04615404, 0.06782785], + [0.21531623, 0.09745078, 0.35536881], + [0.25442570, 0.18330481, 0.35550206], + [0.28168018, 0.25193267, 0.01940794], + [0.29237178, 0.28545428, 0.03329913], + [0.29693117, 0.23909467, 0.08279249], + [0.28631319, 0.19476441, 0.31831349], + [0.27195968, 0.12087420, 0.22952523], + [0.25988140, 0.01581316, 0.03590514], + [0.24222660, 0.07912972, 0.06974551], + [0.23069698, 0.18583667, 0.17749567], + [0.20831983, 0.26745561, 0.13252356], + [0.19437168, 0.32425009, 0.04031449], + [0.18470894, 0.34768079, 0.15702008], + [0.18056180, 0.35983221, 0.20231477], + [0.17141337, 0.35067306, 0.06430943], + [0.14712541, 0.30423172, 0.01311752], + [0.02897026, 0.04573993, 0.01639128], + [0.00190228, 0.00461591, 0.00181558], + [0.00069122, 0.00118817, 0.00067073], + [0.00045559, 0.00015286, 0.00094386], + [0.00039509, 0.00049719, 0.00108501], + ], + [ + [0.03283371, 0.04707162, 0.99686696], + [0.05932690, 0.07529740, 0.04777270], + [0.11947381, 0.07977219, 0.00288653], + [0.18492233, 0.26127374, 0.00866928], + [0.22091564, 0.29279976, 0.00603777], + [0.25377875, 0.30677709, 0.00676444], + [0.29969822, 0.26541777, 0.00148929], + [0.30232755, 0.25378622, 0.00171034], + [0.30031732, 0.19751184, 0.00110617], + [0.28072276, 0.11804285, 0.00208954], + [0.26005747, 0.01836333, 0.00966177], + [0.23839367, 0.07182421, 0.01710700], + [0.21721831, 0.14245410, 0.01392494], + [0.19828405, 0.17684950, 0.01418532], + [0.19018451, 0.20137781, 0.01321413], + [0.18196762, 0.22086321, 0.01368810], + [0.17168644, 0.22771873, 0.02026592], + [0.16977073, 0.23504018, 0.01653275], + [0.16277670, 0.22897797, 0.01885420], + [0.15880423, 0.22583675, 0.01867897], + [0.14966812, 0.21494312, 0.01549527], + [0.13480155, 0.19511162, 0.01726259], + [0.12541764, 0.18113238, 0.01356430], + [0.12355731, 0.17835150, 0.01510287], + [0.11175064, 0.15997651, 0.01354602], + [0.09440304, 0.13423453, 0.01141010], + [0.01670581, 0.02019670, 0.00146819], + [0.00045002, 0.00147362, 0.00006780], + [0.00102919, 0.00095904, 0.00010087], + [0.00097397, 0.00123434, 0.00010268], + [0.00097116, 0.00124835, 0.00011990], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( np.array(v), - np.array( - [ - [10.55160659, 0.72837380, 0.00000000], - [20.09177982, 1.57662524, 0.00000000], - [19.04142816, 2.60426480, 0.00000000], - ] - ), + [ + [10.55160659, 0.72837380, 0.00000000], + [20.09177982, 1.57662524, 0.00000000], + [19.04142816, 2.60426480, 0.00000000], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test with additional_data=False (default) R_w, G_w, B_w = PCA_Jiang2013(camera_sensitivities, 3) # type: ignore[misc] - np.testing.assert_allclose( + xp_assert_close( np.abs(R_w), np.abs(w[0]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( np.abs(G_w), np.abs(w[1]), atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( np.abs(B_w), np.abs(w[2]), atol=TOLERANCE_ABSOLUTE_TESTS, @@ -255,7 +251,7 @@ def test_RGB_to_sd_camera_sensitivity_Jiang2013(self) -> None: R_w, _G_w, _B_w = tsplit(np.moveaxis(BASIS_FUNCTIONS_DYER2017, 0, 1)) - np.testing.assert_allclose( + xp_assert_close( RGB_to_sd_camera_sensitivity_Jiang2013( self._RGB[..., 0], self._sd_D65, @@ -263,41 +259,39 @@ def test_RGB_to_sd_camera_sensitivity_Jiang2013(self) -> None: R_w, SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017, ).values, - np.array( - [ - 0.00072067, - -0.00089699, - 0.0046872, - 0.0077695, - 0.00693355, - 0.00531349, - 0.004482, - 0.00463938, - 0.00518667, - 0.00438283, - 0.00420012, - 0.00540655, - 0.00964451, - 0.01427711, - 0.00799507, - 0.00464298, - 0.00534238, - 0.01051938, - 0.05288944, - 0.09785117, - 0.09960038, - 0.08384089, - 0.06918086, - 0.05696785, - 0.04293031, - 0.03024127, - 0.02323005, - 0.01372194, - 0.00409449, - -0.00044223, - -0.00061428, - ] - ), + [ + 0.00072067, + -0.00089699, + 0.0046872, + 0.0077695, + 0.00693355, + 0.00531349, + 0.004482, + 0.00463938, + 0.00518667, + 0.00438283, + 0.00420012, + 0.00540655, + 0.00964451, + 0.01427711, + 0.00799507, + 0.00464298, + 0.00534238, + 0.01051938, + 0.05288944, + 0.09785117, + 0.09960038, + 0.08384089, + 0.06918086, + 0.05696785, + 0.04293031, + 0.03024127, + 0.02323005, + 0.01372194, + 0.00409449, + -0.00044223, + -0.00061428, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -317,43 +311,41 @@ def test_RGB_to_sd_camera_sensitivity_Jiang2013(self) -> None: assert "Aligning" in str(w[0].message) # Result should still be valid - np.testing.assert_allclose( + xp_assert_close( sd.values, - np.array( - [ - 0.00072067, - -0.00089699, - 0.0046872, - 0.0077695, - 0.00693355, - 0.00531349, - 0.004482, - 0.00463938, - 0.00518667, - 0.00438283, - 0.00420012, - 0.00540655, - 0.00964451, - 0.01427711, - 0.00799507, - 0.00464298, - 0.00534238, - 0.01051938, - 0.05288944, - 0.09785117, - 0.09960038, - 0.08384089, - 0.06918086, - 0.05696785, - 0.04293031, - 0.03024127, - 0.02323005, - 0.01372194, - 0.00409449, - -0.00044223, - -0.00061428, - ] - ), + [ + 0.00072067, + -0.00089699, + 0.0046872, + 0.0077695, + 0.00693355, + 0.00531349, + 0.004482, + 0.00463938, + 0.00518667, + 0.00438283, + 0.00420012, + 0.00540655, + 0.00964451, + 0.01427711, + 0.00799507, + 0.00464298, + 0.00534238, + 0.01051938, + 0.05288944, + 0.09785117, + 0.09960038, + 0.08384089, + 0.06918086, + 0.05696785, + 0.04293031, + 0.03024127, + 0.02323005, + 0.01372194, + 0.00409449, + -0.00044223, + -0.00061428, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -375,7 +367,7 @@ def test_RGB_to_msds_camera_sensitivities_Jiang2013(self) -> None: RGB_to_msds_camera_sensitivities_Jiang2013` definition. """ - np.testing.assert_allclose( + xp_assert_close( RGB_to_msds_camera_sensitivities_Jiang2013( self._RGB, self._sd_D65, @@ -383,41 +375,39 @@ def test_RGB_to_msds_camera_sensitivities_Jiang2013(self) -> None: BASIS_FUNCTIONS_DYER2017, SPECTRAL_SHAPE_BASIS_FUNCTIONS_DYER2017, ).values, - np.array( - [ - [7.04378461e-03, 9.21260449e-03, -7.64080878e-03], - [-8.76715607e-03, 1.12726694e-02, 6.37434190e-03], - [4.58126856e-02, 7.18000418e-02, 4.00001696e-01], - [7.59391152e-02, 1.15620933e-01, 7.11521550e-01], - [6.77685732e-02, 1.53406449e-01, 8.52668310e-01], - [5.19341313e-02, 1.88575472e-01, 9.38957846e-01], - [4.38070562e-02, 2.61086603e-01, 9.72130729e-01], - [4.53453213e-02, 3.75440392e-01, 9.61450686e-01], - [5.06945146e-02, 4.47658155e-01, 8.86481146e-01], - [4.28378252e-02, 4.50713447e-01, 7.51770770e-01], - [4.10520309e-02, 6.16577286e-01, 5.52730730e-01], - [5.28436974e-02, 7.80199548e-01, 3.82269175e-01], - [9.42655432e-02, 9.17674257e-01, 2.40354614e-01], - [1.39544593e-01, 1.00000000e00, 1.55374812e-01], - [7.81438836e-02, 9.27720273e-01, 1.04409358e-01], - [4.53805297e-02, 8.56701565e-01, 6.51222854e-02], - [5.22164960e-02, 7.52322921e-01, 3.42954473e-02], - [1.02816526e-01, 6.25809730e-01, 2.09495104e-02], - [5.16941760e-01, 4.92746166e-01, 1.48524616e-02], - [9.56397935e-01, 3.43364817e-01, 1.08983186e-02], - [9.73494777e-01, 2.08587708e-01, 7.00494396e-03], - [8.19461415e-01, 1.11784838e-01, 4.47180002e-03], - [6.76174158e-01, 6.59071962e-02, 4.10135388e-03], - [5.56804177e-01, 4.46268353e-02, 4.18528982e-03], - [4.19601114e-01, 3.33671033e-02, 4.49165886e-03], - [2.95578342e-01, 2.39487762e-02, 4.45932739e-03], - [2.27050628e-01, 1.87787770e-02, 4.31697313e-03], - [1.34118359e-01, 1.06954985e-02, 3.41192651e-03], - [4.00195568e-02, 5.55512389e-03, 1.36794925e-03], - [-4.32240535e-03, 2.49731193e-03, 3.80303275e-04], - [-6.00395414e-03, 1.54678227e-03, 5.40394352e-04], - ] - ), + [ + [7.04378461e-03, 9.21260449e-03, -7.64080878e-03], + [-8.76715607e-03, 1.12726694e-02, 6.37434190e-03], + [4.58126856e-02, 7.18000418e-02, 4.00001696e-01], + [7.59391152e-02, 1.15620933e-01, 7.11521550e-01], + [6.77685732e-02, 1.53406449e-01, 8.52668310e-01], + [5.19341313e-02, 1.88575472e-01, 9.38957846e-01], + [4.38070562e-02, 2.61086603e-01, 9.72130729e-01], + [4.53453213e-02, 3.75440392e-01, 9.61450686e-01], + [5.06945146e-02, 4.47658155e-01, 8.86481146e-01], + [4.28378252e-02, 4.50713447e-01, 7.51770770e-01], + [4.10520309e-02, 6.16577286e-01, 5.52730730e-01], + [5.28436974e-02, 7.80199548e-01, 3.82269175e-01], + [9.42655432e-02, 9.17674257e-01, 2.40354614e-01], + [1.39544593e-01, 1.00000000e00, 1.55374812e-01], + [7.81438836e-02, 9.27720273e-01, 1.04409358e-01], + [4.53805297e-02, 8.56701565e-01, 6.51222854e-02], + [5.22164960e-02, 7.52322921e-01, 3.42954473e-02], + [1.02816526e-01, 6.25809730e-01, 2.09495104e-02], + [5.16941760e-01, 4.92746166e-01, 1.48524616e-02], + [9.56397935e-01, 3.43364817e-01, 1.08983186e-02], + [9.73494777e-01, 2.08587708e-01, 7.00494396e-03], + [8.19461415e-01, 1.11784838e-01, 4.47180002e-03], + [6.76174158e-01, 6.59071962e-02, 4.10135388e-03], + [5.56804177e-01, 4.46268353e-02, 4.18528982e-03], + [4.19601114e-01, 3.33671033e-02, 4.49165886e-03], + [2.95578342e-01, 2.39487762e-02, 4.45932739e-03], + [2.27050628e-01, 1.87787770e-02, 4.31697313e-03], + [1.34118359e-01, 1.06954985e-02, 3.41192651e-03], + [4.00195568e-02, 5.55512389e-03, 1.36794925e-03], + [-4.32240535e-03, 2.49731193e-03, 3.80303275e-04], + [-6.00395414e-03, 1.54678227e-03, 5.40394352e-04], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -437,42 +427,40 @@ def test_RGB_to_msds_camera_sensitivities_Jiang2013(self) -> None: assert "Aligning" in str(w[0].message) # Result should still be valid - np.testing.assert_allclose( + xp_assert_close( msds.values, - np.array( - [ - [7.04378461e-03, 9.21260449e-03, -7.64080878e-03], - [-8.76715607e-03, 1.12726694e-02, 6.37434190e-03], - [4.58126856e-02, 7.18000418e-02, 4.00001696e-01], - [7.59391152e-02, 1.15620933e-01, 7.11521550e-01], - [6.77685732e-02, 1.53406449e-01, 8.52668310e-01], - [5.19341313e-02, 1.88575472e-01, 9.38957846e-01], - [4.38070562e-02, 2.61086603e-01, 9.72130729e-01], - [4.53453213e-02, 3.75440392e-01, 9.61450686e-01], - [5.06945146e-02, 4.47658155e-01, 8.86481146e-01], - [4.28378252e-02, 4.50713447e-01, 7.51770770e-01], - [4.10520309e-02, 6.16577286e-01, 5.52730730e-01], - [5.28436974e-02, 7.80199548e-01, 3.82269175e-01], - [9.42655432e-02, 9.17674257e-01, 2.40354614e-01], - [1.39544593e-01, 1.00000000e00, 1.55374812e-01], - [7.81438836e-02, 9.27720273e-01, 1.04409358e-01], - [4.53805297e-02, 8.56701565e-01, 6.51222854e-02], - [5.22164960e-02, 7.52322921e-01, 3.42954473e-02], - [1.02816526e-01, 6.25809730e-01, 2.09495104e-02], - [5.16941760e-01, 4.92746166e-01, 1.48524616e-02], - [9.56397935e-01, 3.43364817e-01, 1.08983186e-02], - [9.73494777e-01, 2.08587708e-01, 7.00494396e-03], - [8.19461415e-01, 1.11784838e-01, 4.47180002e-03], - [6.76174158e-01, 6.59071962e-02, 4.10135388e-03], - [5.56804177e-01, 4.46268353e-02, 4.18528982e-03], - [4.19601114e-01, 3.33671033e-02, 4.49165886e-03], - [2.95578342e-01, 2.39487762e-02, 4.45932739e-03], - [2.27050628e-01, 1.87787770e-02, 4.31697313e-03], - [1.34118359e-01, 1.06954985e-02, 3.41192651e-03], - [4.00195568e-02, 5.55512389e-03, 1.36794925e-03], - [-4.32240535e-03, 2.49731193e-03, 3.80303275e-04], - [-6.00395414e-03, 1.54678227e-03, 5.40394352e-04], - ] - ), + [ + [7.04378461e-03, 9.21260449e-03, -7.64080878e-03], + [-8.76715607e-03, 1.12726694e-02, 6.37434190e-03], + [4.58126856e-02, 7.18000418e-02, 4.00001696e-01], + [7.59391152e-02, 1.15620933e-01, 7.11521550e-01], + [6.77685732e-02, 1.53406449e-01, 8.52668310e-01], + [5.19341313e-02, 1.88575472e-01, 9.38957846e-01], + [4.38070562e-02, 2.61086603e-01, 9.72130729e-01], + [4.53453213e-02, 3.75440392e-01, 9.61450686e-01], + [5.06945146e-02, 4.47658155e-01, 8.86481146e-01], + [4.28378252e-02, 4.50713447e-01, 7.51770770e-01], + [4.10520309e-02, 6.16577286e-01, 5.52730730e-01], + [5.28436974e-02, 7.80199548e-01, 3.82269175e-01], + [9.42655432e-02, 9.17674257e-01, 2.40354614e-01], + [1.39544593e-01, 1.00000000e00, 1.55374812e-01], + [7.81438836e-02, 9.27720273e-01, 1.04409358e-01], + [4.53805297e-02, 8.56701565e-01, 6.51222854e-02], + [5.22164960e-02, 7.52322921e-01, 3.42954473e-02], + [1.02816526e-01, 6.25809730e-01, 2.09495104e-02], + [5.16941760e-01, 4.92746166e-01, 1.48524616e-02], + [9.56397935e-01, 3.43364817e-01, 1.08983186e-02], + [9.73494777e-01, 2.08587708e-01, 7.00494396e-03], + [8.19461415e-01, 1.11784838e-01, 4.47180002e-03], + [6.76174158e-01, 6.59071962e-02, 4.10135388e-03], + [5.56804177e-01, 4.46268353e-02, 4.18528982e-03], + [4.19601114e-01, 3.33671033e-02, 4.49165886e-03], + [2.95578342e-01, 2.39487762e-02, 4.45932739e-03], + [2.27050628e-01, 1.87787770e-02, 4.31697313e-03], + [1.34118359e-01, 1.06954985e-02, 3.41192651e-03], + [4.00195568e-02, 5.55512389e-03, 1.36794925e-03], + [-4.32240535e-03, 2.49731193e-03, 3.80303275e-04], + [-6.00395414e-03, 1.54678227e-03, 5.40394352e-04], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/recovery/tests/test_meng2015.py b/colour/recovery/tests/test_meng2015.py index 310d6f4270..36bb22c1fb 100644 --- a/colour/recovery/tests/test_meng2015.py +++ b/colour/recovery/tests/test_meng2015.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np import pytest @@ -15,7 +20,12 @@ ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.recovery import XYZ_to_sd_Meng2015 -from colour.utilities import domain_range_scale, is_scipy_installed +from colour.utilities import ( + domain_range_scale, + is_scipy_installed, + xp_as_array, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -45,14 +55,15 @@ def setup_method(self) -> None: self._sd_D65 = reshape_sd(SDS_ILLUMINANTS["D65"], self._cmfs.shape) self._sd_E = reshape_sd(SDS_ILLUMINANTS["E"], self._cmfs.shape) - def test_XYZ_to_sd_Meng2015(self) -> None: + @pytest.mark.mps_xfail("MPS float32 singular matrix") + def test_XYZ_to_sd_Meng2015(self, xp: ModuleType) -> None: """Test :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition.""" if not is_scipy_installed(): # pragma: no cover return - XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) - np.testing.assert_allclose( + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + xp_assert_close( sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ, self._cmfs, self._sd_D65), self._cmfs, @@ -63,7 +74,7 @@ def test_XYZ_to_sd_Meng2015(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ, self._cmfs, self._sd_E), self._cmfs, @@ -74,7 +85,7 @@ def test_XYZ_to_sd_Meng2015(self) -> None: atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( XYZ_to_sd_Meng2015( XYZ, @@ -96,7 +107,7 @@ def test_XYZ_to_sd_Meng2015(self) -> None: shape = SpectralShape(400, 700, 5) cmfs = reshape_msds(self._cmfs, shape) - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ, cmfs, self._sd_D65), cmfs, self._sd_D65 ) @@ -114,16 +125,14 @@ def test_raise_exception_XYZ_to_sd_Meng2015(self) -> None: if not is_scipy_installed(): # pragma: no cover return - pytest.raises( - RuntimeError, - XYZ_to_sd_Meng2015, - np.array([0.0, 0.0, 1.0]), - optimisation_kwargs={ - "options": {"maxiter": 10}, - }, - ) + with pytest.raises(RuntimeError): + XYZ_to_sd_Meng2015( + np.array([0.0, 0.0, 1.0]), + optimisation_kwargs={"options": {"maxiter": 10}}, + ) - def test_domain_range_scale_XYZ_to_sd_Meng2015(self) -> None: + @pytest.mark.mps_xfail("MPS float32 singular matrix") + def test_domain_range_scale_XYZ_to_sd_Meng2015(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition domain and range scale support. @@ -132,7 +141,7 @@ def test_domain_range_scale_XYZ_to_sd_Meng2015(self) -> None: if not is_scipy_installed(): # pragma: no cover return - XYZ_i = np.array([0.20654008, 0.12197225, 0.05136952]) + XYZ_i = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) XYZ_o = sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ_i, self._cmfs, self._sd_D65), self._cmfs, @@ -142,9 +151,13 @@ def test_domain_range_scale_XYZ_to_sd_Meng2015(self) -> None: d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ_integration( - XYZ_to_sd_Meng2015(XYZ_i * factor_a, self._cmfs, self._sd_D65), + XYZ_to_sd_Meng2015( + XYZ_i * xp_as_array(factor_a, xp=xp), + self._cmfs, + self._sd_D65, + ), self._cmfs, self._sd_D65, ), diff --git a/colour/recovery/tests/test_otsu2018.py b/colour/recovery/tests/test_otsu2018.py index 9247b79adc..399c83becd 100644 --- a/colour/recovery/tests/test_otsu2018.py +++ b/colour/recovery/tests/test_otsu2018.py @@ -6,6 +6,10 @@ import platform import shutil import tempfile +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType import numpy as np import pytest @@ -33,7 +37,12 @@ Node_Otsu2018, PartitionAxis, ) -from colour.utilities import domain_range_scale, metric_mse +from colour.utilities import ( + as_ndarray, + domain_range_scale, + metric_mse, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -151,7 +160,8 @@ def test_raise_exception_select(self) -> None: raised exception. """ - pytest.raises(ValueError, Dataset_Otsu2018().select, np.array([0, 0])) + with pytest.raises(ValueError): + Dataset_Otsu2018().select(np.array([0, 0])) def test_cluster(self) -> None: """Test :meth:`colour.recovery.otsu2018.Dataset_Otsu2018.cluster` method.""" @@ -166,7 +176,8 @@ def test_raise_exception_cluster(self) -> None: raised exception. """ - pytest.raises(ValueError, Dataset_Otsu2018().cluster, np.array([0, 0])) + with pytest.raises(ValueError): + Dataset_Otsu2018().cluster(np.array([0, 0])) def test_read(self) -> None: """Test :meth:`colour.recovery.otsu2018.Dataset_Otsu2018.read` method.""" @@ -204,7 +215,8 @@ def test_raise_exception_write(self) -> None: raised exception. """ - pytest.raises(ValueError, Dataset_Otsu2018().write, "") + with pytest.raises(ValueError): + Dataset_Otsu2018().write("") class TestXYZ_to_sd_Otsu2018: @@ -227,11 +239,11 @@ def test_XYZ_to_sd_Otsu2018(self) -> None: # Tests the round-trip with values of a colour checker. for sd in SDS_COLOURCHECKERS["ColorChecker N Ohta"].values(): XYZ = sd_to_XYZ(sd, self._cmfs, self._sd_D65) / 100 - Lab = XYZ_to_Lab(XYZ, self._xy_D65) + Lab = as_ndarray(XYZ_to_Lab(XYZ, self._xy_D65)) recovered_sd = XYZ_to_sd_Otsu2018(XYZ, self._cmfs, self._sd_D65, clip=False) recovered_XYZ = sd_to_XYZ(recovered_sd, self._cmfs, self._sd_D65) / 100 - recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65) + recovered_Lab = as_ndarray(XYZ_to_Lab(recovered_XYZ, self._xy_D65)) error = metric_mse( reshape_sd(sd, SPECTRAL_SHAPE_OTSU2018).values, @@ -248,16 +260,15 @@ def test_raise_exception_XYZ_to_sd_Otsu2018(self) -> None: raised_exception. """ - pytest.raises( - ValueError, - XYZ_to_sd_Otsu2018, - np.array([0, 0, 0]), - self._cmfs, - self._sd_D65, - Dataset_Otsu2018(), - ) + with pytest.raises(ValueError): + XYZ_to_sd_Otsu2018( + np.array([0, 0, 0]), self._cmfs, self._sd_D65, Dataset_Otsu2018() + ) - def test_domain_range_scale_XYZ_to_sd_Otsu2018(self) -> None: + def test_domain_range_scale_XYZ_to_sd_Otsu2018( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.recovery.otsu2018.XYZ_to_sd_Otsu2018` definition domain and range scale support. @@ -273,7 +284,7 @@ def test_domain_range_scale_XYZ_to_sd_Otsu2018(self) -> None: d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( + xp_assert_close( sd_to_XYZ( XYZ_to_sd_Otsu2018(XYZ_i * factor_a, self._cmfs, self._sd_D65), self._cmfs, @@ -399,7 +410,7 @@ def test__len__(self) -> None: def test_origin(self) -> None: """Test :meth:`colour.recovery.otsu2018.Data_Otsu2018.origin` method.""" - np.testing.assert_allclose( + xp_assert_close( self._data.origin(4, 1), 0.255284008578559, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -411,12 +422,8 @@ def test_raise_exception_origin(self) -> None: raised exception. """ - pytest.raises( - ValueError, - Data_Otsu2018(None, self._cmfs, self._sd_D65).origin, - 4, - 1, - ) + with pytest.raises(ValueError): + Data_Otsu2018(None, self._cmfs, self._sd_D65).origin(4, 1) def test_partition(self) -> None: """Test :meth:`colour.recovery.otsu2018.Data_Otsu2018.partition` method.""" @@ -431,11 +438,8 @@ def test_raise_exception_partition(self) -> None: raised exception. """ - pytest.raises( - ValueError, - Data_Otsu2018(None, self._cmfs, self._sd_D65).partition, - PartitionAxis(4, 1), - ) + with pytest.raises(ValueError): + Data_Otsu2018(None, self._cmfs, self._sd_D65).partition(PartitionAxis(4, 1)) @pytest.mark.skipif( platform.system() in ("Windows", "Microsoft", "Linux"), @@ -450,173 +454,169 @@ def test_PCA(self) -> None: assert data.basis_functions is not None - np.testing.assert_allclose( + xp_assert_close( np.abs(data.basis_functions), - np.array( + [ [ - [ - 0.04391241, - 0.08560996, - 0.15556120, - 0.20826672, - 0.22981218, - 0.23117641, - 0.22718022, - 0.21742869, - 0.19854261, - 0.16868383, - 0.12020268, - 0.05958463, - 0.01015508, - 0.08775193, - 0.16957532, - 0.23186776, - 0.26516404, - 0.27409402, - 0.27856619, - 0.27685075, - 0.25597708, - 0.21331000, - 0.15372029, - 0.08746878, - 0.02744494, - 0.01725581, - 0.04756055, - 0.07184639, - 0.09090063, - 0.10317253, - 0.10830387, - 0.10872694, - 0.10645999, - 0.10766424, - 0.11170078, - 0.11620896, - ], - [ - 0.03137588, - 0.06204234, - 0.11364884, - 0.17579436, - 0.20914074, - 0.22152351, - 0.23120105, - 0.24039823, - 0.24730359, - 0.25195045, - 0.25237533, - 0.24672212, - 0.23538236, - 0.22094141, - 0.20389065, - 0.18356599, - 0.15952882, - 0.13567812, - 0.11401807, - 0.09178015, - 0.06539517, - 0.03173809, - 0.00658524, - 0.04710763, - 0.08379987, - 0.11074555, - 0.12606191, - 0.13630094, - 0.13988107, - 0.14193361, - 0.14671866, - 0.15164795, - 0.15772737, - 0.16328073, - 0.16588768, - 0.16947164, - ], - [ - 0.01360289, - 0.02375832, - 0.04262545, - 0.07345243, - 0.09081235, - 0.09227928, - 0.08922710, - 0.08626299, - 0.08584571, - 0.08843734, - 0.09475094, - 0.10376740, - 0.11331399, - 0.12109706, - 0.12678070, - 0.13401030, - 0.14417036, - 0.15408359, - 0.16265529, - 0.17079814, - 0.17972656, - 0.19005983, - 0.20053986, - 0.21017531, - 0.21808806, - 0.22347400, - 0.22650876, - 0.22895376, - 0.22982598, - 0.23001787, - 0.23036398, - 0.22917409, - 0.22684271, - 0.22387883, - 0.22065773, - 0.21821049, - ], - ] - ), + 0.04391241, + 0.08560996, + 0.15556120, + 0.20826672, + 0.22981218, + 0.23117641, + 0.22718022, + 0.21742869, + 0.19854261, + 0.16868383, + 0.12020268, + 0.05958463, + 0.01015508, + 0.08775193, + 0.16957532, + 0.23186776, + 0.26516404, + 0.27409402, + 0.27856619, + 0.27685075, + 0.25597708, + 0.21331000, + 0.15372029, + 0.08746878, + 0.02744494, + 0.01725581, + 0.04756055, + 0.07184639, + 0.09090063, + 0.10317253, + 0.10830387, + 0.10872694, + 0.10645999, + 0.10766424, + 0.11170078, + 0.11620896, + ], + [ + 0.03137588, + 0.06204234, + 0.11364884, + 0.17579436, + 0.20914074, + 0.22152351, + 0.23120105, + 0.24039823, + 0.24730359, + 0.25195045, + 0.25237533, + 0.24672212, + 0.23538236, + 0.22094141, + 0.20389065, + 0.18356599, + 0.15952882, + 0.13567812, + 0.11401807, + 0.09178015, + 0.06539517, + 0.03173809, + 0.00658524, + 0.04710763, + 0.08379987, + 0.11074555, + 0.12606191, + 0.13630094, + 0.13988107, + 0.14193361, + 0.14671866, + 0.15164795, + 0.15772737, + 0.16328073, + 0.16588768, + 0.16947164, + ], + [ + 0.01360289, + 0.02375832, + 0.04262545, + 0.07345243, + 0.09081235, + 0.09227928, + 0.08922710, + 0.08626299, + 0.08584571, + 0.08843734, + 0.09475094, + 0.10376740, + 0.11331399, + 0.12109706, + 0.12678070, + 0.13401030, + 0.14417036, + 0.15408359, + 0.16265529, + 0.17079814, + 0.17972656, + 0.19005983, + 0.20053986, + 0.21017531, + 0.21808806, + 0.22347400, + 0.22650876, + 0.22895376, + 0.22982598, + 0.23001787, + 0.23036398, + 0.22917409, + 0.22684271, + 0.22387883, + 0.22065773, + 0.21821049, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert data.mean is not None - np.testing.assert_allclose( + xp_assert_close( data.mean, - np.array( - [ - 0.08795833, - 0.12050000, - 0.16787500, - 0.20675000, - 0.22329167, - 0.22837500, - 0.23229167, - 0.23579167, - 0.23658333, - 0.23779167, - 0.23866667, - 0.23975000, - 0.24345833, - 0.25054167, - 0.25791667, - 0.26150000, - 0.26437500, - 0.26566667, - 0.26475000, - 0.26554167, - 0.27137500, - 0.28279167, - 0.29529167, - 0.31070833, - 0.32575000, - 0.33829167, - 0.34675000, - 0.35554167, - 0.36295833, - 0.37004167, - 0.37854167, - 0.38675000, - 0.39587500, - 0.40266667, - 0.40683333, - 0.41287500, - ] - ), + [ + 0.08795833, + 0.12050000, + 0.16787500, + 0.20675000, + 0.22329167, + 0.22837500, + 0.23229167, + 0.23579167, + 0.23658333, + 0.23779167, + 0.23866667, + 0.23975000, + 0.24345833, + 0.25054167, + 0.25791667, + 0.26150000, + 0.26437500, + 0.26566667, + 0.26475000, + 0.26554167, + 0.27137500, + 0.28279167, + 0.29529167, + 0.31070833, + 0.32575000, + 0.33829167, + 0.34675000, + 0.35554167, + 0.36295833, + 0.37004167, + 0.37854167, + 0.38675000, + 0.39587500, + 0.40266667, + 0.40683333, + 0.41287500, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -630,56 +630,52 @@ def test_reconstruct(self) -> None: data.PCA() - np.testing.assert_allclose( + xp_assert_close( data.reconstruct( - np.array( - [ - 0.20654008, - 0.12197225, - 0.05136952, - ] - ) - ).values, - np.array( [ - 0.06899964, - 0.08241919, - 0.09768650, - 0.08938555, - 0.07872582, - 0.07140930, - 0.06385099, - 0.05471747, - 0.04281364, - 0.03073280, - 0.01761134, - 0.00772535, - 0.00379120, - 0.00405617, - 0.00595014, - 0.01323536, - 0.03229711, - 0.05661531, - 0.07763041, - 0.10271461, - 0.14276781, - 0.20239859, - 0.27288559, - 0.35044541, - 0.42170481, - 0.47567859, - 0.50910276, - 0.53578140, - 0.55251101, - 0.56530032, - 0.58029915, - 0.59367723, - 0.60830542, - 0.62100871, - 0.62881635, - 0.63971254, + 0.20654008, + 0.12197225, + 0.05136952, ] - ), + ).values, + [ + 0.06899964, + 0.08241919, + 0.09768650, + 0.08938555, + 0.07872582, + 0.07140930, + 0.06385099, + 0.05471747, + 0.04281364, + 0.03073280, + 0.01761134, + 0.00772535, + 0.00379120, + 0.00405617, + 0.00595014, + 0.01323536, + 0.03229711, + 0.05661531, + 0.07763041, + 0.10271461, + 0.14276781, + 0.20239859, + 0.27288559, + 0.35044541, + 0.42170481, + 0.47567859, + 0.50910276, + 0.53578140, + 0.55251101, + 0.56530032, + 0.58029915, + 0.59367723, + 0.60830542, + 0.62100871, + 0.62881635, + 0.63971254, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -689,11 +685,10 @@ def test_raise_exception_reconstruct(self) -> None: raised exception. """ - pytest.raises( - ValueError, - Data_Otsu2018(None, self._cmfs, self._sd_D65).reconstruct, - np.array([0, 0, 0]), - ) + with pytest.raises(ValueError): + Data_Otsu2018(None, self._cmfs, self._sd_D65).reconstruct( + np.array([0, 0, 0]) + ) def test_reconstruction_error(self) -> None: """ @@ -703,7 +698,7 @@ def test_reconstruction_error(self) -> None: data = Data_Otsu2018(self._reflectances, self._cmfs, self._sd_D65) - np.testing.assert_allclose( + xp_assert_close( data.reconstruction_error(), 2.753352549148681, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -715,10 +710,8 @@ def test_raise_exception_reconstruction_error(self) -> None: reconstruction_error` method raised exception. """ - pytest.raises( - ValueError, - Data_Otsu2018(None, self._cmfs, self._sd_D65).reconstruction_error, - ) + with pytest.raises(ValueError): + Data_Otsu2018(None, self._cmfs, self._sd_D65).reconstruction_error() class TestNode_Otsu2018: @@ -800,7 +793,8 @@ def test_raise_exception_row(self) -> None: raised exception. """ - pytest.raises(ValueError, lambda: Node_Otsu2018().row) + with pytest.raises(ValueError): + _ = Node_Otsu2018().row def test_split(self) -> None: """Test :meth:`colour.recovery.otsu2018.Node_Otsu2018.split` method.""" @@ -820,12 +814,8 @@ def test_minimise(self) -> None: assert (len(partition[0].data), len(partition[1].data)) == (10, 14) - np.testing.assert_allclose( - axis.origin, 0.324111380117147, atol=TOLERANCE_ABSOLUTE_TESTS - ) - np.testing.assert_allclose( - partition_error, 2.0402980027, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(axis.origin, 0.324111380117147, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(partition_error, 2.0402980027, atol=TOLERANCE_ABSOLUTE_TESTS) def test_leaf_reconstruction_error(self) -> None: """ @@ -833,7 +823,7 @@ def test_leaf_reconstruction_error(self) -> None: leaf_reconstruction_error` method. """ - np.testing.assert_allclose( + xp_assert_close( self._node_b.leaf_reconstruction_error(), 1.145340908277367e-29, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -845,7 +835,7 @@ def test_branch_reconstruction_error(self) -> None: branch_reconstruction_error` method. """ - np.testing.assert_allclose( + xp_assert_close( self._node_a.branch_reconstruction_error(), 3.900015991807948e-25, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -905,7 +895,7 @@ def test_reflectances(self) -> None: property. """ - np.testing.assert_allclose( + xp_assert_close( self._tree.reflectances, np.transpose( reshape_msds( @@ -942,13 +932,13 @@ def test_optimise(self) -> None: for sd in SDS_COLOURCHECKERS["ColorChecker N Ohta"].values(): XYZ = sd_to_XYZ(sd, self._cmfs, self._sd_D65) / 100 - Lab = XYZ_to_Lab(XYZ, self._xy_D65) + Lab = as_ndarray(XYZ_to_Lab(XYZ, self._xy_D65)) recovered_sd = XYZ_to_sd_Otsu2018( XYZ, self._cmfs, self._sd_D65, dataset, False ) recovered_XYZ = sd_to_XYZ(recovered_sd, self._cmfs, self._sd_D65) / 100 - recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65) + recovered_Lab = as_ndarray(XYZ_to_Lab(recovered_XYZ, self._xy_D65)) error = metric_mse( reshape_sd(sd, SPECTRAL_SHAPE_OTSU2018).values, diff --git a/colour/recovery/tests/test_smits1999.py b/colour/recovery/tests/test_smits1999.py index 0cf87a6735..f0d3005f89 100644 --- a/colour/recovery/tests/test_smits1999.py +++ b/colour/recovery/tests/test_smits1999.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.colorimetry import sd_to_XYZ_integration @@ -13,7 +18,7 @@ RGB_to_sd_Smits1999, ) from colour.recovery.smits1999 import XYZ_to_RGB_Smits1999 -from colour.utilities import domain_range_scale +from colour.utilities import domain_range_scale, xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -36,66 +41,65 @@ class TestMsds_from_RGB_Smits1999: definition unit tests methods. """ - def test_RGB_to_msds_Smits1999(self) -> None: + def test_RGB_to_msds_Smits1999(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.smits1999.RGB_to_msds_Smits1999` definition. """ - RGB = np.array( + RGB = xp_as_array( [ [0.45623196, 0.03080455, 0.04093343], [0.05438271, 0.29877169, 0.07188444], [0.01863137, 0.05139773, 0.28887675], - ] + ], + xp=xp, ) - msds = RGB_to_msds_Smits1999(RGB, MSDS_SMITS1999) + msds = RGB_to_msds_Smits1999(RGB, MSDS_SMITS1999, as_array=True) assert msds.shape == (3, 10) - np.testing.assert_allclose( + xp_assert_close( msds, - np.array( + [ + [ + 0.08296164, + 0.06232130, + 0.04061129, + 0.03304071, + 0.03077991, + 0.03126229, + 0.38501744, + 0.46241991, + 0.46241991, + 0.46237838, + ], [ - [ - 0.08296164, - 0.06232130, - 0.04061129, - 0.03304071, - 0.03077991, - 0.03126229, - 0.38501744, - 0.46241991, - 0.46241991, - 0.46237838, - ], - [ - 0.07137689, - 0.07087984, - 0.07808527, - 0.25193903, - 0.29874044, - 0.28556823, - 0.09612190, - 0.05438271, - 0.05438271, - 0.05494993, - ], - [ - 0.28792653, - 0.28699596, - 0.26315510, - 0.13032190, - 0.05140576, - 0.05141694, - 0.02382727, - 0.02739435, - 0.03010161, - 0.03041033, - ], - ] - ), + 0.07137689, + 0.07087984, + 0.07808527, + 0.25193903, + 0.29874044, + 0.28556823, + 0.09612190, + 0.05438271, + 0.05438271, + 0.05494993, + ], + [ + 0.28792653, + 0.28699596, + 0.26315510, + 0.13032190, + 0.05140576, + 0.05141694, + 0.02382727, + 0.02739435, + 0.03010161, + 0.03041033, + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -106,30 +110,70 @@ class TestRGB_to_msds_Smits1999: definition unit tests methods. """ - def test_RGB_to_msds_Smits1999(self) -> None: + def test_RGB_to_msds_Smits1999(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.smits1999.RGB_to_msds_Smits1999` definition. """ - RGB = np.array( + RGB = xp_as_array( [ [0.45623196, 0.03080455, 0.04093343], [0.05438271, 0.29877169, 0.07188444], [0.01863137, 0.05139773, 0.28887675], - ] + ], + xp=xp, ) - msds = RGB_to_msds_Smits1999(RGB) + msds = RGB_to_msds_Smits1999(RGB, as_array=True) assert msds.shape == (3, 10) - np.testing.assert_allclose( + xp_assert_close( msds[0, 0], 0.08296164, atol=TOLERANCE_ABSOLUTE_TESTS, ) + # Across the six possible *RGB* channel orderings, exercising every + # combination of ``argmin`` / ``argmax`` driving the secondary and + # primary basis selection in *Smits (1999)* §3. + # ``argmin``, ``argmax`` table: + # R > G > B -> primary red, secondary yellow (B is min, R is max) + # R > B > G -> primary red, secondary cyan (G is min, R is max) + # G > R > B -> primary green, secondary yellow (B is min, G is max) + # G > B > R -> primary green, secondary cyan (R is min, G is max) + # B > R > G -> primary blue, secondary magenta (G is min, B is max) + # B > G > R -> primary blue, secondary cyan (R is min, B is max) + RGBs = xp_as_array( + [ + [0.7, 0.4, 0.1], # R > G > B + [0.7, 0.1, 0.4], # R > B > G + [0.4, 0.7, 0.1], # G > R > B + [0.1, 0.7, 0.4], # G > B > R + [0.4, 0.1, 0.7], # B > R > G + [0.1, 0.4, 0.7], # B > G > R + ], + xp=xp, + ) + + msds = RGB_to_msds_Smits1999(RGBs, as_array=True) + + assert msds.shape == (6, 10) + # All six recoveries must produce strictly positive reflectances + # on the visible bands of ``MSDS_SMITS1999``. + assert bool(xp.all(msds > 0)) + + # Tied-channel inputs (white, gray): ``argmin`` / ``argmax`` + # tie-break behaviour maps to the canonical + # ``MSDS_SMITS1999["white"]`` reflectance for the achromatic cases. + RGBs = xp_as_array([[1.0, 1.0, 1.0], [0.5, 0.5, 0.5]], xp=xp) + msds = RGB_to_msds_Smits1999(RGBs, as_array=True) + + white_values = xp_as_array(SDS_SMITS1999["white"].values, xp=xp) + xp_assert_close(msds[0], white_values, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(msds[1], white_values * 0.5, atol=TOLERANCE_ABSOLUTE_TESTS) + class TestSd_from_RGB_Smits1999: """ @@ -149,28 +193,26 @@ def test_RGB_to_sd_Smits1999(self) -> None: np.testing.assert_equal(sd.name, "test") np.testing.assert_equal(sd.shape, SDS_SMITS1999["white"].shape) - np.testing.assert_allclose( + xp_assert_close( sd.values, - np.array( - [ - 0.076432, - 0.05854, - 0.039682, - 0.032208, - 0.029976, - 0.030452, - 0.338069, - 0.405364, - 0.405364, - 0.405323, - ] - ), + [ + 0.076432, + 0.05854, + 0.039682, + 0.032208, + 0.029976, + 0.030452, + 0.338069, + 0.405364, + 0.405364, + 0.405323, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) RGB_white = np.array([1.0, 1.0, 1.0]) sd_white = RGB_to_sd_Smits1999(RGB_white, MSDS_SMITS1999, "white") - np.testing.assert_allclose( + xp_assert_close( sd_white.values, SDS_SMITS1999["white"].values, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -178,7 +220,7 @@ def test_RGB_to_sd_Smits1999(self) -> None: RGB_red = np.array([1.0, 0.0, 0.0]) sd_red = RGB_to_sd_Smits1999(RGB_red, MSDS_SMITS1999, "red") - np.testing.assert_allclose( + xp_assert_close( sd_red.values, SDS_SMITS1999["red"].values, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -186,7 +228,7 @@ def test_RGB_to_sd_Smits1999(self) -> None: RGB_green = np.array([0.0, 1.0, 0.0]) sd_green = RGB_to_sd_Smits1999(RGB_green, MSDS_SMITS1999, "green") - np.testing.assert_allclose( + xp_assert_close( sd_green.values, SDS_SMITS1999["green"].values, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -194,7 +236,7 @@ def test_RGB_to_sd_Smits1999(self) -> None: RGB_blue = np.array([0.0, 0.0, 1.0]) sd_blue = RGB_to_sd_Smits1999(RGB_blue, MSDS_SMITS1999, "blue") - np.testing.assert_allclose( + xp_assert_close( sd_blue.values, SDS_SMITS1999["blue"].values, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -213,141 +255,131 @@ def test_RGB_to_sd_Smits1999(self) -> None: definition. """ - np.testing.assert_allclose( + xp_assert_close( RGB_to_sd_Smits1999( - XYZ_to_RGB_Smits1999(np.array([0.21781186, 0.12541048, 0.04697113])) + XYZ_to_RGB_Smits1999([0.21781186, 0.12541048, 0.04697113]) ).values, - np.array( - [ - 0.07691923, - 0.05870050, - 0.03943195, - 0.03024978, - 0.02750692, - 0.02808645, - 0.34298985, - 0.41185795, - 0.41185795, - 0.41180754, - ] - ), + [ + 0.07691923, + 0.05870050, + 0.03943195, + 0.03024978, + 0.02750692, + 0.02808645, + 0.34298985, + 0.41185795, + 0.41185795, + 0.41180754, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_sd_Smits1999( - XYZ_to_RGB_Smits1999(np.array([0.15434689, 0.22960951, 0.09620221])) + XYZ_to_RGB_Smits1999([0.15434689, 0.22960951, 0.09620221]) ).values, - np.array( - [ - 0.06981477, - 0.06981351, - 0.07713379, - 0.25139495, - 0.30063408, - 0.28797045, - 0.11990414, - 0.08186170, - 0.08198613, - 0.08272671, - ] - ), + [ + 0.06981477, + 0.06981351, + 0.07713379, + 0.25139495, + 0.30063408, + 0.28797045, + 0.11990414, + 0.08186170, + 0.08198613, + 0.08272671, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_to_sd_Smits1999( - XYZ_to_RGB_Smits1999(np.array([0.07683480, 0.06006092, 0.25833845])) + XYZ_to_RGB_Smits1999([0.07683480, 0.06006092, 0.25833845]) ).values, - np.array( - [ - 0.29091152, - 0.29010285, - 0.26572455, - 0.13140471, - 0.05160646, - 0.05162034, - 0.02765638, - 0.03199188, - 0.03472939, - 0.03504156, - ] - ), + [ + 0.29091152, + 0.29010285, + 0.26572455, + 0.13140471, + 0.05160646, + 0.05162034, + 0.02765638, + 0.03199188, + 0.03472939, + 0.03504156, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_sd_Smits1999(XYZ_to_RGB_Smits1999(np.array([0.0, 1.0, 0.0]))).values, - np.array( - [ - -0.2549796, - -0.2848386, - -0.1634905, - 1.5254829, - 1.9800433, - 1.8510762, - -0.7327702, - -1.2758621, - -1.2758621, - -1.2703551, - ] - ), + xp_assert_close( + RGB_to_sd_Smits1999(XYZ_to_RGB_Smits1999([0.0, 1.0, 0.0])).values, + [ + -0.2549796, + -0.2848386, + -0.1634905, + 1.5254829, + 1.9800433, + 1.8510762, + -0.7327702, + -1.2758621, + -1.2758621, + -1.2703551, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_sd_Smits1999(XYZ_to_RGB_Smits1999(np.array([1.0, 1.0, 0.0]))).values, - np.array( - [ - -0.1168428, - -0.1396982, - -0.0414535, - 0.581391, - 0.9563091, - 0.9562111, - 1.3366949, - 1.3742666, - 1.3853491, - 1.4027005, - ] - ), + xp_assert_close( + RGB_to_sd_Smits1999(XYZ_to_RGB_Smits1999([1.0, 1.0, 0.0])).values, + [ + -0.1168428, + -0.1396982, + -0.0414535, + 0.581391, + 0.9563091, + 0.9562111, + 1.3366949, + 1.3742666, + 1.3853491, + 1.4027005, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - RGB_to_sd_Smits1999(XYZ_to_RGB_Smits1999(np.array([0.5, 0.0, 1.0]))).values, - np.array( - [ - 1.1938776, - 1.1938776, - 1.1213867, - -0.067889, - -0.4668587, - -0.4030985, - 0.703056, - 0.9407334, - 0.9437298, - 0.9383386, - ] - ), + xp_assert_close( + RGB_to_sd_Smits1999(XYZ_to_RGB_Smits1999([0.5, 0.0, 1.0])).values, + [ + 1.1938776, + 1.1938776, + 1.1213867, + -0.067889, + -0.4668587, + -0.4030985, + 0.703056, + 0.9407334, + 0.9437298, + 0.9383386, + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_domain_range_scale_RGB_to_sd_Smits1999(self) -> None: + def test_domain_range_scale_RGB_to_sd_Smits1999(self, xp: ModuleType) -> None: """ Test :func:`colour.recovery.smits1999.RGB_to_sd_Smits1999` definition domain and range scale support. """ - XYZ_i = np.array([0.20654008, 0.12197225, 0.05136952]) - RGB_i = XYZ_to_RGB_Smits1999(XYZ_i) + XYZ_i = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + RGB_i = xp_as_array(XYZ_to_RGB_Smits1999(XYZ_i), xp=xp) XYZ_o = sd_to_XYZ_integration(RGB_to_sd_Smits1999(RGB_i)) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): - np.testing.assert_allclose( - sd_to_XYZ_integration(RGB_to_sd_Smits1999(RGB_i * factor_a)), + xp_assert_close( + sd_to_XYZ_integration( + RGB_to_sd_Smits1999(RGB_i * xp_as_array(factor_a, xp=xp)) + ), XYZ_o * factor_b, atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/temperature/__init__.py b/colour/temperature/__init__.py index ea0887c747..317ffb87d6 100644 --- a/colour/temperature/__init__.py +++ b/colour/temperature/__init__.py @@ -48,6 +48,15 @@ validate_method, ) +from .common import ( + CCT_INVERSION_GRID_SAMPLES, + solve_CCT_Newton, + solve_xy_Newton, + x0_CCT_grid, +) + +# isort: split + from .cie_d import CCT_to_xy_CIE_D, xy_to_CCT_CIE_D from .hernandez1999 import CCT_to_xy_Hernandez1999, xy_to_CCT_Hernandez1999 from .kang2002 import CCT_to_xy_Kang2002, xy_to_CCT_Kang2002 @@ -71,6 +80,12 @@ ) __all__ = [ + "CCT_INVERSION_GRID_SAMPLES", + "solve_CCT_Newton", + "solve_xy_Newton", + "x0_CCT_grid", +] +__all__ += [ "CCT_to_xy_CIE_D", "xy_to_CCT_CIE_D", ] @@ -169,8 +184,11 @@ def uv_to_CCT( {:func:`colour.temperature.uv_to_CCT_Ohno2013`}, Number of planckian tables to generate. optimisation_kwargs - {:func:`colour.temperature.uv_to_CCT_Krystek1985`}, - Parameters for :func:`scipy.optimize.minimize` definition. + {:func:`colour.temperature.uv_to_CCT_Krystek1985`, + :func:`colour.temperature.uv_to_CCT_Planck1900`}, + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_CCT_Newton`. start {:func:`colour.temperature.uv_to_CCT_Ohno2013`}, Temperature range start in kelvins. @@ -330,6 +348,7 @@ def xy_to_CCT( ] | str ) = "CIE Illuminant D Series", + **kwargs: Any, ) -> NDArrayFloat: """ Compute the correlated colour temperature :math:`T_{cp}` from the @@ -347,7 +366,9 @@ def xy_to_CCT( optimisation_kwargs {:func:`colour.temperature.xy_to_CCT_CIE_D`, :func:`colour.temperature.xy_to_CCT_Kang2002`}, - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_CCT_Newton`. Returns ------- @@ -363,7 +384,7 @@ def xy_to_CCT( -------- >>> import numpy as np >>> xy_to_CCT(np.array([0.31270, 0.32900])) # doctest: +ELLIPSIS - np.float64(6508.1175148...) + np.float64(6508.117542...) >>> xy_to_CCT(np.array([0.31270, 0.32900]), "Hernandez 1999") ... # doctest: +ELLIPSIS np.float64(6500.7420431...) @@ -371,7 +392,9 @@ def xy_to_CCT( method = validate_method(method, tuple(XY_TO_CCT_METHODS)) - return XY_TO_CCT_METHODS[method](xy) + function = XY_TO_CCT_METHODS[method] + + return function(xy, **filter_kwargs(function, **kwargs)) CCT_TO_XY_METHODS: CanonicalMapping = CanonicalMapping( @@ -415,6 +438,7 @@ def CCT_to_xy( ] | str ) = "CIE Illuminant D Series", + **kwargs: Any, ) -> NDArrayFloat: """ Compute the *CIE xy* chromaticity coordinates from the specified @@ -432,7 +456,9 @@ def CCT_to_xy( optimisation_kwargs {:func:`colour.temperature.CCT_to_xy_Hernandez1999`, :func:`colour.temperature.CCT_to_xy_McCamy1992`}, - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_xy_Newton`. Returns ------- @@ -455,7 +481,9 @@ def CCT_to_xy( method = validate_method(method, tuple(CCT_TO_XY_METHODS)) - return CCT_TO_XY_METHODS[method](CCT) + function = CCT_TO_XY_METHODS[method] + + return function(CCT, **filter_kwargs(function, **kwargs)) __all__ += [ diff --git a/colour/temperature/cie_d.py b/colour/temperature/cie_d.py index 4e289080cc..f024164be7 100644 --- a/colour/temperature/cie_d.py +++ b/colour/temperature/cie_d.py @@ -24,14 +24,24 @@ import typing -import numpy as np - from colour.colorimetry import daylight_locus_function if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, DTypeFloat, NDArrayFloat - -from colour.utilities import as_float, as_float_array, required, tstack, usage_warning + from colour.hints import ArrayLike, NDArrayFloat + +from colour.temperature.common import ( + CCT_INVERSION_GRID_SAMPLES, + solve_CCT_Newton, + x0_CCT_grid, +) +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + optional, + tstack, + usage_warning, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -46,7 +56,6 @@ ] -@required("SciPy") def xy_to_CCT_CIE_D( xy: ArrayLike, optimisation_kwargs: dict | None = None ) -> NDArrayFloat: @@ -60,7 +69,13 @@ def xy_to_CCT_CIE_D( xy *CIE xy* chromaticity coordinates. optimisation_kwargs - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_CCT_Newton`. Accepted keys are + ``samples`` (grid density for the initial guess, default + :attr:`colour.temperature.CCT_INVERSION_GRID_SAMPLES`), + ``newton_iterations``, ``backtrack_iterations`` and ``tolerance`` + (forwarded to :func:`solve_CCT_Newton`). Returns ------- @@ -72,8 +87,9 @@ def xy_to_CCT_CIE_D( The *CIE Illuminant D Series* method does not provide an analytical inverse transformation to compute the correlated colour temperature :math:`T_{cp}` from the specified *CIE xy* chromaticity coordinates. The current - implementation relies on optimisation using :func:`scipy.optimize.minimize` - definition and thus has reduced precision and poor performance. + implementation seeds a damped *Gauss-Newton* iteration with a + nearest-neighbour lookup against a coarse grid sampled from the + analytical forward, vectorised across all input samples. References ---------- @@ -81,46 +97,22 @@ def xy_to_CCT_CIE_D( Examples -------- - >>> xy_to_CCT_CIE_D(np.array([0.31270775, 0.32911283])) - ... # doctest: +ELLIPSIS - np.float64(6504.3895840...) + >>> xy_to_CCT_CIE_D([0.31270775, 0.32911283]) # doctest: +ELLIPSIS + np.float64(6504.389564...) """ - from scipy.optimize import minimize # noqa: PLC0415 + optimisation_kwargs = dict(optional(optimisation_kwargs, {})) xy = as_float_array(xy) - shape = xy.shape - xy = np.atleast_1d(np.reshape(xy, (-1, 2))) - - def objective_function(CCT: NDArrayFloat, xy: NDArrayFloat) -> DTypeFloat: - """Objective function.""" - - objective = np.linalg.norm(CCT_to_xy_CIE_D(CCT) - xy) - - return as_float(objective) - - optimisation_settings = { - "method": "Nelder-Mead", - "options": { - "fatol": 1e-10, - }, - } - if optimisation_kwargs is not None: - optimisation_settings.update(optimisation_kwargs) - - CCT = as_float_array( - [ - minimize( - objective_function, - x0=[6500], - args=(xy_i,), - **optimisation_settings, - ).x - for xy_i in xy - ] + + x0 = x0_CCT_grid( + CCT_to_xy_CIE_D, + xy, + (4000.0, 25000.0), + samples=optimisation_kwargs.pop("samples", CCT_INVERSION_GRID_SAMPLES), ) - return as_float(np.reshape(CCT, shape[:-1])) + return as_float(solve_CCT_Newton(CCT_to_xy_CIE_D, xy, x0=x0, **optimisation_kwargs)) def CCT_to_xy_CIE_D(CCT: ArrayLike) -> NDArrayFloat: @@ -157,7 +149,9 @@ def CCT_to_xy_CIE_D(CCT: ArrayLike) -> NDArrayFloat: CCT = as_float_array(CCT) - if np.any(CCT[np.asarray(np.logical_or(CCT < 4000, CCT > 25000))]): + xp = array_namespace(CCT) + + if xp.any(xp.logical_or(CCT < 4000, CCT > 25000)): usage_warning( "Correlated colour temperature must be in domain " "[4000, 25000], unpredictable results may occur!" @@ -167,7 +161,7 @@ def CCT_to_xy_CIE_D(CCT: ArrayLike) -> NDArrayFloat: CCT_2 = CCT**2 x = as_float( - np.where( + xp.where( CCT <= 7000, -4.607 * 10**9 / CCT_3 + 2.9678 * 10**6 / CCT_2 diff --git a/colour/temperature/common.py b/colour/temperature/common.py new file mode 100644 index 0000000000..f10b589809 --- /dev/null +++ b/colour/temperature/common.py @@ -0,0 +1,440 @@ +""" +Common Correlated Colour Temperature Utilities +============================================== + +Define common utilities for correlated colour temperature :math:`T_{cp}` +computation methods: + +- :func:`colour.temperature.x0_CCT_grid`: Compute a per-sample initial + guess for :func:`colour.temperature.solve_CCT_Newton` by + nearest-neighbour lookup against a coarse grid sampled from a forward + transform. +- :func:`colour.temperature.solve_CCT_Newton`: Solve correlated colour + temperature :math:`T_{cp}` from chromaticity coordinates given a + forward transform using a vectorised *Gauss-Newton* iteration. +- :func:`colour.temperature.solve_xy_Newton`: Solve *CIE xy* chromaticity + coordinates from a target correlated colour temperature + :math:`T_{cp}` given a forward 2-D-to-1-D transform using a + Tikhonov-regularised vectorised *Gauss-Newton* iteration. +""" + +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ArrayLike, Callable, NDArrayFloat + +from colour.hints import cast +from colour.utilities import ( + array_namespace, + as_float_array, + tstack, + usage_warning, + xp_as_float_array, + xp_broadcast_to, + xp_linspace, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "CCT_INVERSION_GRID_SAMPLES", + "x0_CCT_grid", + "solve_CCT_Newton", + "solve_xy_Newton", +] + + +CCT_INVERSION_GRID_SAMPLES: int = 50 +""" +Default number of samples used by :func:`x0_CCT_grid` balancing +basin-coverage against grid construction cost; the resulting spacing +(e.g. ~470 K over the :math:`T_{cp} \\in [1667, 25000] K` *Kang et al. +(2002)* domain) lands the per-pixel initial guess within the basin of +the true root either side of any piecewise discontinuity in the forward, +where the central-difference Jacobian otherwise stagnates. +""" + + +_JACOBIAN_FLOOR: float = 1e-30 +"""*Tikhonov* floor on the squared Jacobian norm ``J . J`` used by +:func:`solve_CCT_Newton` and :func:`solve_xy_Newton` to damp the +*Gauss-Newton* update when the forward goes flat at a domain edge. +Numerical-stability constant; not a user knob.""" + + +def x0_CCT_grid( + forward: Callable[[NDArrayFloat], NDArrayFloat], + target: ArrayLike, + domain: tuple[float, float], + samples: int = CCT_INVERSION_GRID_SAMPLES, +) -> NDArrayFloat: + """ + Compute a per-sample initial guess for + :func:`colour.temperature.solve_CCT_Newton` by nearest-neighbour lookup + against a coarse linearly-spaced grid of correlated colour temperature + :math:`T_{cp}` values mapped through the analytical forward. + + Parameters + ---------- + forward + Callable mapping a correlated colour temperature :math:`T_{cp}` array + of shape ``(...,)`` to a chromaticity coordinates array of shape + ``(..., 2)``. + target + Target chromaticity coordinates of shape ``(..., 2)``. + domain + Inclusive ``(low, high)`` bounds in kelvins of the linearly-spaced + grid; should match the published valid domain of ``forward``. + samples + Number of grid samples; defaults to + :attr:`CCT_INVERSION_GRID_SAMPLES`. + + Returns + ------- + :class:`numpy.ndarray` + Per-sample initial guess :math:`T_{cp}` of shape ``(...,)``. + + Examples + -------- + >>> from colour.temperature import CCT_to_xy_Kang2002 + >>> x0_CCT_grid( + ... CCT_to_xy_Kang2002, + ... [0.31342600, 0.32359597], + ... (1667.0, 25000.0), + ... ) # doctest: +ELLIPSIS + np.float64(6428.836734...) + """ + + target = as_float_array(target) + + xp = array_namespace(target) + + # ``like=target`` creates the seed grid on the target's device; without + # it ``xp.linspace`` defaults to the host device and mismatches a + # device-resident ``target`` (e.g. *PyTorch* on *MPS*). + grid_CCT = cast( + "NDArrayFloat", + xp_linspace(domain[0], domain[1], num=samples, xp=xp, like=target), + ) + grid_target = forward(grid_CCT) + distances_squared = xp.sum((target[..., None, :] - grid_target) ** 2, axis=-1) + return grid_CCT[xp.argmin(distances_squared, axis=-1)] + + +def solve_CCT_Newton( + forward: Callable[[NDArrayFloat], NDArrayFloat], + target: ArrayLike, + x0: ArrayLike = 6500, + tolerance: float = 1e-10, + newton_iterations: int = 30, + backtrack_iterations: int = 20, +) -> NDArrayFloat: + """ + Solve the correlated colour temperature :math:`T_{cp}` from the specified + target chromaticity coordinates using a vectorised damped *Gauss-Newton* + iteration on the given forward transform. + + Parameters + ---------- + forward + Callable mapping a correlated colour temperature :math:`T_{cp}` array + of shape ``(...,)`` to a chromaticity coordinates array of shape + ``(..., 2)``. + target + Target chromaticity coordinates of shape ``(..., 2)``. + x0 + Initial guess for the correlated colour temperature :math:`T_{cp}` in + kelvins. Scalar values broadcast to the leading shape of ``target``; + an array-like of matching shape ``(...,)`` may be passed when a + per-sample initial guess is required, for example a + nearest-neighbour lookup against a coarse grid sampled from the + analytical forward when the latter is non-monotonic outside its + valid domain (see + :func:`colour.temperature.xy_to_CCT_Kang2002`). + tolerance + Convergence tolerance on the maximum absolute Newton step. + newton_iterations + Maximum number of *Gauss-Newton* outer iterations. The default of + 30 covers the slow tail of the iteration when the + central-difference Jacobian straddles a piecewise boundary in the + forward (e.g. *Kang et al. (2002)* at :math:`T_{cp} = 4000 K`). + backtrack_iterations + Maximum number of step-halvings performed by the per-sample + backtracking line search. The default of 20 reduces the step by a + factor of :math:`2^{-20} \\approx 10^{-6}` in the worst case, + which is below the convergence ``tolerance`` for any realistic + :math:`T_{cp}`. + + Returns + ------- + :class:`numpy.ndarray` + Correlated colour temperature :math:`T_{cp}` of shape ``(...,)``. + + Notes + ----- + - *Gauss-Newton* on the residual :math:`r(T) = forward(T) - target` + with central-difference Jacobian; the 1-D normal equations + collapse to :math:`\\delta T = -(J \\cdot r) / (J \\cdot J)`. + - A per-sample backtracking line search halves the step until the + squared residual decreases on every sample, guarding against + overshoot on highly non-linear forwards (e.g. + *Krystek (1985)*'s rational polynomial). + - A :func:`colour.utilities.usage_warning` is issued if the maximum + absolute step has not dropped below ``tolerance`` within + ``newton_iterations`` updates. + + Examples + -------- + >>> from colour.temperature import CCT_to_uv_Krystek1985 + >>> solve_CCT_Newton( + ... CCT_to_uv_Krystek1985, [0.20047203, 0.31029290] + ... ) # doctest: +ELLIPSIS + np.float64(6504.389416...) + """ + + target = as_float_array(target) + + xp = array_namespace(target) + + # Carries ``target``'s namespace and device through the broadcast. + CCT = xp.zeros_like(target[..., 0]) + xp_as_float_array(x0, xp=xp, like=target) + + residual = forward(CCT) - target + objective = xp.sum(residual * residual, axis=-1) + + converged = False + for _iteration in range(newton_iterations): + # Relative step ``1e-5 * |CCT| + 1e-6`` sits near + # ``epsilon ** (1 / 3) ~= 6.06e-6`` for float64, the central- + # difference truncation/roundoff balance derived in Dennis & + # Schnabel (1983), *Numerical Methods for Unconstrained + # Optimization and Nonlinear Equations*, Section 5.4. The + # additive ``1e-6`` floor prevents zero-step at ``CCT == 0``. + h = xp.abs(CCT) * 1e-5 + 1e-6 + jacobian = (forward(CCT + h) - forward(CCT - h)) / (2 * h[..., None]) + + # 1-D *Gauss-Newton*: :math:`\\delta T = -(J \\cdot r) / + # \\max(J \\cdot J, \\lambda)`. The ``\\lambda`` *Tikhonov* floor + # only kicks in when the Jacobian collapses (flat ``forward`` at + # a domain edge); without it ``step`` blows up to ``inf`` / + # ``nan`` and the line search never recovers because + # ``nan < objective`` is ``False``. + numerator = xp.sum(jacobian * residual, axis=-1) + denominator = xp.sum(jacobian * jacobian, axis=-1) + step = -numerator / xp.where( + denominator > _JACOBIAN_FLOOR, denominator, _JACOBIAN_FLOOR + ) + + # Backtracking line search. Halve per-sample until every sample's + # squared residual decreases; guards against the local + # linearisation overshooting on highly non-linear forwards + # (e.g. *Krystek (1985)*'s rational polynomial) without + # sacrificing the quadratic regime inside the trust region. + # Runs the full iteration count rather than early-exiting on + # ``xp.all(improved)`` so the inner loop stays free of + # device-host syncs on ``jax`` / ``torch``; per-sample masking + # naturally freezes the step once a sample improves. + for _backtrack in range(backtrack_iterations): + residual_trial = forward(CCT + step) - target + objective_trial = xp.sum(residual_trial * residual_trial, axis=-1) + improved = objective_trial < objective + step = xp.where(improved, step, step * 0.5) + + CCT = CCT + step + residual = forward(CCT) - target + objective = xp.sum(residual * residual, axis=-1) + + # One device-host sync per outer iteration to enable early-exit. + if bool(xp.max(xp.abs(step)) < tolerance): + converged = True + break + + if not converged: + usage_warning( + f'"Newton" iteration for "CCT" inversion did not converge to ' + f"tolerance {tolerance:.1e} within {newton_iterations} " + "iterations." + ) + + return CCT + + +def solve_xy_Newton( + forward: Callable[[NDArrayFloat], NDArrayFloat], + target: ArrayLike, + x0: ArrayLike = (0.31270, 0.32900), + reference_xy: ArrayLike = (0.31270, 0.32900), + reference_weight: float = 1e-6, + tolerance: float = 1e-10, + newton_iterations: int = 30, + backtrack_iterations: int = 20, +) -> NDArrayFloat: + """ + Solve the *CIE xy* chromaticity coordinates from the specified target + correlated colour temperature :math:`T_{cp}` using a vectorised damped + *Gauss-Newton* iteration on the given forward transform with *Tikhonov* + regularisation toward a reference *CIE xy* anchor. + + Parameters + ---------- + forward + Callable mapping a *CIE xy* chromaticity coordinates array of shape + ``(..., 2)`` to a correlated colour temperature :math:`T_{cp}` + array of shape ``(...,)``. + target + Target correlated colour temperature :math:`T_{cp}` of shape + ``(...,)``. + x0 + Initial guess for the *CIE xy* chromaticity coordinates. Scalar + ``(2,)`` values broadcast to the shape of ``target``; an array-like + of shape ``target.shape + (2,)`` may be passed when a per-sample + initial guess is required. Defaults to the *CIE Standard Illuminant + D65* chromaticity coordinates. + reference_xy + Reference *CIE xy* chromaticity coordinates of shape ``(2,)`` or + ``target.shape + (2,)`` toward which the iteration is biased to + resolve the rank-deficiency of the inversion. The level set of + ``forward`` at ``target`` is a curve in the *CIE xy* plane and the + regularisation picks the point on that curve closest to + ``reference_xy``. Defaults to the *CIE Standard Illuminant D65* + chromaticity coordinates. + reference_weight + Weight of the *Tikhonov* regularisation term. Should be small + enough to leave the data fit dominant; the default of ``1e-6`` + works for chromaticity coordinates in the unit interval and + correlated colour temperatures in :math:`T_{cp} \\in [10^3, + 10^5] K`. + tolerance + Convergence tolerance on the maximum absolute Newton step. + newton_iterations + Maximum number of *Gauss-Newton* outer iterations. + backtrack_iterations + Maximum number of step-halvings performed by the per-sample + backtracking line search. The default of 20 reduces the step by a + factor of :math:`2^{-20} \\approx 10^{-6}` in the worst case, + which is below the convergence ``tolerance`` for any realistic + *CIE xy* coordinates. + + Returns + ------- + :class:`numpy.ndarray` + *CIE xy* chromaticity coordinates of shape ``target.shape + (2,)``. + + Notes + ----- + - Solves :math:`\\min_{xy}\\,(forward(xy) - target)^2 + + \\lambda\\,\\|xy - reference\\|^2` by *Gauss-Newton* update with + an analytical 2x2 *Hessian* inversion. The :math:`\\lambda I` + term resolves the rank-1 deficiency of :math:`J^T J` and pulls + the solution toward ``reference_xy`` along the level set. + - A per-sample backtracking line search halves the step until the + augmented squared residual decreases on every sample, guarding + against overshoot on highly non-linear forwards (e.g. + *McCamy (1992)*'s rational ``n``-polynomial). + - A :func:`colour.utilities.usage_warning` is issued if the maximum + absolute step has not dropped below ``tolerance`` within + ``newton_iterations`` updates. + + Examples + -------- + >>> from colour.temperature import xy_to_CCT_McCamy1992 + >>> solve_xy_Newton(xy_to_CCT_McCamy1992, 6500.0) # doctest: +ELLIPSIS + array([0.312791..., 0.329012...]) + """ + + target = as_float_array(target) + + xp = array_namespace(target) + + x0_array = xp_as_float_array(x0, xp=xp, like=target) + xy = xp_broadcast_to(x0_array, (*target.shape, x0_array.shape[-1]), xp=xp) + reference_xy = xp_as_float_array(reference_xy, xp=xp, like=target) + + residual = forward(xy) - target + anchor_residual = xy - reference_xy + objective = residual * residual + reference_weight * xp.sum( + anchor_residual * anchor_residual, axis=-1 + ) + + converged = False + for _iteration in range(newton_iterations): + x = xy[..., 0] + y = xy[..., 1] + # See ``solve_CCT_Newton`` for the central-difference step + # rationale (Dennis & Schnabel 1983, Section 5.4). + h_x = xp.abs(x) * 1e-5 + 1e-6 + h_y = xp.abs(y) * 1e-5 + 1e-6 + df_dx = (forward(tstack([x + h_x, y])) - forward(tstack([x - h_x, y]))) / ( + 2 * h_x + ) + df_dy = (forward(tstack([x, y + h_y])) - forward(tstack([x, y - h_y]))) / ( + 2 * h_y + ) + + # *Tikhonov*-regularised 2-D *Gauss-Newton*. The naive determinant + # :math:`(J_x^2 + \\lambda)(J_y^2 + \\lambda) - (J_x J_y)^2` + # cancels to roundoff once :math:`\\|J\\|^2 \\gg \\lambda`; the + # algebraic identity :math:`\\det H = \\lambda (\\|J\\|^2 + + # \\lambda)` and the closed-form step below absorb the + # cancellation analytically. + anchor_x = x - reference_xy[..., 0] + anchor_y = y - reference_xy[..., 1] + cross = df_dy * anchor_x - df_dx * anchor_y + denominator = df_dx * df_dx + df_dy * df_dy + reference_weight + step_x = ( + -(df_dx * residual + df_dy * cross + reference_weight * anchor_x) + / denominator + ) + step_y = ( + -(df_dy * residual - df_dx * cross + reference_weight * anchor_y) + / denominator + ) + step = tstack([step_x, step_y]) + + # Backtracking line search on the augmented squared residual. + # Halve per-sample until every sample's augmented squared + # residual decreases; guards against overshooting on highly + # non-linear forwards (e.g. *McCamy (1992)*'s rational + # ``n``-polynomial). Runs the full iteration count rather than + # early-exiting on ``xp.all(improved)`` so the inner loop stays + # free of device-host syncs on ``jax`` / ``torch``; per-sample + # masking naturally freezes the step once a sample improves. + for _backtrack in range(backtrack_iterations): + xy_trial = xy + step + residual_trial = forward(xy_trial) - target + anchor_residual_trial = xy_trial - reference_xy + objective_trial = ( + residual_trial * residual_trial + + reference_weight + * xp.sum(anchor_residual_trial * anchor_residual_trial, axis=-1) + ) + improved = objective_trial < objective + step = xp.where(improved[..., None], step, step * 0.5) + + xy = xy + step + residual = forward(xy) - target + anchor_residual = xy - reference_xy + objective = residual * residual + reference_weight * xp.sum( + anchor_residual * anchor_residual, axis=-1 + ) + + # One device-host sync per outer iteration to enable early-exit. + if bool(xp.max(xp.abs(step)) < tolerance): + converged = True + break + + if not converged: + usage_warning( + f'"Newton" iteration for "xy" inversion did not converge to ' + f"tolerance {tolerance:.1e} within {newton_iterations} " + "iterations." + ) + + return xy diff --git a/colour/temperature/hernandez1999.py b/colour/temperature/hernandez1999.py index fdefbee859..9dc909dcb2 100644 --- a/colour/temperature/hernandez1999.py +++ b/colour/temperature/hernandez1999.py @@ -24,15 +24,20 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode -from colour.colorimetry import CCS_ILLUMINANTS if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, DTypeFloat, NDArrayFloat - -from colour.utilities import as_float, as_float_array, required, tsplit, usage_warning + from colour.hints import ArrayLike, NDArrayFloat + +from colour.temperature.common import solve_xy_Newton +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + optional, + tsplit, + usage_warning, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -47,7 +52,6 @@ ] -@required("SciPy") def xy_to_CCT_Hernandez1999(xy: ArrayLike) -> NDArrayFloat: """ Compute the correlated colour temperature :math:`T_{cp}` from the @@ -70,11 +74,14 @@ def xy_to_CCT_Hernandez1999(xy: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> xy = np.array([0.31270, 0.32900]) >>> xy_to_CCT_Hernandez1999(xy) # doctest: +ELLIPSIS np.float64(6500.7420431...) """ + xp = array_namespace(xy) + x, y = tsplit(xy) with sdiv_mode(): @@ -82,18 +89,18 @@ def xy_to_CCT_Hernandez1999(xy: ArrayLike) -> NDArrayFloat: CCT = ( -949.86315 - + 6253.80338 * np.exp(-n / 0.92159) - + 28.70599 * np.exp(-n / 0.20039) - + 0.00004 * np.exp(-n / 0.07125) + + 6253.80338 * xp.exp(-n / 0.92159) + + 28.70599 * xp.exp(-n / 0.20039) + + 0.00004 * xp.exp(-n / 0.07125) ) - n = np.where(CCT > 50000, (x - 0.3356) / (y - 0.1691), n) + n = xp.where(CCT > 50000, (x - 0.3356) / (y - 0.1691), n) - CCT = np.where( + CCT = xp.where( CCT > 50000, 36284.48953 - + 0.00228 * np.exp(-n / 0.07861) - + 5.4535e-36 * np.exp(-n / 0.01543), + + 0.00228 * xp.exp(-n / 0.07861) + + 5.4535e-36 * xp.exp(-n / 0.01543), CCT, ) @@ -113,7 +120,10 @@ def CCT_to_xy_Hernandez1999( CCT Correlated colour temperature :math:`T_{cp}`. optimisation_kwargs - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.solve_xy_Newton`. Accepted keys are + ``x0``, ``reference_xy``, ``reference_weight``, + ``newton_iterations``, ``backtrack_iterations`` and ``tolerance``. Returns ------- @@ -123,13 +133,14 @@ def CCT_to_xy_Hernandez1999( Warnings -------- *Hernandez-Andres et al. (1999)* method for computing *CIE xy* - chromaticity coordinates from the specified correlated colour temperature - is not a bijective function and might produce unexpected results. It is - provided for consistency with other correlated colour temperature - computation methods but should be avoided for practical applications. The - current implementation relies on optimisation using - :func:`scipy.optimize.minimize` definition and thus has reduced precision - and poor performance. + chromaticity coordinates from the specified correlated colour + temperature is not a bijective function and might produce unexpected + results. It is provided for consistency with other correlated colour + temperature computation methods but should be avoided for practical + applications. The current implementation seeds a *Tikhonov*- + regularised damped *Gauss-Newton* iteration anchored to the + *CIE Standard Illuminant D65* chromaticity coordinates, vectorised + across all input samples. References ---------- @@ -141,8 +152,6 @@ def CCT_to_xy_Hernandez1999( array([0.3127..., 0.329...]) """ - from scipy.optimize import minimize # noqa: PLC0415 - usage_warning( '"Hernandez-Andres et al. (1999)" method for computing "CIE xy" ' "chromaticity coordinates from given correlated colour temperature is " @@ -151,36 +160,10 @@ def CCT_to_xy_Hernandez1999( "computation methods but should be avoided for practical applications." ) + optimisation_kwargs = dict(optional(optimisation_kwargs, {})) + CCT = as_float_array(CCT) - shape = list(CCT.shape) - CCT = np.atleast_1d(np.reshape(CCT, (-1, 1))) - - def objective_function(xy: NDArrayFloat, CCT: NDArrayFloat) -> DTypeFloat: - """Objective function.""" - - objective = np.linalg.norm(xy_to_CCT_Hernandez1999(xy) - CCT) - - return as_float(objective) - - optimisation_settings = { - "method": "Nelder-Mead", - "options": { - "fatol": 1e-10, - }, - } - if optimisation_kwargs is not None: - optimisation_settings.update(optimisation_kwargs) - - xy = as_float_array( - [ - minimize( - objective_function, - x0=CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"], - args=(CCT_i,), - **optimisation_settings, - ).x - for CCT_i in CCT - ] - ) - return np.reshape(xy, ([*shape, 2])) + return as_float_array( + solve_xy_Newton(xy_to_CCT_Hernandez1999, CCT, **optimisation_kwargs) + ) diff --git a/colour/temperature/kang2002.py b/colour/temperature/kang2002.py index 65edcf6b85..2d8cef5413 100644 --- a/colour/temperature/kang2002.py +++ b/colour/temperature/kang2002.py @@ -24,12 +24,23 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, DTypeFloat, NDArrayFloat - -from colour.utilities import as_float, as_float_array, required, tstack, usage_warning + from colour.hints import ArrayLike, NDArrayFloat + +from colour.temperature.common import ( + CCT_INVERSION_GRID_SAMPLES, + solve_CCT_Newton, + x0_CCT_grid, +) +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + optional, + tstack, + usage_warning, + xp_select, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -44,7 +55,6 @@ ] -@required("SciPy") def xy_to_CCT_Kang2002( xy: ArrayLike, optimisation_kwargs: dict | None = None ) -> NDArrayFloat: @@ -58,7 +68,13 @@ def xy_to_CCT_Kang2002( xy *CIE xy* chromaticity coordinates. optimisation_kwargs - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_CCT_Newton`. Accepted keys are + ``samples`` (grid density for the initial guess, default + :attr:`colour.temperature.CCT_INVERSION_GRID_SAMPLES`), + ``newton_iterations``, ``backtrack_iterations`` and ``tolerance`` + (forwarded to :func:`solve_CCT_Newton`). Returns ------- @@ -70,9 +86,11 @@ def xy_to_CCT_Kang2002( The *Kang et al. (2002)* method does not provide an analytical inverse transformation to compute the correlated colour temperature :math:`T_{cp}` from the specified *CIE xy* chromaticity coordinates. - The current implementation relies on optimisation using - :func:`scipy.optimize.minimize` definition and thus has reduced - precision and poor performance. + The current implementation relies on a damped *Gauss-Newton* iteration + seeded by nearest-neighbour lookup against a coarse grid sampled from + the analytical forward over the [1667, 25000] domain. The lookup keeps + the iteration in the correct basin near the domain edges where the + polynomial is non-monotonic if extrapolated. References ---------- @@ -80,46 +98,24 @@ def xy_to_CCT_Kang2002( Examples -------- - >>> xy_to_CCT_Kang2002(np.array([0.31342600, 0.32359597])) - ... # doctest: +ELLIPSIS - np.float64(6504.3893128...) + >>> xy_to_CCT_Kang2002([0.31342600, 0.32359597]) # doctest: +ELLIPSIS + np.float64(6504.389303...) """ - from scipy.optimize import minimize # noqa: PLC0415 + optimisation_kwargs = dict(optional(optimisation_kwargs, {})) xy = as_float_array(xy) - shape = xy.shape - xy = np.atleast_1d(np.reshape(xy, (-1, 2))) - - def objective_function(CCT: NDArrayFloat, xy: NDArrayFloat) -> DTypeFloat: - """Objective function.""" - - objective = np.linalg.norm(CCT_to_xy_Kang2002(CCT) - xy) - - return as_float(objective) - - optimisation_settings = { - "method": "Nelder-Mead", - "options": { - "fatol": 1e-10, - }, - } - if optimisation_kwargs is not None: - optimisation_settings.update(optimisation_kwargs) - - CCT = as_float_array( - [ - minimize( - objective_function, - x0=[6500], - args=(xy_i,), - **optimisation_settings, - ).x - for xy_i in xy - ] + + x0 = x0_CCT_grid( + CCT_to_xy_Kang2002, + xy, + (1667.0, 25000.0), + samples=optimisation_kwargs.pop("samples", CCT_INVERSION_GRID_SAMPLES), ) - return as_float(np.reshape(CCT, shape[:-1])) + return as_float( + solve_CCT_Newton(CCT_to_xy_Kang2002, xy, x0=x0, **optimisation_kwargs) + ) def CCT_to_xy_Kang2002(CCT: ArrayLike) -> NDArrayFloat: @@ -155,7 +151,9 @@ def CCT_to_xy_Kang2002(CCT: ArrayLike) -> NDArrayFloat: CCT = as_float_array(CCT) - if np.any(CCT[np.asarray(np.logical_or(CCT < 1667, CCT > 25000))]): + xp = array_namespace(CCT) + + if xp.any(xp.logical_or(CCT < 1667, CCT > 25000)): usage_warning( "Correlated colour temperature must be in domain " "[1667, 25000], unpredictable results may occur!" @@ -164,7 +162,7 @@ def CCT_to_xy_Kang2002(CCT: ArrayLike) -> NDArrayFloat: CCT_3 = CCT**3 CCT_2 = CCT**2 - x = np.where( + x = xp.where( CCT <= 4000, -0.2661239 * 10**9 / CCT_3 - 0.2343589 * 10**6 / CCT_2 @@ -179,10 +177,10 @@ def CCT_to_xy_Kang2002(CCT: ArrayLike) -> NDArrayFloat: x_3 = x**3 x_2 = x**2 - cnd_l = [CCT <= 2222, np.logical_and(CCT > 2222, CCT <= 4000), CCT > 4000] + cnd_l = [CCT <= 2222, xp.logical_and(CCT > 2222, CCT <= 4000), CCT > 4000] i = -1.1063814 * x_3 - 1.34811020 * x_2 + 2.18555832 * x - 0.20219683 j = -0.9549476 * x_3 - 1.37418593 * x_2 + 2.09137015 * x - 0.16748867 k = 3.0817580 * x_3 - 5.8733867 * x_2 + 3.75112997 * x - 0.37001483 - y = np.select(cnd_l, [i, j, k]) + y = xp_select(cnd_l, [i, j, k], xp=xp) return tstack([x, y]) diff --git a/colour/temperature/krystek1985.py b/colour/temperature/krystek1985.py index 10c6e8fbdf..40aa8f3bf9 100644 --- a/colour/temperature/krystek1985.py +++ b/colour/temperature/krystek1985.py @@ -23,12 +23,22 @@ import typing -import numpy as np - if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, DTypeFloat, NDArrayFloat - -from colour.utilities import as_float, as_float_array, required, tstack + from colour.hints import ArrayLike, NDArrayFloat + +from colour.temperature.common import ( + CCT_INVERSION_GRID_SAMPLES, + solve_CCT_Newton, + x0_CCT_grid, +) +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + optional, + tstack, + usage_warning, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -43,7 +53,6 @@ ] -@required("SciPy") def uv_to_CCT_Krystek1985( uv: ArrayLike, optimisation_kwargs: dict | None = None ) -> NDArrayFloat: @@ -57,7 +66,13 @@ def uv_to_CCT_Krystek1985( uv *CIE UCS* colourspace *uv* chromaticity coordinates. optimisation_kwargs - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_CCT_Newton`. Accepted keys are + ``samples`` (grid density for the initial guess, default + :attr:`colour.temperature.CCT_INVERSION_GRID_SAMPLES`), + ``newton_iterations``, ``backtrack_iterations`` and ``tolerance`` + (forwarded to :func:`solve_CCT_Newton`). Returns ------- @@ -69,9 +84,9 @@ def uv_to_CCT_Krystek1985( *Krystek (1985)* does not provide an analytical inverse transformation to compute the correlated colour temperature :math:`T_{cp}` from the specified *CIE UCS* colourspace *uv* chromaticity coordinates. The - current implementation relies on optimisation using - :func:`scipy.optimize.minimize` definition and thus has reduced - precision and poor performance. + current implementation seeds a damped *Gauss-Newton* iteration with a + nearest-neighbour lookup against a coarse grid sampled from the + analytical forward, vectorised across all input samples. Notes ----- @@ -85,46 +100,24 @@ def uv_to_CCT_Krystek1985( Examples -------- - >>> uv_to_CCT_Krystek1985(np.array([0.20047203, 0.31029290])) - ... # doctest: +ELLIPSIS - np.float64(6504.3894290...) + >>> uv_to_CCT_Krystek1985([0.20047203, 0.31029290]) # doctest: +ELLIPSIS + np.float64(6504.389416...) """ - from scipy.optimize import minimize # noqa: PLC0415 + optimisation_kwargs = dict(optional(optimisation_kwargs, {})) uv = as_float_array(uv) - shape = uv.shape - uv = np.atleast_1d(np.reshape(uv, (-1, 2))) - - def objective_function(CCT: NDArrayFloat, uv: NDArrayFloat) -> DTypeFloat: - """Objective function.""" - - objective = np.linalg.norm(CCT_to_uv_Krystek1985(CCT) - uv) - - return as_float(objective) - - optimisation_settings = { - "method": "Nelder-Mead", - "options": { - "fatol": 1e-10, - }, - } - if optimisation_kwargs is not None: - optimisation_settings.update(optimisation_kwargs) - - CCT = as_float_array( - [ - minimize( - objective_function, - x0=[6500], - args=(uv_i,), - **optimisation_settings, - ).x - for uv_i in uv - ] + + x0 = x0_CCT_grid( + CCT_to_uv_Krystek1985, + uv, + (1000.0, 15000.0), + samples=optimisation_kwargs.pop("samples", CCT_INVERSION_GRID_SAMPLES), ) - return as_float(np.reshape(CCT, shape[:-1])) + return as_float( + solve_CCT_Newton(CCT_to_uv_Krystek1985, uv, x0=x0, **optimisation_kwargs) + ) def CCT_to_uv_Krystek1985(CCT: ArrayLike) -> NDArrayFloat: @@ -160,6 +153,14 @@ def CCT_to_uv_Krystek1985(CCT: ArrayLike) -> NDArrayFloat: T = as_float_array(CCT) + xp = array_namespace(T) + + if xp.any(xp.logical_or(T < 1000, T > 15000)): + usage_warning( + "Correlated colour temperature must be in domain " + "[1000, 15000], unpredictable results may occur!" + ) + T_2 = T**2 u = (0.860117757 + 1.54118254 * 10**-4 * T + 1.28641212 * 10**-7 * T_2) / ( diff --git a/colour/temperature/mccamy1992.py b/colour/temperature/mccamy1992.py index 21322d6f9f..277466dca7 100644 --- a/colour/temperature/mccamy1992.py +++ b/colour/temperature/mccamy1992.py @@ -22,15 +22,19 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode -from colour.colorimetry import CCS_ILLUMINANTS if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, DTypeFloat, NDArrayFloat + from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import as_float, as_float_array, required, tsplit, usage_warning +from colour.temperature.common import solve_xy_Newton +from colour.utilities import ( + as_float, + as_float_array, + optional, + tsplit, + usage_warning, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -45,7 +49,6 @@ ] -@required("SciPy") def xy_to_CCT_McCamy1992(xy: ArrayLike) -> NDArrayFloat: """ Compute the correlated colour temperature :math:`T_{cp}` from the @@ -97,7 +100,10 @@ def CCT_to_xy_McCamy1992( CCT Correlated colour temperature :math:`T_{cp}`. optimisation_kwargs - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.solve_xy_Newton`. Accepted keys are + ``x0``, ``reference_xy``, ``reference_weight``, + ``newton_iterations``, ``backtrack_iterations`` and ``tolerance``. Returns ------- @@ -106,14 +112,15 @@ def CCT_to_xy_McCamy1992( Warnings -------- - The *McCamy (1992)* method for computing *CIE xy* chromaticity coordinates - from the specified correlated colour temperature is not a bijective - function and might produce unexpected results. It is provided for - consistency with other correlated colour temperature computation methods - but should be avoided for practical applications. The current - implementation relies on optimisation using - :func:`scipy.optimize.minimize` definition and thus has reduced precision - and poor performance. + The *McCamy (1992)* method for computing *CIE xy* chromaticity + coordinates from the specified correlated colour temperature is not + a bijective function and might produce unexpected results. It is + provided for consistency with other correlated colour temperature + computation methods but should be avoided for practical + applications. The current implementation seeds a *Tikhonov*- + regularised damped *Gauss-Newton* iteration anchored to the + *CIE Standard Illuminant D65* chromaticity coordinates, vectorised + across all input samples. References ---------- @@ -125,8 +132,6 @@ def CCT_to_xy_McCamy1992( array([0.3127..., 0.329...]) """ - from scipy.optimize import minimize # noqa: PLC0415 - usage_warning( '"McCamy (1992)" method for computing "CIE xy" chromaticity ' "coordinates from given correlated colour temperature is not a " @@ -135,36 +140,10 @@ def CCT_to_xy_McCamy1992( "methods but should be avoided for practical applications." ) + optimisation_kwargs = dict(optional(optimisation_kwargs, {})) + CCT = as_float_array(CCT) - shape = list(CCT.shape) - CCT = np.atleast_1d(np.reshape(CCT, (-1, 1))) - - def objective_function(xy: NDArrayFloat, CCT: NDArrayFloat) -> DTypeFloat: - """Objective function.""" - - objective = np.linalg.norm(xy_to_CCT_McCamy1992(xy) - CCT) - - return as_float(objective) - - optimisation_settings = { - "method": "Nelder-Mead", - "options": { - "fatol": 1e-10, - }, - } - if optimisation_kwargs is not None: - optimisation_settings.update(optimisation_kwargs) - - xy = as_float_array( - [ - minimize( - objective_function, - x0=CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"], - args=(CCT_i,), - **optimisation_settings, - ).x - for CCT_i in CCT - ] - ) - return np.reshape(xy, ([*shape, 2])) + return as_float_array( + solve_xy_Newton(xy_to_CCT_McCamy1992, CCT, **optimisation_kwargs) + ) diff --git a/colour/temperature/ohno2013.py b/colour/temperature/ohno2013.py index 80930975df..4c54768c2b 100644 --- a/colour/temperature/ohno2013.py +++ b/colour/temperature/ohno2013.py @@ -22,9 +22,7 @@ from __future__ import annotations -import numpy as np - -from colour.algebra import euclidean_distance, sdiv, sdiv_mode +from colour.algebra import sdiv, sdiv_mode from colour.colorimetry import MultiSpectralDistributions, handle_spectral_arguments from colour.hints import ( # noqa: TC001 ArrayLike, @@ -36,6 +34,7 @@ from colour.temperature import CCT_to_uv_Planck1900 from colour.utilities import ( CACHE_REGISTRY, + array_namespace, as_float_array, attest, is_caching_enabled, @@ -43,6 +42,8 @@ runtime_warning, tsplit, tstack, + xp_as_float_array, + xp_reshape, ) __author__ = "Colour Developers" @@ -67,6 +68,13 @@ CCT_MAXIMAL_OHNO2013: float = 100000 CCT_DEFAULT_SPACING_OHNO2013: float = 1.001 +# Finite-difference step :math:`\\Delta T = 0.01\\ K` used by +# :func:`CCT_to_XYZ_Ohno2013` to evaluate the *Planckian* derivative +# numerically. The value is small enough to remain accurate at +# ``CCT_MINIMAL_OHNO2013`` (relative error ~1e-5) and large enough not +# to be lost to float32 round-off at ``CCT_MAXIMAL_OHNO2013``. +_FINITE_DIFFERENCE_STEP_OHNO2013: float = 0.01 + _CACHE_PLANCKIAN_TABLE: dict = CACHE_REGISTRY.register_cache( f"{__name__}._CACHE_PLANCKIAN_TABLE" ) @@ -106,6 +114,7 @@ def planckian_table( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] @@ -140,10 +149,14 @@ def planckian_table( ) D = min(max(D, 0), 1) next_spacing = spacing * (1 - D) + (1 + (spacing - 1) / 10) * D - Ti = np.concatenate([Ti, [end - 1, end]]) + Ti = as_float_array(Ti) + + xp = array_namespace(Ti) - table = np.concatenate( - [np.reshape(Ti, (-1, 1)), CCT_to_uv_Planck1900(Ti, cmfs)], axis=1 + Ti = xp.concat([Ti, xp_as_float_array([end - 1, end], xp=xp)]) + + table = xp.concat( + [xp_reshape(Ti, (-1, 1), xp=xp), CCT_to_uv_Planck1900(Ti, cmfs)], axis=1 ) _CACHE_PLANCKIAN_TABLE[hash_key] = table.copy() @@ -192,6 +205,7 @@ def uv_to_CCT_Ohno2013( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] @@ -204,58 +218,84 @@ def uv_to_CCT_Ohno2013( """ uv = as_float_array(uv) + + xp = array_namespace(uv) + cmfs, _illuminant = handle_spectral_arguments(cmfs) start = optional(start, CCT_MINIMAL_OHNO2013) end = optional(end, CCT_MAXIMAL_OHNO2013) spacing = optional(spacing, CCT_DEFAULT_SPACING_OHNO2013) shape = uv.shape - uv = np.reshape(uv, (-1, 2)) - - # Planckian tables creation through cascade expansion. - tables_data = [] - for uv_i in uv: - table = planckian_table(cmfs, start, end, spacing) - dists = euclidean_distance(table[:, 1:], uv_i) - index = np.argmin(dists) - if index == 0: - runtime_warning( - "Minimal distance index is on lowest planckian table bound, " - "unpredictable results may occur!" - ) - index += 1 - elif index == len(table) - 1: - runtime_warning( - "Minimal distance index is on highest planckian table bound, " - "unpredictable results may occur!" - ) - index -= 1 + uv = xp_reshape(uv, (-1, 2), xp=xp) - tables_data.append( - np.vstack( - [ - [*table[index - 1, ...], dists[index - 1]], - [*table[index, ...], dists[index]], - [*table[index + 1, ...], dists[index + 1]], - ] - ) + # The Planckian table depends only on ``cmfs`` / ``start`` / ``end`` + # / ``spacing`` so it is computed once and reused; the per-sample + # nearest-row lookup is vectorised below. + table = xp_as_float_array( + planckian_table(cmfs, start, end, spacing), xp=xp, like=uv + ) + n_rows = table.shape[0] + + distances = xp.linalg.vector_norm(table[None, :, 1:] - uv[:, None, :], axis=-1) + indices = xp.argmin(distances, axis=-1) + + # Emit a single warning per boundary irrespective of sample count, + # then clip so the neighbour gather below stays in range. + if bool(xp.any(indices == 0)): + runtime_warning( + "Minimal distance index is on lowest planckian table bound, " + "unpredictable results may occur!" + ) + if bool(xp.any(indices == n_rows - 1)): + runtime_warning( + "Minimal distance index is on highest planckian table bound, " + "unpredictable results may occur!" ) - tables = as_float_array(tables_data) + indices = xp.clip(indices, 1, n_rows - 2) + + # ``(N, 3, 4)`` neighbour triple expected by the *Ohno (2014)* + # triangular and parabolic solutions below. + sample_indices = xp.arange(uv.shape[0]) + tables = xp.stack( + [ + xp.concat( + [ + table[indices - 1], + distances[sample_indices, indices - 1][..., None], + ], + axis=-1, + ), + xp.concat( + [table[indices], distances[sample_indices, indices][..., None]], + axis=-1, + ), + xp.concat( + [ + table[indices + 1], + distances[sample_indices, indices + 1][..., None], + ], + axis=-1, + ), + ], + axis=1, + ) Tip, uip, vip, dip = tsplit(tables[:, 0, :]) Ti, _ui, _vi, di = tsplit(tables[:, 1, :]) Tin, uin, vin, din = tsplit(tables[:, 2, :]) - # Triangular solution. - l = np.hypot(uin - uip, vin - vip) # noqa: E741 + # *Ohno (2014)* Eqs. (23)-(25), triangular solution. + l = xp.hypot(uin - uip, vin - vip) # noqa: E741 x = (dip**2 - din**2 + l**2) / (2 * l) T_t = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) - sign = np.sign(uv[..., 1] - vtx) - D_uv_t = (dip**2 - x**2) ** (1 / 2) * sign + sign = xp.sign(uv[..., 1] - vtx) + radicand = dip**2 - x**2 + D_uv_t = xp.sqrt(xp.where(radicand > 0, radicand, 0.0)) * sign - # Parabolic solution. + # *Ohno (2014)* Eqs. (26)-(28), parabolic solution. X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X**-1 b = -(Tip**2 * (din - di) + Ti**2 * (dip - din) + Tin**2 * (di - dip)) * X**-1 @@ -271,13 +311,13 @@ def uv_to_CCT_Ohno2013( T_p = -b / (2 * a) D_uv_p = (a * T_p**2 + b * T_p + c) * sign - CCT_D_uv = np.where( - (np.abs(D_uv_t) >= 0.002)[..., None], + CCT_D_uv = xp.where( + (xp.abs(D_uv_t) >= 0.002)[..., None], tstack([T_p, D_uv_p]), tstack([T_t, D_uv_t]), ) - return np.reshape(CCT_D_uv, shape) + return xp_reshape(CCT_D_uv, shape, xp=xp) def CCT_to_uv_Ohno2013( @@ -308,6 +348,7 @@ def CCT_to_uv_Ohno2013( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] @@ -319,16 +360,18 @@ def CCT_to_uv_Ohno2013( array([0.1977999..., 0.3122004...]) """ + xp = array_namespace(CCT_D_uv) + CCT, D_uv = tsplit(CCT_D_uv) cmfs, _illuminant = handle_spectral_arguments(cmfs) uv_0 = CCT_to_uv_Planck1900(CCT, cmfs) - uv_1 = CCT_to_uv_Planck1900(CCT + 0.01, cmfs) + uv_1 = CCT_to_uv_Planck1900(CCT + _FINITE_DIFFERENCE_STEP_OHNO2013, cmfs) du, dv = tsplit(uv_0 - uv_1) - h = np.hypot(du, dv) + h = xp.hypot(du, dv) with sdiv_mode(): uv = tstack( @@ -338,7 +381,7 @@ def CCT_to_uv_Ohno2013( ] ) - return np.where((D_uv == 0)[..., None], uv_0, uv) + return xp.where((D_uv == 0)[..., None], uv_0, uv) def XYZ_to_CCT_Ohno2013( @@ -396,6 +439,7 @@ def XYZ_to_CCT_Ohno2013( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] @@ -441,6 +485,7 @@ def CCT_to_XYZ_Ohno2013( Examples -------- + >>> import numpy as np >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT >>> cmfs = ( ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] diff --git a/colour/temperature/planck1900.py b/colour/temperature/planck1900.py index c61363359f..e435f741bb 100644 --- a/colour/temperature/planck1900.py +++ b/colour/temperature/planck1900.py @@ -20,8 +20,6 @@ import typing -import numpy as np - from colour.colorimetry import ( MultiSpectralDistributions, handle_spectral_arguments, @@ -30,10 +28,22 @@ ) if typing.TYPE_CHECKING: - from colour.hints import ArrayLike, DTypeFloat, NDArrayFloat + from colour.hints import ArrayLike, NDArrayFloat from colour.models import UCS_to_uv, XYZ_to_UCS -from colour.utilities import as_float, as_float_array, required +from colour.temperature.common import ( + CCT_INVERSION_GRID_SAMPLES, + solve_CCT_Newton, + x0_CCT_grid, +) +from colour.utilities import ( + array_namespace, + as_float, + as_float_array, + optional, + xp_matrix_transpose, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -48,7 +58,6 @@ ] -@required("SciPy") def uv_to_CCT_Planck1900( uv: ArrayLike, cmfs: MultiSpectralDistributions | None = None, @@ -67,7 +76,13 @@ def uv_to_CCT_Planck1900( Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. optimisation_kwargs - Parameters for :func:`scipy.optimize.minimize` definition. + Inversion parameters forwarded to + :func:`colour.temperature.x0_CCT_grid` and + :func:`colour.temperature.solve_CCT_Newton`. Accepted keys are + ``samples`` (grid density for the initial guess, default + :attr:`colour.temperature.CCT_INVERSION_GRID_SAMPLES`), + ``newton_iterations``, ``backtrack_iterations`` and ``tolerance`` + (forwarded to :func:`solve_CCT_Newton`). Returns ------- @@ -76,9 +91,9 @@ def uv_to_CCT_Planck1900( Warnings -------- - The current implementation relies on optimisation using - :func:`scipy.optimize.minimize` definition and thus has reduced - precision and poor performance. + The current implementation seeds a damped *Gauss-Newton* iteration with + a nearest-neighbour lookup against a coarse grid sampled from the + analytical forward, vectorised across all input samples. References ---------- @@ -86,48 +101,26 @@ def uv_to_CCT_Planck1900( Examples -------- - >>> uv_to_CCT_Planck1900(np.array([0.20042808, 0.31033343])) - ... # doctest: +ELLIPSIS - np.float64(6504.0000617...) + >>> uv_to_CCT_Planck1900([0.20042808, 0.31033343]) # doctest: +ELLIPSIS + np.float64(6504.000071...) """ - from scipy.optimize import minimize # noqa: PLC0415 + optimisation_kwargs = dict(optional(optimisation_kwargs, {})) - uv = as_float_array(uv) cmfs, _illuminant = handle_spectral_arguments(cmfs) + uv = as_float_array(uv) - shape = uv.shape - uv = np.atleast_1d(np.reshape(uv, (-1, 2))) - - def objective_function(CCT: NDArrayFloat, uv: NDArrayFloat) -> DTypeFloat: - """Objective function.""" - - objective = np.linalg.norm(CCT_to_uv_Planck1900(CCT, cmfs) - uv) - - return as_float(objective) - - optimisation_settings = { - "method": "Nelder-Mead", - "options": { - "fatol": 1e-10, - }, - } - if optimisation_kwargs is not None: - optimisation_settings.update(optimisation_kwargs) - - CCT = as_float_array( - [ - minimize( - objective_function, - x0=[6500], - args=(uv_i,), - **optimisation_settings, - ).x - for uv_i in uv - ] + def forward(CCT: NDArrayFloat) -> NDArrayFloat: + return CCT_to_uv_Planck1900(CCT, cmfs) + + x0 = x0_CCT_grid( + forward, + uv, + (1000.0, 25000.0), + samples=optimisation_kwargs.pop("samples", CCT_INVERSION_GRID_SAMPLES), ) - return as_float(np.reshape(CCT, shape[:-1])) + return as_float(solve_CCT_Newton(forward, uv, x0=x0, **optimisation_kwargs)) def CCT_to_uv_Planck1900( @@ -163,10 +156,23 @@ def CCT_to_uv_Planck1900( """ CCT = as_float_array(CCT) + + xp = array_namespace(CCT) + cmfs, _illuminant = handle_spectral_arguments(cmfs) + radiance = ( + planck_law( + cmfs.wavelengths * 1e-9, + xp_reshape(CCT, (-1,), xp=xp), + ) + * 1e-9 + ) + if radiance.ndim >= 2: + radiance = xp_matrix_transpose(radiance, xp=xp) + XYZ = msds_to_XYZ_integration( - np.transpose(planck_law(cmfs.wavelengths * 1e-9, np.ravel(CCT)) * 1e-9), + radiance, cmfs, shape=cmfs.shape, ) @@ -174,4 +180,4 @@ def CCT_to_uv_Planck1900( UVW = XYZ_to_UCS(XYZ) uv = UCS_to_uv(UVW) - return np.reshape(uv, [*list(CCT.shape), 2]) + return xp_reshape(uv, [*list(CCT.shape), 2], xp=xp) diff --git a/colour/temperature/robertson1968.py b/colour/temperature/robertson1968.py index eeb90940e4..67a11c4d16 100644 --- a/colour/temperature/robertson1968.py +++ b/colour/temperature/robertson1968.py @@ -34,14 +34,21 @@ import typing from dataclasses import dataclass -import numpy as np - from colour.algebra import sdiv, sdiv_mode if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat -from colour.utilities import as_float_array, tsplit +from colour.constants import DTYPE_INT_DEFAULT +from colour.utilities import ( + array_namespace, + as_float_array, + tsplit, + tstack, + xp_as_float_array, + xp_astype, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -220,28 +227,32 @@ def uv_to_CCT_Robertson1968(uv: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> uv = np.array([0.193741375998230, 0.315221043940594]) >>> uv_to_CCT_Robertson1968(uv) # doctest: +ELLIPSIS array([6.5000162...e+03, 8.3333289...e-03]) """ uv = as_float_array(uv) + + xp = array_namespace(uv) + shape = uv.shape - uv = uv.reshape(-1, 2) + uv = xp_reshape(uv, (-1, 2), xp=xp) r_itl, u_itl, v_itl, t_itl = tsplit( - np.array(DATA_ISOTEMPERATURE_LINES_ROBERTSON1968) + xp_as_float_array(DATA_ISOTEMPERATURE_LINES_ROBERTSON1968, xp=xp, like=uv) ) # Normalized direction vectors - length = np.hypot(1.0, t_itl) + length = xp.hypot(xp_as_float_array(1.0, xp=xp, like=uv), t_itl) du_itl = 1.0 / length dv_itl = t_itl / length # Vectorized computation for all UV pairs at once u, v = tsplit(uv) - u = u[:, np.newaxis] # Shape (N, 1) - v = v[:, np.newaxis] # Shape (N, 1) + u = u[:, None] # Shape (N, 1) + v = v[:, None] # Shape (N, 1) # Compute distances for all UV pairs against all isotemperature lines # Broadcasting: (N, 1) - (30,) = (N, 30) @@ -250,15 +261,15 @@ def uv_to_CCT_Robertson1968(uv: ArrayLike) -> NDArrayFloat: dt = -uu * dv_itl[1:] + vv * du_itl[1:] # Shape (N, 30) # Find the first crossing point for each UV pair - mask = dt <= 0 - i = np.where(np.any(mask, axis=1), np.argmax(mask, axis=1) + 1, 30) + mask = xp_astype(dt <= 0, DTYPE_INT_DEFAULT, xp=xp) + i = xp.where(xp.any(mask, axis=1), xp.argmax(mask, axis=1) + 1, 30) # Interpolation factor - idx = np.arange(len(i)) - dt_current = -np.minimum(dt[idx, i - 1], 0.0) + idx = xp.arange(len(i)) + dt_current = -xp.clip(dt[idx, i - 1], max=0.0) dt_previous = dt[idx, i - 2] - f = np.where( - i == 1, 0.0, np.where(i > 1, dt_current / (dt_previous + dt_current), 0.0) + f = xp.where( + i == 1, 0.0, xp.where(i > 1, dt_current / (dt_previous + dt_current), 0.0) ) # Interpolate temperature @@ -273,18 +284,18 @@ def uv_to_CCT_Robertson1968(uv: ArrayLike) -> NDArrayFloat: dv_i = dv_itl[i] * (1 - f) + dv_itl[i - 1] * f # Normalize interpolated direction - length_i = np.hypot(du_i, dv_i) + length_i = xp.hypot(du_i, dv_i) du_i /= length_i dv_i /= length_i # Calculate D_uv - uu = u.ravel() - u_i - vv = v.ravel() - v_i + uu = xp_reshape(u, (-1,), xp=xp) - u_i + vv = xp_reshape(v, (-1,), xp=xp) - v_i D_uv = uu * du_i + vv * dv_i - result = np.stack([T, -D_uv], axis=-1) + result = tstack([T, -D_uv]) - return result.reshape(shape) + return xp_reshape(result, shape, xp=xp) def CCT_to_uv_Robertson1968(CCT_D_uv: ArrayLike) -> NDArrayFloat: @@ -309,21 +320,25 @@ def CCT_to_uv_Robertson1968(CCT_D_uv: ArrayLike) -> NDArrayFloat: Examples -------- + >>> import numpy as np >>> CCT_D_uv = np.array([6500.0081378199056, 0.008333331244225]) >>> CCT_to_uv_Robertson1968(CCT_D_uv) # doctest: +ELLIPSIS array([0.1937413..., 0.3152210...]) """ CCT_D_uv = as_float_array(CCT_D_uv) + + xp = array_namespace(CCT_D_uv) + shape = CCT_D_uv.shape - CCT_D_uv = CCT_D_uv.reshape(-1, 2) + CCT_D_uv = xp_reshape(CCT_D_uv, (-1, 2), xp=xp) r_itl, u_itl, v_itl, t_itl = tsplit( - np.array(DATA_ISOTEMPERATURE_LINES_ROBERTSON1968) + xp_as_float_array(DATA_ISOTEMPERATURE_LINES_ROBERTSON1968, xp=xp, like=CCT_D_uv) ) # Precompute normalized direction vectors - length = np.hypot(1.0, t_itl) + length = xp.hypot(xp_as_float_array(1.0, xp=xp, like=CCT_D_uv), t_itl) du_itl = 1.0 / length dv_itl = t_itl / length @@ -332,8 +347,8 @@ def CCT_to_uv_Robertson1968(CCT_D_uv: ArrayLike) -> NDArrayFloat: r = CCT_to_mired(CCT) # Find the isotemperature range containing r for all values - mask = r[:, np.newaxis] < r_itl[1:] - i = np.where(np.any(mask, axis=1), np.argmax(mask, axis=1), 29) + mask = xp_astype(r[:, None] < r_itl[1:], DTYPE_INT_DEFAULT, xp=xp) + i = xp.where(xp.any(mask, axis=1), xp.argmax(mask, axis=1), 29) # Interpolation factor f = (r_itl[i + 1] - r) / (r_itl[i + 1] - r_itl[i]) @@ -347,7 +362,7 @@ def CCT_to_uv_Robertson1968(CCT_D_uv: ArrayLike) -> NDArrayFloat: dv_i = dv_itl[i] * f + dv_itl[i + 1] * (1 - f) # Normalize interpolated direction - length_i = np.hypot(du_i, dv_i) + length_i = xp.hypot(du_i, dv_i) du_i /= length_i dv_i /= length_i @@ -355,6 +370,6 @@ def CCT_to_uv_Robertson1968(CCT_D_uv: ArrayLike) -> NDArrayFloat: u += du_i * -D_uv v += dv_i * -D_uv - result = np.stack([u, v], axis=-1) + result = tstack([u, v]) - return result.reshape(shape) + return xp_reshape(result, shape, xp=xp) diff --git a/colour/temperature/tests/test__init__.py b/colour/temperature/tests/test__init__.py index ab3cd7e047..67a87af81b 100644 --- a/colour/temperature/tests/test__init__.py +++ b/colour/temperature/tests/test__init__.py @@ -2,10 +2,15 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_xy, xy_to_CCT +from colour.utilities import xp_as_array, xp_assert_close __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -25,30 +30,30 @@ class TestXy_to_CCT: Define :func:`colour.temperature.xy_to_CCT` definition unit tests methods. """ - def test_xy_to_CCT(self) -> None: + def test_xy_to_CCT(self, xp: ModuleType) -> None: """Test :func:`colour.temperature.xy_to_CCT` definition.""" - xy = np.array([0.31270, 0.32900]) + xy = xp_as_array([0.31270, 0.32900], xp=xp) # Test default method (CIE Illuminant D Series) - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT(xy), 6508.1175148, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) # Test Hernandez 1999 method - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT(xy, "Hernandez 1999"), 6500.7420431, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) # Test McCamy 1992 method - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT(xy, "McCamy 1992"), 6505.08059131, - atol=0.01, + atol=TOLERANCE_ABSOLUTE_TESTS * 100000, ) @@ -57,26 +62,26 @@ class TestCCT_to_xy: Define :func:`colour.temperature.CCT_to_xy` definition unit tests methods. """ - def test_CCT_to_xy(self) -> None: + def test_CCT_to_xy(self, xp: ModuleType) -> None: """Test :func:`colour.temperature.CCT_to_xy` definition.""" # Test default method (CIE Illuminant D Series) - np.testing.assert_allclose( - CCT_to_xy(6500), - np.array([0.31277888, 0.3291835]), + xp_assert_close( + CCT_to_xy(xp_as_array([6500], xp=xp)), + [[0.31277888, 0.3291835]], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test explicit CIE Illuminant D Series method - np.testing.assert_allclose( - CCT_to_xy(6500, method="CIE Illuminant D Series"), - np.array([0.31277888, 0.3291835]), + xp_assert_close( + CCT_to_xy(xp_as_array([6500], xp=xp), method="CIE Illuminant D Series"), + [[0.31277888, 0.3291835]], atol=TOLERANCE_ABSOLUTE_TESTS, ) # Test Hernandez 1999 method - np.testing.assert_allclose( - CCT_to_xy(6500, "Hernandez 1999"), - np.array([0.31191663, 0.33419]), + xp_assert_close( + CCT_to_xy(xp_as_array([6500], xp=xp), "Hernandez 1999"), + [[0.31271354, 0.32900208]], atol=TOLERANCE_ABSOLUTE_TESTS, ) diff --git a/colour/temperature/tests/test_cie_d.py b/colour/temperature/tests/test_cie_d.py index c2249cda27..32b0b20a8b 100644 --- a/colour/temperature/tests/test_cie_d.py +++ b/colour/temperature/tests/test_cie_d.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_xy_CIE_D, xy_to_CCT_CIE_D -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,37 +41,34 @@ class TestXy_to_CCT_CIE_D: tests methods. """ - def test_xy_to_CCT_CIE_D(self) -> None: + def test_xy_to_CCT_CIE_D(self, xp: ModuleType) -> None: """Test :func:`colour.temperature.cie_d.xy_to_CCT_CIE_D` definition.""" - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT_CIE_D( - np.array([0.382343625000000, 0.383766261015578]), - {"method": "Nelder-Mead"}, + xp_as_array([0.382343625000000, 0.383766261015578], xp=xp), ), 4000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT_CIE_D( - np.array([0.305357431486880, 0.321646345474552]), - {"method": "Nelder-Mead"}, + xp_as_array([0.305357431486880, 0.321646345474552], xp=xp), ), 7000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT_CIE_D( - np.array([0.24985367, 0.254799464210944]), - {"method": "Nelder-Mead"}, + xp_as_array([0.24985367, 0.254799464210944], xp=xp), ), 25000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_CCT_CIE_D(self) -> None: + def test_n_dimensional_xy_to_CCT_CIE_D(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.cie_d.xy_to_CCT_CIE_D` definition n-dimensional arrays support. @@ -68,20 +77,16 @@ def test_n_dimensional_xy_to_CCT_CIE_D(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.382343625000000, 0.383766261015578]) - CCT = xy_to_CCT_CIE_D(xy) + xy = xp_as_array([0.382343625000000, 0.383766261015578], xp=xp) + CCT = as_ndarray(xy_to_CCT_CIE_D(xy)) - xy = np.tile(xy, (6, 1)) - CCT = np.tile(CCT, 6) - np.testing.assert_allclose( - xy_to_CCT_CIE_D(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xp_assert_close(xy_to_CCT_CIE_D(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - CCT = np.reshape(CCT, (2, 3)) - np.testing.assert_allclose( - xy_to_CCT_CIE_D(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xp_assert_close(xy_to_CCT_CIE_D(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_CCT_CIE_D(self) -> None: @@ -104,47 +109,43 @@ class TestCCT_to_xy_CIE_D: unit tests methods. """ - def test_CCT_to_xy_CIE_D(self) -> None: + def test_CCT_to_xy_CIE_D(self, xp: ModuleType) -> None: """Test :func:`colour.temperature.cie_d.CCT_to_xy_CIE_D` definition.""" - np.testing.assert_allclose( - CCT_to_xy_CIE_D(4000), - np.array([0.382343625000000, 0.383766261015578]), + xp_assert_close( + CCT_to_xy_CIE_D(xp_as_array([4000], xp=xp)), + [[0.382343625000000, 0.383766261015578]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_CIE_D(7000), - np.array([0.305357431486880, 0.321646345474552]), + xp_assert_close( + CCT_to_xy_CIE_D(xp_as_array([7000], xp=xp)), + [[0.305357431486880, 0.321646345474552]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_CIE_D(25000), - np.array([0.24985367, 0.254799464210944]), + xp_assert_close( + CCT_to_xy_CIE_D(xp_as_array([25000], xp=xp)), + [[0.24985367, 0.254799464210944]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_xy_CIE_D(self) -> None: + def test_n_dimensional_CCT_to_xy_CIE_D(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.cie_d.CCT_to_xy_CIE_D` definition n-dimensional arrays support. """ CCT = 4000 - xy = CCT_to_xy_CIE_D(CCT) + xy = as_ndarray(CCT_to_xy_CIE_D(CCT)) - CCT = np.tile(CCT, 6) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose( - CCT_to_xy_CIE_D(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(CCT_to_xy_CIE_D(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT = np.reshape(CCT, (2, 3)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_xy_CIE_D(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_xy_CIE_D(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_xy_CIE_D(self) -> None: diff --git a/colour/temperature/tests/test_common.py b/colour/temperature/tests/test_common.py new file mode 100644 index 0000000000..0dd9d99a5d --- /dev/null +++ b/colour/temperature/tests/test_common.py @@ -0,0 +1,298 @@ +"""Define the unit tests for the :mod:`colour.temperature.common` module.""" + +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType, NDArrayFloat + +import numpy as np +import pytest + +from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.temperature import solve_CCT_Newton, solve_xy_Newton, x0_CCT_grid +from colour.utilities import ( + ColourUsageWarning, + tstack, + xp_as_array, + xp_assert_close, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "TestX0_CCT_grid", + "TestSolve_CCT_Newton", + "TestSolve_xy_Newton", +] + + +def _forward_linear(CCT: NDArrayFloat) -> NDArrayFloat: + """ + Smooth, monotonic synthetic forward where *Newton* converges in a + single iteration. + + .. math:: + + f(T) = \\left(10^{-4}\\,T,\\; 10^{-4}\\,T + 0.1\\right) + """ + + return tstack([CCT * 1.0e-4, CCT * 1.0e-4 + 0.1]) + + +def _forward_rational(CCT: NDArrayFloat) -> NDArrayFloat: + """ + Highly non-linear synthetic forward with rational structure that + forces the per-sample backtracking line search to engage from a + sufficiently distant initial guess. + + .. math:: + + f(T) = \\left(\\frac{1000}{T + 500},\\; \\frac{T}{T + 5000}\\right) + """ + + return tstack([1000.0 / (CCT + 500.0), CCT / (CCT + 5000.0)]) + + +def _forward_xy_linear(xy: NDArrayFloat) -> NDArrayFloat: + """ + Smooth, monotonic synthetic 2-D-to-1-D forward where *Newton* converges + in a single iteration. + + .. math:: + + f(x, y) = x + 2 y + """ + + return xy[..., 0] + 2.0 * xy[..., 1] + + +def _forward_xy_rational(xy: NDArrayFloat) -> NDArrayFloat: + """ + Non-linear synthetic 2-D-to-1-D forward with rational structure that + forces the per-sample backtracking line search to engage from a + sufficiently distant initial guess. + + .. math:: + + f(x, y) = \\frac{1000}{x + y + 0.5} + """ + + return 1000.0 / (xy[..., 0] + xy[..., 1] + 0.5) + + +class TestX0_CCT_grid: + """ + Define :func:`colour.temperature.common.x0_CCT_grid` definition unit + tests methods. + """ + + def test_x0_CCT_grid(self, xp: ModuleType) -> None: + """Test :func:`colour.temperature.common.x0_CCT_grid` definition.""" + + # 25 samples over [1000, 25000] yields a step of 1000 K so the + # nearest grid sample to the forward image at ``T = 4321 K`` is + # ``T = 4000 K``. + target = _forward_linear(xp_as_array([4321.0], xp=xp)) + xp_assert_close( + x0_CCT_grid(_forward_linear, target, (1000.0, 25000.0), samples=25), + xp_as_array([4000.0], xp=xp), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # A denser grid lands the initial guess closer to the true root. + coarse = x0_CCT_grid(_forward_linear, target, (1000.0, 25000.0), samples=25) + dense = x0_CCT_grid(_forward_linear, target, (1000.0, 25000.0), samples=2401) + assert abs(coarse.item() - 4321.0) > abs(dense.item() - 4321.0) + + def test_n_dimensional_x0_CCT_grid(self, xp: ModuleType) -> None: + """ + Test :func:`colour.temperature.common.x0_CCT_grid` definition + n-dimensional arrays support. + """ + + target = _forward_linear( + xp_as_array([[1500.0, 6500.0], [10000.0, 25000.0]], xp=xp) + ) + x0 = x0_CCT_grid(_forward_linear, target, (1000.0, 25000.0)) + assert x0.shape == (2, 2) + + +class TestSolve_CCT_Newton: + """ + Define :func:`colour.temperature.common.solve_CCT_Newton` definition + unit tests methods. + """ + + @pytest.mark.mps_tolerance_absolute(1) + def test_solve_CCT_Newton(self, xp: ModuleType) -> None: + """ + Test :func:`colour.temperature.common.solve_CCT_Newton` definition. + """ + + # Convergence on a smooth monotonic forward. + for CCT_reference in (1500.0, 6500.0, 15000.0, 25000.0): + target = _forward_linear(xp_as_array([CCT_reference], xp=xp)) + xp_assert_close( + solve_CCT_Newton(_forward_linear, target), + xp_as_array([CCT_reference], xp=xp), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # Convergence on a highly non-linear rational forward; the + # backtracking line search must engage to keep the iteration in + # the trust region. + for CCT_reference in (1500.0, 6500.0, 15000.0): + target = _forward_rational(xp_as_array([CCT_reference], xp=xp)) + xp_assert_close( + solve_CCT_Newton(_forward_rational, target), + xp_as_array([CCT_reference], xp=xp), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # Explicit per-sample initial guess. + CCT_reference = xp_as_array([1500.0, 6500.0, 15000.0], xp=xp) + target = _forward_rational(CCT_reference) + xp_assert_close( + solve_CCT_Newton( + _forward_rational, + target, + x0=xp_as_array([2000.0, 7000.0, 14000.0], xp=xp), + ), + CCT_reference, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # ``tolerance``, ``newton_iterations`` and ``backtrack_iterations`` + # plumbing. + CCT_reference = xp_as_array([6500.0], xp=xp) + target = _forward_rational(CCT_reference) + xp_assert_close( + solve_CCT_Newton( + _forward_rational, + target, + tolerance=1e-12, + newton_iterations=50, + backtrack_iterations=30, + ), + CCT_reference, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # ``ColourUsageWarning`` raised when the iteration budget is + # exhausted before ``tolerance`` is met. + with pytest.warns(ColourUsageWarning): + solve_CCT_Newton( + _forward_linear, + _forward_linear(np.array(6500.0)), + newton_iterations=0, + ) + + def test_n_dimensional_solve_CCT_Newton(self, xp: ModuleType) -> None: + """ + Test :func:`colour.temperature.common.solve_CCT_Newton` definition + n-dimensional arrays support. + """ + + CCT_reference = xp_as_array([[1500.0, 6500.0], [15000.0, 25000.0]], xp=xp) + target = _forward_linear(CCT_reference) + xp_assert_close( + solve_CCT_Newton(_forward_linear, target), + CCT_reference, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestSolve_xy_Newton: + """ + Define :func:`colour.temperature.common.solve_xy_Newton` definition + unit tests methods. + """ + + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_solve_xy_Newton(self, xp: ModuleType) -> None: + """ + Test :func:`colour.temperature.common.solve_xy_Newton` definition. + """ + + # Convergence on a smooth linear forward; the regularisation picks + # the unique level-set point closest to ``reference_xy``. + for target_value in (0.5, 1.0, 1.5): + target = xp_as_array([target_value], xp=xp) + xp_assert_close( + _forward_xy_linear(solve_xy_Newton(_forward_xy_linear, target)), + target, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # Convergence on a non-linear rational forward; the backtracking + # line search must engage to keep the iteration in the trust + # region. + for target_value in (500.0, 1000.0, 1500.0): + target = xp_as_array([target_value], xp=xp) + xp_assert_close( + _forward_xy_rational(solve_xy_Newton(_forward_xy_rational, target)), + target, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # Explicit per-sample ``x0`` and ``reference_xy``. + target = xp_as_array([1.0, 1.5, 2.0], xp=xp) + xp_assert_close( + _forward_xy_linear( + solve_xy_Newton( + _forward_xy_linear, + target, + x0=xp_as_array([[0.5, 0.5], [0.4, 0.6], [0.3, 0.7]], xp=xp), + reference_xy=xp_as_array([0.4, 0.4], xp=xp), + ) + ), + target, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # ``reference_weight``, ``tolerance``, ``newton_iterations`` and + # ``backtrack_iterations`` plumbing. + target = xp_as_array([1000.0], xp=xp) + xp_assert_close( + _forward_xy_rational( + solve_xy_Newton( + _forward_xy_rational, + target, + reference_weight=1e-8, + tolerance=1e-12, + newton_iterations=50, + backtrack_iterations=30, + ) + ), + target, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # ``ColourUsageWarning`` raised when the iteration budget is + # exhausted before ``tolerance`` is met. + with pytest.warns(ColourUsageWarning): + solve_xy_Newton( + _forward_xy_linear, + xp_as_array([0.5], xp=xp), + newton_iterations=0, + ) + + def test_n_dimensional_solve_xy_Newton(self, xp: ModuleType) -> None: + """ + Test :func:`colour.temperature.common.solve_xy_Newton` definition + n-dimensional arrays support. + """ + + target = xp_as_array([[0.5, 1.0], [1.5, 2.0]], xp=xp) + xp_assert_close( + _forward_xy_linear(solve_xy_Newton(_forward_xy_linear, target)), + target, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) diff --git a/colour/temperature/tests/test_hernandez1999.py b/colour/temperature/tests/test_hernandez1999.py index 86005f781e..9f2c4fe63e 100644 --- a/colour/temperature/tests/test_hernandez1999.py +++ b/colour/temperature/tests/test_hernandez1999.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_xy_Hernandez1999, xy_to_CCT_Hernandez1999 -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,31 +42,34 @@ class Testxy_to_CCT_Hernandez1999: definition unit tests methods. """ - def test_xy_to_CCT_Hernandez1999(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_xy_to_CCT_Hernandez1999(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.hernandez1999.xy_to_CCT_McCamy1992` definition. """ - np.testing.assert_allclose( - xy_to_CCT_Hernandez1999(np.array([0.31270, 0.32900])), + xp_assert_close( + xy_to_CCT_Hernandez1999(xp_as_array([0.31270, 0.32900], xp=xp)), 6500.74204318, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_CCT_Hernandez1999(np.array([0.44757, 0.40745])), + xp_assert_close( + xy_to_CCT_Hernandez1999(xp_as_array([0.44757, 0.40745], xp=xp)), 2790.64222533, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_CCT_Hernandez1999(np.array([0.244162248213914, 0.240333674758318])), + xp_assert_close( + xy_to_CCT_Hernandez1999( + xp_as_array([0.244162248213914, 0.240333674758318], xp=xp) + ), 64448.11092565, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_CCT_Hernandez1999(self) -> None: + def test_n_dimensional_xy_to_CCT_Hernandez1999(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.hernandez1999.xy_to_CCT_Hernandez1999` definition n-dimensional arrays support. @@ -62,20 +78,16 @@ def test_n_dimensional_xy_to_CCT_Hernandez1999(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.31270, 0.32900]) - CCT = xy_to_CCT_Hernandez1999(xy) + xy = xp_as_array([0.31270, 0.32900], xp=xp) + CCT = as_ndarray(xy_to_CCT_Hernandez1999(xy)) - xy = np.tile(xy, (6, 1)) - CCT = np.tile(CCT, 6) - np.testing.assert_allclose( - xy_to_CCT_Hernandez1999(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xp_assert_close(xy_to_CCT_Hernandez1999(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) - CCT = np.reshape(CCT, (2, 3)) - np.testing.assert_allclose( - xy_to_CCT_Hernandez1999(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xp_assert_close(xy_to_CCT_Hernandez1999(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_CCT_Hernandez1999(self) -> None: @@ -107,25 +119,25 @@ def test_CCT_to_xy_Hernandez1999(self) -> None: if not is_scipy_installed(): # pragma: no cover return - np.testing.assert_allclose( - CCT_to_xy_Hernandez1999(6500.74204318, {"method": "Nelder-Mead"}), - np.array([0.31269943, 0.32900373]), + xp_assert_close( + CCT_to_xy_Hernandez1999(6500.74204318), + [0.31270000, 0.32900000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_Hernandez1999(2790.64222533, {"method": "Nelder-Mead"}), - np.array([0.42864308, 0.36754776]), + xp_assert_close( + CCT_to_xy_Hernandez1999(2790.64222533), + [0.39242193, 0.29118533], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_Hernandez1999(64448.11092565, {"method": "Nelder-Mead"}), - np.array([0.08269106, 0.36612620]), + xp_assert_close( + CCT_to_xy_Hernandez1999(64448.11092565), + [0.24382815, 0.24059395], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_xy_Hernandez1999(self) -> None: + def test_n_dimensional_CCT_to_xy_Hernandez1999(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.hernandez1999.CCT_to_xy_Hernandez1999` definition n-dimensional arrays support. @@ -135,19 +147,15 @@ def test_n_dimensional_CCT_to_xy_Hernandez1999(self) -> None: return CCT = 6500.74204318 - xy = CCT_to_xy_Hernandez1999(CCT) + xy = as_ndarray(CCT_to_xy_Hernandez1999(CCT)) - CCT = np.tile(CCT, 6) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose( - CCT_to_xy_Hernandez1999(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(CCT_to_xy_Hernandez1999(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT = np.reshape(CCT, (2, 3)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_xy_Hernandez1999(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_xy_Hernandez1999(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_xy_Hernandez1999(self) -> None: diff --git a/colour/temperature/tests/test_kang2002.py b/colour/temperature/tests/test_kang2002.py index 7825ee3e15..51f2b4514b 100644 --- a/colour/temperature/tests/test_kang2002.py +++ b/colour/temperature/tests/test_kang2002.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_xy_Kang2002, xy_to_CCT_Kang2002 -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,40 +42,38 @@ class TestXy_to_CCT_Kang2002: definition unit tests methods. """ - def test_xy_to_CCT_Kang2002(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_xy_to_CCT_Kang2002(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.kang2002.xy_to_CCT_Kang2002` definition. """ - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT_Kang2002( - np.array([0.380528282812500, 0.376733530961114]), - {"method": "Nelder-Mead"}, + xp_as_array([0.380528282812500, 0.376733530961114], xp=xp), ), 4000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT_Kang2002( - np.array([0.306374019533528, 0.316552869726577]), - {"method": "Nelder-Mead"}, + xp_as_array([0.306374019533528, 0.316552869726577], xp=xp), ), 7000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( xy_to_CCT_Kang2002( - np.array([0.252472994438400, 0.252254791243654]), - {"method": "Nelder-Mead"}, + xp_as_array([0.252472994438400, 0.252254791243654], xp=xp), ), 25000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_CCT_Kang2002(self) -> None: + def test_n_dimensional_xy_to_CCT_Kang2002(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.kang2002.xy_to_CCT_Kang2002` definition n-dimensional arrays support. @@ -71,20 +82,16 @@ def test_n_dimensional_xy_to_CCT_Kang2002(self) -> None: if not is_scipy_installed(): # pragma: no cover return - uv = np.array([0.380528282812500, 0.376733530961114]) - CCT = xy_to_CCT_Kang2002(uv) + uv = xp_as_array([0.380528282812500, 0.376733530961114], xp=xp) + CCT = as_ndarray(xy_to_CCT_Kang2002(uv)) - uv = np.tile(uv, (6, 1)) - CCT = np.tile(CCT, 6) - np.testing.assert_allclose( - xy_to_CCT_Kang2002(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xp_assert_close(xy_to_CCT_Kang2002(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - CCT = np.reshape(CCT, (2, 3)) - np.testing.assert_allclose( - xy_to_CCT_Kang2002(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xp_assert_close(xy_to_CCT_Kang2002(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_CCT_Kang2002(self) -> None: @@ -107,50 +114,46 @@ class TestCCT_to_xy_Kang2002: unit tests methods. """ - def test_CCT_to_xy_Kang2002(self) -> None: + def test_CCT_to_xy_Kang2002(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.kang2002.CCT_to_xy_Kang2002` definition. """ - np.testing.assert_allclose( - CCT_to_xy_Kang2002(4000), - np.array([0.380528282812500, 0.376733530961114]), + xp_assert_close( + CCT_to_xy_Kang2002(xp_as_array([4000], xp=xp)), + [[0.380528282812500, 0.376733530961114]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_Kang2002(7000), - np.array([0.306374019533528, 0.316552869726577]), + xp_assert_close( + CCT_to_xy_Kang2002(xp_as_array([7000], xp=xp)), + [[0.306374019533528, 0.316552869726577]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_Kang2002(25000), - np.array([0.252472994438400, 0.252254791243654]), + xp_assert_close( + CCT_to_xy_Kang2002(xp_as_array([25000], xp=xp)), + [[0.252472994438400, 0.252254791243654]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_xy_Kang2002(self) -> None: + def test_n_dimensional_CCT_to_xy_Kang2002(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.kang2002.CCT_to_xy_Kang2002` definition n-dimensional arrays support. """ CCT = 4000 - xy = CCT_to_xy_Kang2002(CCT) + xy = as_ndarray(CCT_to_xy_Kang2002(CCT)) - CCT = np.tile(CCT, 6) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose( - CCT_to_xy_Kang2002(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(CCT_to_xy_Kang2002(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT = np.reshape(CCT, (2, 3)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_xy_Kang2002(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_xy_Kang2002(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_xy_Kang2002(self) -> None: diff --git a/colour/temperature/tests/test_krystek1985.py b/colour/temperature/tests/test_krystek1985.py index 37164591a5..1b1808e369 100644 --- a/colour/temperature/tests/test_krystek1985.py +++ b/colour/temperature/tests/test_krystek1985.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_uv_Krystek1985, uv_to_CCT_Krystek1985 -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -28,40 +41,38 @@ class TestUv_to_CCT_Krystek1985: definition unit tests methods. """ - def test_uv_to_CCT_Krystek1985(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_uv_to_CCT_Krystek1985(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.krystek1985.uv_to_CCT_Krystek1985` definition. """ - np.testing.assert_allclose( + xp_assert_close( uv_to_CCT_Krystek1985( - np.array([0.448087794140145, 0.354731965027727]), - {"method": "Nelder-Mead"}, + xp_as_array([0.448087794140145, 0.354731965027727], xp=xp), ), 1000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_CCT_Krystek1985( - np.array([0.198152565091092, 0.307023596915037]), - {"method": "Nelder-Mead"}, + xp_as_array([0.198152565091092, 0.307023596915037], xp=xp), ), 7000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_CCT_Krystek1985( - np.array([0.185675876767054, 0.282233658593898]), - {"method": "Nelder-Mead"}, + xp_as_array([0.185675876767054, 0.282233658593898], xp=xp), ), 15000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_uv_to_CCT_Krystek1985(self) -> None: + def test_n_dimensional_uv_to_CCT_Krystek1985(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.krystek1985.uv_to_CCT_Krystek1985` definition n-dimensional arrays support. @@ -70,20 +81,16 @@ def test_n_dimensional_uv_to_CCT_Krystek1985(self) -> None: if not is_scipy_installed(): # pragma: no cover return - uv = np.array([0.198152565091092, 0.307023596915037]) - CCT = uv_to_CCT_Krystek1985(uv) + uv = xp_as_array([0.198152565091092, 0.307023596915037], xp=xp) + CCT = as_ndarray(uv_to_CCT_Krystek1985(uv)) - uv = np.tile(uv, (6, 1)) - CCT = np.tile(CCT, 6) - np.testing.assert_allclose( - uv_to_CCT_Krystek1985(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xp_assert_close(uv_to_CCT_Krystek1985(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - CCT = np.reshape(CCT, (2, 3)) - np.testing.assert_allclose( - uv_to_CCT_Krystek1985(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xp_assert_close(uv_to_CCT_Krystek1985(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_uv_to_CCT_Krystek1985(self) -> None: @@ -106,50 +113,46 @@ class TestCCT_to_uv_Krystek1985: definition unit tests methods. """ - def test_CCT_to_uv_Krystek1985(self) -> None: + def test_CCT_to_uv_Krystek1985(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.krystek1985.CCT_to_uv_Krystek1985` definition. """ - np.testing.assert_allclose( - CCT_to_uv_Krystek1985(1000), - np.array([0.448087794140145, 0.354731965027727]), + xp_assert_close( + CCT_to_uv_Krystek1985(xp_as_array([1000], xp=xp)), + [[0.448087794140145, 0.354731965027727]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_uv_Krystek1985(7000), - np.array([0.198152565091092, 0.307023596915037]), + xp_assert_close( + CCT_to_uv_Krystek1985(xp_as_array([7000], xp=xp)), + [[0.198152565091092, 0.307023596915037]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_uv_Krystek1985(15000), - np.array([0.185675876767054, 0.282233658593898]), + xp_assert_close( + CCT_to_uv_Krystek1985(xp_as_array([15000], xp=xp)), + [[0.185675876767054, 0.282233658593898]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_uv_Krystek1985(self) -> None: + def test_n_dimensional_CCT_to_uv_Krystek1985(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.krystek1985.CCT_to_uv_Krystek1985` definition n-dimensional arrays support. """ CCT = 7000 - uv = CCT_to_uv_Krystek1985(CCT) + uv = as_ndarray(CCT_to_uv_Krystek1985(CCT)) - CCT = np.tile(CCT, 6) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose( - CCT_to_uv_Krystek1985(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(CCT_to_uv_Krystek1985(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT = np.reshape(CCT, (2, 3)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_uv_Krystek1985(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_uv_Krystek1985(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_uv_Krystek1985(self) -> None: diff --git a/colour/temperature/tests/test_mccamy1992.py b/colour/temperature/tests/test_mccamy1992.py index 4f73d556c9..525b1d6994 100644 --- a/colour/temperature/tests/test_mccamy1992.py +++ b/colour/temperature/tests/test_mccamy1992.py @@ -2,13 +2,25 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_xy_McCamy1992, xy_to_CCT_McCamy1992 -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,31 +41,33 @@ class Testxy_to_CCT_McCamy1992: definition unit tests methods. """ - def test_xy_to_CCT_McCamy1992(self) -> None: + def test_xy_to_CCT_McCamy1992(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.mccamy1992.xy_to_CCT_McCamy1992` definition. """ - np.testing.assert_allclose( - xy_to_CCT_McCamy1992(np.array([0.31270, 0.32900])), + xp_assert_close( + xy_to_CCT_McCamy1992(xp_as_array([0.31270, 0.32900], xp=xp)), 6505.08059131, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_CCT_McCamy1992(np.array([0.44757, 0.40745])), + xp_assert_close( + xy_to_CCT_McCamy1992(xp_as_array([0.44757, 0.40745], xp=xp)), 2857.28961266, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - xy_to_CCT_McCamy1992(np.array([0.252520939374083, 0.252220883926284])), + xp_assert_close( + xy_to_CCT_McCamy1992( + xp_as_array([0.252520939374083, 0.252220883926284], xp=xp) + ), 19501.61953130, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_xy_to_CCT_McCamy1992(self) -> None: + def test_n_dimensional_xy_to_CCT_McCamy1992(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.mccamy1992.xy_to_CCT_McCamy1992` definition n-dimensional arrays support. @@ -62,20 +76,16 @@ def test_n_dimensional_xy_to_CCT_McCamy1992(self) -> None: if not is_scipy_installed(): # pragma: no cover return - xy = np.array([0.31270, 0.32900]) - CCT = xy_to_CCT_McCamy1992(xy) + xy = xp_as_array([0.31270, 0.32900], xp=xp) + CCT = as_ndarray(xy_to_CCT_McCamy1992(xy)) - xy = np.tile(xy, (6, 1)) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) CCT = np.tile(CCT, 6) - np.testing.assert_allclose( - xy_to_CCT_McCamy1992(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(xy_to_CCT_McCamy1992(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - xy = np.reshape(xy, (2, 3, 2)) + xy = xp_reshape(xp_as_array(xy, xp=xp), (2, 3, 2), xp=xp) CCT = np.reshape(CCT, (2, 3)) - np.testing.assert_allclose( - xy_to_CCT_McCamy1992(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xp_assert_close(xy_to_CCT_McCamy1992(xy), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_xy_to_CCT_McCamy1992(self) -> None: @@ -107,25 +117,25 @@ def test_CCT_to_xy_McCamy1992(self) -> None: if not is_scipy_installed(): # pragma: no cover return - np.testing.assert_allclose( - CCT_to_xy_McCamy1992(6505.08059131, {"method": "Nelder-Mead"}), - np.array([0.31269945, 0.32900411]), + xp_assert_close( + CCT_to_xy_McCamy1992(6505.08059131), + [0.31270000, 0.32900000], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_McCamy1992(2857.28961266, {"method": "Nelder-Mead"}), - np.array([0.42350314, 0.36129253]), + xp_assert_close( + CCT_to_xy_McCamy1992(2857.28961266), + [0.38658009, 0.29047836], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_xy_McCamy1992(19501.61953130, {"method": "Nelder-Mead"}), - np.array([0.11173782, 0.36987375]), + xp_assert_close( + CCT_to_xy_McCamy1992(19501.61953130), + [0.25017434, 0.25418195], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_xy_McCamy1992(self) -> None: + def test_n_dimensional_CCT_to_xy_McCamy1992(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.mccamy1992.CCT_to_xy_McCamy1992` definition n-dimensional arrays support. @@ -135,19 +145,15 @@ def test_n_dimensional_CCT_to_xy_McCamy1992(self) -> None: return CCT = 6505.08059131 - xy = CCT_to_xy_McCamy1992(CCT) + xy = as_ndarray(CCT_to_xy_McCamy1992(CCT)) CCT = np.tile(CCT, 6) - xy = np.tile(xy, (6, 1)) - np.testing.assert_allclose( - CCT_to_xy_McCamy1992(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp.tile(xp_as_array(xy, xp=xp), (6, 1)) + xp_assert_close(CCT_to_xy_McCamy1992(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) CCT = np.reshape(CCT, (2, 3)) - xy = np.reshape(xy, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_xy_McCamy1992(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS - ) + xy = xp_reshape(xy, (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_xy_McCamy1992(CCT), xy, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_xy_McCamy1992(self) -> None: diff --git a/colour/temperature/tests/test_ohno2013.py b/colour/temperature/tests/test_ohno2013.py index 944020362a..245cc9f5c3 100644 --- a/colour/temperature/tests/test_ohno2013.py +++ b/colour/temperature/tests/test_ohno2013.py @@ -2,9 +2,15 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.colorimetry import MSDS_CMFS from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -15,7 +21,13 @@ uv_to_CCT_Ohno2013, ) from colour.temperature.ohno2013 import planckian_table -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -42,41 +54,39 @@ class TestPlanckianTable: def test_planckian_table(self) -> None: """Test :func:`colour.temperature.ohno2013.planckian_table` definition.""" - np.testing.assert_allclose( + xp_assert_close( planckian_table( MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], 5000, 6000, 1.01, ), - np.array( - [ - [5.00000000e03, 2.11424442e-01, 3.23115810e-01], - [5.00100000e03, 2.11414166e-01, 3.23105716e-01], - [5.05101000e03, 2.10906941e-01, 3.22603850e-01], - [5.09965995e03, 2.10425840e-01, 3.22121155e-01], - [5.14875592e03, 2.09952257e-01, 3.21639518e-01], - [5.19830158e03, 2.09486095e-01, 3.21159015e-01], - [5.24830059e03, 2.09027261e-01, 3.20679719e-01], - [5.29875665e03, 2.08575658e-01, 3.20201701e-01], - [5.34967349e03, 2.08131192e-01, 3.19725033e-01], - [5.40105483e03, 2.07693769e-01, 3.19249784e-01], - [5.45290444e03, 2.07263296e-01, 3.18776019e-01], - [5.50522609e03, 2.06839680e-01, 3.18303806e-01], - [5.55802360e03, 2.06422828e-01, 3.17833209e-01], - [5.61130078e03, 2.06012650e-01, 3.17364290e-01], - [5.66506148e03, 2.05609054e-01, 3.16897111e-01], - [5.71930956e03, 2.05211949e-01, 3.16431730e-01], - [5.77404891e03, 2.04821246e-01, 3.15968207e-01], - [5.82928344e03, 2.04436856e-01, 3.15506598e-01], - [5.88501707e03, 2.04058690e-01, 3.15046958e-01], - [5.94125375e03, 2.03686660e-01, 3.14589340e-01], - [5.99799745e03, 2.03320679e-01, 3.14133796e-01], - [5.99900000e03, 2.03314296e-01, 3.14125803e-01], - [6.00000000e03, 2.03307932e-01, 3.14117832e-01], - ] - ), - atol=1e-6, + [ + [5.00000000e03, 2.11424442e-01, 3.23115810e-01], + [5.00100000e03, 2.11414166e-01, 3.23105716e-01], + [5.05101000e03, 2.10906941e-01, 3.22603850e-01], + [5.09965995e03, 2.10425840e-01, 3.22121155e-01], + [5.14875592e03, 2.09952257e-01, 3.21639518e-01], + [5.19830158e03, 2.09486095e-01, 3.21159015e-01], + [5.24830059e03, 2.09027261e-01, 3.20679719e-01], + [5.29875665e03, 2.08575658e-01, 3.20201701e-01], + [5.34967349e03, 2.08131192e-01, 3.19725033e-01], + [5.40105483e03, 2.07693769e-01, 3.19249784e-01], + [5.45290444e03, 2.07263296e-01, 3.18776019e-01], + [5.50522609e03, 2.06839680e-01, 3.18303806e-01], + [5.55802360e03, 2.06422828e-01, 3.17833209e-01], + [5.61130078e03, 2.06012650e-01, 3.17364290e-01], + [5.66506148e03, 2.05609054e-01, 3.16897111e-01], + [5.71930956e03, 2.05211949e-01, 3.16431730e-01], + [5.77404891e03, 2.04821246e-01, 3.15968207e-01], + [5.82928344e03, 2.04436856e-01, 3.15506598e-01], + [5.88501707e03, 2.04058690e-01, 3.15046958e-01], + [5.94125375e03, 2.03686660e-01, 3.14589340e-01], + [5.99799745e03, 2.03320679e-01, 3.14133796e-01], + [5.99900000e03, 2.03314296e-01, 3.14125803e-01], + [6.00000000e03, 2.03307932e-01, 3.14117832e-01], + ], + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) @@ -86,7 +96,8 @@ class TestUv_to_CCT_Ohno2013: unit tests methods. """ - def test_uv_to_CCT_Ohno2013(self) -> None: + @pytest.mark.mps_tolerance_absolute(1) + def test_uv_to_CCT_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.uv_to_CCT_Ohno2013` definition. @@ -97,48 +108,46 @@ def test_uv_to_CCT_Ohno2013(self) -> None: CCT, D_uv = np.meshgrid(CCT, D_uv) table_r = np.transpose((np.ravel(CCT), np.ravel(D_uv))) - table_t = uv_to_CCT_Ohno2013(CCT_to_uv_Ohno2013(table_r)) + table_t = as_ndarray(uv_to_CCT_Ohno2013(CCT_to_uv_Ohno2013(table_r))) - np.testing.assert_allclose(table_t[1, :], table_r[1, :], atol=1) + xp_assert_close( + table_t[1, :], table_r[1, :], atol=TOLERANCE_ABSOLUTE_TESTS * 10000000 + ) - np.testing.assert_allclose( - uv_to_CCT_Ohno2013(np.array([0.1978, 0.3122])), - np.array([6507.474788799616363, 0.003223346337596]), + xp_assert_close( + uv_to_CCT_Ohno2013(xp_as_array([0.1978, 0.3122], xp=xp)), + [6507.474788799616363, 0.003223346337596], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_CCT_Ohno2013(np.array([0.4328, 0.2883])), - np.array([1041.678320000468375, -0.067378053475797]), + xp_assert_close( + uv_to_CCT_Ohno2013(xp_as_array([0.4328, 0.2883], xp=xp)), + [1041.678320000468375, -0.067378053475797], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - uv_to_CCT_Ohno2013(np.array([0.2927, 0.2722])), - np.array([2444.971818951082696, -0.084370641205118]), + xp_assert_close( + uv_to_CCT_Ohno2013(xp_as_array([0.2927, 0.2722], xp=xp)), + [2444.971818951082696, -0.084370641205118], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_uv_to_CCT_Ohno2013(self) -> None: + def test_n_dimensional_uv_to_CCT_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.uv_to_CCT_Ohno2013` definition n-dimensional arrays support. """ - uv = np.array([0.1978, 0.3122]) - CCT_D_uv = uv_to_CCT_Ohno2013(uv) + uv = xp_as_array([0.1978, 0.3122], xp=xp) + CCT_D_uv = as_ndarray(uv_to_CCT_Ohno2013(uv)) - uv = np.tile(uv, (6, 1)) - CCT_D_uv = np.tile(CCT_D_uv, (6, 1)) - np.testing.assert_allclose( - uv_to_CCT_Ohno2013(uv), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + CCT_D_uv = xp.tile(xp_as_array(CCT_D_uv, xp=xp), (6, 1)) + xp_assert_close(uv_to_CCT_Ohno2013(uv), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - CCT_D_uv = np.reshape(CCT_D_uv, (2, 3, 2)) - np.testing.assert_allclose( - uv_to_CCT_Ohno2013(uv), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + CCT_D_uv = xp_reshape(xp_as_array(CCT_D_uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(uv_to_CCT_Ohno2013(uv), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_uv_to_CCT_Ohno2013(self) -> None: @@ -158,50 +167,47 @@ class TestCCT_to_uv_Ohno2013: unit tests methods. """ - def test_CCT_to_uv_Ohno2013(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_CCT_to_uv_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.CCT_to_uv_Ohno2013` definition. """ - np.testing.assert_allclose( - CCT_to_uv_Ohno2013(np.array([6507.47380460, 0.00322335])), - np.array([0.19779997, 0.31219997]), + xp_assert_close( + CCT_to_uv_Ohno2013(xp_as_array([6507.47380460, 0.00322335], xp=xp)), + [0.19779997, 0.31219997], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_uv_Ohno2013(np.array([1041.68315360, -0.06737802])), - np.array([0.43279885, 0.28830013]), + xp_assert_close( + CCT_to_uv_Ohno2013(xp_as_array([1041.68315360, -0.06737802], xp=xp)), + [0.43279885, 0.28830013], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_uv_Ohno2013(np.array([2452.15316417, -0.08437064])), - np.array([0.29247364, 0.27215157]), + xp_assert_close( + CCT_to_uv_Ohno2013(xp_as_array([2452.15316417, -0.08437064], xp=xp)), + [0.29247364, 0.27215157], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_uv_Ohno2013(self) -> None: + def test_n_dimensional_CCT_to_uv_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.CCT_to_uv_Ohno2013` definition n-dimensional arrays support. """ - CCT_D_uv = np.array([6507.47380460, 0.00322335]) - uv = CCT_to_uv_Ohno2013(CCT_D_uv) + CCT_D_uv = xp_as_array([6507.47380460, 0.00322335], xp=xp) + uv = as_ndarray(CCT_to_uv_Ohno2013(CCT_D_uv)) - CCT_D_uv = np.tile(CCT_D_uv, (6, 1)) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose( - CCT_to_uv_Ohno2013(CCT_D_uv), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT_D_uv = xp.tile(xp_as_array(CCT_D_uv, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(CCT_to_uv_Ohno2013(CCT_D_uv), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT_D_uv = np.reshape(CCT_D_uv, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_uv_Ohno2013(CCT_D_uv), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT_D_uv = xp_reshape(xp_as_array(CCT_D_uv, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_uv_Ohno2013(CCT_D_uv), uv, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_uv_Ohno2013(self) -> None: @@ -221,36 +227,41 @@ class Test_XYZ_to_CCT_Ohno2013: unit tests methods. """ - def test_XYZ_to_CCT_Ohno2013(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_XYZ_to_CCT_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.XYZ_to_CCT_Ohno2013` definition. """ - np.testing.assert_allclose( - XYZ_to_CCT_Ohno2013(np.array([95.04, 100.00, 108.88])), - np.array([6503.30711709, 0.00321729]), + xp_assert_close( + XYZ_to_CCT_Ohno2013(xp_as_array([95.04, 100.00, 108.88], xp=xp)), + [6503.30711709, 0.00321729], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_XYZ_to_CCT_Ohno2013(self) -> None: + def test_n_dimensional_XYZ_to_CCT_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.XYZ_to_CCT_Ohno2013` definition n-dimensional arrays support. """ - XYZ = np.array([95.04, 100.00, 108.88]) - CCT_D_uv = XYZ_to_CCT_Ohno2013(XYZ) + XYZ = xp_as_array([95.04, 100.00, 108.88], xp=xp) + CCT_D_uv = as_ndarray(XYZ_to_CCT_Ohno2013(XYZ)) - XYZ = np.tile(XYZ, (6, 1)) - CCT_D_uv = np.tile(CCT_D_uv, (6, 1)) - np.testing.assert_allclose( - XYZ_to_CCT_Ohno2013(XYZ), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + CCT_D_uv = xp.tile(xp_as_array(CCT_D_uv, xp=xp), (6, 1)) + xp_assert_close( + XYZ_to_CCT_Ohno2013(XYZ), + CCT_D_uv, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - XYZ = np.reshape(XYZ, (2, 3, 3)) - CCT_D_uv = np.reshape(CCT_D_uv, (2, 3, 2)) - np.testing.assert_allclose( - XYZ_to_CCT_Ohno2013(XYZ), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + CCT_D_uv = xp_reshape(xp_as_array(CCT_D_uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close( + XYZ_to_CCT_Ohno2013(XYZ), + CCT_D_uv, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors @@ -271,36 +282,42 @@ class Test_CCT_to_XYZ_Ohno2013: unit tests methods. """ - def test_CCT_to_XYZ_Ohno2013(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-1) + def test_CCT_to_XYZ_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.CCT_to_XYZ_Ohno2013` definition. """ - np.testing.assert_allclose( - CCT_to_XYZ_Ohno2013(np.array([6503.30711709, 0.00321729])), - np.array([95.04, 100.00, 108.88]) / 100, - atol=1e-6, + xp_assert_close( + CCT_to_XYZ_Ohno2013(xp_as_array([6503.30711709, 0.00321729], xp=xp)), + xp_as_array([95.04, 100.00, 108.88], xp=xp) / 100, + atol=TOLERANCE_ABSOLUTE_TESTS * 10, ) - def test_n_dimensional_CCT_to_XYZ_Ohno2013(self) -> None: + @pytest.mark.mps_tolerance_absolute(1e-2) + def test_n_dimensional_CCT_to_XYZ_Ohno2013(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.ohno2013.CCT_to_XYZ_Ohno2013` definition n-dimensional arrays support. """ - CCT_D_uv = np.array([6503.30711709, 0.00321729]) - XYZ = CCT_to_XYZ_Ohno2013(CCT_D_uv) + CCT_D_uv = xp_as_array([6503.30711709, 0.00321729], xp=xp) + XYZ = as_ndarray(CCT_to_XYZ_Ohno2013(CCT_D_uv)) - CCT_D_uv = np.tile(CCT_D_uv, (6, 1)) - XYZ = np.tile(XYZ, (6, 1)) - np.testing.assert_allclose( - CCT_to_XYZ_Ohno2013(CCT_D_uv), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS + CCT_D_uv = xp.tile(xp_as_array(CCT_D_uv, xp=xp), (6, 1)) + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + xp_assert_close( + CCT_to_XYZ_Ohno2013(CCT_D_uv), + XYZ, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - CCT_D_uv = np.reshape(CCT_D_uv, (2, 3, 2)) - XYZ = np.reshape(XYZ, (2, 3, 3)) - np.testing.assert_allclose( - CCT_to_XYZ_Ohno2013(CCT_D_uv), XYZ, atol=TOLERANCE_ABSOLUTE_TESTS + CCT_D_uv = xp_reshape(xp_as_array(CCT_D_uv, xp=xp), (2, 3, 2), xp=xp) + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close( + CCT_to_XYZ_Ohno2013(CCT_D_uv), + XYZ, + atol=TOLERANCE_ABSOLUTE_TESTS, ) @ignore_numpy_errors diff --git a/colour/temperature/tests/test_planck1900.py b/colour/temperature/tests/test_planck1900.py index 79b31c20ee..49b62b7f69 100644 --- a/colour/temperature/tests/test_planck1900.py +++ b/colour/temperature/tests/test_planck1900.py @@ -2,13 +2,26 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np +import pytest from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.temperature import CCT_to_uv_Planck1900, uv_to_CCT_Planck1900 -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -29,40 +42,38 @@ class TestUv_to_CCT_Planck1900: definition unit tests methods. """ - def test_uv_to_CCT_Planck1900(self) -> None: + @pytest.mark.mps_tolerance_absolute(1) + def test_uv_to_CCT_Planck1900(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.planck1900.uv_to_CCT_Planck1900` definition. """ - np.testing.assert_allclose( + xp_assert_close( uv_to_CCT_Planck1900( - np.array([0.225109670227493, 0.334387366663923]), - optimisation_kwargs={"method": "Nelder-Mead"}, + xp_as_array([0.225109670227493, 0.334387366663923], xp=xp), ), 4000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_CCT_Planck1900( - np.array([0.198126929048352, 0.307025980523306]), - optimisation_kwargs={"method": "Nelder-Mead"}, + xp_as_array([0.198126929048352, 0.307025980523306], xp=xp), ), 7000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( uv_to_CCT_Planck1900( - np.array([0.182932683590136, 0.274073232217536]), - optimisation_kwargs={"method": "Nelder-Mead"}, + xp_as_array([0.182932683590136, 0.274073232217536], xp=xp), ), 25000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_uv_to_CCT_Planck1900(self) -> None: + def test_n_dimensional_uv_to_CCT_Planck1900(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.planck1900.uv_to_CCT_Planck1900` definition n-dimensional arrays support. @@ -71,20 +82,16 @@ def test_n_dimensional_uv_to_CCT_Planck1900(self) -> None: if not is_scipy_installed(): # pragma: no cover return - uv = np.array([0.225109670227493, 0.334387366663923]) - CCT = uv_to_CCT_Planck1900(uv) + uv = xp_as_array([0.225109670227493, 0.334387366663923], xp=xp) + CCT = as_ndarray(uv_to_CCT_Planck1900(uv)) - uv = np.tile(uv, (6, 1)) - CCT = np.tile(CCT, 6) - np.testing.assert_allclose( - uv_to_CCT_Planck1900(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + xp_assert_close(uv_to_CCT_Planck1900(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - uv = np.reshape(uv, (2, 3, 2)) - CCT = np.reshape(CCT, (2, 3)) - np.testing.assert_allclose( - uv_to_CCT_Planck1900(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + xp_assert_close(uv_to_CCT_Planck1900(uv), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_uv_to_CCT_Planck1900(self) -> None: @@ -107,50 +114,46 @@ class TestCCT_to_uv_Planck1900: unit tests methods. """ - def test_CCT_to_uv_Planck1900(self) -> None: + def test_CCT_to_uv_Planck1900(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.planck1900.CCT_to_uv_Planck1900` definition. """ - np.testing.assert_allclose( - CCT_to_uv_Planck1900(4000), - np.array([0.225109670227493, 0.334387366663923]), + xp_assert_close( + CCT_to_uv_Planck1900(xp_as_array([4000], xp=xp)), + [[0.225109670227493, 0.334387366663923]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_uv_Planck1900(7000), - np.array([0.198126929048352, 0.307025980523306]), + xp_assert_close( + CCT_to_uv_Planck1900(xp_as_array([7000], xp=xp)), + [[0.198126929048352, 0.307025980523306]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_uv_Planck1900(25000), - np.array([0.182932683590136, 0.274073232217536]), + xp_assert_close( + CCT_to_uv_Planck1900(xp_as_array([25000], xp=xp)), + [[0.182932683590136, 0.274073232217536]], atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_uv_Planck1900(self) -> None: + def test_n_dimensional_CCT_to_uv_Planck1900(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.planck1900.CCT_to_uv_Planck1900` definition n-dimensional arrays support. """ CCT = 4000 - uv = CCT_to_uv_Planck1900(CCT) + uv = as_ndarray(CCT_to_uv_Planck1900(CCT)) - CCT = np.tile(CCT, 6) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose( - CCT_to_uv_Planck1900(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6,)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close(CCT_to_uv_Planck1900(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT = np.reshape(CCT, (2, 3)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose( - CCT_to_uv_Planck1900(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close(CCT_to_uv_Planck1900(CCT), uv, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_uv_Planck1900(self) -> None: diff --git a/colour/temperature/tests/test_robertson1968.py b/colour/temperature/tests/test_robertson1968.py index ea6aad7b03..118379a128 100644 --- a/colour/temperature/tests/test_robertson1968.py +++ b/colour/temperature/tests/test_robertson1968.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -13,7 +18,13 @@ mired_to_CCT, uv_to_CCT_Robertson1968, ) -from colour.utilities import ignore_numpy_errors +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + xp_as_array, + xp_assert_close, + xp_reshape, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -139,46 +150,46 @@ class TestMired_to_CCT: definition unit tests methods. """ - def test_mired_to_CCT(self) -> None: + def test_mired_to_CCT(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.mired_to_CCT` definition. """ - np.testing.assert_allclose( - CCT_to_mired(312.5), 3200, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + CCT_to_mired(xp_as_array([312.5], xp=xp)), + 3200, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_mired(153.846153846154), 6500, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + CCT_to_mired(xp_as_array([153.846153846154], xp=xp)), + 6500, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_mired(66.666666666666667), + xp_assert_close( + CCT_to_mired(xp_as_array([66.666666666666667], xp=xp)), 15000, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_mired_to_CCT(self) -> None: + def test_n_dimensional_mired_to_CCT(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.mired_to_CCT` definition n-dimensional arrays support. """ mired = 312.5 - CCT = mired_to_CCT(mired) + CCT = as_ndarray(mired_to_CCT(mired)) - mired = np.tile(mired, (6, 1)) - CCT = np.tile(CCT, (6, 1)) - np.testing.assert_allclose( - mired_to_CCT(mired), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + mired = xp.tile(xp_as_array(mired, xp=xp), (6, 1)) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6, 1)) + xp_assert_close(mired_to_CCT(mired), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) - mired = np.reshape(mired, (2, 3, 1)) - CCT = np.reshape(CCT, (2, 3, 1)) - np.testing.assert_allclose( - mired_to_CCT(mired), CCT, atol=TOLERANCE_ABSOLUTE_TESTS - ) + mired = xp_reshape(xp_as_array(mired, xp=xp), (2, 3, 1), xp=xp) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(mired_to_CCT(mired), CCT, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_mired_to_CCT(self) -> None: @@ -198,46 +209,46 @@ class TestCCT_to_mired: definition unit tests methods. """ - def test_CCT_to_mired(self) -> None: + def test_CCT_to_mired(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.CCT_to_mired` definition. """ - np.testing.assert_allclose( - CCT_to_mired(3200), 312.5, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + CCT_to_mired(xp_as_array([3200.0], xp=xp)), + 312.5, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_mired(6500), 153.846153846154, atol=TOLERANCE_ABSOLUTE_TESTS + xp_assert_close( + CCT_to_mired(xp_as_array([6500.0], xp=xp)), + 153.846153846154, + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - CCT_to_mired(15000), + xp_assert_close( + CCT_to_mired(xp_as_array([15000.0], xp=xp)), 66.666666666666667, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_mired(self) -> None: + def test_n_dimensional_CCT_to_mired(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.CCT_to_mired` definition n-dimensional arrays support. """ CCT = 3200 - mired = CCT_to_mired(CCT) + mired = as_ndarray(CCT_to_mired(CCT)) - CCT = np.tile(CCT, (6, 1)) - mired = np.tile(mired, (6, 1)) - np.testing.assert_allclose( - CCT_to_mired(CCT), mired, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp.tile(xp_as_array(CCT, xp=xp), (6, 1)) + mired = xp.tile(xp_as_array(mired, xp=xp), (6, 1)) + xp_assert_close(CCT_to_mired(CCT), mired, atol=TOLERANCE_ABSOLUTE_TESTS) - CCT = np.reshape(CCT, (2, 3, 1)) - mired = np.reshape(mired, (2, 3, 1)) - np.testing.assert_allclose( - CCT_to_mired(CCT), mired, atol=TOLERANCE_ABSOLUTE_TESTS - ) + CCT = xp_reshape(xp_as_array(CCT, xp=xp), (2, 3, 1), xp=xp) + mired = xp_reshape(xp_as_array(mired, xp=xp), (2, 3, 1), xp=xp) + xp_assert_close(CCT_to_mired(CCT), mired, atol=TOLERANCE_ABSOLUTE_TESTS) @ignore_numpy_errors def test_nan_CCT_to_mired(self) -> None: @@ -257,35 +268,39 @@ class TestUv_to_CCT_Robertson1968: definition unit tests methods. """ - def test_uv_to_CCT_Robertson1968(self) -> None: + def test_uv_to_CCT_Robertson1968(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.uv_to_CCT_Robertson1968` definition. """ for key, value in TEMPERATURE_DUV_TO_UV.items(): - np.testing.assert_allclose(uv_to_CCT_Robertson1968(value), key, atol=0.25) + xp_assert_close( + uv_to_CCT_Robertson1968(xp_as_array(value, xp=xp)), + key, + atol=TOLERANCE_ABSOLUTE_TESTS * 2500000, + ) - def test_n_dimensional_uv_to_CCT_Robertson1968(self) -> None: + def test_n_dimensional_uv_to_CCT_Robertson1968(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.uv_to_CCT_Robertson1968` definition n-dimensional arrays support. """ - uv = np.array([0.1978, 0.3122]) - CCT_D_uv = uv_to_CCT_Robertson1968(uv) + uv = xp_as_array([0.1978, 0.3122], xp=xp) + CCT_D_uv = as_ndarray(uv_to_CCT_Robertson1968(uv)) - uv = np.tile(uv, (6, 1)) - CCT_D_uv = np.tile(CCT_D_uv, (6, 1)) - np.testing.assert_allclose( + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + CCT_D_uv = xp.tile(xp_as_array(CCT_D_uv, xp=xp), (6, 1)) + xp_assert_close( uv_to_CCT_Robertson1968(uv), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS, ) - uv = np.reshape(uv, (2, 3, 2)) - CCT_D_uv = np.reshape(CCT_D_uv, (2, 3, 2)) - np.testing.assert_allclose( + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + CCT_D_uv = xp_reshape(xp_as_array(CCT_D_uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close( uv_to_CCT_Robertson1968(uv), CCT_D_uv, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -309,39 +324,39 @@ class TestCCT_to_uv_Robertson1968: definition unit tests methods. """ - def test_CCT_to_uv_Robertson1968(self) -> None: + def test_CCT_to_uv_Robertson1968(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.CCT_to_uv_Robertson1968` definition. """ for key, value in TEMPERATURE_DUV_TO_UV.items(): - np.testing.assert_allclose( - CCT_to_uv_Robertson1968(key), + xp_assert_close( + CCT_to_uv_Robertson1968(xp_as_array(key, xp=xp)), value, atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_n_dimensional_CCT_to_uv_Robertson1968(self) -> None: + def test_n_dimensional_CCT_to_uv_Robertson1968(self, xp: ModuleType) -> None: """ Test :func:`colour.temperature.robertson1968.CCT_to_uv_Robertson1968` definition n-dimensional arrays support. """ - CCT_D_uv = np.array([4500, 0.0250]) - uv = CCT_to_uv_Robertson1968(CCT_D_uv) + CCT_D_uv = xp_as_array([4500, 0.0250], xp=xp) + uv = as_ndarray(CCT_to_uv_Robertson1968(CCT_D_uv)) - CCT_D_uv = np.tile(CCT_D_uv, (6, 1)) - uv = np.tile(uv, (6, 1)) - np.testing.assert_allclose( + CCT_D_uv = xp.tile(xp_as_array(CCT_D_uv, xp=xp), (6, 1)) + uv = xp.tile(xp_as_array(uv, xp=xp), (6, 1)) + xp_assert_close( CCT_to_uv_Robertson1968(CCT_D_uv), uv, atol=TOLERANCE_ABSOLUTE_TESTS, ) - CCT_D_uv = np.reshape(CCT_D_uv, (2, 3, 2)) - uv = np.reshape(uv, (2, 3, 2)) - np.testing.assert_allclose( + CCT_D_uv = xp_reshape(xp_as_array(CCT_D_uv, xp=xp), (2, 3, 2), xp=xp) + uv = xp_reshape(xp_as_array(uv, xp=xp), (2, 3, 2), xp=xp) + xp_assert_close( CCT_to_uv_Robertson1968(CCT_D_uv), uv, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/utilities/__init__.py b/colour/utilities/__init__.py index b9f397686b..bd24f99333 100644 --- a/colour/utilities/__init__.py +++ b/colour/utilities/__init__.py @@ -42,6 +42,8 @@ # isort: split from .requirements import ( + is_array_api_compat_installed, + is_array_api_extra_installed, is_ctlrender_installed, is_imageio_installed, is_matplotlib_installed, @@ -71,7 +73,6 @@ batch, caching_enable, copy_definition, - disable_multiprocessing, filter_kwargs, filter_mapping, first_item, @@ -84,11 +85,10 @@ is_iterable, is_numeric, is_sibling, - multiprocessing_pool, optional, print_numpy_errors, raise_numpy_errors, - set_caching_enable, + set_caching_enabled, slugify, validate_method, warn_numpy_errors, @@ -101,6 +101,8 @@ MixinDataclassArray, MixinDataclassFields, MixinDataclassIterable, + array_api_enable, + array_namespace, as_array, as_complex_array, as_float, @@ -109,6 +111,8 @@ as_int, as_int_array, as_int_scalar, + as_ndarray, + cast_non_ndarray, centroid, closest, closest_indexes, @@ -127,7 +131,10 @@ in_array, index_along_last_axis, interval, + is_array_api_enabled, is_ndarray_copy_enabled, + is_non_ndarray, + is_numpy_namespace, is_uniform, ndarray_copy, ndarray_copy_enable, @@ -135,17 +142,57 @@ ones, orient, row_as_diagonal, + set_array_api_enabled, + set_default_complex_dtype, set_default_float_dtype, set_default_int_dtype, set_domain_range_scale, - set_ndarray_copy_enable, + set_ndarray_copy_enabled, to_domain_1, to_domain_10, to_domain_100, to_domain_degrees, to_domain_int, + trace_array_namespace, tsplit, tstack, + xp_as_array, + xp_as_float_array, + xp_as_int_array, + xp_ascontiguousarray, + xp_assert_close, + xp_assert_equal, + xp_astype, + xp_atleast_1d, + xp_atleast_2d, + xp_average, + xp_broadcast_to, + xp_create_diagonal, + xp_degrees, + xp_eig, + xp_eigh, + xp_gradient, + xp_insert, + xp_interp, + xp_isclose, + xp_isin, + xp_linspace, + xp_lstsq, + xp_matrix_transpose, + xp_median, + xp_nan_to_num, + xp_nanmean, + xp_pad, + xp_radians, + xp_reshape, + xp_resize, + xp_round, + xp_select, + xp_setxor1d, + xp_sinc, + xp_squeeze, + xp_trapezoid, + xp_unique, zeros, ) from .common import download_url, hash_sha256 @@ -198,6 +245,8 @@ "Structure", ] __all__ += [ + "is_array_api_compat_installed", + "is_array_api_extra_installed", "is_ctlrender_installed", "is_imageio_installed", "is_matplotlib_installed", @@ -224,7 +273,6 @@ "batch", "caching_enable", "copy_definition", - "disable_multiprocessing", "filter_kwargs", "filter_mapping", "first_item", @@ -237,11 +285,10 @@ "is_iterable", "is_numeric", "is_sibling", - "multiprocessing_pool", "optional", "print_numpy_errors", "raise_numpy_errors", - "set_caching_enable", + "set_caching_enabled", "slugify", "validate_method", "warn_numpy_errors", @@ -285,10 +332,11 @@ "ones", "orient", "row_as_diagonal", + "set_default_complex_dtype", "set_default_float_dtype", "set_default_int_dtype", "set_domain_range_scale", - "set_ndarray_copy_enable", + "set_ndarray_copy_enabled", "to_domain_1", "to_domain_10", "to_domain_100", @@ -298,6 +346,54 @@ "tstack", "zeros", ] +__all__ += [ + "array_api_enable", + "array_namespace", + "as_ndarray", + "cast_non_ndarray", + "is_array_api_enabled", + "is_non_ndarray", + "is_numpy_namespace", + "set_array_api_enabled", + "trace_array_namespace", + "xp_as_array", + "xp_as_float_array", + "xp_as_int_array", + "xp_ascontiguousarray", + "xp_assert_close", + "xp_assert_equal", + "xp_astype", + "xp_atleast_1d", + "xp_atleast_2d", + "xp_average", + "xp_broadcast_to", + "xp_create_diagonal", + "xp_degrees", + "xp_eig", + "xp_eigh", + "xp_gradient", + "xp_insert", + "xp_interp", + "xp_isclose", + "xp_isin", + "xp_linspace", + "xp_lstsq", + "xp_matrix_transpose", + "xp_median", + "xp_nan_to_num", + "xp_nanmean", + "xp_pad", + "xp_radians", + "xp_reshape", + "xp_resize", + "xp_round", + "xp_select", + "xp_setxor1d", + "xp_sinc", + "xp_squeeze", + "xp_trapezoid", + "xp_unique", +] __all__ += [ "metric_mse", "metric_psnr", diff --git a/colour/utilities/array.py b/colour/utilities/array.py index e03796d61e..3d1b1b1fea 100644 --- a/colour/utilities/array.py +++ b/colour/utilities/array.py @@ -18,7 +18,10 @@ from __future__ import annotations +import contextlib +import contextvars import functools +import os import re import sys import typing @@ -30,11 +33,31 @@ import numpy as np +# NOTE: ``array_api_compat`` and ``array_api_extra`` are optional +# dependencies bound to *None* when unavailable; the static branch keeps +# *Pyright* seeing the real modules so no narrowing is required at the use +# sites, which are all guarded at runtime via the requirements predicates. +if typing.TYPE_CHECKING: + import array_api_compat as xpc + import array_api_extra as xpx +else: + try: + import array_api_compat as xpc + except ImportError: + xpc = None + + try: + import array_api_extra as xpx + except ImportError: + xpx = None + from colour.constants import ( DTYPE_COMPLEX_DEFAULT, DTYPE_FLOAT_DEFAULT, DTYPE_INT_DEFAULT, EPSILON, + TOLERANCE_ABSOLUTE_TESTS, + TOLERANCE_RELATIVE_TESTS, ) if typing.TYPE_CHECKING: @@ -43,12 +66,13 @@ Callable, DType, DTypeBoolean, - DTypeComplex, DTypeReal, Dataclass, Generator, Literal, + ModuleType, NDArray, + NDArrayBoolean, NDArrayComplex, NDArrayFloat, NDArrayInt, @@ -58,76 +82,2379 @@ Type, ) -from colour.hints import ArrayLike, DTypeComplex, DTypeFloat, DTypeInt, cast -from colour.utilities import ( - CACHE_REGISTRY, - attest, - int_digest, - is_caching_enabled, - optional, - suppress_warnings, - validate_method, -) +from colour.hints import ArrayLike, DTypeComplex, DTypeFloat, DTypeInt, cast +from colour.utilities import ( + CACHE_REGISTRY, + as_bool, + attest, + int_digest, + is_array_api_compat_installed, + is_array_api_extra_installed, + is_caching_enabled, + optional, + runtime_warning, + suppress_warnings, + validate_method, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "is_array_api_enabled", + "set_array_api_enabled", + "array_api_enable", + "trace_array_namespace", + "array_namespace", + "is_numpy_namespace", + "is_non_ndarray", + "as_ndarray", + "cast_non_ndarray", + "xp_as_array", + "xp_as_float_array", + "xp_as_int_array", + "xp_ascontiguousarray", + "xp_astype", + "xp_matrix_transpose", + "xp_select", + "xp_interp", + "xp_trapezoid", + "xp_average", + "xp_gradient", + "xp_resize", + "xp_nanmean", + "xp_median", + "xp_round", + "xp_radians", + "xp_degrees", + "xp_atleast_1d", + "xp_atleast_2d", + "xp_squeeze", + "xp_sinc", + "xp_isclose", + "xp_nan_to_num", + "xp_create_diagonal", + "xp_reshape", + "xp_broadcast_to", + "xp_lstsq", + "xp_eig", + "xp_eigh", + "xp_isin", + "xp_linspace", + "xp_pad", + "xp_unique", + "xp_insert", + "xp_setxor1d", + "xp_assert_close", + "xp_assert_equal", + "MixinDataclassFields", + "MixinDataclassIterable", + "MixinDataclassArray", + "MixinDataclassArithmetic", + "as_array", + "as_int", + "as_float", + "as_int_array", + "as_float_array", + "as_int_scalar", + "as_float_scalar", + "as_complex_array", + "set_default_int_dtype", + "set_default_float_dtype", + "set_default_complex_dtype", + "get_domain_range_scale", + "set_domain_range_scale", + "domain_range_scale", + "get_domain_range_scale_metadata", + "to_domain_1", + "to_domain_10", + "to_domain_100", + "to_domain_degrees", + "to_domain_int", + "from_range_1", + "from_range_10", + "from_range_100", + "from_range_degrees", + "from_range_int", + "is_ndarray_copy_enabled", + "set_ndarray_copy_enabled", + "ndarray_copy_enable", + "ndarray_copy", + "closest_indexes", + "closest", + "interval", + "is_uniform", + "in_array", + "tstack", + "tsplit", + "row_as_diagonal", + "orient", + "centroid", + "fill_nan", + "has_only_nan", + "ndarray_write", + "zeros", + "ones", + "full", + "index_along_last_axis", + "format_array_as_row", +] + +_ARRAY_API_ENABLED: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_ARRAY_API_ENABLED", default=False +) +""" +:class:`contextvars.ContextVar` storing the current *Colour* Array API +dispatch enabled state. The :class:`contextvars.ContextVar` keeps nested +:class:`array_api_enable` contexts independent across concurrent threads +and async tasks. Read it via :func:`is_array_api_enabled` and toggle it +via :func:`set_array_api_enabled` or :class:`array_api_enable`. +""" +_ARRAY_API_ENABLED.set(as_bool(os.environ.get("COLOUR_SCIENCE__ARRAY_API", "False"))) + +_CACHE_ARRAY_NAMESPACE: dict = CACHE_REGISTRY.register_cache( + f"{__name__}._CACHE_ARRAY_NAMESPACE" +) +"""Cache for :func:`array_namespace` results, keyed by array type.""" + +_CACHE_SCALAR_PROMOTION: dict = CACHE_REGISTRY.register_cache( + f"{__name__}._CACHE_SCALAR_PROMOTION" +) +"""Cache for scalar-to-backend promotions in :func:`xp_as_array`.""" + +_CACHE_BACKEND_DTYPE: dict = CACHE_REGISTRY.register_cache( + f"{__name__}._CACHE_BACKEND_DTYPE" +) +"""Cache mapping ``(id(xp), dtype)`` pairs to the backend-native dtype.""" + + +def _resolve_backend_dtype(xp: ModuleType, dtype: Any) -> Any: + """Resolve a *NumPy* dtype to the equivalent dtype in ``xp``. + + Resolution is memoised through :attr:`_CACHE_BACKEND_DTYPE` (keyed by + ``(id(xp), dtype)``) when caching is enabled to avoid the + ``np.dtype(...).name`` + ``getattr`` lookups on every call. Falls back + to ``dtype`` unchanged when it is already a backend-native type. + """ + + key = (id(xp), dtype) + + if is_caching_enabled(): + resolved = _CACHE_BACKEND_DTYPE.get(key) + if resolved is not None: + return resolved + + try: + resolved = getattr(xp, np.dtype(dtype).name, dtype) + except TypeError: + resolved = dtype + + if is_caching_enabled(): + _CACHE_BACKEND_DTYPE[key] = resolved + + return resolved + + +def is_array_api_enabled() -> bool: + """ + Determine whether *Colour* Array API dispatch is enabled. + + The Array API dispatch state is controlled by the global + *COLOUR_SCIENCE__ARRAY_API* environment variable and can be + temporarily modified using the :func:`set_array_api_enabled` function + or the :class:`array_api_enable` context manager. + + Returns + ------- + :class:`bool` + Whether *Colour* Array API dispatch is enabled. + + Examples + -------- + >>> with array_api_enable(False): + ... is_array_api_enabled() + False + >>> with array_api_enable(True): + ... is_array_api_enabled() + True + """ + + return _ARRAY_API_ENABLED.get() + + +def set_array_api_enabled(enable: bool) -> None: + """ + Set the *Colour* Array API dispatch enabled state. + + Parameters + ---------- + enable + Whether to enable *Colour* Array API dispatch. + + Examples + -------- + >>> with array_api_enable(True): + ... print(is_array_api_enabled()) + ... set_array_api_enabled(False) + ... print(is_array_api_enabled()) + True + False + """ + + _ARRAY_API_ENABLED.set(enable) + + +class array_api_enable: + """ + Define a context manager and decorator to temporarily set the *Colour* + Array API dispatch enabled state. + + Parameters + ---------- + enable + Whether to enable or disable *Colour* Array API dispatch. + """ + + def __init__(self, enable: bool) -> None: + self._enable = enable + # Token stack: nested or recursive ``__enter__`` / ``__exit__`` + # pairs against the same instance (e.g. via the decorator form on + # a recursive function) push and pop independent reset tokens. + self._tokens: list[contextvars.Token[bool]] = [] + + def __enter__(self) -> Self: + """Enter the context and set the Array API dispatch state.""" + + self._tokens.append(_ARRAY_API_ENABLED.set(self._enable)) + + return self + + def __exit__(self, *args: Any) -> None: + """Exit the context and restore the previous Array API state.""" + + _ARRAY_API_ENABLED.reset(self._tokens.pop()) + + def __call__(self, function: Callable) -> Callable: + """Decorate and call the specified function with Array API control.""" + + @functools.wraps(function) + def wrapper(*args: Any, **kwargs: Any) -> Any: + with self: + return function(*args, **kwargs) + + return wrapper + + +class trace_array_namespace: + """ + Define a context manager to trace :func:`array_namespace` calls and + array type flow through *Colour* functions using :func:`sys.settrace`. + + When active, every function call under ``colour/`` is logged with the + types of all array arguments (positional and keyword). Return values + are logged with their types. Calls where multiple array backends + coexist in the same argument list are flagged as ``MIXED``. + + The trace output is indented to reflect the call stack depth. + + Examples + -------- + >>> import torch # doctest: +SKIP + >>> with array_api_enable(True), trace_array_namespace(): + ... pass # doctest: +SKIP + """ + + _ARRAY_TYPES: tuple = (np.ndarray, np.generic) + + def __init__(self) -> None: + self._depth: int = 0 + self._previous_trace: Any = None + + with contextlib.suppress(ImportError): + import torch # noqa: PLC0415 + + self._ARRAY_TYPES = (*self._ARRAY_TYPES, torch.Tensor) + + with contextlib.suppress(ImportError): + import jax # noqa: PLC0415 + + self._ARRAY_TYPES = (*self._ARRAY_TYPES, jax.Array) + + def _type_label(self, obj: Any) -> str: + """Return a short type label for the specified object.""" + + cls = type(obj) + module = cls.__module__.split(".")[0] + + if isinstance(obj, np.ndarray): + return f"ndarray{list(obj.shape)}" + + return ( + f"{module}.{cls.__name__}{list(obj.shape) if hasattr(obj, 'shape') else ''}" + ) + + def _format_args( + self, + code: Any, + local_vars: dict, + ) -> str: + """Format function arguments with array type annotations.""" + + parts = [] + param_names = list(code.co_varnames[: code.co_argcount]) + + for name in param_names: + if name == "self": + continue + + value = local_vars.get(name) + + if value is None: + parts.append(f"{name}: None") + elif isinstance(value, self._ARRAY_TYPES): + parts.append(f"{name}: {self._type_label(value)}") + else: + parts.append(f"{name}: {type(value).__name__}") + + return ", ".join(parts) + + def _has_mixed_backends(self, local_vars: dict) -> bool: + """Check whether the local variables contain mixed array backends.""" + + backends = set() + + for value in local_vars.values(): + if isinstance(value, self._ARRAY_TYPES): + if isinstance(value, (np.ndarray, np.generic)): + backends.add("numpy") + else: + backends.add(type(value).__module__.split(".")[0]) + + return len(backends) > 1 + + def _is_colour_frame(self, frame: Any) -> bool: + """Check whether the specified frame belongs to *Colour*.""" + + filename = frame.f_code.co_filename or "" + + return "colour/" in filename and "/site-packages/" not in filename + + def _trace(self, frame: Any, event: str, arg: Any) -> Any: + """Trace function for :func:`sys.settrace`.""" + + if not self._is_colour_frame(frame): + return self._trace + + if event == "call": + code = frame.f_code + name = code.co_name + + if name.startswith("<") or ( + name.startswith("_") and not name.startswith("__") + ): + return self._trace + + args_str = self._format_args(code, frame.f_locals) + mixed = self._has_mixed_backends(frame.f_locals) + marker = " [MIXED]" if mixed else "" + + indent = " " * self._depth + print(f"{indent}{name}({args_str}){marker}") # noqa: T201 + + self._depth += 1 + + return self._trace + + if event == "return": + self._depth = max(0, self._depth - 1) + + if isinstance(arg, self._ARRAY_TYPES): + indent = " " * self._depth + print(f"{indent}-> {self._type_label(arg)}") # noqa: T201 + + return self._trace + + return self._trace + + def __enter__(self) -> Self: + """Enter the context and install the trace hook.""" + + self._previous_trace = sys.gettrace() + self._depth = 0 + sys.settrace(self._trace) + + return self + + def __exit__(self, *args: Any) -> None: + """Exit the context and restore the previous trace hook.""" + + sys.settrace(self._previous_trace) + + +def array_namespace(*arrays: Any) -> ModuleType: + """ + Return the array namespace for the specified arrays. + + When Array API dispatch is disabled (default), return :mod:`numpy`. + When enabled, use :func:`array_api_compat.array_namespace` to detect + the appropriate namespace from the input arrays. + + Parameters + ---------- + *arrays + Arrays to determine the namespace from. *NumPy* is returned as the + explicit default fallback when no arrays are provided or all + arrays are *None* / pure-*Python* scalars (no backend signal to + dispatch on). + + Returns + ------- + :class:`types.ModuleType` + Array namespace module. + + Examples + -------- + >>> array_namespace(np.array([1, 2, 3])) # doctest: +ELLIPSIS + + """ + + if not is_array_api_enabled(): + return np + + if xpc is None: # pragma: no cover + is_array_api_compat_installed(raise_exception=True) + + # Fast path: cache by array type to avoid the full resolution chain + # on every call. Only cache-hit when there is exactly one distinct + # non-*NumPy* type; mixed backends (e.g. *JAX* + *PyTorch*) must + # fall through to ``xpc.array_namespace`` so it can raise. + if is_caching_enabled(): + non_numpy_types = { + type(a) + for a in arrays + if a is not None and not isinstance(a, (np.ndarray, np.generic)) + } + if len(non_numpy_types) == 1: + cached = _CACHE_ARRAY_NAMESPACE.get(next(iter(non_numpy_types))) + if cached is not None: + return cached + + arrays = tuple( + a + for a in arrays + if a is not None + and ( + hasattr(a, "__array_namespace__") + or isinstance(a, np.ndarray) + or xpc.is_array_api_obj(a) + ) + ) + + if not arrays: + return np + + # When inputs mix NumPy arrays (e.g., module-level constants) with a + # non-NumPy backend, promote to the non-NumPy backend. Only mixed + # non-NumPy backends (e.g., JAX + CuPy) raise a ``TypeError``. + non_numpy = tuple(a for a in arrays if not isinstance(a, (np.ndarray, np.generic))) + + if non_numpy: + arrays = non_numpy + + xp = xpc.array_namespace(*arrays) + + if is_caching_enabled() and non_numpy: + _CACHE_ARRAY_NAMESPACE[type(non_numpy[0])] = xp + + return xp + + +def is_numpy_namespace(xp: ModuleType) -> bool: + """ + Determine whether the specified namespace is :mod:`numpy`. + + Parameters + ---------- + xp + Namespace module to test. + + Returns + ------- + :class:`bool` + Whether the namespace is :mod:`numpy`. + + Examples + -------- + >>> is_numpy_namespace(np) + True + """ + + if xp is np: + return True + + if xpc is not None: + return xpc.is_numpy_namespace(xp) + + return False + + +def is_non_ndarray(a: Any) -> bool: + """ + Determine whether the specified object is a non-*NumPy* array. + + Parameters + ---------- + a + Object to test. + + Returns + ------- + :class:`bool` + Whether the object is a non-*NumPy* array (e.g., *JAX*, *PyTorch*, + *CuPy*). + + Examples + -------- + >>> is_non_ndarray(np.array([1, 2, 3])) + False + >>> is_non_ndarray([1, 2, 3]) + False + """ + + if isinstance(a, (np.ndarray, np.generic)): + return False + + if hasattr(a, "__array_namespace__"): + return True + + if xpc is not None: + return xpc.is_array_api_obj(a) + + return False + + +def as_ndarray(a: Any) -> np.ndarray: + """ + Convert the specified array :math:`a` to a :class:`numpy.ndarray`. + + This function handles arrays from any backend (*JAX*, *PyTorch*, *CuPy*, + etc.) by using the *DLPack* protocol when direct conversion is not + possible, e.g., for device-resident arrays. + + Parameters + ---------- + a + Array, scalar, or *Python* sequence to convert. + + Returns + ------- + :class:`numpy.ndarray` + *NumPy* array. + + Notes + ----- + - Unlike :func:`as_array` / :func:`as_float_array` siblings, + :func:`as_ndarray` does **not** honour + :attr:`_NDARRAY_COPY_ENABLED`. It is a *backend-host hand-off* + boundary helper, not a copy toggle; the returned array shares + storage with the input wherever the backend allows. + + Examples + -------- + >>> import numpy as np + >>> as_ndarray(np.array([1, 2, 3])) + array([1, 2, 3]) + + Round-trip a *PyTorch* tensor on a non-host device: + + >>> import torch # doctest: +SKIP + >>> as_ndarray(torch.tensor([1, 2, 3], device="mps")) # doctest: +SKIP + array([1, 2, 3]) + """ + + # The first two rungs raise :class:`TypeError` on backends without a + # zero-copy ``__array__`` / *DLPack* hand-off; the *DLPack* rung also + # raises :class:`RuntimeError` for device-resident tensors (notably + # *PyTorch* on *MPS*: "Unsupported device in DLTensor") which is + # equally recoverable via the ``.detach().cpu()`` / ``to_device`` + # rungs below. + try: + return np.asarray(a) + except TypeError: + pass + + try: + return np.from_dlpack(a) + except (TypeError, RuntimeError): + pass + + # *PyTorch* tensors on a non-*CPU* device with ``requires_grad=True`` need + # ``detach().cpu()`` before *DLPack* / ``__array__`` can succeed; for + # other backends, ``array_namespace(a).to_device(a, "cpu")`` is the + # *Array API* standard host hand-off. + if hasattr(a, "detach") and hasattr(a, "cpu"): + return np.asarray(a.detach().cpu()) + + return np.asarray(array_namespace(a).to_device(a, "cpu")) + + +def xp_as_array( + a: ArrayLike, + *, + dtype: Any = None, + xp: ModuleType | None = None, + like: Any = None, + copy: bool | None = None, +) -> NDArray: + """ + Convert the specified variable :math:`a` to the target namespace. + + When the namespace is :mod:`numpy`, the original variable :math:`a` is + returned as a :class:`numpy.ndarray` without unnecessary copying. For + other namespaces, the variable :math:`a` is converted via ``xp.asarray``, + optionally matching the device of a reference array ``like``. + + Parameters + ---------- + a + Variable :math:`a` to convert. + dtype + Target dtype. When provided, the result is cast to this dtype. + Accepts *NumPy* dtype objects (e.g. ``np.float64``) which are + mapped to the backend equivalent. + xp + Array namespace module. If *None*, derived from ``a``. + like + Reference array whose device to match (for backends like *PyTorch* + that support multiple devices). + copy + When *True*, always return a fresh copy of the input even when no + dtype change is needed (the *Array API* ``xp.asarray(a, copy=True)`` + semantics). When *None* (default), copy only when necessary + (dtype change, namespace promotion). The scalar-promotion cache + is bypassed when ``copy=True``. + + Returns + ------- + :class:`object` + Variable :math:`a` in the target namespace. + + Examples + -------- + >>> xp_as_array([1, 2, 3], xp=np) + array([1, 2, 3]) + >>> xp_as_array([1, 2, 3], dtype=np.float64, xp=np) + array([1., 2., 3.]) + """ + + xp = array_namespace(a) if xp is None else xp + + # When the *Array API* dispatch is disabled the input is *NumPy* by + # construction; bypass the namespace + non-ndarray probes that the + # full path performs. + if not is_array_api_enabled(): + result = as_array(a, dtype) + return np.copy(result) if copy else result + + if is_numpy_namespace(xp): + result = as_array(as_ndarray(a) if is_non_ndarray(a) else a) + + if dtype is not None and hasattr(result, "dtype") and result.dtype != dtype: + result = result.astype(dtype) + + return np.copy(result) if copy else result + + # Non-*NumPy* namespace, input already on a backend device: short- + # circuit when no dtype is requested or the dtype already matches. + if is_non_ndarray(a): + result = a + if dtype is not None: + a_dtype = getattr(a, "dtype", None) + if a_dtype is not None and a_dtype != dtype: + xp_target_dtype = _resolve_backend_dtype(xp, dtype) + if a_dtype != xp_target_dtype: + try: + result = xp_astype(a, xp_target_dtype, xp=xp) + except (TypeError, RuntimeError): + runtime_warning( + f'Backend "{xp.__name__}" does not support ' + f'dtype "{xp_target_dtype}"; keeping input ' + f'dtype "{a_dtype}".' + ) + if copy and result is a: + result = xp.asarray(a, copy=True) + return result # pyright: ignore + + # Non-*NumPy* namespace: convert from *NumPy* / *Python* to the target + # backend, caching scalar / small constant promotions to avoid repeated + # CPU-to-GPU transfers for module-level constants. The cache is bypassed + # when ``copy=True`` so callers asking for a fresh copy cannot + # accidentally mutate the cached entry. + device = getattr(like, "device", None) + + hash_key = None + if is_caching_enabled() and not copy: + if isinstance(a, (int, float)): + hash_key = hash((a, id(xp), str(device), dtype)) + elif isinstance(a, np.ndarray) and a.size <= 16: + hash_key = hash( + (int_digest(a.tobytes()), a.shape, id(xp), str(device), dtype) + ) + + if hash_key is not None: + cached = _CACHE_SCALAR_PROMOTION.get(hash_key) + if cached is not None: + return cached + + device_kwarg = device if device is not None and hasattr(device, "type") else None + try: + result = ( + xp.asarray(a, device=device_kwarg) + if device_kwarg is not None + else xp.asarray(a) + ) + except TypeError: + # Backend does not support the input dtype (e.g., *MPS* + float64). + a = np.asarray(a) + original_dtype = a.dtype + a = a.astype(np.complex64 if np.iscomplexobj(a) else np.float32) + _runtime_warning_xp_downcast(xp, original_dtype, a.dtype) + result = ( + xp.asarray(a, device=device_kwarg) + if device_kwarg is not None + else xp.asarray(a) + ) + + if dtype is not None and hasattr(result, "dtype"): + xp_target_dtype = _resolve_backend_dtype(xp, dtype) + if result.dtype != xp_target_dtype: + try: + result = xp_astype(result, xp_target_dtype, xp=xp) + except (TypeError, RuntimeError): + runtime_warning( + f'Backend "{xp.__name__}" does not support ' + f'dtype "{xp_target_dtype}"; keeping result dtype ' + f'"{result.dtype}".' + ) + + if hash_key is not None: + _CACHE_SCALAR_PROMOTION[hash_key] = result + + return result + + +def xp_as_float_array( + a: ArrayLike, *, xp: ModuleType | None = None, like: Any = None +) -> NDArrayFloat: + """ + Convert the specified variable :math:`a` to a float array in the target + namespace using :attr:`colour.constants.DTYPE_FLOAT_DEFAULT`. + + Shorthand for ``xp_as_array(a, dtype=DTYPE_FLOAT_DEFAULT, xp=xp, like=like)``. + + Parameters + ---------- + a + Variable :math:`a` to convert. + xp + Array namespace module. If *None*, derived from ``a``. + like + Reference array whose device to match. + + Returns + ------- + :class:`object` + Variable :math:`a` as a float array in the target namespace. + + Examples + -------- + >>> xp_as_float_array([1, 2, 3], xp=np) + array([1., 2., 3.]) + """ + + return xp_as_array(a, dtype=DTYPE_FLOAT_DEFAULT, xp=xp, like=like) + + +def xp_as_int_array( + a: ArrayLike, *, xp: ModuleType | None = None, like: Any = None +) -> NDArrayInt: + """ + Convert the specified variable :math:`a` to an integer array in the target + namespace using :attr:`colour.constants.DTYPE_INT_DEFAULT`. + + Shorthand for ``xp_as_array(a, dtype=DTYPE_INT_DEFAULT, xp=xp, like=like)``. + + Parameters + ---------- + a + Variable :math:`a` to convert. + xp + Array namespace module. If *None*, derived from ``a``. + like + Reference array whose device to match. + + Returns + ------- + :class:`object` + Variable :math:`a` as an integer array in the target namespace. + + Examples + -------- + >>> xp_as_int_array([1.5, 2.7, 3.9], xp=np) + array([1, 2, 3]) + """ + + return xp_as_array(a, dtype=DTYPE_INT_DEFAULT, xp=xp, like=like) + + +def xp_ascontiguousarray(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.ascontiguousarray`. + + Materialise ``a`` into a C-contiguous array with the same shape and + dtype. The lazy stride-permuted view returned by + :func:`xp.matrix_transpose` (and ``.T`` / ``.mT``) poisons downstream + broadcasts and forces *BLAS* to copy internally on every subsequent + ``matmul``; calling this function at the transpose boundary cascades + the contiguous layout through all downstream operations. + + Parameters + ---------- + a + Variable :math:`a` to materialise. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + C-contiguous copy of :math:`a` on the same backend. + + Examples + -------- + >>> xp_ascontiguousarray(np.array([[1, 2], [3, 4]]).T, xp=np).flags["C_CONTIGUOUS"] + True + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.ascontiguousarray(a) + + # ``PyTorch`` exposes a ``.contiguous()`` method on tensors; other + # backends (e.g. *JAX*) manage contiguity as an implementation + # detail and don't expose a corresponding primitive. + contiguous = getattr(a, "contiguous", None) + if callable(contiguous): + return contiguous() # pyright: ignore + + return a # pyright: ignore + + +def xp_matrix_transpose(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.matrix_transpose` + materialising the result to a C-contiguous array. + + Equivalent to ``xp.matrix_transpose(a)`` followed by + :func:`xp_ascontiguousarray`. Use whenever the transposed array will + participate in subsequent broadcasts or ``matmul`` operations; the + lazy stride-permuted view returned by the standard + ``matrix_transpose`` poisons broadcast outputs (the *NumPy* broadcast + machinery inherits the strided layout into the freshly-allocated + output) and forces *BLAS* to copy internally on every matmul. The + cost of materialising once is amortised by keeping all downstream + broadcasts and matmuls on contiguous memory. + + Parameters + ---------- + a + Variable :math:`a`; the last two axes are swapped. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Matrix-transposed and C-contiguous array. + + Examples + -------- + >>> a = np.arange(6).reshape(2, 3) + >>> xp_matrix_transpose(a, xp=np) + array([[0, 3], + [1, 4], + [2, 5]]) + """ + + if xp is None or not hasattr(xp, "matrix_transpose"): + xp = array_namespace(a) + + return xp_ascontiguousarray(xp.matrix_transpose(a), xp=xp) + + +def xp_astype(a: ArrayLike, dtype: Any, *, xp: ModuleType | None = None) -> NDArray: + """ + *Array API* compatible implementation of :meth:`numpy.ndarray.astype`. + + *NumPy* uses ``a.astype(dtype)`` while the *Array API* standard uses + ``xp.astype(a, dtype)`` with backend-native dtype objects. + + Parameters + ---------- + a + Array to cast. + dtype + Target dtype (*NumPy* dtype accepted, automatically translated for + non-*NumPy* backends). + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Cast array. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return a.astype(dtype) # pyright: ignore + + xp_dtype = _resolve_backend_dtype(xp, dtype) + + if a.dtype == xp_dtype: # pyright: ignore + return a # pyright: ignore + + # NOTE: ``array_namespace(a)`` is called again to obtain the + # ``array-api-compat`` wrapped namespace which provides ``astype`` for + # backends (e.g., *PyTorch*) that lack a module-level ``astype``. + try: + return array_namespace(a).astype(a, xp_dtype) + except (TypeError, RuntimeError): + # Fall back to float32 for backends that don't support float64 + # (e.g., MPS on Apple Silicon). + xp_dtype_f32 = getattr(xp, "float32", None) + if xp_dtype_f32 is not None and xp_dtype_f32 != xp_dtype: + _runtime_warning_xp_downcast(xp, xp_dtype, xp_dtype_f32) + return array_namespace(a).astype(a, xp_dtype_f32) + raise + + +# NOTE: Backend capability probing follows a single canonical pattern: attempt +# the native call and catch ``AttributeError`` (the backend does not provide +# the function) and ``TypeError`` (the backend signature is incompatible), +# then warn via :func:`_runtime_warning_xp_fallback` and fall back to *NumPy*. +# ``linalg`` probes additionally catch ``NotImplementedError`` and +# ``RuntimeError`` which *PyTorch* raises at call time for operations +# unsupported on the active device (e.g. *MPS*). + + +def _runtime_warning_xp_fallback(name: str) -> None: + """Emit the standard *falling back to NumPy* runtime warning.""" + + runtime_warning( + f'"{name}" is falling back to "NumPy" for non-"NumPy" ' + "arrays, this will incur a performance penalty due to array " + "conversion." + ) + + +def _runtime_warning_xp_downcast(xp: ModuleType, dtype: Any, dtype_target: Any) -> None: + """Emit the standard backend dtype downcast runtime warning.""" + + runtime_warning( + f'Backend "{xp.__name__}" does not support dtype "{dtype}"; ' + f'downcasting to "{dtype_target}".' + ) + + +def _xpx() -> ModuleType: + """ + Return the :mod:`array_api_extra` module, raising when it is not + installed: together with :mod:`array_api_compat`, it is required for + *Array API* dispatch but both are optional dependencies; *NumPy*-only + code paths never reach this guard. + """ + + is_array_api_extra_installed(raise_exception=True) + + return xpx + + +def xp_select( + condlist: Any, + choicelist: Any, + *, + default: Any = 0, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.select`. + + Parameters + ---------- + condlist + List of boolean arrays for conditions. + choicelist + List of arrays from which output elements are taken. + default + Value used when all conditions are ``False``. + xp + Array namespace module. If *None*, derived from ``condlist`` and + ``choicelist``. + + Returns + ------- + :class:`object` + Array with elements from *choicelist* where *condlist* is ``True``. + """ + + xp = array_namespace(*condlist, *choicelist) if xp is None else xp + + if is_numpy_namespace(xp): + return np.select(condlist, choicelist, default) + + like = None + for item in (*condlist, *choicelist): + if hasattr(item, "device"): + like = item + break + + condlist = [xp_as_array(c, xp=xp, like=like) for c in condlist] + choicelist = [xp_as_float_array(c, xp=xp, like=like) for c in choicelist] + + if hasattr(default, "shape"): + result = xp_as_float_array(default, xp=xp, like=like) + else: + result = xp.full( + condlist[0].shape, + fill_value=default, + dtype=choicelist[0].dtype, + device=getattr(like, "device", None), + ) + + for condition, choice in zip(reversed(condlist), reversed(choicelist), strict=True): + result = xp.where(xp_astype(condition, bool, xp=xp), choice, result) + + return result + + +def xp_interp( + x: ArrayLike, + x_data: ArrayLike, + fp: ArrayLike, + *, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.interp`. + + Parameters + ---------- + x + x-coordinates at which to evaluate the interpolation. + x_data + x-coordinates of the data points. + fp + y-coordinates of the data points. + xp + Array namespace module. If *None*, derived from ``x``, ``x_data`` + and ``fp``. + + Returns + ------- + :class:`object` + Interpolated values. + """ + + xp = array_namespace(x, x_data, fp) if xp is None else xp + + if is_numpy_namespace(xp): + return np.interp(x, x_data, fp) # pyright: ignore + + try: + return xp.interp(x, x_data, fp) + except (AttributeError, TypeError): + pass + + _runtime_warning_xp_fallback("xp_interp") + + fp_nd = as_ndarray(fp) + result = np.interp(as_ndarray(x), as_ndarray(x_data), fp_nd) + result = result.astype(fp_nd.dtype) + + device = getattr(x, "device", None) + if device is not None and hasattr(device, "type"): + like = x + like_dtype = getattr(like, "dtype", None) + like_is_f32 = like_dtype is not None and ( + getattr(like_dtype, "name", None) == "float32" + or str(like_dtype) in ("torch.float32", "float32") + ) + if like_is_f32 and result.dtype == np.float64: + result = result.astype(np.float32) + return xp.asarray(result, device=device) + + return xp.asarray(result) + + +def xp_trapezoid( + y: ArrayLike, + *, + x: ArrayLike | None = None, + dx: float = 1.0, + axis: int = -1, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.trapezoid`. + + Parameters + ---------- + y + y-coordinates of the function values. + x + x-coordinates of the function values. + dx + Spacing between sample points when *x* is ``None``. + axis + Axis along which to integrate. + xp + Array namespace module. If *None*, derived from ``y`` and ``x``. + + Returns + ------- + :class:`object` + Approximation of the integral. + """ + + xp = array_namespace(y, x) if xp is None else xp + + if is_numpy_namespace(xp): + return np.trapezoid(y, x=x, dx=dx, axis=axis) # pyright: ignore + + try: + if x is not None: + return xp.trapezoid(y, x=x, axis=axis) + + return xp.trapezoid(y, dx=dx, axis=axis) + except (AttributeError, TypeError): + pass + + _runtime_warning_xp_fallback("xp_trapezoid") + + result = np.trapezoid( + as_ndarray(y), x=as_ndarray(x) if x is not None else None, dx=dx, axis=axis + ) + + return xp.asarray(result) + + +def xp_average( + a: ArrayLike, + *, + axis: int | None = None, + weights: ArrayLike | None = None, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.average`. + + Parameters + ---------- + a + Array to average. + axis + Axis along which to average. + weights + Weights associated with the values in *a*. + xp + Array namespace module. If *None*, derived from ``a`` and + ``weights``. + + Returns + ------- + :class:`object` + Weighted average. + """ + + xp = array_namespace(a, weights) if xp is None else xp + + if is_numpy_namespace(xp): + return np.average(a, axis=axis, weights=weights) # pyright: ignore + + a = xp_as_float_array(a, xp=xp) + + if weights is None: + return xp.mean(a, axis=axis) + + weights = xp_as_float_array(weights, xp=xp, like=a) + if weights.ndim == 1 and a.ndim != 1 and axis is not None: + # Broadcast 1-D ``weights`` along ``axis`` to match ``np.average`` + # semantics for an N-D ``a``. + broadcast_shape = [1] * a.ndim + broadcast_shape[axis] = weights.shape[0] + weights = xp_reshape(weights, tuple(broadcast_shape), xp=xp) + + return xp.sum(a * weights, axis=axis) / xp.sum(weights, axis=axis) + + +@typing.overload +def xp_gradient( + f: ArrayLike, *varargs: Any, xp: ModuleType | None = None, axis: int +) -> NDArrayFloat: ... +@typing.overload +def xp_gradient( + f: ArrayLike, *varargs: Any, xp: ModuleType | None = None, axis: None = None +) -> NDArrayFloat | list[NDArrayFloat]: ... +def xp_gradient( + f: ArrayLike, *varargs: Any, xp: ModuleType | None = None, axis: Any = None +) -> NDArrayFloat | list[NDArrayFloat]: + """ + *Array API* compatible implementation of :func:`numpy.gradient`. + + Parameters + ---------- + f + Array of function values. + *varargs + Spacing between values. + xp + Array namespace module. If *None*, derived from ``f``. + axis + Axis along which to compute the gradient. + + Returns + ------- + :class:`object` + Gradient of *f*. + """ + + xp = array_namespace(f) if xp is None else xp + + if is_numpy_namespace(xp): + return np.gradient(f, *varargs, axis=axis) + + try: + result = xp.gradient(f, *varargs, axis=axis) + except (AttributeError, TypeError): + pass + else: + # Some backends (e.g., torch) return a tuple of tensors. + if isinstance(result, (tuple, list)) and len(result) == 1: + return result[0] + return result # pyright: ignore + + _runtime_warning_xp_fallback("xp_gradient") + + result = np.gradient(as_ndarray(f), *(as_ndarray(v) for v in varargs), axis=axis) + + if isinstance(result, list): + return [xp.asarray(r) for r in result] + + return xp.asarray(result) + + +def xp_resize(a: ArrayLike, new_shape: Any, *, xp: ModuleType | None = None) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.resize`. + + Parameters + ---------- + a + Array to resize. + new_shape + Shape of the resized array. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Resized array. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.resize(a, new_shape) + + try: + return xp.resize(a, new_shape) + except (AttributeError, TypeError): + pass + + # Native implementation via tile + slice for backends without resize. + # ``numpy.resize`` accepts ``int``, ``tuple``, or ``list`` shapes; + # normalise once at the boundary. + shape_tuple = tuple(new_shape) if hasattr(new_shape, "__iter__") else (new_shape,) + a = xp.asarray(a) + raveled = xp.reshape(a, (-1,)) + target_size = 1 + for shape in shape_tuple: + target_size *= shape + + if raveled.shape[0] == 0: + return xp.zeros( + shape_tuple, + dtype=a.dtype, # pyright: ignore + device=getattr(a, "device", None), + ) + + repeats = (target_size + raveled.shape[0] - 1) // raveled.shape[0] + tiled = xp.tile(raveled, (repeats,))[:target_size] + + return xp.reshape(tiled, shape_tuple) + + +def xp_nanmean( + a: ArrayLike, + *, + axis: int | None = None, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.nanmean`. + + Parameters + ---------- + a + Array containing numbers whose NaN-aware mean is desired. + axis + Axis along which the mean is computed. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + NaN-aware mean. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.nanmean(a, axis=axis) # pyright: ignore + + mask = xp.isnan(a) + zeroed = xp.where(mask, xp.asarray(0.0, dtype=a.dtype), a) # pyright: ignore + count = xp.sum( + xp_astype(~mask, a.dtype, xp=xp), # pyright: ignore + axis=axis, + ) + + return xp.sum(zeroed, axis=axis) / count + + +def xp_median( + a: ArrayLike, + *, + axis: int | None = None, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.median`. + + Parameters + ---------- + a + Array whose median is desired. + axis + Axis along which the median is computed. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Median value(s). + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.median(a, axis=axis) # pyright: ignore + + _runtime_warning_xp_fallback("xp_median") + + result = np.median(as_ndarray(a), axis=axis) + + return xp.asarray(result) + + +def xp_round( + a: ArrayLike, + *, + decimals: int = 0, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.round` with *decimals*. + + The Array API standard ``xp.round`` does not accept a *decimals* + parameter. This helper uses the backend's native ``round`` when it + supports *decimals* (JAX, CuPy), otherwise falls back to a + multiply-round-divide pattern using the standard ``xp.round``. + + Parameters + ---------- + a + Array to round. + decimals + Number of decimal places. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Rounded array. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.round(a, decimals) # pyright: ignore + + try: + return xp.round(a, decimals) + except (AttributeError, TypeError): + factor = 10**decimals + return xp.round(a * factor) / factor + + +def _scale_by( + a: ArrayLike, factor: float, *, xp: ModuleType | None = None +) -> NDArrayFloat: + """Multiply array :math:`a` by a scalar factor in its native namespace.""" + + a = as_float_array(a) + + if not is_array_api_enabled(): + return a * factor + + xp = array_namespace(a) if xp is None else xp + + return a * xp_as_float_array(factor, xp=xp, like=a) + + +def xp_radians(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.radians`. + + Parameters + ---------- + a + Angle in degrees. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Angle in radians. + """ + + return _scale_by(a, np.pi / 180, xp=xp) + + +def xp_degrees(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.degrees`. + + Parameters + ---------- + a + Angle in radians. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Angle in degrees. + """ + + return _scale_by(a, 180 / np.pi, xp=xp) + + +# NOTE: The following wrappers around ``array_api_extra`` functions exist for +# typing purposes. ``array_api_extra`` returns a generic ``Array`` type that +# ``pyright`` cannot reconcile with ``NDArrayFloat`` and other *Colour* type +# aliases. These thin wrappers provide properly annotated return types, +# avoiding ``cast()`` noise at every call site. They can be removed once +# ``array-api-typing`` is released and ``array_api_extra`` adopts it. + + +def xp_atleast_1d(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.atleast_1d`. + + Parameters + ---------- + a + Array to ensure is at least 1-D. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Array with ``ndim >= 1``. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.atleast_1d(a) + + if isinstance(a, (np.ndarray, np.generic)): + a = xp_as_array(a, xp=xp) + + return _xpx().atleast_nd(a, ndim=1, xp=xp) + + +def xp_atleast_2d(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.atleast_2d`. + + Parameters + ---------- + a + Array to ensure is at least 2-D. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Array with ``ndim >= 2``. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.atleast_2d(a) + + if isinstance(a, (np.ndarray, np.generic)): + a = xp_as_array(a, xp=xp) + + return _xpx().atleast_nd(a, ndim=2, xp=xp) + + +def xp_squeeze( + a: ArrayLike, + *, + axis: int | tuple[int, ...] | None = None, + xp: ModuleType | None = None, +) -> NDArray: + """ + Squeeze size-1 dimensions from the specified array :math:`a`. + + The *Array API* standard requires an explicit ``axis`` argument for + :func:`squeeze`, unlike *NumPy* which allows omitting it to squeeze all + size-1 dimensions. When ``axis`` is ``None``, this helper computes the + axes automatically. + + Parameters + ---------- + a + Array :math:`a` to squeeze. + axis + Axis or axes to squeeze. When ``None``, all size-1 dimensions are + squeezed. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Squeezed array. + + Examples + -------- + >>> xp_squeeze(np.array([[1.0, 2.0]]), xp=np) + array([1., 2.]) + >>> xp_squeeze(np.array([[[1.0], [2.0]]]), axis=-1, xp=np) + array([[1., 2.]]) + """ + + xp = array_namespace(a) if xp is None else xp + + if axis is not None: + return xp.squeeze(a, axis=axis) + + axes = tuple(i for i in range(a.ndim) if a.shape[i] == 1) # pyright: ignore + + if not axes: + return a # pyright: ignore + + return xp.squeeze(a, axis=axes) + + +def xp_sinc(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.sinc`. + + Parameters + ---------- + a + Array of values. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Sinc of *a*. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.sinc(a) # pyright: ignore + + if isinstance(a, (np.ndarray, np.generic)): + a = xp_as_array(a, xp=xp) + + return _xpx().sinc(a, xp=xp) + + +def xp_isclose( + a: ArrayLike, + b: ArrayLike, + *, + rtol: float = 1e-5, + atol: float = 1e-8, + xp: ModuleType | None = None, +) -> NDArrayBoolean: + """ + *Array API* compatible implementation of :func:`numpy.isclose`. + + Parameters + ---------- + a + First array. + b + Second array. + rtol + Relative tolerance. + atol + Absolute tolerance. + xp + Array namespace module. If *None*, derived from ``a`` and ``b``. + + Returns + ------- + :class:`object` + Boolean array of element-wise comparisons. + """ + + xp = array_namespace(a, b) if xp is None else xp + + if is_numpy_namespace(xp): + return np.isclose(a, b, rtol=rtol, atol=atol) + + if isinstance(a, (np.ndarray, np.generic)): + a = xp_as_array(a, xp=xp) + if isinstance(b, (np.ndarray, np.generic)): + b = xp_as_array(b, xp=xp) + + return _xpx().isclose(a, b, rtol=rtol, atol=atol, xp=xp) + + +def xp_nan_to_num( + a: ArrayLike, + *, + nan: float = 0.0, + posinf: float | None = None, + neginf: float | None = None, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.nan_to_num`. + + Parameters + ---------- + a + Array to process. + nan + Value to replace NaN entries. + posinf + Value to replace positive infinity entries. + neginf + Value to replace negative infinity entries. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Array with NaN/Inf replaced. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.nan_to_num(a, nan=nan, posinf=posinf, neginf=neginf) + + a = as_float_array(a) + + result = as_float_array(_xpx().nan_to_num(a, fill_value=nan, xp=xp)) + + if posinf is not None: + result = as_float_array(xp.where(xp.isinf(a) & (a > 0), posinf, result)) + + if neginf is not None: + result = as_float_array(xp.where(xp.isinf(a) & (a < 0), neginf, result)) + + return result + + +def xp_create_diagonal(a: ArrayLike, *, xp: ModuleType | None = None) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.diagflat`. + + Parameters + ---------- + a + 1-D array of diagonal values. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + 2-D array with *a* on the diagonal. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.diagflat(a) + + if isinstance(a, (np.ndarray, np.generic)): + a = xp_as_array(a, xp=xp) + + return _xpx().create_diagonal(a, xp=xp) + + +def xp_reshape(a: ArrayLike, shape: Any, *, xp: ModuleType | None = None) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.reshape`. + + This typed wrapper exists because ``xp`` is typed as :class:`Any`, so + ``xp.reshape(...)`` returns :class:`Any`. When the result is assigned + back to a variable that was originally a function parameter (e.g., + ``a: ArrayLike``), *Pyright* reverts the variable to its declared type + instead of narrowing it. This wrapper declares ``-> NDArray`` so + that *Pyright* can track the narrowed type through reassignments. + + Callers must promote *Python* scalars / lists to the backend + namespace beforehand (e.g. via :func:`xp_as_float_array`), since + strict backends like *PyTorch* reject raw scalars and the wrapper + cannot infer the target device for a scalar input. + + Parameters + ---------- + a + Input array. + shape + New shape. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Reshaped array. + """ + + xp = array_namespace(a) if xp is None else xp + + return xp.reshape(a, shape) + + +def xp_broadcast_to( + a: ArrayLike, shape: Any, *, xp: ModuleType | None = None +) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.broadcast_to`. + + This typed wrapper exists because ``xp`` is typed as :class:`Any`, so + ``xp.broadcast_to(...)`` returns :class:`Any`. When the result is + assigned back to a variable that was originally a function parameter + (e.g., ``a: ArrayLike``), *Pyright* reverts the variable to its + declared type instead of narrowing it. This wrapper declares ``-> + NDArray`` so that *Pyright* can track the narrowed type through + reassignments. + + Callers must promote *Python* scalars / lists to the backend + namespace beforehand (e.g. via :func:`xp_as_float_array`), since + strict backends like *PyTorch* reject raw scalars and the wrapper + cannot infer the target device for a scalar input. + + Parameters + ---------- + a + Input array. + shape + Target shape. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`object` + Broadcast array. + """ + + xp = array_namespace(a) if xp is None else xp + + return xp.broadcast_to(a, shape) + + +def xp_lstsq( + a: ArrayLike, + b: ArrayLike, + *, + rcond: float | None = None, + xp: ModuleType | None = None, +) -> NDArrayFloat: + """ + *Array API* compatible implementation of :func:`numpy.linalg.lstsq`. + + Returns only the least-squares solution (the first element of the tuple + returned by :func:`numpy.linalg.lstsq`). Backends that provide + ``xp.linalg.lstsq`` (e.g., *JAX*, *PyTorch*) are used natively; others + fall back to *NumPy* with a `ColourRuntimeWarning`. + + Parameters + ---------- + a + Coefficient matrix. + b + Ordinate values. + rcond + Cut-off ratio for small singular values (passed to *NumPy* only). + xp + Array namespace module. If *None*, derived from ``a`` and ``b``. + + Returns + ------- + :class:`object` + Least-squares solution. + """ + + xp = array_namespace(a, b) if xp is None else xp + + if is_numpy_namespace(xp): + return np.linalg.lstsq(a, b, rcond=rcond)[0] # pyright: ignore + + try: + return xp.linalg.lstsq(a, b)[0] + except (AttributeError, TypeError, NotImplementedError, RuntimeError): + pass + + _runtime_warning_xp_fallback("xp_lstsq") + + a_nd = as_ndarray(a) + result = np.linalg.lstsq(a_nd, as_ndarray(b), rcond=rcond)[0] + result = result.astype(a_nd.dtype) + + return xp.asarray(result) + + +def _xp_eig_generic(a: ArrayLike, xp: ModuleType, name: str) -> tuple[NDArray, NDArray]: + """Shared implementation for :func:`xp_eig` and :func:`xp_eigh`.""" + + try: + return getattr(xp.linalg, name)(a) + except (AttributeError, TypeError, NotImplementedError, RuntimeError): + _runtime_warning_xp_fallback(f"xp_{name}") + + w, v = getattr(np.linalg, name)(as_ndarray(a)) + + return xp_as_array(w, xp=xp), xp_as_array(v, xp=xp) + + +def xp_eig(a: ArrayLike, *, xp: ModuleType | None = None) -> tuple[NDArray, NDArray]: + """ + *Array API* compatible implementation of :func:`numpy.linalg.eig`. + + Falls back to *NumPy* when the backend does not implement + ``linalg.eig`` (e.g., *PyTorch* MPS). + + Parameters + ---------- + a + Input square matrix. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`tuple` + Eigenvalues and eigenvectors. + """ + + xp = array_namespace(a) if xp is None else xp + + return _xp_eig_generic(a, xp, "eig") + + +def xp_eigh(a: ArrayLike, *, xp: ModuleType | None = None) -> tuple[NDArray, NDArray]: + """ + *Array API* compatible implementation of :func:`numpy.linalg.eigh`. + + Falls back to *NumPy* when the backend does not implement + ``linalg.eigh`` (e.g., *PyTorch* MPS). + + Parameters + ---------- + a + Input symmetric/Hermitian matrix. + xp + Array namespace module. If *None*, derived from ``a``. + + Returns + ------- + :class:`tuple` + Eigenvalues and eigenvectors. + """ + + xp = array_namespace(a) if xp is None else xp + + return _xp_eig_generic(a, xp, "eigh") + + +def xp_isin( + element: ArrayLike, + test_elements: ArrayLike, + *, + xp: ModuleType | None = None, + like: Any = None, +) -> NDArrayBoolean: + """ + *Array API* compatible implementation of :func:`numpy.isin`. + + Use the backend's native ``isin`` when available (JAX, CuPy), + otherwise fall back to NumPy. + + Parameters + ---------- + element + Input array. + test_elements + Values against which to test each element of *element*. + xp + Array namespace module. If *None*, derived from ``element`` and + ``test_elements``. + like + Reference array whose device ``test_elements`` should be placed + on when promoted to ``xp``. Defaults to ``element`` when *None*. + + Returns + ------- + :class:`object` + Boolean array of the same shape as *element*. + + Notes + ----- + - :class:`NaN` is treated as not equal to itself, matching + :func:`numpy.isin` semantics; ``xp_isin([NaN], [NaN])`` returns + ``[False]``. + """ + + xp = array_namespace(element, test_elements) if xp is None else xp + + if is_numpy_namespace(xp): + return np.isin(element, test_elements) + + reference = element if like is None else like + + try: + if isinstance(test_elements, np.ndarray) and hasattr(reference, "device"): + test_elements = xp.asarray(test_elements, device=reference.device) # pyright: ignore + return xp.isin(element, test_elements) + except (AttributeError, TypeError): + pass + + _runtime_warning_xp_fallback("xp_isin") + + result = np.isin(np.asarray(element), np.asarray(test_elements)) + + return xp.asarray(result) + + +def xp_linspace( + start: ArrayLike, + stop: ArrayLike, + *, + num: int = 50, + xp: ModuleType | None = None, + like: Any = None, + **kwargs: Any, +) -> NDArrayFloat | tuple[NDArrayFloat, float]: + """ + *Array API* compatible implementation of :func:`numpy.linspace` with + extra keyword arguments such as *retstep* and *dtype*. + + The Array API standard ``xp.linspace`` does not accept *retstep*. + This helper tries the backend's native ``linspace`` first, falling + back to NumPy if the keyword is unsupported. + + Parameters + ---------- + start + Start of the interval. + stop + End of the interval. + num + Number of samples. + xp + Array namespace module. If *None*, derived from ``start`` and + ``stop``. + like + Reference array whose device to match (for backends like *PyTorch* + that support multiple devices); the result is created on that device + rather than the backend's default. + **kwargs + Extra keyword arguments (e.g., ``retstep``, ``dtype``). + + Returns + ------- + :class:`object` + Array of evenly spaced values (and step size if *retstep=True*). + """ + + xp = array_namespace(start, stop) if xp is None else xp + + if is_numpy_namespace(xp): + return np.linspace(start, stop, num, **kwargs) # pyright: ignore + + device = getattr(like, "device", None) + + try: + return xp.linspace(start, stop, num, device=device, **kwargs) + except (AttributeError, TypeError): + _runtime_warning_xp_fallback("xp_linspace") + + result = np.linspace(start, stop, num, **kwargs) # pyright: ignore + + def _to_backend(arr: NDArrayFloat) -> NDArrayFloat: + try: + return xp.asarray(arr, device=device) + except TypeError: + _runtime_warning_xp_downcast(xp, arr.dtype, "float32") + return xp.asarray(arr.astype(np.float32), device=device) + + if isinstance(result, tuple): + return _to_backend(np.asarray(result[0])), result[1] + + return _to_backend(np.asarray(result)) + + +def xp_pad( + a: ArrayLike, + pad_width: Any, + *args: Any, + xp: ModuleType | None = None, + **kwargs: Any, +) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.pad`. + + Use the backend's native ``pad`` when available (JAX, CuPy), + otherwise fall back to NumPy. + + Parameters + ---------- + a + Array to pad. + pad_width + Number of values padded to the edges of each axis. + *args + Positional arguments passed to the padding function. + xp + Array namespace module. If *None*, derived from ``a``. + **kwargs + Keyword arguments passed to the padding function. + + Returns + ------- + :class:`object` + Padded array. + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.pad(a, pad_width, *args, **kwargs) + + try: + return xp.pad(a, pad_width, *args, **kwargs) + except (AttributeError, TypeError): + pass + + _runtime_warning_xp_fallback("xp_pad") + + result = np.pad(as_ndarray(a), pad_width, *args, **kwargs) + + return xp.asarray(result) + + +def xp_unique( + a: ArrayLike, *, xp: ModuleType | None = None, **kwargs: Any +) -> NDArray | tuple[NDArray, ...]: + """ + *Array API* compatible implementation of :func:`numpy.unique` with + extra keyword arguments such as *return_index* and *axis*. + + The Array API standard only provides ``xp.unique_values`` and + related functions without *return_index* or *axis* support. + This helper tries the backend's native ``unique`` first, falling + back to NumPy if the keywords are unsupported. + + Parameters + ---------- + a + Input array. + xp + Array namespace module. If *None*, derived from ``a``. + **kwargs + Extra keyword arguments (e.g., ``return_index``, ``axis``). + + Returns + ------- + :class:`object` + Unique values (and optional indices). + """ + + xp = array_namespace(a) if xp is None else xp + + if is_numpy_namespace(xp): + return np.unique(a, **kwargs) + + try: + return xp.unique(a, **kwargs) + except (AttributeError, TypeError): + pass + + _runtime_warning_xp_fallback("xp_unique") + + result = np.unique(as_ndarray(a), **kwargs) + + if isinstance(result, tuple): + return tuple(xp.asarray(r) for r in result) + + return xp.asarray(result) + + +def xp_insert( + a: ArrayLike, + indices: ArrayLike, + values: ArrayLike, + *, + axis: int | None = None, + xp: ModuleType | None = None, +) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.insert` for sorted + indices. + + Parameters + ---------- + a + Array to insert into. + indices + Indices before which to insert *values*. + values + Values to insert. + axis + Axis along which to insert; ``None`` flattens ``a`` first. + xp + Array namespace module. If *None*, derived from ``a``, ``indices`` + and ``values``. + + Returns + ------- + :class:`object` + Array with *values* inserted. + """ + + xp = array_namespace(a, indices, values) if xp is None else xp + + if is_numpy_namespace(xp): + return np.insert(a, indices, values, axis=axis) # pyright: ignore + + a = xp_as_array(a, xp=xp) + indices = xp_as_array(indices, xp=xp) + values = xp_as_array(values, xp=xp) + + if axis is None: + a = xp_reshape(a, (-1,), xp=xp) + axis = 0 + + def slice_along(arr: NDArray, indexer: slice | NDArray) -> NDArray: + """Index ``arr`` along ``axis`` with ``indexer``.""" + + selector = [slice(None)] * arr.ndim + selector[cast("int", axis)] = indexer # pyright: ignore + return arr[tuple(selector)] + + # ``numpy.insert`` accepts unsorted indices and normalises to the + # sorted equivalent; the sequential concat below requires sorted + # indices, so sort them and reorder ``values`` along ``axis`` to + # match. Materialise sorted indices to *NumPy* once (single host + # sync) to avoid per-iteration ``int(indices[i])`` syncs inside the + # loop below. + order = xp.argsort(indices) + values = slice_along(values, order) + indices = as_ndarray(indices[order]) + + a_axis = a.shape[axis] + parts = [] + prev = 0 + for i in range(indices.shape[0]): + idx = int(indices[i]) + parts.append(slice_along(a, slice(prev, idx))) + parts.append(slice_along(values, slice(i, i + 1))) + prev = idx + parts.append(slice_along(a, slice(prev, a_axis))) + + return xp.concat(parts, axis=axis) + + +def xp_setxor1d( + a: ArrayLike, + b: ArrayLike, + *, + xp: ModuleType | None = None, +) -> NDArray: + """ + *Array API* compatible implementation of :func:`numpy.setxor1d`. + + Return sorted unique values that are in only one of the two input arrays. + + Parameters + ---------- + a + First array. + b + Second array. + xp + Array namespace module. If *None*, derived from ``a`` and ``b``. + + Returns + ------- + :class:`object` + Sorted symmetric difference. + """ + + xp = array_namespace(a, b) if xp is None else xp + + if is_numpy_namespace(xp): + return np.setxor1d(a, b) + + a = xp_as_array(a, xp=xp) + b = xp_as_array(b, xp=xp, like=a) + + # NOTE: ``cast`` is used to bridge ``NDArrayFloat`` and the ``Array`` + # protocol from ``array-api-extra`` which *Pyright* cannot reconcile + # across environments with and without type stubs. + xpx_typed = _xpx() + a_in_b = xpx_typed.isin(cast("Any", a), cast("Any", b)) + b_in_a = xpx_typed.isin(cast("Any", b), cast("Any", a)) + + a_only = cast("Any", a)[~xp.asarray(a_in_b)] + b_only = cast("Any", b)[~xp.asarray(b_in_a)] + + # ``torch.sort`` returns a ``(values, indices)`` tuple; the *Array + # API* standard's ``xp.sort`` returns the values array. Tolerate both + # so this helper works with raw and wrapped namespaces. + result = xp.sort(xp.concat([a_only, b_only])) + return result[0] if isinstance(result, tuple) else result + + +def xp_assert_close( + actual: ArrayLike, + desired: ArrayLike, + *, + rtol: float | None = None, + atol: float | None = None, + err_msg: str = "", +) -> None: + """ + *Array API* compatible implementation of :func:`numpy.testing.assert_allclose`. + + Both arrays are converted to *NumPy* via :func:`as_ndarray` before + comparison. + + Parameters + ---------- + actual + Array produced by the tested function. + desired + Expected array. + rtol + Relative tolerance. If *None*, + :attr:`colour.constants.TOLERANCE_RELATIVE_TESTS`, resolved at call + time so that test fixtures relaxing the module-level constant also + relax calls relying on the default. + atol + Absolute tolerance. If *None*, + :attr:`colour.constants.TOLERANCE_ABSOLUTE_TESTS`, resolved at call + time so that test fixtures relaxing the module-level constant also + relax calls relying on the default. + err_msg + Error message to display on failure. + """ + + rtol = TOLERANCE_RELATIVE_TESTS if rtol is None else rtol + atol = TOLERANCE_ABSOLUTE_TESTS if atol is None else atol + + np.testing.assert_allclose( + as_ndarray(actual), + as_ndarray(desired), + atol=atol, + rtol=rtol, + err_msg=err_msg, + ) + -__author__ = "Colour Developers" -__copyright__ = "Copyright 2013 Colour Developers" -__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" -__maintainer__ = "Colour Developers" -__email__ = "colour-developers@colour-science.org" -__status__ = "Production" +def xp_assert_equal( + actual: ArrayLike, + desired: ArrayLike, + *, + err_msg: str = "", +) -> None: + """ + *Array API* compatible implementation of :func:`numpy.testing.assert_array_equal`. -__all__ = [ - "MixinDataclassFields", - "MixinDataclassIterable", - "MixinDataclassArray", - "MixinDataclassArithmetic", - "as_array", - "as_int", - "as_float", - "as_int_array", - "as_float_array", - "as_int_scalar", - "as_float_scalar", - "as_complex_array", - "set_default_int_dtype", - "set_default_float_dtype", - "get_domain_range_scale", - "set_domain_range_scale", - "domain_range_scale", - "get_domain_range_scale_metadata", - "to_domain_1", - "to_domain_10", - "to_domain_100", - "to_domain_degrees", - "to_domain_int", - "from_range_1", - "from_range_10", - "from_range_100", - "from_range_degrees", - "from_range_int", - "is_ndarray_copy_enabled", - "set_ndarray_copy_enable", - "ndarray_copy_enable", - "ndarray_copy", - "closest_indexes", - "closest", - "interval", - "is_uniform", - "in_array", - "tstack", - "tsplit", - "row_as_diagonal", - "orient", - "centroid", - "fill_nan", - "has_only_nan", - "ndarray_write", - "zeros", - "ones", - "full", - "index_along_last_axis", - "format_array_as_row", -] + Both arrays are converted to *NumPy* via :func:`as_ndarray` before + comparison. + + Parameters + ---------- + actual + Array produced by the tested function. + desired + Expected array. + err_msg + Error message to display on failure. + """ + + np.testing.assert_array_equal( + as_ndarray(actual), + as_ndarray(desired), + err_msg=err_msg, + ) class MixinDataclassFields: @@ -298,7 +2625,10 @@ def __array__( return tstack( cast( "ArrayLike", - [value if value is not None else default for value in self.values], + [ + as_ndarray(value) if value is not None else default + for value in self.values + ], ), dtype=dtype, ) @@ -544,7 +2874,10 @@ def arithmetical_operation( if is_dataclass(a): a = as_float_array(a) # pyright: ignore - values = tsplit(callable_operation(as_float_array(self), a)) + self_array = as_float_array(self) + a = as_ndarray(a) if is_non_ndarray(a) else a + + values = tsplit(callable_operation(self_array, a)) field_values = {field: values[i] for i, field in enumerate(self.keys)} field_values.update({field: None for field, value in self if value is None}) @@ -573,13 +2906,82 @@ def arithmetical_operation( ) +def cast_non_ndarray(a: ArrayLike, dtype: Any) -> Any | None: + """ + Cast the specified non-:class:`numpy.ndarray` array :math:`a` to the + specified :class:`numpy.dtype` in its native namespace. + + This is the *Array API* sibling of :func:`as_ndarray`; it preserves a + non-NumPy array's namespace and device while applying a dtype change + via :func:`xp_astype`, returning ``None`` when the input is a NumPy + array, when *Array API* dispatch is disabled, when the input is not + an array (no ``dtype`` attribute), or when the namespace does not + expose an equivalent dtype. + + Parameters + ---------- + a + Array to cast. Returned as-is when its dtype already matches the + target. + dtype + Target :class:`numpy.dtype`. Resolved against the input's native + namespace via attribute lookup on the dtype name. + + Returns + ------- + :class:`object` or :py:obj:`None` + Cast array in its native namespace, or ``None`` when the input is + not eligible for a non-NumPy cast. + + Examples + -------- + >>> import numpy as np + >>> cast_non_ndarray(np.array([1, 2, 3]), np.float32) is None + True + + Cast a *PyTorch* tensor while preserving its device: + + >>> import torch # doctest: +SKIP + >>> set_array_api_enabled(True) # doctest: +SKIP + >>> cast_non_ndarray(torch.tensor([1, 2, 3]), np.float32).dtype # doctest: +SKIP + torch.float32 + >>> set_array_api_enabled(False) # doctest: +SKIP + """ + + if not ( + is_array_api_enabled() and hasattr(a, "dtype") and not isinstance(a, np.ndarray) + ): + return None + + xp = array_namespace(a) + + xp_dtype = getattr(xp, np.dtype(dtype).name, None) + + if xp_dtype is None: + return None + + if a.dtype == xp_dtype: # pyright: ignore + return a + + try: + return xp_astype(a, xp_dtype, xp=xp) + except (TypeError, AttributeError, RuntimeError): + return None + + def as_array( a: ArrayLike | KeysView | ValuesView, dtype: Type[DType] | None = None, ) -> NDArray: """ - Convert the specified variable :math:`a` to :class:`numpy.ndarray` using - the specified :class:`numpy.dtype`. + Convert the specified variable :math:`a` to an array using the specified + :class:`numpy.dtype`. + + This is a namespace-aware boundary helper. When *Array API* dispatch is + enabled and :math:`a` is a non-*NumPy* array (e.g. *JAX*, *PyTorch*), + the result is returned in :math:`a`'s native namespace, on its device, + and cast to ``dtype``. Otherwise the result is a + :class:`numpy.ndarray`. Parameters ---------- @@ -592,8 +2994,8 @@ def as_array( Returns ------- - :class:`numpy.ndarray` - Variable :math:`a` converted to :class:`numpy.ndarray`. + :class:`numpy.ndarray` or backend tensor + Variable :math:`a` converted to an array in the input's namespace. Examples -------- @@ -608,7 +3010,35 @@ def as_array( if isinstance(a, (KeysView, ValuesView)): a = list(a) - return np.asarray(a, dtype) + if is_array_api_enabled(): + # When ``a`` is a list/tuple of non-NumPy arrays, resolve the + # namespace from the first element and use ``xp.stack`` since + # ``xp.asarray(list)`` would fall back to NumPy. + if isinstance(a, list) and len(a) > 0 and is_non_ndarray(a[0]): + xp = array_namespace(a[0]) + + if dtype is not None: + dtype = getattr(xp, np.dtype(dtype).name, dtype) + + return xp.stack([xp.asarray(x) for x in a]) + + xp = array_namespace(a) + + if dtype is not None and not is_numpy_namespace(xp): + dtype = getattr(xp, np.dtype(dtype).name, dtype) + + return xp.asarray(a, dtype=dtype) + + try: + return np.asarray(a, dtype) + except TypeError: + # Device-resident tensors (e.g. *PyTorch* on *MPS*) reject *NumPy*'s + # ``__array__`` hand-off; route them through :func:`as_ndarray`. The + # common *NumPy* / sequence path above pays nothing for this. + if isinstance(a, (list, tuple)): + return np.asarray([as_ndarray(x) for x in a], dtype) + + return np.asarray(as_ndarray(a), dtype) @typing.overload @@ -623,12 +3053,13 @@ def as_int( ) -> DTypeInt | NDArrayInt: ... def as_int(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> DTypeInt | NDArrayInt: """ - Convert the specified variable :math:`a` to :class:`numpy.integer` using - the specified :class:`numpy.dtype`. + Convert the specified variable :math:`a` to an integer value. - The function converts variable :math:`a` to an integer type. If variable - :math:`a` is not a scalar or 0-dimensional array, it is converted to - :class:`numpy.ndarray`. + Scalars and 0-dimensional arrays are returned as a Python / *NumPy* + integer scalar. Higher-dimensional arrays go through + :func:`as_int_array`, which is namespace-aware: when *Array API* dispatch + is enabled and :math:`a` is a non-*NumPy* array, the result is returned + in :math:`a`'s native namespace. Parameters ---------- @@ -641,8 +3072,9 @@ def as_int(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> DTypeInt | NDAr Returns ------- - :class:`numpy.ndarray` - Variable :math:`a` converted to :class:`numpy.integer`. + :class:`numpy.integer` or :class:`numpy.ndarray` or backend tensor + Variable :math:`a` converted to an integer scalar or array in the + input's namespace. Examples -------- @@ -658,6 +3090,9 @@ def as_int(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> DTypeInt | NDAr attest(dtype in DTypeInt.__args__, _ASSERTION_MESSAGE_DTYPE_INT) + if is_array_api_enabled() and is_non_ndarray(a): + return as_int_array(a, dtype) + return dtype(a) # pyright: ignore @@ -677,11 +3112,13 @@ def as_float( a: ArrayLike, dtype: Type[DTypeFloat] | None = None ) -> DTypeFloat | NDArrayFloat: """ - Convert the specified variable :math:`a` to :class:`numpy.floating` using - the specified :class:`numpy.dtype`. + Convert the specified variable :math:`a` to a floating-point value. - If variable :math:`a` is not a scalar or 0-dimensional, it is converted - to :class:`numpy.ndarray`. + Scalars and 0-dimensional arrays are returned as a Python / *NumPy* + float scalar. Higher-dimensional arrays go through + :func:`as_float_array`, which is namespace-aware: when *Array API* + dispatch is enabled and :math:`a` is a non-*NumPy* array, the result is + returned in :math:`a`'s native namespace. Parameters ---------- @@ -694,8 +3131,9 @@ def as_float( Returns ------- - :class:`numpy.ndarray` - Variable :math:`a` converted to :class:`numpy.floating`. + :class:`numpy.floating` or :class:`numpy.ndarray` or backend tensor + Variable :math:`a` converted to a floating-point scalar or array in + the input's namespace. Examples -------- @@ -711,6 +3149,9 @@ def as_float( attest(dtype in DTypeFloat.__args__, _ASSERTION_MESSAGE_DTYPE_FLOAT) + if is_array_api_enabled() and not isinstance(a, np.ndarray): + return as_float_array(a, dtype) + # NOTE: "np.float64" reduces dimensionality: # >>> np.int64(np.array([[1]])) # array([[1]]) @@ -725,8 +3166,14 @@ def as_float( def as_int_array(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> NDArrayInt: """ - Convert the specified variable :math:`a` to :class:`numpy.ndarray` using - the specified integer :class:`numpy.dtype`. + Convert the specified variable :math:`a` to an integer array using the + specified :class:`numpy.dtype`. + + This is a namespace-aware boundary helper. When *Array API* dispatch is + enabled and :math:`a` is a non-*NumPy* array (e.g. *JAX*, *PyTorch*), + the result is returned in :math:`a`'s native namespace, on its device, + and cast to ``dtype``. Otherwise the result is a + :class:`numpy.ndarray`. Parameters ---------- @@ -739,8 +3186,9 @@ def as_int_array(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> NDArrayIn Returns ------- - :class:`numpy.ndarray` - Variable :math:`a` converted to integer :class:`numpy.ndarray`. + :class:`numpy.ndarray` or backend tensor + Variable :math:`a` converted to an integer array in the input's + namespace. Examples -------- @@ -752,13 +3200,28 @@ def as_int_array(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> NDArrayIn attest(dtype in DTypeInt.__args__, _ASSERTION_MESSAGE_DTYPE_INT) + result = cast_non_ndarray(a, dtype) + if result is not None: + return result + + if is_array_api_enabled() and is_non_ndarray(a): + a = as_ndarray(a) + return as_array(a, dtype) def as_float_array(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat: """ - Convert the specified variable :math:`a` to :class:`numpy.ndarray` using - the specified floating-point :class:`numpy.dtype`. + Convert the specified variable :math:`a` to a floating-point array using + the specified :class:`numpy.dtype`. + + This is a namespace-aware boundary helper. When *Array API* dispatch is + enabled and :math:`a` is a non-*NumPy* array (e.g. *JAX*, *PyTorch*), + the result is returned in :math:`a`'s native namespace, on its device, + and cast to ``dtype``. Otherwise the result is a + :class:`numpy.ndarray`. This is the convention used at function + boundaries: ``a = as_float_array(a)`` followed by + ``xp = array_namespace(a)`` recovers the caller's backend. Parameters ---------- @@ -771,9 +3234,9 @@ def as_float_array(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> NDArr Returns ------- - :class:`numpy.ndarray` - Variable :math:`a` converted to floating-point - :class:`numpy.ndarray`. + :class:`numpy.ndarray` or backend tensor + Variable :math:`a` converted to a floating-point array in the + input's namespace. Examples -------- @@ -785,6 +3248,11 @@ def as_float_array(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> NDArr attest(dtype in DTypeFloat.__args__, _ASSERTION_MESSAGE_DTYPE_FLOAT) + result = cast_non_ndarray(a, dtype) + + if result is not None: + return result + return as_array(a, dtype) @@ -818,7 +3286,11 @@ def as_int_scalar(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> int: np.int64(1) """ - a = np.squeeze(as_int_array(a, dtype)) + a = as_int_array(a, dtype) + + xp = array_namespace(a) + + a = xp_reshape(a, (), xp=xp) attest(a.ndim == 0, f'"{a}" cannot be converted to "int" scalar!') @@ -856,7 +3328,11 @@ def as_float_scalar(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> floa np.float64(1.0) """ - a = np.squeeze(as_float_array(a, dtype)) + a = as_float_array(a, dtype) + + xp = array_namespace(a) + + a = xp_reshape(a, (), xp=xp) attest(a.ndim == 0, f'"{a}" cannot be converted to "float" scalar!') @@ -869,8 +3345,14 @@ def as_complex_array( dtype: Type[DTypeComplex] | None = None, ) -> NDArrayComplex: """ - Convert the specified variable :math:`a` to :class:`numpy.ndarray` using - the specified complex :class:`numpy.dtype`. + Convert the specified variable :math:`a` to a complex array using the + specified :class:`numpy.dtype`. + + This is a namespace-aware boundary helper. When *Array API* dispatch is + enabled and :math:`a` is a non-*NumPy* array (e.g. *JAX*, *PyTorch*), + the result is returned in :math:`a`'s native namespace, on its device, + and cast to ``dtype``. Otherwise the result is a + :class:`numpy.ndarray`. Parameters ---------- @@ -883,9 +3365,9 @@ def as_complex_array( Returns ------- - :class:`numpy.ndarray` - Variable :math:`a` converted to complex - :class:`numpy.ndarray`. + :class:`numpy.ndarray` or backend tensor + Variable :math:`a` converted to a complex array in the input's + namespace. Examples -------- @@ -899,6 +3381,21 @@ def as_complex_array( attest(dtype in DTypeComplex.__args__, _ASSERTION_MESSAGE_DTYPE_COMPLEX) + result = cast_non_ndarray(a, dtype) + if result is not None: + return result + + # Fallback for backends that do not support the target complex dtype + # (e.g., MPS does not support complex128): try the backend's complex64, + # else hand the array off to host *NumPy* and cast there (mirroring the + # :func:`as_int_array` / :func:`as_float_array` fall-through rather than + # returning the input uncast under a complex contract). + if is_array_api_enabled() and is_non_ndarray(a): + result = cast_non_ndarray(a, np.complex64) + if result is not None: + return result + a = as_ndarray(a) + return as_array(a, dtype) @@ -1005,13 +3502,65 @@ def set_default_float_dtype( CACHE_REGISTRY.clear_all_caches() -# TODO: Annotate with "Union[Literal['ignore', 'reference', '1', '100'], str]" -# when Python 3.7 is dropped. -_DOMAIN_RANGE_SCALE = "reference" -""" -Global variable storing the current *Colour* domain-range scale. +def set_default_complex_dtype( + dtype: Type[DTypeComplex] = DTYPE_COMPLEX_DEFAULT, +) -> None: + """ + Set the *Colour* default :class:`numpy.complexfloating` precision by + setting :attr:`colour.constant.DTYPE_COMPLEX_DEFAULT` attribute with the + specified :class:`numpy.dtype` wherever the attribute is imported. + + Parameters + ---------- + dtype + :class:`numpy.dtype` to set + :attr:`colour.constant.DTYPE_COMPLEX_DEFAULT` with. + + Notes + ----- + - It is possible to define the *complex* precision at import time by + setting the *COLOUR_SCIENCE__DEFAULT_COMPLEX_DTYPE* environment + variable, for example + `set COLOUR_SCIENCE__DEFAULT_COMPLEX_DTYPE=complex64`. + + Warnings + -------- + Changing *complex* precision might result in various *Colour* + functionality breaking entirely. With great power comes great + responsibility. + + Examples + -------- + >>> as_complex_array(np.ones(3)).dtype + dtype('complex128') + >>> set_default_complex_dtype(np.complex64) # doctest: +SKIP + >>> as_complex_array(np.ones(3)).dtype # doctest: +SKIP + dtype('complex64') + >>> set_default_complex_dtype(np.complex128) + >>> as_complex_array(np.ones(3)).dtype + dtype('complex128') + """ + + with suppress_warnings(colour_usage_warnings=True): + for module in sys.modules.values(): + if not hasattr(module, "DTYPE_COMPLEX_DEFAULT"): + continue -_DOMAIN_RANGE_SCALE + module.DTYPE_COMPLEX_DEFAULT = dtype # pyright: ignore + + CACHE_REGISTRY.clear_all_caches() + + +_DOMAIN_RANGE_SCALE: contextvars.ContextVar[ + Literal["ignore", "reference", "1", "100"] | str +] = contextvars.ContextVar("_DOMAIN_RANGE_SCALE", default="reference") +""" +:class:`contextvars.ContextVar` storing the current *Colour* domain-range +scale. The :class:`contextvars.ContextVar` keeps nested +:class:`domain_range_scale` contexts independent across concurrent +threads and async tasks. Read it via :func:`get_domain_range_scale` and +toggle it via :func:`set_domain_range_scale` or +:class:`domain_range_scale`. """ @@ -1040,7 +3589,7 @@ def get_domain_range_scale() -> Literal["ignore", "reference", "1", "100"] | str internal usage only! """ - return _DOMAIN_RANGE_SCALE + return _DOMAIN_RANGE_SCALE.get() def set_domain_range_scale( @@ -1072,12 +3621,12 @@ def set_domain_range_scale( internal usage only! """ - global _DOMAIN_RANGE_SCALE # noqa: PLW0603 - - _DOMAIN_RANGE_SCALE = validate_method( - str(scale), - ("ignore", "reference", "1", "100"), - '"{0}" scale is invalid, it must be one of {1}!', + _DOMAIN_RANGE_SCALE.set( + validate_method( + str(scale), + ("ignore", "reference", "1", "100"), + '"{0}" scale is invalid, it must be one of {1}!', + ) ) @@ -1143,12 +3692,25 @@ def __init__( ), ) -> None: self._scale = scale - self._previous_scale = get_domain_range_scale() + # Token stack: nested or recursive ``__enter__`` / ``__exit__`` + # pairs against the same instance (e.g. via the decorator form on + # a recursive function) push and pop independent reset tokens. + self._tokens: list[ + contextvars.Token[Literal["ignore", "reference", "1", "100"] | str] + ] = [] def __enter__(self) -> Self: """Set the new domain-range scale upon entering the context manager.""" - set_domain_range_scale(self._scale) + self._tokens.append( + _DOMAIN_RANGE_SCALE.set( + validate_method( + str(self._scale), + ("ignore", "reference", "1", "100"), + '"{0}" scale is invalid, it must be one of {1}!', + ) + ) + ) return self @@ -1158,7 +3720,7 @@ def __exit__(self, *args: Any) -> None: manager. """ - set_domain_range_scale(self._previous_scale) + _DOMAIN_RANGE_SCALE.reset(self._tokens.pop()) def __call__(self, function: Callable) -> Any: """ @@ -1311,6 +3873,37 @@ def extract_scale_from_hint(hint: Any) -> Any | None: return metadata +def _scale_at( + a: ArrayLike, + target_scale: str, + scale_factor: ArrayLike, + dtype: Type[DTypeFloat] | None, + *, + divide: bool = False, +) -> NDArray: + """ + Apply ``a * scale_factor`` or ``a / scale_factor`` when the current + domain-range scale matches ``target_scale``. + """ + + if target_scale != _DOMAIN_RANGE_SCALE.get(): + return a # pyright: ignore + + if not is_array_api_enabled(): + a = np.asarray(a, dtype=dtype) + # ``np.asarray`` re-wraps the result so that ``NumPy >= 2`` does not + # collapse a 0-d array divided / multiplied by a Python scalar to + # a :class:`numpy.float*` instance, which would break downstream + # callers expecting an :class:`numpy.ndarray`. + return np.asarray(a / scale_factor if divide else a * scale_factor) + + xp = array_namespace(a) + + factor = xp_as_array(scale_factor, dtype=dtype, xp=xp, like=a) + + return xp.asarray(a / factor if divide else a * factor) + + def to_domain_1( a: ArrayLike, scale_factor: ArrayLike = 100, @@ -1366,12 +3959,9 @@ def to_domain_1( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - a = as_float_array(a, dtype).copy() - - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a / np.asarray(scale_factor), dtype) + a = ndarray_copy(as_float_array(a, dtype)) - return a + return _scale_at(a, "100", scale_factor, dtype, divide=True) def to_domain_10( @@ -1433,15 +4023,11 @@ def to_domain_10( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - a = as_float_array(a, dtype).copy() - - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a * np.asarray(scale_factor), dtype) + a = ndarray_copy(as_float_array(a, dtype)) - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a / np.asarray(scale_factor), dtype) + a = _scale_at(a, "1", scale_factor, dtype) - return a + return _scale_at(a, "100", scale_factor, dtype, divide=True) def to_domain_100( @@ -1500,12 +4086,9 @@ def to_domain_100( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - a = as_float_array(a, dtype).copy() - - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a * np.asarray(scale_factor), dtype) + a = ndarray_copy(as_float_array(a, dtype)) - return a + return _scale_at(a, "1", scale_factor, dtype) def to_domain_degrees( @@ -1565,15 +4148,11 @@ def to_domain_degrees( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - a = as_float_array(a, dtype).copy() - - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a * np.asarray(scale_factor), dtype) + a = ndarray_copy(as_float_array(a, dtype)) - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a * np.asarray(scale_factor) / 100, dtype) + a = _scale_at(a, "1", scale_factor, dtype) - return a + return _scale_at(a, "100", scale_factor / 100, dtype) # pyright: ignore def to_domain_int( @@ -1638,16 +4217,13 @@ def to_domain_int( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - a = as_float_array(a, dtype).copy() + a = ndarray_copy(as_float_array(a, dtype)) - maximum_code_value: NDArray[DTypeInt] = np.power(2, bit_depth) - 1 - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a * maximum_code_value, dtype) + maximum_code_value = 2**bit_depth - 1 # pyright: ignore - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a * maximum_code_value / 100, dtype) + a = _scale_at(a, "1", maximum_code_value, dtype) - return a + return _scale_at(a, "100", maximum_code_value / 100, dtype) def from_range_1( @@ -1712,10 +4288,7 @@ def from_range_1( a = as_float_array(a, dtype) - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a * np.asarray(scale_factor), dtype) - - return a + return _scale_at(a, "100", scale_factor, dtype) def from_range_10( @@ -1783,13 +4356,9 @@ def from_range_10( a = as_float_array(a, dtype) - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a / np.asarray(scale_factor), dtype) + a = _scale_at(a, "1", scale_factor, dtype, divide=True) - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a * np.asarray(scale_factor), dtype) - - return a + return _scale_at(a, "100", scale_factor, dtype) def from_range_100( @@ -1854,10 +4423,7 @@ def from_range_100( a = as_float_array(a, dtype) - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a / np.asarray(scale_factor), dtype) - - return a + return _scale_at(a, "1", scale_factor, dtype, divide=True) def from_range_degrees( @@ -1924,13 +4490,9 @@ def from_range_degrees( a = as_float_array(a, dtype) - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a / np.asarray(scale_factor), dtype) + a = _scale_at(a, "1", scale_factor, dtype, divide=True) - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a / (np.asarray(scale_factor) / 100), dtype) - - return a + return _scale_at(a, "100", scale_factor / 100, dtype, divide=True) # pyright: ignore def from_range_int( @@ -2003,20 +4565,23 @@ def from_range_int( a = as_float_array(a, dtype) - maximum_code_value: NDArray[DTypeInt] = np.power(2, bit_depth) - 1 - if _DOMAIN_RANGE_SCALE == "1": - a = as_float_array(a / maximum_code_value, dtype) + maximum_code_value = 2**bit_depth - 1 # pyright: ignore - if _DOMAIN_RANGE_SCALE == "100": - a = as_float_array(a / (maximum_code_value / 100), dtype) + a = _scale_at(a, "1", maximum_code_value, dtype, divide=True) - return a + return _scale_at(a, "100", maximum_code_value / 100, dtype, divide=True) -_NDARRAY_COPY_ENABLED: bool = True +_NDARRAY_COPY_ENABLED: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_NDARRAY_COPY_ENABLED", default=True +) """ -Global variable storing the current *Colour* state for -:class:`numpy.ndarray` copy. +:class:`contextvars.ContextVar` storing the current *Colour* state for +:class:`numpy.ndarray` copy. The :class:`contextvars.ContextVar` keeps +nested :class:`ndarray_copy_enable` contexts independent across +concurrent threads and async tasks. Read it via +:func:`is_ndarray_copy_enabled` and toggle it via +:func:`set_ndarray_copy_enabled` or :class:`ndarray_copy_enable`. """ @@ -2043,10 +4608,10 @@ def is_ndarray_copy_enabled() -> bool: True """ - return _NDARRAY_COPY_ENABLED + return _NDARRAY_COPY_ENABLED.get() -def set_ndarray_copy_enable(enable: bool) -> None: +def set_ndarray_copy_enabled(enable: bool) -> None: """ Set the *Colour* :class:`numpy.ndarray` copy enabled state. @@ -2059,15 +4624,13 @@ def set_ndarray_copy_enable(enable: bool) -> None: -------- >>> with ndarray_copy_enable(is_ndarray_copy_enabled()): ... print(is_ndarray_copy_enabled()) - ... set_ndarray_copy_enable(False) + ... set_ndarray_copy_enabled(False) ... print(is_ndarray_copy_enabled()) True False """ - global _NDARRAY_COPY_ENABLED # noqa: PLW0603 - - _NDARRAY_COPY_ENABLED = enable + _NDARRAY_COPY_ENABLED.set(enable) class ndarray_copy_enable: @@ -2083,7 +4646,10 @@ class ndarray_copy_enable: def __init__(self, enable: bool) -> None: self._enable = enable - self._previous_state = is_ndarray_copy_enabled() + # Token stack: nested or recursive ``__enter__`` / ``__exit__`` + # pairs against the same instance (e.g. via the decorator form on + # a recursive function) push and pop independent reset tokens. + self._tokens: list[contextvars.Token[bool]] = [] def __enter__(self) -> Self: """ @@ -2091,7 +4657,7 @@ def __enter__(self) -> Self: entering the context manager. """ - set_ndarray_copy_enable(self._enable) + self._tokens.append(_NDARRAY_COPY_ENABLED.set(self._enable)) return self @@ -2101,7 +4667,7 @@ def __exit__(self, *args: Any) -> None: exiting the context manager. """ - set_ndarray_copy_enable(self._previous_state) + _NDARRAY_COPY_ENABLED.reset(self._tokens.pop()) def __call__(self, function: Callable) -> Callable: """ @@ -2156,8 +4722,12 @@ def ndarray_copy(a: NDArray) -> NDArray: True """ - if _NDARRAY_COPY_ENABLED: - return np.copy(a) + if _NDARRAY_COPY_ENABLED.get(): + xp = array_namespace(a) + + if is_numpy_namespace(xp): + return np.copy(a) + return xp.asarray(a, copy=True) return a @@ -2196,10 +4766,15 @@ def closest_indexes(a: ArrayLike, b: ArrayLike) -> NDArray: [3 5] """ - a = np.ravel(a)[:, None] - b = np.ravel(b)[None, :] + xp = array_namespace(a, b) - return np.abs(a - b).argmin(axis=0) + a = xp_as_float_array(a, xp=xp, like=b) + b = xp_as_float_array(b, xp=xp, like=a) + + a = xp_reshape(a, (-1,), xp=xp)[:, None] + b = xp_reshape(b, (-1,), xp=xp)[None, :] + + return xp.abs(a - b).argmin(axis=0) def closest(a: ArrayLike, b: ArrayLike) -> NDArray: @@ -2237,7 +4812,11 @@ def closest(a: ArrayLike, b: ArrayLike) -> NDArray: array([62.70988028, 25.40026416]) """ - a = np.array(a) + b = as_float_array(b) + + xp = array_namespace(a, b) + + a = xp_as_float_array(a, xp=xp, like=b) return a[closest_indexes(a, b)] @@ -2284,27 +4863,30 @@ def interval(distribution: ArrayLike, unique: bool = True) -> NDArray: """ distribution = as_float_array(distribution) + + xp = array_namespace(distribution) + hash_key = hash( ( - int_digest(distribution.tobytes()), + int_digest(as_ndarray(distribution).tobytes()), distribution.shape, unique, ) ) if is_caching_enabled() and hash_key in _CACHE_DISTRIBUTION_INTERVAL: - return np.copy(_CACHE_DISTRIBUTION_INTERVAL[hash_key]) + return xp_as_array(_CACHE_DISTRIBUTION_INTERVAL[hash_key], xp=xp, copy=True) - differences = np.abs(distribution[1:] - distribution[:-1]) + differences = xp.abs(distribution[1:] - distribution[:-1]) - if unique and np.all(differences == differences[0]): - interval_ = np.array([differences[0]]) + if unique and xp.all(differences == differences[0]): + interval_ = xp.asarray([differences[0]]) elif unique: - interval_ = np.unique(differences) + interval_ = xp.unique_values(differences) else: interval_ = differences - _CACHE_DISTRIBUTION_INTERVAL[hash_key] = np.copy(interval_) + _CACHE_DISTRIBUTION_INTERVAL[hash_key] = xp_as_array(interval_, xp=xp, copy=True) return interval_ @@ -2338,7 +4920,7 @@ def is_uniform(distribution: ArrayLike) -> bool: False """ - return bool(interval(distribution).size == 1) + return len(interval(distribution)) == 1 def in_array(a: ArrayLike, b: ArrayLike, tolerance: Real = EPSILON) -> NDArray: @@ -2380,9 +4962,11 @@ def in_array(a: ArrayLike, b: ArrayLike, tolerance: Real = EPSILON) -> NDArray: a = as_float_array(a) b = as_float_array(b) - d = np.abs(np.ravel(a) - b[..., None]) + xp = array_namespace(a, b) + + d = xp.abs(xp_reshape(a, (-1,), xp=xp) - b[..., None]) - return np.reshape(np.any(d <= tolerance, axis=0), a.shape) + return xp_reshape(xp.any(d <= tolerance, axis=0), a.shape, xp=xp) def tstack( @@ -2410,6 +4994,13 @@ def tstack( :class:`numpy.ndarray` Stacked array. + Notes + ----- + - The returned array is always a freshly-allocated, contiguous stack of + the components along the last axis and never aliases the inputs. It is + the inverse of the :func:`colour.utilities.tsplit` definition, whose + *NumPy* path likewise returns an independent, contiguous copy. + Examples -------- >>> a = 0 @@ -2443,9 +5034,31 @@ def tstack( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) + if ( + is_array_api_enabled() + and isinstance(a, (list, tuple)) + and a + and is_non_ndarray(a[0]) + ): + xp = array_namespace(a[0]) + + return xp.stack([xp_as_array(x, xp=xp, like=a[0]) for x in a], axis=-1) + + if isinstance(a, (list, tuple)) and a: + # Stack the components directly, avoiding the ``as_array(list)`` + # round-trip that materialises an intermediate ``(n, ...)`` array only + # to re-split and re-stack it on the tail axis. + components = [as_array(x, dtype) for x in a] + + xp = array_namespace(components[0]) + + return xp.stack(components, axis=-1) + a = as_array(a, dtype) - return np.concatenate([x[..., np.newaxis] for x in a], axis=-1) + xp = array_namespace(a) + + return xp.stack(list(a), axis=-1) def tsplit( @@ -2473,6 +5086,19 @@ def tsplit( :class:`numpy.ndarray` Array of arrays. + Notes + ----- + - On the *NumPy* path, the returned array is an **independent, + contiguous** copy: the leading-axis sub-arrays do not alias the input + :math:`a` and can be safely written to in-place. This copy is + deliberate and not an avoidable overhead, splitting with a + :func:`numpy.moveaxis` view instead would alias :math:`a` (breaking + that guarantee), and the contiguity also keeps downstream operations + such as matrix multiplications fast. + - On the *Array API* path, the split is a zero-copy + :func:`numpy.moveaxis` view whose contiguity and aliasing semantics + are managed by the backend. + Examples -------- >>> a = np.array([0, 0, 0]) @@ -2507,7 +5133,12 @@ def tsplit( a = as_array(a, dtype) - return np.array([a[..., x] for x in range(a.shape[-1])]) + xp = array_namespace(a) + + if is_numpy_namespace(xp): + return xp.stack([a[..., x] for x in range(a.shape[-1])]) + + return xp.moveaxis(a, -1, 0) def row_as_diagonal(a: ArrayLike) -> NDArray: @@ -2563,9 +5194,13 @@ def row_as_diagonal(a: ArrayLike) -> NDArray: d = as_array(a) - d = np.expand_dims(d, -2) + xp = array_namespace(d) + + d = xp.expand_dims(d, axis=-2) + + eye = xp.eye(d.shape[-1], device=getattr(d, "device", None)) - return np.eye(d.shape[-1]) * d + return eye * d def orient( @@ -2614,22 +5249,25 @@ def orient( a = as_float_array(a) + xp = array_namespace(a) + orientation = validate_method( orientation, ("Ignore", "Flip", "Flop", "90 CW", "90 CCW", "180") ) + oriented = a if orientation == "ignore": oriented = a elif orientation == "flip": - oriented = np.fliplr(a) + oriented = xp.flip(a, axis=1) elif orientation == "flop": - oriented = np.flipud(a) + oriented = xp.flip(a, axis=0) elif orientation == "90 cw": - oriented = np.rot90(a, 3) + oriented = xp_matrix_transpose(xp.flip(a, axis=0), xp=xp) elif orientation == "90 ccw": - oriented = np.rot90(a) + oriented = xp_matrix_transpose(xp.flip(a, axis=1), xp=xp) elif orientation == "180": - oriented = np.rot90(a, 2) + oriented = xp.flip(xp.flip(a, axis=0), axis=1) return oriented @@ -2657,24 +5295,27 @@ def centroid(a: ArrayLike) -> NDArrayInt: a = as_float_array(a) - a_s = np.sum(a) + xp = array_namespace(a) + + a_s = xp.sum(a) - ranges = [np.arange(0, a.shape[i]) for i in range(a.ndim)] - coordinates = np.meshgrid(*ranges) + device = getattr(a, "device", None) + ranges = [xp.arange(0, a.shape[i], device=device) for i in range(a.ndim)] + coordinates = xp.meshgrid(*ranges) a_ci = [] for axis in coordinates: - axis = np.transpose(axis) # noqa: PLW2901 + axis = xp.permute_dims(axis, tuple(reversed(range(axis.ndim)))) # noqa: PLW2901 # Aligning axis for N-D arrays where N is normalised to # range [3, :math:`\\\infty`] for i in range(axis.ndim - 2, 0, -1): - axis = np.rollaxis(axis, i - 1, axis.ndim) # noqa: PLW2901 + axis = xp.moveaxis(axis, i - 1, -1) # noqa: PLW2901 - a_ci.append(np.sum(axis * a) // a_s) + a_ci.append(xp.sum(axis * a) // a_s) - # NOTE: Cannot use `as_int_array` as presence of NaN will raise a ValueError - # exception. - return np.array(a_ci).astype(DTYPE_INT_DEFAULT) + # NOTE: Cannot use ``as_int_array`` as presence of NaN will raise a + # ``ValueError`` exception. + return xp_astype(xp.stack(a_ci), DTYPE_INT_DEFAULT, xp=xp) def fill_nan( @@ -2710,25 +5351,31 @@ def fill_nan( array([0.1, 0.2, 0. , 0.4, 0.5]) """ - a = np.array(a, copy=True) + a = as_float_array(a) + + xp = array_namespace(a) + + a = xp_as_array(a, xp=xp, copy=True) + method = validate_method(method, ("Interpolation", "Constant")) - mask = np.isnan(a) + mask = xp.isnan(a) - if not np.any(mask): + if not xp.any(mask): return a if method == "interpolation": - # Interpolate at all indices, then use np.where to replace only NaN positions - a = np.where( + indices = xp.arange(len(a), device=getattr(a, "device", None)) + valid = ~mask + a = xp.where( mask, - np.interp(np.arange(len(a)), np.flatnonzero(~mask), a[~mask]), + xp_interp(indices, indices[valid], a[valid], xp=xp), a, ) elif method == "constant": - a = np.where(mask, default, a) + a = xp.where(mask, default, a) - return a + return a # pyright: ignore def has_only_nan(a: ArrayLike) -> bool: @@ -2759,7 +5406,9 @@ def has_only_nan(a: ArrayLike) -> bool: a = as_float_array(a) - return bool(np.all(np.isnan(a))) + xp = array_namespace(a) + + return bool(xp.all(xp.isnan(a))) @contextmanager @@ -2803,7 +5452,6 @@ def ndarray_write(a: ArrayLike) -> Generator: def zeros( shape: int | Sequence[int], dtype: Type[DTypeReal] | None = None, - order: Literal["C", "F"] = "C", ) -> NDArray: """ Create an array of zeros with the active dtype. @@ -2820,9 +5468,6 @@ def zeros( :class:`numpy.dtype` to use for conversion, default to the :class:`numpy.dtype` defined by the :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. - order - Whether to store multi-dimensional data in row-major - (C-style) or column-major (Fortran-style) order in memory. Returns ------- @@ -2838,13 +5483,14 @@ def zeros( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - return np.zeros(shape, dtype, order) + xp = array_namespace() + + return xp.zeros(shape, dtype=dtype) def ones( shape: int | Sequence[int], dtype: Type[DTypeReal] | None = None, - order: Literal["C", "F"] = "C", ) -> NDArray: """ Create an array of ones with the active dtype. @@ -2861,9 +5507,6 @@ def ones( :class:`numpy.dtype` to use for conversion, default to the :class:`numpy.dtype` defined by the :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. - order - Whether to store multi-dimensional data in row-major (C-style) or - column-major (Fortran-style) order in memory. Returns ------- @@ -2878,14 +5521,15 @@ def ones( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - return np.ones(shape, dtype, order) + xp = array_namespace() + + return xp.ones(shape, dtype=dtype) def full( shape: int | Sequence[int], fill_value: Real, dtype: Type[DTypeReal] | None = None, - order: Literal["C", "F"] = "C", ) -> NDArray: """ Create an array of the specified value with the active dtype. @@ -2904,9 +5548,6 @@ def full( :class:`numpy.dtype` to use for conversion, default to the :class:`numpy.dtype` defined by the :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. - order - Whether to store multi-dimensional data in row-major (C-style) or - column-major (Fortran-style) order in memory. Returns ------- @@ -2922,7 +5563,9 @@ def full( dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) - return np.full(shape, fill_value, dtype, order) + xp = array_namespace() + + return xp.full(shape, fill_value, dtype=dtype) def index_along_last_axis(a: ArrayLike, indexes: ArrayLike) -> NDArray: @@ -2955,6 +5598,7 @@ def index_along_last_axis(a: ArrayLike, indexes: ArrayLike) -> NDArray: Examples -------- + >>> import numpy as np >>> a = np.array( ... [ ... [ @@ -3008,8 +5652,10 @@ def index_along_last_axis(a: ArrayLike, indexes: ArrayLike) -> NDArray: [4.8, 6.9, 7.1, 1.9]]) """ - a = np.array(a) - indexes = np.array(indexes) + a = as_float_array(a) + indexes = as_int_array(indexes) + + xp = array_namespace(a) if a.shape[:-1] != indexes.shape: error = ( @@ -3018,7 +5664,7 @@ def index_along_last_axis(a: ArrayLike, indexes: ArrayLike) -> NDArray: raise ValueError(error) - return np.take_along_axis(a, indexes[..., None], axis=-1).squeeze(axis=-1) + return xp.take_along_axis(a, indexes[..., None], axis=-1).squeeze(axis=-1) def format_array_as_row(a: ArrayLike, decimals: int = 7, separator: str = " ") -> str: @@ -3049,7 +5695,11 @@ def format_array_as_row(a: ArrayLike, decimals: int = 7, separator: str = " ") - '1.250, 2.500, 3.750' """ - a = np.ravel(a) + a = as_float_array(a) + + xp = array_namespace(a) + + a = xp_reshape(a, (-1,), xp=xp) return separator.join( "{1:0.{0}f}".format(decimals, x) diff --git a/colour/utilities/common.py b/colour/utilities/common.py index 401e95c0d7..07bd2fa953 100644 --- a/colour/utilities/common.py +++ b/colour/utilities/common.py @@ -18,6 +18,7 @@ from __future__ import annotations +import contextvars import functools import hashlib import inspect @@ -29,7 +30,6 @@ import urllib.error import urllib.request import warnings -from contextlib import contextmanager from copy import copy from pprint import pformat from urllib.parse import urlparse @@ -64,7 +64,7 @@ __all__ = [ "is_caching_enabled", - "set_caching_enable", + "set_caching_enabled", "caching_enable", "CacheRegistry", "CACHE_REGISTRY", @@ -76,8 +76,6 @@ "ignore_python_warnings", "attest", "batch", - "disable_multiprocessing", - "multiprocessing_pool", "is_iterable", "is_numeric", "is_integer", @@ -94,12 +92,19 @@ "download_url", ] -_CACHING_ENABLED: bool = not as_bool( - os.environ.get("COLOUR_SCIENCE__DISABLE_CACHING", "False") +_CACHING_ENABLED: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_CACHING_ENABLED", default=True ) """ -Global variable storing the current *Colour* caching enabled state. +:class:`contextvars.ContextVar` storing the current *Colour* caching +enabled state. The :class:`contextvars.ContextVar` keeps nested +:class:`caching_enable` contexts independent across concurrent threads +and async tasks. Read it via :func:`is_caching_enabled` and toggle it +via :func:`set_caching_enabled` or :class:`caching_enable`. """ +_CACHING_ENABLED.set( + not as_bool(os.environ.get("COLOUR_SCIENCE__DISABLE_CACHING", "False")) +) def is_caching_enabled() -> bool: @@ -108,7 +113,7 @@ def is_caching_enabled() -> bool: The caching state is controlled by the global *COLOUR_SCIENCE__DISABLE_CACHING* environment variable and can be - temporarily modified using the :func:`set_caching_enable` function or the + temporarily modified using the :func:`set_caching_enabled` function or the :class:`caching_enable` context manager. Returns @@ -126,10 +131,10 @@ def is_caching_enabled() -> bool: True """ - return _CACHING_ENABLED + return _CACHING_ENABLED.get() -def set_caching_enable(enable: bool) -> None: +def set_caching_enabled(enable: bool) -> None: """ Set the *Colour* caching enabled state. @@ -142,15 +147,13 @@ def set_caching_enable(enable: bool) -> None: -------- >>> with caching_enable(True): ... print(is_caching_enabled()) - ... set_caching_enable(False) + ... set_caching_enabled(False) ... print(is_caching_enabled()) True False """ - global _CACHING_ENABLED # noqa: PLW0603 - - _CACHING_ENABLED = enable + _CACHING_ENABLED.set(enable) class caching_enable: @@ -166,14 +169,17 @@ class caching_enable: def __init__(self, enable: bool) -> None: self._enable = enable - self._previous_state = is_caching_enabled() + # Token stack: nested or recursive ``__enter__`` / ``__exit__`` + # pairs against the same instance (e.g. via the decorator form on + # a recursive function) push and pop independent reset tokens. + self._tokens: list[contextvars.Token[bool]] = [] def __enter__(self) -> Self: """ Enter the caching context and set the *Colour* caching state. """ - set_caching_enable(self._enable) + self._tokens.append(_CACHING_ENABLED.set(self._enable)) return self @@ -183,7 +189,7 @@ def __exit__(self, *args: Any) -> None: caching state. """ - set_caching_enable(self._previous_state) + _CACHING_ENABLED.reset(self._tokens.pop()) def __call__(self, function: Callable) -> Callable: """ @@ -518,170 +524,6 @@ def batch(sequence: Sequence, k: int | Literal[3] = 3) -> Generator: yield sequence[i : i + k] -_MULTIPROCESSING_ENABLED: bool = True -"""*Colour* multiprocessing state.""" - - -class disable_multiprocessing: - """ - Define a context manager and decorator to temporarily disable *Colour* - multiprocessing state. - """ - - def __enter__(self) -> Self: - """ - Disable *Colour* multiprocessing state upon entering the context - manager. - """ - - global _MULTIPROCESSING_ENABLED # noqa: PLW0603 - - _MULTIPROCESSING_ENABLED = False - - return self - - def __exit__(self, *args: Any) -> None: - """ - Enable *Colour* multiprocessing state upon exiting the context - manager. - """ - - global _MULTIPROCESSING_ENABLED # noqa: PLW0603 - - _MULTIPROCESSING_ENABLED = True - - def __call__(self, function: Callable) -> Callable: - """ - Execute the decorated function with optional multiprocessing support. - """ - - @functools.wraps(function) - def wrapper(*args: Any, **kwargs: Any) -> Any: - """Wrap specified function.""" - - with self: - return function(*args, **kwargs) - - return wrapper - - -def _initializer(kwargs: Any) -> None: - """ - Initialize a multiprocessing pool worker process. - - Ensure that worker processes on *Windows* correctly inherit the current - domain-range scale configuration from the parent process. - - Parameters - ---------- - kwargs - Initialization arguments for configuring the worker process state. - """ - - # NOTE: No coverage information is available as this code is executed in - # sub-processes. - - import colour.utilities.array # pragma: no cover # noqa: PLC0415 - - colour.utilities.array._DOMAIN_RANGE_SCALE = kwargs.get( # noqa: SLF001 - "scale", "reference" - ) # pragma: no cover - - import colour.algebra.common # pragma: no cover # noqa: PLC0415 - - colour.algebra.common._SDIV_MODE = kwargs.get( # noqa: SLF001 - "sdiv_mode", "Ignore Zero Conversion" - ) # pragma: no cover - colour.algebra.common._SPOW_ENABLED = kwargs.get( # noqa: SLF001 - "spow_enabled", True - ) # pragma: no cover - - -@contextmanager -def multiprocessing_pool(*args: Any, **kwargs: Any) -> Generator: - """ - Provide a context manager for a multiprocessing pool. - - Other Parameters - ---------------- - args - Arguments passed to the multiprocessing pool constructor. - kwargs - Keyword arguments passed to the multiprocessing pool - constructor. - - Yields - ------ - Generator - Multiprocessing pool context manager. - - Examples - -------- - >>> from functools import partial - >>> def _add(a, b): - ... return a + b - >>> with multiprocessing_pool() as pool: - ... pool.map(partial(_add, b=2), range(10)) - ... # doctest: +SKIP - [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - """ - - from colour.algebra import get_sdiv_mode, is_spow_enabled # noqa: PLC0415 - from colour.utilities import get_domain_range_scale # noqa: PLC0415 - - class _DummyPool: - """ - A dummy multiprocessing pool that does not perform multiprocessing. - - Other Parameters - ---------------- - args - Arguments. - kwargs - Keywords arguments. - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass - - def map( - self, - func: Callable, - iterable: Sequence, - chunksize: int | None = None, # noqa: ARG002 - ) -> list[Any]: - """Apply specified function to each element of the specified iterable.""" - - return [func(a) for a in iterable] - - def terminate(self) -> None: - """Terminate the process.""" - - kwargs["initializer"] = _initializer - kwargs["initargs"] = ( - { - "scale": get_domain_range_scale(), - "sdiv_mode": get_sdiv_mode(), - "spow_enabled": is_spow_enabled(), - }, - ) - - pool_factory: Callable - if _MULTIPROCESSING_ENABLED: - import multiprocessing # noqa: PLC0415 - - pool_factory = multiprocessing.Pool - else: - pool_factory = _DummyPool - - pool = pool_factory(*args, **kwargs) - - try: - yield pool - finally: - pool.terminate() - - def is_iterable(a: Any) -> bool: """ Determine whether the specified variable :math:`a` is iterable. @@ -780,12 +622,16 @@ def is_integer(a: Any) -> bool: Examples -------- >>> is_integer(1) - np.True_ + True >>> is_integer(1.01) - np.False_ + False """ - return abs(a - np.around(a)) <= THRESHOLD_INTEGER + try: + a_float = float(a) + return abs(a_float - round(a_float)) <= THRESHOLD_INTEGER + except (OverflowError, ValueError, TypeError): + return False def is_sibling(element: Any, mapping: Mapping) -> bool: diff --git a/colour/utilities/metrics.py b/colour/utilities/metrics.py index 522c771117..6cb673b030 100644 --- a/colour/utilities/metrics.py +++ b/colour/utilities/metrics.py @@ -20,8 +20,6 @@ import typing -import numpy as np - from colour.algebra import sdiv, sdiv_mode if typing.TYPE_CHECKING: @@ -32,7 +30,7 @@ Tuple, ) -from colour.utilities import as_float, as_float_array +from colour.utilities import array_namespace, as_float, as_float_array __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -79,13 +77,18 @@ def metric_mse( Examples -------- + >>> import numpy as np >>> a = np.array([0.48222001, 0.31654775, 0.22070353]) >>> b = a * 0.9 >>> metric_mse(a, b) # doctest: +ELLIPSIS np.float64(0.0012714...) """ - return as_float(np.mean((as_float_array(a) - as_float_array(b)) ** 2, axis=axis)) + diff = as_float_array(a) - as_float_array(b) + + xp = array_namespace(diff) + + return as_float(xp.mean(diff**2, axis=axis)) def metric_psnr( @@ -123,6 +126,7 @@ def metric_psnr( Examples -------- + >>> import numpy as np >>> a = np.array([0.48222001, 0.31654775, 0.22070353]) >>> b = a * 0.9 >>> metric_psnr(a, b) # doctest: +ELLIPSIS @@ -131,7 +135,9 @@ def metric_psnr( mse = as_float_array(metric_mse(a, b, axis)) + xp = array_namespace(mse) + with sdiv_mode(): - psnr = np.where(mse != 0, 10 * np.log10(sdiv(max_a**2, mse)), 0) + psnr = xp.where(mse != 0, 10 * xp.log10(sdiv(max_a**2, mse)), 0) return as_float(psnr) diff --git a/colour/utilities/network.py b/colour/utilities/network.py index 0a2e8fcbb8..3259125d1a 100644 --- a/colour/utilities/network.py +++ b/colour/utilities/network.py @@ -2018,7 +2018,7 @@ def is_graph_member(node: PortNode) -> bool: shape="record", ) ) - input_edges, output_edges = node.edges + input_edges, _output_edges = node.edges for edge in input_edges: # Not drawing node edges that involve a node member of graph. diff --git a/colour/utilities/requirements.py b/colour/utilities/requirements.py index 4bf0571b93..e8bad11a00 100644 --- a/colour/utilities/requirements.py +++ b/colour/utilities/requirements.py @@ -29,6 +29,8 @@ __status__ = "Production" __all__ = [ + "is_array_api_compat_installed", + "is_array_api_extra_installed", "is_ctlrender_installed", "is_imageio_installed", "is_openimageio_installed", @@ -47,6 +49,80 @@ ] +def is_array_api_compat_installed(raise_exception: bool = False) -> bool: + """ + Determine whether *array-api-compat* is installed and available. + + Parameters + ---------- + raise_exception + Whether to raise an exception if *array-api-compat* is unavailable. + + Returns + ------- + :class:`bool` + Whether *array-api-compat* is installed. + + Raises + ------ + :class:`ImportError` + If *array-api-compat* is not installed. + """ + + try: # pragma: no cover + import array_api_compat # noqa: F401, PLC0415 + except ImportError as exception: # pragma: no cover + if raise_exception: + error = ( + '"array-api-compat" related API features are not available: ' + f'"{exception}".\nSee the installation guide for more information: ' + "https://www.colour-science.org/installation-guide/" + ) + + raise ImportError(error) from exception + + return False + else: + return True + + +def is_array_api_extra_installed(raise_exception: bool = False) -> bool: + """ + Determine whether *array-api-extra* is installed and available. + + Parameters + ---------- + raise_exception + Whether to raise an exception if *array-api-extra* is unavailable. + + Returns + ------- + :class:`bool` + Whether *array-api-extra* is installed. + + Raises + ------ + :class:`ImportError` + If *array-api-extra* is not installed. + """ + + try: # pragma: no cover + import array_api_extra # noqa: F401, PLC0415 + except ImportError as exception: # pragma: no cover + if raise_exception: + error = ( + '"array-api-extra" related API features are not available: ' + f'"{exception}".\nSee the installation guide for more information: ' + "https://www.colour-science.org/installation-guide/" + ) + + raise ImportError(error) from exception + + return False + else: + return True + + def is_ctlrender_installed(raise_exception: bool = False) -> bool: """ Determine whether *ctlrender* is installed and available. @@ -552,6 +628,8 @@ def is_xxhash_installed(raise_exception: bool = False) -> bool: REQUIREMENTS_TO_CALLABLE: CanonicalMapping = CanonicalMapping( { + "array_api_compat": is_array_api_compat_installed, + "array_api_extra": is_array_api_extra_installed, "ctlrender": is_ctlrender_installed, "Imageio": is_imageio_installed, "OpenImageIO": is_openimageio_installed, @@ -574,6 +652,8 @@ def is_xxhash_installed(raise_exception: bool = False) -> bool: def required( *requirements: Literal[ + "array_api_compat", + "array_api_extra", "ctlrender", "Imageio", "OpenImageIO", diff --git a/colour/utilities/tests/test_array.py b/colour/utilities/tests/test_array.py index c408922799..6026aa0090 100644 --- a/colour/utilities/tests/test_array.py +++ b/colour/utilities/tests/test_array.py @@ -2,8 +2,8 @@ from __future__ import annotations +import types import typing -import unittest from copy import deepcopy from dataclasses import dataclass, field, fields from functools import partial @@ -11,6 +11,7 @@ import numpy as np import pytest +import colour.utilities.array as utilities_array from colour.constants import ( DTYPE_COMPLEX_DEFAULT, DTYPE_FLOAT_DEFAULT, @@ -19,6 +20,8 @@ ) if typing.TYPE_CHECKING: + from collections.abc import Generator + from colour.hints import ( Annotated, Any, @@ -29,6 +32,7 @@ Domain100_100_360, Domain360, DType, + ModuleType, NDArray, NDArrayFloat, Range1, @@ -63,6 +67,8 @@ MixinDataclassArray, MixinDataclassFields, MixinDataclassIterable, + array_api_enable, + array_namespace, as_array, as_complex_array, as_float, @@ -71,6 +77,8 @@ as_int, as_int_array, as_int_scalar, + as_ndarray, + cast_non_ndarray, centroid, closest, closest_indexes, @@ -89,8 +97,11 @@ in_array, index_along_last_axis, interval, + is_array_api_enabled, is_ndarray_copy_enabled, is_networkx_installed, + is_non_ndarray, + is_numpy_namespace, is_scipy_installed, is_uniform, ndarray_copy, @@ -99,10 +110,11 @@ ones, orient, row_as_diagonal, + set_array_api_enabled, set_default_float_dtype, set_default_int_dtype, set_domain_range_scale, - set_ndarray_copy_enable, + set_ndarray_copy_enabled, to_domain_1, to_domain_10, to_domain_100, @@ -110,6 +122,43 @@ to_domain_int, tsplit, tstack, + xp_as_array, + xp_as_float_array, + xp_as_int_array, + xp_ascontiguousarray, + xp_assert_close, + xp_assert_equal, + xp_astype, + xp_atleast_1d, + xp_atleast_2d, + xp_average, + xp_broadcast_to, + xp_create_diagonal, + xp_degrees, + xp_eig, + xp_eigh, + xp_gradient, + xp_insert, + xp_interp, + xp_isclose, + xp_isin, + xp_linspace, + xp_lstsq, + xp_matrix_transpose, + xp_median, + xp_nan_to_num, + xp_nanmean, + xp_pad, + xp_radians, + xp_reshape, + xp_resize, + xp_round, + xp_select, + xp_setxor1d, + xp_sinc, + xp_squeeze, + xp_trapezoid, + xp_unique, zeros, ) @@ -121,6 +170,48 @@ __status__ = "Production" __all__ = [ + "TestIsArrayApiEnabled", + "TestSetArrayApiEnabled", + "TestArrayApiEnable", + "TestArrayNamespace", + "TestIsNumpyNamespace", + "TestIsNonnumpyArray", + "TestAsNdarray", + "TestCastNonNdarray", + "TestXpAsArray", + "TestXpAsFloatArray", + "TestXpAsIntArray", + "TestXpAscontiguousarray", + "TestXpAstype", + "TestXpMatrixTranspose", + "TestXpSelect", + "TestXpInterp", + "TestXpTrapezoid", + "TestXpAverage", + "TestXpGradient", + "TestXpResize", + "TestXpNanmean", + "TestXpMedian", + "TestXpRound", + "TestXpRadians", + "TestXpDegrees", + "TestXpAtleast1d", + "TestXpAtleast2d", + "TestXpSinc", + "TestXpSqueeze", + "TestXpIsclose", + "TestXpNanToNum", + "TestXpCreateDiagonal", + "TestXpReshape", + "TestXpEig", + "TestXpEigh", + "TestXpLstsq", + "TestXpIsin", + "TestXpLinspace", + "TestXpPad", + "TestXpUnique", + "TestXpInsert", + "TestXpSetxor1d", "TestMixinDataclassFields", "TestMixinDataclassIterable", "TestMixinDataclassArray", @@ -170,16 +261,1263 @@ "TestOnes", "TestFull", "TestIndexAlongLastAxis", + "TestFormatArrayAsRow", + "TestAsArrayArrayApi", + "TestTstackArrayApi", + "TestTsplitArrayApi", ] -class TestMixinDataclassFields(unittest.TestCase): +class TestIsArrayApiEnabled: + """Define :func:`colour.utilities.is_array_api_enabled` unit tests.""" + + def test_is_array_api_enabled(self) -> None: + """Test :func:`colour.utilities.is_array_api_enabled` definition.""" + + with array_api_enable(False): + assert not is_array_api_enabled() + + with array_api_enable(True): + assert is_array_api_enabled() + + +class TestSetArrayApiEnabled: + """Define :func:`colour.utilities.set_array_api_enabled` unit tests.""" + + def test_set_array_api_enabled(self) -> None: + """Test :func:`colour.utilities.set_array_api_enabled` definition.""" + + with array_api_enable(is_array_api_enabled()): + set_array_api_enabled(True) + assert is_array_api_enabled() + set_array_api_enabled(False) + assert not is_array_api_enabled() + + +class TestArrayApiEnable: + """Define :class:`colour.utilities.array_api_enable` unit tests.""" + + def test_array_api_enable(self) -> None: + """Test :class:`colour.utilities.array_api_enable` definition.""" + + with array_api_enable(True): + assert is_array_api_enabled() + + with array_api_enable(False): + assert not is_array_api_enabled() + + with array_api_enable(False): + original = is_array_api_enabled() + with array_api_enable(True): + assert is_array_api_enabled() + assert is_array_api_enabled() == original + + @array_api_enable(True) + def fn_enabled() -> bool: + return is_array_api_enabled() + + @array_api_enable(False) + def fn_disabled() -> bool: + return is_array_api_enabled() + + assert fn_enabled() + assert not fn_disabled() + + +class TestArrayNamespace: + """Define :func:`colour.utilities.array_namespace` unit tests.""" + + def test_array_namespace(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.array_namespace` definition.""" + + with array_api_enable(False): + assert array_namespace(np.array([1, 2, 3])) is np + + with array_api_enable(True): + xp = array_namespace(np.array([1, 2, 3])) + + assert is_numpy_namespace(xp) + + with array_api_enable(True): + assert array_namespace() is np + assert array_namespace(1.0, 2.0) is np + assert array_namespace(None) is np + + +class TestIsNumpyNamespace: + """Define :func:`colour.utilities.is_numpy_namespace` unit tests.""" + + def test_is_numpy_namespace(self) -> None: + """Test :func:`colour.utilities.is_numpy_namespace` definition.""" + + assert is_numpy_namespace(np) + + mock_ns = types.ModuleType("jax.numpy") + assert not is_numpy_namespace(mock_ns) + + +class TestIsNonnumpyArray: + """Define :func:`colour.utilities.is_non_ndarray` unit tests.""" + + def test_is_non_ndarray(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.is_non_ndarray` definition.""" + + assert not is_non_ndarray(np.array([1, 2, 3])) + assert not is_non_ndarray(np.float64(1.0)) + assert not is_non_ndarray([1, 2, 3]) + assert not is_non_ndarray(1.0) + assert not is_non_ndarray(None) + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + if is_numpy_namespace(xp): + assert not is_non_ndarray(a) + else: + assert is_non_ndarray(a) + + +class TestAsNdarray: + """Define :func:`colour.utilities.as_ndarray` unit tests.""" + + def test_as_ndarray(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.as_ndarray` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = as_ndarray(a) + assert isinstance(result, np.ndarray) + xp_assert_equal(result, [1.0, 2.0, 3.0]) + + result = as_ndarray(np.array([4, 5, 6])) + assert isinstance(result, np.ndarray) + xp_assert_equal(result, [4, 5, 6]) + + +class TestCastNonNdarray: + """Define :func:`colour.utilities.cast_non_ndarray` unit tests.""" + + def test_cast_non_ndarray(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.cast_non_ndarray` definition.""" + + # A *NumPy* array is never cast. + with array_api_enable(True): + assert cast_non_ndarray(np.array([1.0, 2.0]), np.float32) is None + + # Disabled *Array API* dispatch returns ``None``. + with array_api_enable(False): + assert cast_non_ndarray(np.array([1.0, 2.0]), np.float32) is None + + if is_numpy_namespace(xp): + return + + with array_api_enable(True): + # A non-*NumPy* array is cast to the specified dtype. + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = cast_non_ndarray(a, np.float32) + assert result is not None + assert result.dtype == getattr(xp, "float32", None) + + # The array is returned unchanged when the dtype already + # matches. Casting to the array's *actual* dtype keeps the + # precondition true on every backend, including *MPS* which + # silently substitutes ``float32`` for ``float64``; + # ``as_ndarray`` yields a genuine :class:`numpy.dtype` that + # :func:`cast_non_ndarray` resolves back to the native dtype. + a = xp_as_array([1.0, 2.0], xp=xp) + assert cast_non_ndarray(a, as_ndarray(a).dtype) is a + + +class TestXpAsArray: + """Define :func:`colour.utilities.xp_as_array` unit tests.""" + + def test_xp_as_array(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_as_array` definition.""" + + from colour.utilities import caching_enable # noqa: PLC0415 + + # Python sequence promotion. + result = xp_as_array([1, 2, 3], xp=xp) + xp_assert_equal(result, [1, 2, 3]) + + # Dtype enforcement. + result = xp_as_array([1, 2, 3], dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + assert result.dtype == getattr( + xp, np.dtype(DTYPE_FLOAT_DEFAULT).name, DTYPE_FLOAT_DEFAULT + ) + xp_assert_close(result, [1.0, 2.0, 3.0]) + + result = xp_as_array([1.5, 2.5], dtype=DTYPE_INT_DEFAULT, xp=xp) + assert result.dtype == getattr( + xp, np.dtype(DTYPE_INT_DEFAULT).name, DTYPE_INT_DEFAULT + ) + xp_assert_equal(result, [1, 2]) + + # Empty input survives the round-trip. + result = xp_as_array([], dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + assert result.shape == (0,) + + if is_numpy_namespace(xp): + # *NumPy* identity when no dtype conversion is required. + a = np.array([1.0, 2.0, 3.0]) + assert xp_as_array(a, xp=xp) is a + assert xp_as_array(a, dtype=a.dtype, xp=xp) is a + + # *NumPy* with mismatched dtype is cast and copied. + result = xp_as_array(a, dtype=np.float32, xp=xp) + assert result.dtype == np.float32 + assert result is not a + + # *Array API* disabled coerces to *NumPy*. + with array_api_enable(False): + result = xp_as_array([1.0, 2.0], dtype=np.float32, xp=xp) + assert isinstance(result, np.ndarray) + assert result.dtype == np.float32 + + return + + # Already on a backend, no dtype, returned identity. + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + assert xp_as_array(a, xp=xp) is a + + # Already on a backend, dtype matches, returned identity, including + # when a *NumPy* dtype alias is passed in. + a = xp_as_float_array([1.0, 2.0, 3.0], xp=xp) + assert xp_as_array(a, dtype=a.dtype, xp=xp) is a + + backend_dtype_name = getattr(a.dtype, "name", str(a.dtype)).rsplit(".", 1)[-1] + numpy_dtype = getattr(np, backend_dtype_name, None) + if numpy_dtype is not None: + assert xp_as_array(a, dtype=numpy_dtype, xp=xp) is a + + # Already on a backend, dtype differs, cast through ``xp_astype``. + a_64 = xp_astype(xp_as_array([1.0, 2.0, 3.0], xp=xp), np.float64, xp=xp) + result = xp_as_array(a_64, dtype=np.float32, xp=xp) + assert result.dtype == getattr(xp, "float32", np.float32) + + # Backend object without a ``dtype`` attribute is returned as-is. + class _NoDtype: + def __array_namespace__(self) -> ModuleType: + return xp + + sentinel: Any = _NoDtype() + assert xp_as_array(sentinel, dtype=np.float32, xp=xp) is sentinel + + # Scalar cache hit. + first = xp_as_array(0.5, dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + second = xp_as_array(0.5, dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + assert first is second + + # Small constant array cache hit. + constant = np.array([0.1, 0.2, 0.3], dtype=np.float64) + first = xp_as_array(constant, dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + second = xp_as_array(constant, dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + assert first is second + + # Caching disabled bypasses the scalar cache. + with caching_enable(False): + first = xp_as_array(0.75, dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + second = xp_as_array(0.75, dtype=DTYPE_FLOAT_DEFAULT, xp=xp) + assert first is not second + + # ``like`` argument honoured for backends that expose a device. + reference = xp_as_array([1.0, 2.0], xp=xp) + if getattr(reference, "device", None) is not None: + result = xp_as_array([3.0, 4.0], xp=xp, like=reference) + assert getattr(result, "device", None) == reference.device + + # ``copy=True`` returns a fresh object for backend arrays + # (short-circuit path). + original = xp_as_array([1.0, 2.0, 3.0], xp=xp) + copied = xp_as_array(original, xp=xp, copy=True) + assert copied is not original + xp_assert_equal(copied, [1.0, 2.0, 3.0]) + + # ``copy=True`` bypasses the scalar-promotion cache. + constant = np.array([0.4, 0.5, 0.6], dtype=np.float64) + first = xp_as_array(constant, dtype=DTYPE_FLOAT_DEFAULT, xp=xp, copy=True) + second = xp_as_array(constant, dtype=DTYPE_FLOAT_DEFAULT, xp=xp, copy=True) + if is_array_api_enabled() and not is_numpy_namespace(xp): + assert first is not second + + # ``copy=None`` (default) preserves the no-copy short-circuit. + no_copy = xp_as_array(original, xp=xp) + assert no_copy is original + + +class TestXpAsFloatArray: + """Define :func:`colour.utilities.xp_as_float_array` unit tests.""" + + def test_xp_as_float_array(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_as_float_array` definition.""" + + result = xp_as_float_array([1, 2, 3], xp=xp) + xp_assert_close(result, [1.0, 2.0, 3.0]) + + # Dtype enforcement. + result = xp_as_float_array([1, 2, 3], xp=np) + assert result.dtype == DTYPE_FLOAT_DEFAULT + + +class TestXpAsIntArray: + """Define :func:`colour.utilities.xp_as_int_array` unit tests.""" + + def test_xp_as_int_array(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_as_int_array` definition.""" + + result = xp_as_int_array([1.5, 2.7, 3.9], xp=xp) + xp_assert_equal(result, [1, 2, 3]) + + # Dtype enforcement. + result = xp_as_int_array([1.5, 2.7], xp=np) + assert result.dtype == DTYPE_INT_DEFAULT + + +class TestXpAstype: + """Define :func:`colour.utilities.xp_astype` unit tests.""" + + def test_xp_astype(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_astype` definition.""" + + a = xp_as_array([1.0, 2.5, 3.7], xp=xp) + + result = xp_astype(a, np.float32) + assert as_ndarray(result).dtype == np.float32 + + result = xp_astype(a, np.int32) + xp_assert_equal(result, [1, 2, 3]) + + a_int = xp_as_array([1, 2, 3], xp=xp) + result = xp_astype(a_int, DTYPE_FLOAT_DEFAULT) + assert as_ndarray(result).dtype == DTYPE_FLOAT_DEFAULT + + +class TestXpAscontiguousarray: + """Define :func:`colour.utilities.xp_ascontiguousarray` unit tests.""" + + def test_xp_ascontiguousarray(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_ascontiguousarray` definition.""" + + a = xp_as_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], xp=xp) + + # Identity-preserving fast path: an already-contiguous *NumPy* + # array is returned with C-contig layout. + result = xp_ascontiguousarray(a, xp=xp) + xp_assert_equal(result, a) + + if is_numpy_namespace(xp): + assert result.flags["C_CONTIGUOUS"] + + # Materialise a transposed view: the value-equivalent of + # ``matrix_transpose`` but C-contiguous downstream. + transposed = array_namespace(a).matrix_transpose(a) + materialised = xp_ascontiguousarray(transposed, xp=xp) + xp_assert_equal(materialised, transposed) + if is_numpy_namespace(xp): + assert not transposed.flags["C_CONTIGUOUS"] + assert materialised.flags["C_CONTIGUOUS"] + + # Broadcast outputs derived from the materialised array stay + # C-contiguous, demonstrating the cascade. + if is_numpy_namespace(xp): + broadcast = materialised[..., None] * xp.asarray([1.0, 2.0, 3.0]) + assert broadcast.flags["C_CONTIGUOUS"] + + # The namespace is derived from the input when none is passed. + a = np.array([[1, 2], [3, 4]]).T + result = xp_ascontiguousarray(a) + assert result.flags["C_CONTIGUOUS"] + xp_assert_equal(result, a) + + +class TestXpMatrixTranspose: + """Define :func:`colour.utilities.xp_matrix_transpose` unit tests.""" + + def test_xp_matrix_transpose(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_matrix_transpose` definition.""" + + xpc = array_namespace(xp.asarray([0.0])) + + # 2-D case: swap the two axes. + a = xp_as_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], xp=xp) + result = xp_matrix_transpose(a, xp=xp) + expected = xpc.matrix_transpose(a) + xp_assert_equal(result, expected) + assert result.shape == (3, 2) + + # 3-D case: swap the last two axes only. + b = xp.reshape( + xp_as_array(list(range(24)), dtype=DTYPE_FLOAT_DEFAULT, xp=xp), (2, 3, 4) + ) + result_3d = xp_matrix_transpose(b, xp=xp) + assert result_3d.shape == (2, 4, 3) + xp_assert_equal(result_3d, xpc.matrix_transpose(b)) + + # Materialised result is C-contiguous on *NumPy* even when the + # input was a transposed view that would be F-contiguous. + if is_numpy_namespace(xp): + assert result.flags["C_CONTIGUOUS"] + assert result_3d.flags["C_CONTIGUOUS"] + + # Downstream broadcast cascade: a *NumPy* broadcast with a + # ``xp_matrix_transpose`` operand stays C-contiguous, unlike the + # strided ``matrix_transpose`` view that propagates the F-stride + # pattern. + c = np.arange(8.0).reshape(2, 4) + strided = np.matrix_transpose(c) + broadcast_strided = strided[..., None] * np.array([1.0, 2.0, 3.0]) + assert not broadcast_strided.flags["C_CONTIGUOUS"] + materialised = xp_matrix_transpose(c, xp=np) + broadcast_materialised = materialised[..., None] * np.array([1.0, 2.0, 3.0]) + assert broadcast_materialised.flags["C_CONTIGUOUS"] + xp_assert_equal(broadcast_materialised, broadcast_strided) + + # The namespace is derived from the input when none is passed. + d = np.arange(6).reshape(2, 3) + result = xp_matrix_transpose(d) + assert result.shape == (3, 2) + assert result.flags["C_CONTIGUOUS"] + + +class TestXpSelect: + """Define :func:`colour.utilities.xp_select` unit tests.""" + + def test_xp_select(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_select` definition.""" + + x = xp.arange(10) + condlist = [x < 3, x > 6] + choicelist = [x * 10, x * 100] + result = xp_select(condlist, choicelist, default=-1.0, xp=xp) + expected = np.select( + [as_ndarray(x < 3), as_ndarray(x > 6)], + [as_ndarray(x * 10), as_ndarray(x * 100)], + default=-1.0, + ) + xp_assert_equal(result, expected) + + x = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_select([x > 2], [x * 10], default=0.0, xp=xp) + expected = np.select([as_ndarray(x > 2)], [as_ndarray(x * 10)], default=0.0) + xp_assert_equal(result, expected) + + # All-False ``condlist``: every element must take the ``default``. + x = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_select([x > 100], [x * 10], default=-1.0, xp=xp) + expected = np.select([as_ndarray(x > 100)], [as_ndarray(x * 10)], default=-1.0) + xp_assert_equal(result, expected) + + +class TestXpInterp: + """Define :func:`colour.utilities.xp_interp` unit tests.""" + + def test_xp_interp(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_interp` definition.""" + + xp_arr = xp_as_array([0.0, 1.0, 2.0, 3.0], xp=xp) + fp = xp_as_array([0.0, 1.0, 4.0, 9.0], xp=xp) + x = xp_as_array([0.5, 1.5, 2.5], xp=xp) + result = xp_interp(x, xp_arr, fp, xp=xp) + expected = np.interp( + np.array([0.5, 1.5, 2.5]), + np.array([0.0, 1.0, 2.0, 3.0]), + np.array([0.0, 1.0, 4.0, 9.0]), + ) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + xp_arr = xp_as_array([1.0, 2.0, 3.0], xp=xp) + fp = xp_as_array([10.0, 20.0, 30.0], xp=xp) + x = xp_as_array([0.0, 4.0], xp=xp) + result = xp_interp(x, xp_arr, fp, xp=xp) + expected = np.interp( + np.array([0.0, 4.0]), + np.array([1.0, 2.0, 3.0]), + np.array([10.0, 20.0, 30.0]), + ) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpTrapezoid: + """Define :func:`colour.utilities.xp_trapezoid` unit tests.""" + + def test_xp_trapezoid(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_trapezoid` definition.""" + + y = xp_as_array([1.0, 2.0, 3.0, 4.0], xp=xp) + x = xp_as_array([0.0, 1.0, 2.0, 3.0], xp=xp) + result = xp_trapezoid(y, x=x, xp=xp) + expected = np.trapezoid( + np.array([1.0, 2.0, 3.0, 4.0]), x=np.array([0.0, 1.0, 2.0, 3.0]) + ) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + y = xp_as_array([1.0, 4.0, 9.0], xp=xp) + result = xp_trapezoid(y, dx=0.5, xp=xp) + expected = np.trapezoid(np.array([1.0, 4.0, 9.0]), dx=0.5) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpAverage: + """Define :func:`colour.utilities.xp_average` unit tests.""" + + def test_xp_average(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_average` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0, 4.0], xp=xp) + result = xp_average(a, xp=xp) + expected = np.average(np.array([1.0, 2.0, 3.0, 4.0])) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + weights = xp_as_array([4.0, 3.0, 2.0, 1.0], xp=xp) + result = xp_average(a, weights=weights, xp=xp) + expected = np.average( + np.array([1.0, 2.0, 3.0, 4.0]), weights=np.array([4.0, 3.0, 2.0, 1.0]) + ) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_as_array([[1.0, 2.0], [3.0, 4.0]], xp=xp) + result = xp_average(a, axis=0, xp=xp) + expected = np.average(np.array([[1.0, 2.0], [3.0, 4.0]]), axis=0) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpGradient: + """Define :func:`colour.utilities.xp_gradient` unit tests.""" + + def test_xp_gradient(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_gradient` definition.""" + + f = xp_as_array([1.0, 4.0, 9.0, 16.0, 25.0], xp=xp) + result = xp_gradient(f, xp=xp) + expected = np.gradient(np.array([1.0, 4.0, 9.0, 16.0, 25.0])) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + f = xp_as_array([1.0, 4.0, 9.0, 16.0], xp=xp) + result = xp_gradient(f, 0.5, xp=xp) + expected = np.gradient(np.array([1.0, 4.0, 9.0, 16.0]), 0.5) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpResize: + """Define :func:`colour.utilities.xp_resize` unit tests.""" + + def test_xp_resize(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_resize` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_resize(a, (6,), xp=xp) + expected = np.resize(np.array([1.0, 2.0, 3.0]), (6,)) + xp_assert_equal(result, expected) + + a = xp_as_array([1.0, 2.0], xp=xp) + result = xp_resize(a, (3, 2), xp=xp) + expected = np.resize(np.array([1.0, 2.0]), (3, 2)) + xp_assert_equal(result, expected) + + # Shape contraction: target smaller than input. + a = xp_as_array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], xp=xp) + result = xp_resize(a, (3,), xp=xp) + expected = np.resize(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), (3,)) + xp_assert_equal(result, expected) + + # Zero-element target. + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_resize(a, (0,), xp=xp) + expected = np.resize(np.array([1.0, 2.0, 3.0]), (0,)) + xp_assert_equal(result, expected) + + +class TestXpNanmean: + """Define :func:`colour.utilities.xp_nanmean` unit tests.""" + + def test_xp_nanmean(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_nanmean` definition.""" + + a = xp_as_array([1.0, np.nan, 3.0, np.nan, 5.0], xp=xp) + result = xp_nanmean(a, xp=xp) + expected = np.nanmean(np.array([1.0, np.nan, 3.0, np.nan, 5.0])) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_nanmean(a, xp=xp) + expected = np.nanmean(np.array([1.0, 2.0, 3.0])) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_as_array([[1.0, np.nan], [3.0, 4.0]], xp=xp) + result = xp_nanmean(a, axis=0, xp=xp) + expected = np.nanmean(np.array([[1.0, np.nan], [3.0, 4.0]]), axis=0) + xp_assert_close( + result, + expected, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpMedian: + """Define :func:`colour.utilities.xp_median` unit tests.""" + + def test_xp_median(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_median` definition.""" + + a = xp_as_array([3.0, 1.0, 2.0], xp=xp) + xp_assert_close( + xp_median(a, xp=xp), + np.median([3.0, 1.0, 2.0]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_as_array([4.0, 1.0, 3.0, 2.0], xp=xp) + xp_assert_close( + xp_median(a, xp=xp), + np.median([4.0, 1.0, 3.0, 2.0]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_as_array([[3.0, 1.0], [2.0, 4.0]], xp=xp) + xp_assert_close( + xp_median(a, axis=1, xp=xp), + np.median([[3.0, 1.0], [2.0, 4.0]], axis=1), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpRound: + """Define :func:`colour.utilities.xp_round` unit tests.""" + + def test_xp_round(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_round` definition.""" + + a = xp_as_array([3.14159, 2.71828, 1.41421], xp=xp) + a_np = np.array([3.14159, 2.71828, 1.41421]) + + xp_assert_close( + xp_round(a, decimals=0, xp=xp), + np.round(a_np, 0), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + xp_assert_close( + xp_round(a, decimals=2, xp=xp), + np.round(a_np, 2), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + xp_assert_close( + xp_round(a, decimals=4, xp=xp), + np.round(a_np, 4), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_as_array([[1.555, 2.444], [3.666, 4.777]], xp=xp) + xp_assert_close( + xp_round(a, decimals=1, xp=xp), + np.round([[1.555, 2.444], [3.666, 4.777]], 1), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpRadians: + """Define :func:`colour.utilities.xp_radians` unit tests.""" + + def test_xp_radians(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_radians` definition.""" + + a = xp_as_array([0.0, 90.0, 180.0, 270.0, 360.0], xp=xp) + xp_assert_close( + xp_radians(a), + np.radians([0.0, 90.0, 180.0, 270.0, 360.0]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + xp_assert_close( + xp_radians(180.0), + np.pi, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpDegrees: + """Define :func:`colour.utilities.xp_degrees` unit tests.""" + + def test_xp_degrees(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_degrees` definition.""" + + a = xp_as_array([0.0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi], xp=xp) + xp_assert_close( + xp_degrees(a), + np.degrees([0.0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + xp_assert_close( + xp_degrees(np.pi), + 180.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpAtleast1d: + """Define :func:`colour.utilities.xp_atleast_1d` unit tests.""" + + def test_xp_atleast_1d(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_atleast_1d` definition.""" + + result = xp_atleast_1d(xp_as_array(1.0, xp=xp)) + assert as_ndarray(result).ndim == 1 + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_atleast_1d(a) + xp_assert_equal(result, [1.0, 2.0, 3.0]) + + # Python scalar input: the canonical scalar-promotion path. + result = xp_atleast_1d(1.0) + assert as_ndarray(result).ndim == 1 + xp_assert_equal(result, [1.0]) + + +class TestXpAtleast2d: + """Define :func:`colour.utilities.xp_atleast_2d` unit tests.""" + + def test_xp_atleast_2d(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_atleast_2d` definition.""" + + result = xp_atleast_2d(xp_as_array([1.0, 2.0, 3.0], xp=xp)) + assert as_ndarray(result).ndim == 2 + assert as_ndarray(result).shape == (1, 3) + + a = xp_as_array([[1.0, 2.0], [3.0, 4.0]], xp=xp) + result = xp_atleast_2d(a) + xp_assert_equal(result, [[1.0, 2.0], [3.0, 4.0]]) + + # Python scalar input: the canonical scalar-promotion path. + result = xp_atleast_2d(1.0) + assert as_ndarray(result).ndim == 2 + assert as_ndarray(result).shape == (1, 1) + xp_assert_equal(result, [[1.0]]) + + +class TestXpSqueeze: + """Define :func:`colour.utilities.xp_squeeze` unit tests.""" + + def test_xp_squeeze(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_squeeze` definition.""" + + a = xp_as_array([[1.0, 2.0]], xp=xp) + xp_assert_close(xp_squeeze(a, xp=xp), [1.0, 2.0]) + + a = xp_as_array([[[1.0], [2.0]]], xp=xp) + result = xp_squeeze(a, axis=0, xp=xp) + xp_assert_close(result, [[1.0], [2.0]]) + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + xp_assert_close(xp_squeeze(a, xp=xp), [1.0, 2.0, 3.0]) + + +class TestXpSinc: + """Define :func:`colour.utilities.xp_sinc` unit tests.""" + + def test_xp_sinc(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_sinc` definition.""" + + a = xp_as_array([0.0, 0.5, 1.0, 1.5], xp=xp) + xp_assert_close( + xp_sinc(a), + np.sinc([0.0, 0.5, 1.0, 1.5]), + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpIsclose: + """Define :func:`colour.utilities.xp_isclose` unit tests.""" + + def test_xp_isclose(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_isclose` definition.""" + + a = xp_as_array([1.0, 2.0001, 3.0], xp=xp) + b = xp_as_array([1.0, 2.0, 3.0], xp=xp) + + xp_assert_equal( + xp_isclose(a, b, atol=TOLERANCE_ABSOLUTE_TESTS * 10000), + np.isclose( + [1.0, 2.0001, 3.0], + [1.0, 2.0, 3.0], + atol=TOLERANCE_ABSOLUTE_TESTS * 10000, + ), + ) + xp_assert_equal( + xp_isclose(a, b, atol=TOLERANCE_ABSOLUTE_TESTS * 100), + np.isclose( + [1.0, 2.0001, 3.0], + [1.0, 2.0, 3.0], + atol=TOLERANCE_ABSOLUTE_TESTS * 100, + ), + ) + + +class TestXpNanToNum: + """Define :func:`colour.utilities.xp_nan_to_num` unit tests.""" + + def test_xp_nan_to_num(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_nan_to_num` definition.""" + + a = xp_as_array([1.0, np.nan, np.inf, -np.inf], xp=xp) + + xp_assert_equal( + xp_nan_to_num(a), + np.nan_to_num( + np.array([1.0, np.nan, np.inf, -np.inf], dtype=DTYPE_FLOAT_DEFAULT) + ), + ) + + xp_assert_equal( + xp_nan_to_num(a, nan=0.0, posinf=999.0, neginf=-999.0), + np.nan_to_num( + [1.0, np.nan, np.inf, -np.inf], + nan=0.0, + posinf=999.0, + neginf=-999.0, + ), + ) + + +class TestXpCreateDiagonal: + """Define :func:`colour.utilities.xp_create_diagonal` unit tests.""" + + def test_xp_create_diagonal(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_create_diagonal` definition.""" + + v = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_create_diagonal(v) + xp_assert_equal(result, np.diag([1.0, 2.0, 3.0])) + + +class TestXpReshape: + """Define :func:`colour.utilities.xp_reshape` unit tests.""" + + def test_xp_reshape(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_reshape` definition.""" + + a = xp.arange(6.0) + + result = xp_reshape(xp_as_array(a, xp=xp), (2, 3), xp=xp) + expected = np.arange(6.0).reshape((2, 3)) + xp_assert_equal(result, expected) + + result = xp_reshape(xp_as_array(a, xp=xp), (-1, 2), xp=xp) + expected = np.arange(6.0).reshape((-1, 2)) + xp_assert_equal(result, expected) + + a_int = xp_as_array([1, 2, 3, 4], xp=xp) + result = xp_reshape(xp_as_array(a_int, xp=xp), (2, 2), xp=xp) + expected = np.array([1, 2, 3, 4]).reshape((2, 2)) + xp_assert_equal(result, expected) + + +class TestXpBroadcastTo: + """Define :func:`colour.utilities.xp_broadcast_to` unit tests.""" + + def test_xp_broadcast_to(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_broadcast_to` definition.""" + + # Scalar to a 2-D shape. + result = xp_broadcast_to(xp_as_array(5.0, xp=xp), (2, 3), xp=xp) + expected = np.broadcast_to(np.array(5.0), (2, 3)) + xp_assert_equal(result, expected) + + # 1-D row to a 2-D shape (broadcast over leading axis). + result = xp_broadcast_to(xp_as_array([1.0, 2.0, 3.0], xp=xp), (4, 3), xp=xp) + expected = np.broadcast_to(np.array([1.0, 2.0, 3.0]), (4, 3)) + xp_assert_equal(result, expected) + + # Identity broadcast: shape unchanged. + a = xp_as_array([[1.0, 2.0], [3.0, 4.0]], xp=xp) + result = xp_broadcast_to(a, (2, 2), xp=xp) + xp_assert_equal(result, [[1.0, 2.0], [3.0, 4.0]]) + + +class TestXpEig: + """Define :func:`colour.utilities.xp_eig` unit tests.""" + + def test_xp_eig(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_eig` definition.""" + + A = xp_as_array([[1.0, 2.0], [3.0, 4.0]], xp=xp) + w, v = xp_eig(A, xp=xp) + w_np, v_np = np.linalg.eig(np.array([[1.0, 2.0], [3.0, 4.0]])) + xp_assert_close(w, w_np, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xp.abs(v), np.abs(v_np), atol=TOLERANCE_ABSOLUTE_TESTS) + + +class TestXpEigh: + """Define :func:`colour.utilities.xp_eigh` unit tests.""" + + def test_xp_eigh(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_eigh` definition.""" + + A = xp_as_array([[2.0, 1.0], [1.0, 3.0]], xp=xp) + w, v = xp_eigh(A, xp=xp) + w_np, v_np = np.linalg.eigh(np.array([[2.0, 1.0], [1.0, 3.0]])) + xp_assert_close(w, w_np, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close(xp.abs(v), np.abs(v_np), atol=TOLERANCE_ABSOLUTE_TESTS) + + +class TestXpLstsq: + """Define :func:`colour.utilities.xp_lstsq` unit tests.""" + + def test_xp_lstsq(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_lstsq` definition.""" + + A = xp_as_array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]], xp=xp) + b = xp_as_array([[1.0], [2.0], [3.0]], xp=xp) + + result = xp_lstsq(A, b) + expected = np.linalg.lstsq( + np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]), + np.array([[1.0], [2.0], [3.0]]), + rcond=None, + )[0] + xp_assert_close(result, expected, atol=TOLERANCE_ABSOLUTE_TESTS) + + +class TestXpIsin: + """Define :func:`colour.utilities.xp_isin` unit tests.""" + + def test_xp_isin(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_isin` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0, 4.0, 5.0], xp=xp) + b = xp_as_array([2.0, 4.0], xp=xp) + + xp_assert_equal( + xp_isin(a, b, xp=xp), + np.isin([1.0, 2.0, 3.0, 4.0, 5.0], [2.0, 4.0]), + ) + + a = xp_as_array([10.0, 20.0, 30.0], xp=xp) + b = xp_as_array([5.0, 10.0, 15.0, 20.0], xp=xp) + + xp_assert_equal( + xp_isin(a, b, xp=xp), + np.isin([10.0, 20.0, 30.0], [5.0, 10.0, 15.0, 20.0]), + ) + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + b = xp_as_array([4.0, 5.0, 6.0], xp=xp) + + xp_assert_equal( + xp_isin(a, b, xp=xp), + np.isin([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]), + ) + + # Empty ``test_elements``: every entry is absent. + xp_assert_equal( + xp_isin(a, xp_as_array([], xp=xp), xp=xp), + np.isin([1.0, 2.0, 3.0], []), + ) + + # :class:`NaN` is not equal to itself in :func:`numpy.isin`; the + # backend wrappers preserve that contract. + a = xp_as_array([1.0, np.nan, 3.0], xp=xp) + b = xp_as_array([np.nan], xp=xp) + + xp_assert_equal( + xp_isin(a, b, xp=xp), + np.isin([1.0, np.nan, 3.0], [np.nan]), + ) + + +class TestXpLinspace: + """Define :func:`colour.utilities.xp_linspace` unit tests.""" + + def test_xp_linspace(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_linspace` definition.""" + + result = xp_linspace(0, 10, num=5, xp=xp) + expected = np.linspace(0, 10, 5) + xp_assert_equal(result, expected) # pyright: ignore + + result, step = xp_linspace(0, 1, retstep=True, num=11, xp=xp) + expected, expected_step = np.linspace(0, 1, 11, retstep=True) + xp_assert_close(result, expected, atol=TOLERANCE_ABSOLUTE_TESTS) + xp_assert_close( + step, + expected_step, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + +class TestXpPad: + """Define :func:`colour.utilities.xp_pad` unit tests.""" + + def test_xp_pad(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_pad` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + + result = xp_pad(a, (2, 3), xp=xp) + expected = np.pad(np.array([1.0, 2.0, 3.0]), (2, 3)) + xp_assert_equal(result, expected) + + result = xp_pad(a, (1, 1), "wrap", xp=xp) + expected = np.pad(np.array([1.0, 2.0, 3.0]), (1, 1), "wrap") + xp_assert_equal(result, expected) + + +class TestXpUnique: + """Define :func:`colour.utilities.xp_unique` unit tests.""" + + def test_xp_unique(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_unique` definition.""" + + a = xp_as_array([3.0, 1.0, 2.0, 1.0, 3.0], xp=xp) + + result = xp_unique(a, xp=xp) + expected = np.unique(np.array([3.0, 1.0, 2.0, 1.0, 3.0])) + xp_assert_equal(result, expected) + + result, indexes = xp_unique(a, return_index=True, xp=xp) + expected, expected_indexes = np.unique( + np.array([3.0, 1.0, 2.0, 1.0, 3.0]), return_index=True + ) + xp_assert_equal(result, expected) + xp_assert_equal(indexes, expected_indexes) + + a = xp_as_array([[1.0, 2.0], [3.0, 4.0], [1.0, 2.0]], xp=xp) + result, indexes = xp_unique(a, axis=0, return_index=True, xp=xp) + expected, expected_indexes = np.unique( + np.array([[1.0, 2.0], [3.0, 4.0], [1.0, 2.0]]), + axis=0, + return_index=True, + ) + xp_assert_equal(result, expected) + xp_assert_equal(indexes, expected_indexes) + + +class TestXpInsert: + """Define :func:`colour.utilities.xp_insert` unit tests.""" + + def test_xp_insert(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_insert` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0, 4.0, 5.0], xp=xp) + indices = xp_as_array([1, 3], xp=xp) + values = xp_as_array([10.0, 30.0], xp=xp) + + result = xp_insert(a, indices, values, xp=xp) + expected = np.insert( + np.array([1.0, 2.0, 3.0, 4.0, 5.0]), + np.array([1, 3]), + np.array([10.0, 30.0]), + ) + xp_assert_equal(result, expected) + + result = xp_insert( + a, xp_as_array([0], xp=xp), xp_as_array([99.0], xp=xp), xp=xp + ) + expected = np.insert(np.array([1.0, 2.0, 3.0, 4.0, 5.0]), [0], [99.0]) + xp_assert_equal(result, expected) + + result = xp_insert( + a, xp_as_array([5], xp=xp), xp_as_array([99.0], xp=xp), xp=xp + ) + expected = np.insert(np.array([1.0, 2.0, 3.0, 4.0, 5.0]), [5], [99.0]) + xp_assert_equal(result, expected) + + # 2-D row insertion: insert two new rows into a ``(4, 3)`` array. + a = xp_as_array( + [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [10.0, 11.0, 12.0]], + xp=xp, + ) + indices = xp_as_array([1, 3], xp=xp) + values = xp_as_array([[-1.0, -2.0, -3.0], [-7.0, -8.0, -9.0]], xp=xp) + result = xp_insert(a, indices, values, axis=0, xp=xp) + expected = np.insert( + np.array( + [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [10.0, 11.0, 12.0]] + ), + np.array([1, 3]), + np.array([[-1.0, -2.0, -3.0], [-7.0, -8.0, -9.0]]), + axis=0, + ) + xp_assert_equal(result, expected) + + # 2-D column insertion: insert one new column at index 2. + indices = xp_as_array([2], xp=xp) + values = xp_as_array([[99.0], [99.0], [99.0], [99.0]], xp=xp) + result = xp_insert(a, indices, values, axis=1, xp=xp) + expected = np.insert( + np.array( + [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [10.0, 11.0, 12.0]] + ), + np.array([2]), + np.array([[99.0], [99.0], [99.0], [99.0]]), + axis=1, + ) + xp_assert_equal(result, expected) + + # Unsorted indices: must normalise to the sorted equivalent, matching + # :func:`numpy.insert` behaviour. + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + indices = xp_as_array([2, 0], xp=xp) + values = xp_as_array([99.0, 88.0], xp=xp) + result = xp_insert(a, indices, values, xp=xp) + expected = np.insert( + np.array([1.0, 2.0, 3.0]), np.array([2, 0]), np.array([99.0, 88.0]) + ) + xp_assert_equal(result, expected) + + # Empty indices / values: identity, no-op. + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + result = xp_insert( + a, + xp_as_array(np.array([], dtype=np.int64), xp=xp), + xp_as_array([], xp=xp), + xp=xp, + ) + xp_assert_equal(result, np.array([1.0, 2.0, 3.0])) + + # Negative axis (axis=-1) on a 2-D input must equal axis=1. + a_2d = xp_as_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], xp=xp) + result = xp_insert( + a_2d, + xp_as_array([1], xp=xp), + xp_as_array([[99.0], [88.0]], xp=xp), + axis=-1, + xp=xp, + ) + expected = np.insert( + np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + np.array([1]), + np.array([[99.0], [88.0]]), + axis=-1, + ) + xp_assert_equal(result, expected) + + +class TestXpSetxor1d: + """Define :func:`colour.utilities.xp_setxor1d` unit tests.""" + + def test_xp_setxor1d(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_setxor1d` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0, 4.0], xp=xp) + b = xp_as_array([3.0, 4.0, 5.0, 6.0], xp=xp) + + result = xp_setxor1d(a, b, xp=xp) + expected = np.setxor1d( + np.array([1.0, 2.0, 3.0, 4.0]), np.array([3.0, 4.0, 5.0, 6.0]) + ) + xp_assert_equal(result, expected) + + result = xp_setxor1d(a, a, xp=xp) + expected = np.setxor1d( + np.array([1.0, 2.0, 3.0, 4.0]), np.array([1.0, 2.0, 3.0, 4.0]) + ) + xp_assert_equal(result, expected) + + result = xp_setxor1d(a, xp_as_array([10.0, 20.0], xp=xp), xp=xp) + expected = np.setxor1d(np.array([1.0, 2.0, 3.0, 4.0]), np.array([10.0, 20.0])) + xp_assert_equal(result, expected) + + # Empty operands: identity-on-the-other. + result = xp_setxor1d(a, xp_as_array([], xp=xp), xp=xp) + xp_assert_equal(result, np.array([1.0, 2.0, 3.0, 4.0])) + + result = xp_setxor1d(xp_as_array([], xp=xp), xp_as_array([], xp=xp), xp=xp) + xp_assert_equal(result, np.array([], dtype=float)) + + +class TestXpAssertClose: + """Define :func:`colour.utilities.xp_assert_close` unit tests.""" + + def test_xp_assert_close(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_assert_close` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + + xp_assert_close(a, [1.0, 2.0, 3.0]) + xp_assert_close(a, xp_as_array([1.0, 2.0, 3.0], xp=xp)) + + with pytest.raises(AssertionError): + xp_assert_close(a, [1.0, 2.0, 3.5]) + + xp_assert_close(a, [1.0, 2.0, 3.5], atol=1.0) + xp_assert_close(a, [1.0, 2.0, 3.5], rtol=1.0) + + with pytest.raises(AssertionError): + xp_assert_close(a, [1.0, 2.0, 3.5], rtol=0.1, atol=0.1) + + # Default tolerances are resolved at call time so that fixtures + # relaxing the module-level constant also relax defaulted calls. + default = utilities_array.TOLERANCE_ABSOLUTE_TESTS + try: + utilities_array.TOLERANCE_ABSOLUTE_TESTS = 1.0 + xp_assert_close(np.array([1.0]), np.array([1.5])) + finally: + utilities_array.TOLERANCE_ABSOLUTE_TESTS = default + + +class TestXpAssertEqual: + """Define :func:`colour.utilities.xp_assert_equal` unit tests.""" + + def test_xp_assert_equal(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.xp_assert_equal` definition.""" + + a = xp_as_array([1.0, 2.0, 3.0], xp=xp) + + xp_assert_equal(a, [1.0, 2.0, 3.0]) + xp_assert_equal(a, xp_as_array([1.0, 2.0, 3.0], xp=xp)) + + with pytest.raises(AssertionError): + xp_assert_equal(a, [1.0, 2.0, 4.0]) + + +class TestMixinDataclassFields: """ Define :class:`colour.utilities.array.MixinDataclassFields` class unit tests methods. """ - def setUp(self) -> None: + def setup_method(self) -> None: """Initialise the common tests attributes.""" @dataclass @@ -207,13 +1545,13 @@ def test_fields(self) -> None: assert self._data.fields == fields(self._data) -class TestMixinDataclassIterable(unittest.TestCase): +class TestMixinDataclassIterable: """ Define :class:`colour.utilities.array.MixinDataclassIterable` class unit tests methods. """ - def setUp(self) -> None: + def setup_method(self) -> None: """Initialise the common tests attributes.""" @dataclass @@ -279,13 +1617,13 @@ def test_items(self) -> None: assert tuple(self._data.items) == (("a", "Foo"), ("b", "Bar"), ("c", "Baz")) -class TestMixinDataclassArray(unittest.TestCase): +class TestMixinDataclassArray: """ Define :class:`colour.utilities.array.MixinDataclassArray` class unit tests methods. """ - def setUp(self) -> None: + def setup_method(self) -> None: """Initialise the common tests attributes.""" @dataclass @@ -327,18 +1665,18 @@ def test__array__(self) -> None: method. """ - np.testing.assert_array_equal(self._data, self._array) + xp_assert_equal(self._data, self._array) assert np.array(self._data, dtype=DTYPE_INT_DEFAULT).dtype == DTYPE_INT_DEFAULT -class TestMixinDataclassArithmetic(unittest.TestCase): +class TestMixinDataclassArithmetic: """ Define :class:`colour.utilities.array.MixinDataclassArithmetic` class unit tests methods. """ - def setUp(self) -> None: + def setup_method(self) -> None: """Initialise the common tests attributes.""" @dataclass @@ -393,61 +1731,61 @@ def test_arithmetical_operation(self) -> None: arithmetical_operation` method. """ - np.testing.assert_allclose( + xp_assert_close( self._data.arithmetical_operation(10, "+", False), self._array + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data.arithmetical_operation(10, "-", False), self._array - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data.arithmetical_operation(10, "*", False), self._array * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data.arithmetical_operation(10, "/", False), self._array / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data.arithmetical_operation(10, "**", False), self._array**10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data + 10, self._array + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data - 10, self._array - 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data * 10, self._array * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data / 10, self._array / 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( self._data**10, self._array**10, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -455,31 +1793,31 @@ def test_arithmetical_operation(self) -> None: data = deepcopy(self._data) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(10, "+", True), self._array + 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(10, "-", True), self._array, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(10, "*", True), self._array * 10, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(10, "/", True), self._array, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(10, "**", True), self._array**10, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -487,13 +1825,13 @@ def test_arithmetical_operation(self) -> None: data = deepcopy(self._data) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(self._array, "+", False), data + self._array, atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( data.arithmetical_operation(data, "+", False), data + data, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -517,120 +1855,140 @@ def test_arithmetical_operation(self) -> None: assert data.a == 1 -class TestAsArray(unittest.TestCase): +class TestAsArray: """ Define :func:`colour.utilities.array.as_array` definition unit tests methods. """ - def test_as_array(self) -> None: + def test_as_array(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.as_array` definition.""" - np.testing.assert_equal(as_array([1, 2, 3]), np.array([1, 2, 3])) + xp_assert_equal(as_array(xp_as_array([1, 2, 3], xp=xp)), [1, 2, 3]) assert as_array([1, 2, 3], DTYPE_FLOAT_DEFAULT).dtype == DTYPE_FLOAT_DEFAULT assert as_array([1, 2, 3], DTYPE_INT_DEFAULT).dtype == DTYPE_INT_DEFAULT - np.testing.assert_equal( + xp_assert_equal( as_array(dict(zip("abc", [1, 2, 3], strict=True)).values()), - np.array([1, 2, 3]), + [1, 2, 3], ) -class TestAsInt(unittest.TestCase): +class TestAsInt: """ Define :func:`colour.utilities.array.as_int` definition unit tests methods. """ - def test_as_int(self) -> None: + def test_as_int(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.as_int` definition.""" assert as_int(1) == 1 - assert as_int(np.array([1])).ndim == 1 + assert as_int(xp_as_array([1], xp=xp)).ndim == 1 - assert as_int(np.array([[1]])).ndim == 2 + assert as_int(xp_as_array([[1]], xp=xp)).ndim == 2 - np.testing.assert_array_equal( - as_int(np.array([1.0, 2.0, 3.0])), np.array([1, 2, 3]) - ) + xp_assert_equal(as_int(xp_as_array([1.0, 2.0, 3.0], xp=xp)), [1, 2, 3]) assert as_int(np.array([1.0, 2.0, 3.0])).dtype == DTYPE_INT_DEFAULT assert isinstance(as_int(1), DTYPE_INT_DEFAULT) -class TestAsFloat(unittest.TestCase): +class TestAsFloat: """ Define :func:`colour.utilities.array.as_float` definition unit tests methods. """ - def test_as_float(self) -> None: + def test_as_float(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.as_float` definition.""" assert as_float(1) == 1.0 - assert as_float(np.array([1])).ndim == 1 + assert as_float(xp_as_array([1], xp=xp)).ndim == 1 - assert as_float(np.array([[1]])).ndim == 2 + assert as_float(xp_as_array([[1]], xp=xp)).ndim == 2 - np.testing.assert_allclose( - as_float(np.array([1, 2, 3])), - np.array([1.0, 2.0, 3.0]), + xp_assert_close( + as_float(xp_as_array([1, 2, 3], xp=xp)), + [1.0, 2.0, 3.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) assert as_float(np.array([1, 2, 3])).dtype == DTYPE_FLOAT_DEFAULT - assert isinstance(as_float(1), DTYPE_FLOAT_DEFAULT) + if is_numpy_namespace(xp): + assert isinstance(as_float(1), DTYPE_FLOAT_DEFAULT) -class TestAsIntArray(unittest.TestCase): +class TestAsIntArray: """ Define :func:`colour.utilities.array.as_int_array` definition unit tests methods. """ - def test_as_int_array(self) -> None: + def test_as_int_array(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.as_int_array` definition.""" - np.testing.assert_equal(as_int_array([1.0, 2.0, 3.0]), np.array([1, 2, 3])) + xp_assert_equal( + as_int_array(xp_as_array([1.0, 2.0, 3.0], xp=xp)), + [1, 2, 3], + ) assert as_int_array([1, 2, 3]).dtype == DTYPE_INT_DEFAULT -class TestAsFloatArray(unittest.TestCase): +class TestAsFloatArray: """ Define :func:`colour.utilities.array.as_float_array` definition unit tests methods. """ - def test_as_float_array(self) -> None: + def test_as_float_array(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.as_float_array` definition.""" - np.testing.assert_equal(as_float_array([1, 2, 3]), np.array([1, 2, 3])) + xp_assert_equal( + as_float_array(xp_as_array([1, 2, 3], xp=xp)), + [1, 2, 3], + ) assert as_float_array([1, 2, 3]).dtype == DTYPE_FLOAT_DEFAULT -class TestAsComplexArray(unittest.TestCase): +class TestAsComplexArray: """ Define :func:`colour.utilities.array.as_complex_array` definition unit tests methods. """ - def test_as_complex_array(self) -> None: + def test_as_complex_array(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.as_complex_array` definition.""" - np.testing.assert_equal( - as_complex_array([1, 2, 3]), np.array([1 + 0j, 2 + 0j, 3 + 0j]) + if not is_numpy_namespace(xp): + probe = xp_as_array([1], xp=xp) + xp_compat = array_namespace(probe) + xp_complex_dtype = getattr( + xp_compat, np.dtype(DTYPE_COMPLEX_DEFAULT).name, None + ) + if xp_complex_dtype is None: + pytest.skip("Backend does not support the default complex dtype.") + try: + xp_compat.asarray(probe, dtype=xp_complex_dtype) + except TypeError: + pytest.skip("Backend does not support the default complex dtype.") + + xp_assert_equal( + as_complex_array(xp_as_array([1, 2, 3], xp=xp)), + [1 + 0j, 2 + 0j, 3 + 0j], ) - np.testing.assert_equal( - as_complex_array([1 + 2j, 3 + 4j]), np.array([1 + 2j, 3 + 4j]) + xp_assert_equal( + as_complex_array(xp_as_array([1 + 2j, 3 + 4j], xp=xp)), + [1 + 2j, 3 + 4j], ) assert as_complex_array([1, 2, 3]).dtype == DTYPE_COMPLEX_DEFAULT @@ -638,7 +1996,7 @@ def test_as_complex_array(self) -> None: assert as_complex_array([1, 2, 3], np.complex64).dtype == np.complex64 -class TestAsIntScalar(unittest.TestCase): +class TestAsIntScalar: """ Define :func:`colour.utilities.array.as_int_scalar` definition unit tests methods. @@ -647,12 +2005,12 @@ class TestAsIntScalar(unittest.TestCase): def test_as_int_scalar(self) -> None: """Test :func:`colour.utilities.array.as_int_scalar` definition.""" - assert as_int_scalar(1.0) == 1 + assert float(as_int_scalar(1.0)) == 1 assert as_int_scalar(1.0).dtype == DTYPE_INT_DEFAULT # pyright: ignore -class TestAsFloatScalar(unittest.TestCase): +class TestAsFloatScalar: """ Define :func:`colour.utilities.array.as_float_scalar` definition unit tests methods. @@ -661,12 +2019,12 @@ class TestAsFloatScalar(unittest.TestCase): def test_as_float_scalar(self) -> None: """Test :func:`colour.utilities.array.as_float_scalar` definition.""" - assert as_float_scalar(1) == 1.0 + assert float(as_float_scalar(1)) == 1.0 assert as_float_scalar(1).dtype == DTYPE_FLOAT_DEFAULT # pyright: ignore -class TestSetDefaultIntegerDtype(unittest.TestCase): +class TestSetDefaultIntegerDtype: """ Define :func:`colour.utilities.array.set_default_int_dtype` definition unit tests methods. @@ -693,30 +2051,37 @@ def tearDown(self) -> None: set_default_int_dtype(np.int64) -class TestSetDefaultFloatDtype(unittest.TestCase): +class TestSetDefaultFloatDtype: """ Define :func:`colour.utilities.array.set_default_float_dtype` definition unit tests methods. """ + @pytest.fixture(autouse=True) + def _restore_default_float_dtype(self) -> Generator[None, None, None]: + """ + Restore the default float dtype after each test to avoid + cross-test bleed under *pytest-xdist*. + """ + + yield + set_default_float_dtype(np.float64) + def test_set_default_float_dtype(self) -> None: """ Test :func:`colour.utilities.array.set_default_float_dtype` definition. """ - try: - assert as_float_array(np.ones(3)).dtype == np.float64 + assert as_float_array(np.ones(3)).dtype == np.float64 - set_default_float_dtype(np.float16) + set_default_float_dtype(np.float16) - assert as_float_array(np.ones(3)).dtype == np.float16 + assert as_float_array(np.ones(3)).dtype == np.float16 - set_default_float_dtype(np.float64) + set_default_float_dtype(np.float64) - assert as_float_array(np.ones(3)).dtype == np.float64 - finally: - set_default_float_dtype(np.float64) + assert as_float_array(np.ones(3)).dtype == np.float64 def test_set_default_float_dtype_enforcement(self) -> None: """ @@ -744,95 +2109,92 @@ def test_set_default_float_dtype_enforcement(self) -> None: convert, ) - try: - dtype = np.float32 - set_default_float_dtype(dtype) - - for source, target, _callable in CONVERSION_SPECIFICATIONS_DATA: - if target in ("Hexadecimal", "Munsell Colour"): - continue - - # Spectral distributions are instantiated with float64 data and - # spectral up-sampling optimization fails. - if ( - "Spectral Distribution" in (source, target) # noqa: PLR1714 - or target == "Complementary Wavelength" - or target == "Dominant Wavelength" - ): - continue + dtype = np.float32 + set_default_float_dtype(dtype) + + for source, target, _callable in CONVERSION_SPECIFICATIONS_DATA: + if target in ("Hexadecimal", "Munsell Colour"): + continue + + # Spectral distributions are instantiated with float64 data and + # spectral up-sampling optimization fails. + if ( + "Spectral Distribution" in (source, target) # noqa: PLR1714 + or target == "Complementary Wavelength" + or target == "Dominant Wavelength" + ): + continue - a = np.array([(0.25, 0.5, 0.25), (0.25, 0.5, 0.25)]) + a = np.array([(0.25, 0.5, 0.25), (0.25, 0.5, 0.25)]) - if source == "CAM16": - a = CAM_Specification_CAM16(J=0.25, M=0.5, h=0.25) + if source == "CAM16": + a = CAM_Specification_CAM16(J=0.25, M=0.5, h=0.25) - if source == "CIECAM02": - a = CAM_Specification_CIECAM02(J=0.25, M=0.5, h=0.25) + if source == "CIECAM02": + a = CAM_Specification_CIECAM02(J=0.25, M=0.5, h=0.25) - if source == "CIECAM16": - a = CAM_Specification_CIECAM16(J=0.25, M=0.5, h=0.25) + if source == "CIECAM16": + a = CAM_Specification_CIECAM16(J=0.25, M=0.5, h=0.25) - if source == "Hellwig 2022": - a = CAM_Specification_Hellwig2022(J=0.25, M=0.5, h=0.25) + if source == "Hellwig 2022": + a = CAM_Specification_Hellwig2022(J=0.25, M=0.5, h=0.25) - if source == "Kim 2009": - a = CAM_Specification_Kim2009(J=0.25, M=0.5, h=0.25) + if source == "Kim 2009": + a = CAM_Specification_Kim2009(J=0.25, M=0.5, h=0.25) - if source == "sCAM": - a = CAM_Specification_sCAM(J=0.25, M=0.5, h=0.25) + if source == "sCAM": + a = CAM_Specification_sCAM(J=0.25, M=0.5, h=0.25) - if source == "ZCAM": - a = CAM_Specification_ZCAM(J=0.25, M=0.5, h=0.25) + if source == "ZCAM": + a = CAM_Specification_ZCAM(J=0.25, M=0.5, h=0.25) - if source == "CMYK": - a = np.array([(0.25, 0.5, 0.25, 0.5), (0.25, 0.5, 0.25, 0.5)]) + if source == "CMYK": + a = np.array([(0.25, 0.5, 0.25, 0.5), (0.25, 0.5, 0.25, 0.5)]) - if source == "Hexadecimal": - a = np.array(["#FFFFFF", "#FFFFFF"]) + if source == "Hexadecimal": + a = np.array(["#FFFFFF", "#FFFFFF"]) - if source == "CSS Color 3": - a = "aliceblue" + if source == "CSS Color 3": + a = "aliceblue" - if source == "Munsell Colour": - a = ["4.2YR 8.1/5.3", "4.2YR 8.1/5.3"] + if source == "Munsell Colour": + a = ["4.2YR 8.1/5.3", "4.2YR 8.1/5.3"] - if source == "Wavelength": - a = 555 + if source == "Wavelength": + a = 555 - if ( - source.startswith("CCT") # noqa: PIE810 - or source.endswith(" xy") - or source.endswith(" uv") + if ( + source.startswith("CCT") # noqa: PIE810 + or source.endswith(" xy") + or source.endswith(" uv") + ): + a = np.array([(0.25, 0.5), (0.25, 0.5)]) + + def dtype_getter(x: NDArray) -> DType: + """Dtype getter callable.""" + + for specification in ( + "ATD95", + "CIECAM02", + "CAM16", + "Hellwig 2022", + "Hunt", + "Kim 2009", + "LLAB", + "Nayatani95", + "RLAB", + "sCAM", + "ZCAM", ): - a = np.array([(0.25, 0.5), (0.25, 0.5)]) - - def dtype_getter(x: NDArray) -> DType: - """Dtype getter callable.""" - - for specification in ( - "ATD95", - "CIECAM02", - "CAM16", - "Hellwig 2022", - "Hunt", - "Kim 2009", - "LLAB", - "Nayatani95", - "RLAB", - "sCAM", - "ZCAM", - ): - if target.endswith(specification): # noqa: B023 - return getattr(x, fields(x)[0].name).dtype # pyright: ignore - - return x.dtype # pyright: ignore - - assert dtype_getter(convert(a, source, target)) == dtype - finally: - set_default_float_dtype(np.float64) + if target.endswith(specification): # noqa: B023 + return getattr(x, fields(x)[0].name).dtype # pyright: ignore + + return x.dtype # pyright: ignore + assert dtype_getter(convert(a, source, target)) == dtype -class TestGetDomainRangeScale(unittest.TestCase): + +class TestGetDomainRangeScale: """ Define :func:`colour.utilities.common.get_domain_range_scale` definition unit tests methods. @@ -854,7 +2216,7 @@ def test_get_domain_range_scale(self) -> None: assert get_domain_range_scale() == "100" -class TestSetDomainRangeScale(unittest.TestCase): +class TestSetDomainRangeScale: """ Define :func:`colour.utilities.common.set_domain_range_scale` definition unit tests methods. @@ -882,7 +2244,7 @@ def test_set_domain_range_scale(self) -> None: set_domain_range_scale("Invalid") -class TestDomainRangeScale(unittest.TestCase): +class TestDomainRangeScale: """ Define :func:`colour.utilities.common.domain_range_scale` definition unit tests methods. @@ -951,7 +2313,7 @@ def fn_b(a: ArrayLike) -> NDArrayFloat: assert fn_b(10) == 2.0 -class TestGetDomainRangeScaleMetadata(unittest.TestCase): +class TestGetDomainRangeScaleMetadata: """ Define :func:`colour.utilities.array.get_domain_range_scale_metadata` definition unit tests methods. @@ -1139,7 +2501,7 @@ def function_q(x: Any) -> Any: assert metadata["range"] == "another_undefined" -class TestToDomain1(unittest.TestCase): +class TestToDomain1: """ Define :func:`colour.utilities.common.to_domain_1` definition unit tests methods. @@ -1149,22 +2511,22 @@ def test_to_domain_1(self) -> None: """Test :func:`colour.utilities.common.to_domain_1` definition.""" with domain_range_scale("Reference"): - assert to_domain_1(1) == 1 + assert float(to_domain_1(1)) == 1 with domain_range_scale("1"): - assert to_domain_1(1) == 1 + assert float(to_domain_1(1)) == 1 with domain_range_scale("100"): - assert to_domain_1(1) == 0.01 + assert float(to_domain_1(1)) == 0.01 with domain_range_scale("100"): - assert to_domain_1(1, np.pi) == 1 / np.pi + assert float(to_domain_1(1, np.pi)) == 1 / np.pi with domain_range_scale("100"): assert to_domain_1(1, dtype=np.float16).dtype == np.float16 -class TestToDomain10(unittest.TestCase): +class TestToDomain10: """ Define :func:`colour.utilities.common.to_domain_10` definition unit tests methods. @@ -1174,22 +2536,22 @@ def test_to_domain_10(self) -> None: """Test :func:`colour.utilities.common.to_domain_10` definition.""" with domain_range_scale("Reference"): - assert to_domain_10(1) == 1 + assert float(to_domain_10(1)) == 1 with domain_range_scale("1"): - assert to_domain_10(1) == 10 + assert float(to_domain_10(1)) == 10 with domain_range_scale("100"): - assert to_domain_10(1) == 0.1 + assert float(to_domain_10(1)) == 0.1 with domain_range_scale("100"): - assert to_domain_10(1, np.pi) == 1 / np.pi + assert float(to_domain_10(1, np.pi)) == 1 / np.pi with domain_range_scale("100"): assert to_domain_10(1, dtype=np.float16).dtype == np.float16 -class TestToDomain100(unittest.TestCase): +class TestToDomain100: """ Define :func:`colour.utilities.common.to_domain_100` definition unit tests methods. @@ -1199,22 +2561,22 @@ def test_to_domain_100(self) -> None: """Test :func:`colour.utilities.common.to_domain_100` definition.""" with domain_range_scale("Reference"): - assert to_domain_100(1) == 1 + assert float(to_domain_100(1)) == 1 with domain_range_scale("1"): - assert to_domain_100(1) == 100 + assert float(to_domain_100(1)) == 100 with domain_range_scale("100"): - assert to_domain_100(1) == 1 + assert float(to_domain_100(1)) == 1 with domain_range_scale("1"): - assert to_domain_100(1, np.pi) == np.pi + assert float(to_domain_100(1, np.pi)) == np.pi with domain_range_scale("100"): assert to_domain_100(1, dtype=np.float16).dtype == np.float16 -class TestToDomainDegrees(unittest.TestCase): +class TestToDomainDegrees: """ Define :func:`colour.utilities.common.to_domain_degrees` definition unit tests methods. @@ -1224,22 +2586,22 @@ def test_to_domain_degrees(self) -> None: """Test :func:`colour.utilities.common.to_domain_degrees` definition.""" with domain_range_scale("Reference"): - assert to_domain_degrees(1) == 1 + assert float(to_domain_degrees(1)) == 1 with domain_range_scale("1"): - assert to_domain_degrees(1) == 360 + assert float(to_domain_degrees(1)) == 360 with domain_range_scale("100"): - assert to_domain_degrees(1) == 3.6 + assert float(to_domain_degrees(1)) == 3.6 with domain_range_scale("100"): - assert to_domain_degrees(1, np.pi) == np.pi / 100 + assert float(to_domain_degrees(1, np.pi)) == np.pi / 100 with domain_range_scale("100"): assert to_domain_degrees(1, dtype=np.float16).dtype == np.float16 -class TestToDomainInt(unittest.TestCase): +class TestToDomainInt: """ Define :func:`colour.utilities.common.to_domain_int` definition unit tests methods. @@ -1249,22 +2611,22 @@ def test_to_domain_int(self) -> None: """Test :func:`colour.utilities.common.to_domain_int` definition.""" with domain_range_scale("Reference"): - assert to_domain_int(1) == 1 + assert float(to_domain_int(1)) == 1 with domain_range_scale("1"): - assert to_domain_int(1) == 255 + assert float(to_domain_int(1)) == 255 with domain_range_scale("100"): - assert to_domain_int(1) == 2.55 + assert float(to_domain_int(1)) == 2.55 with domain_range_scale("100"): - assert to_domain_int(1, 10) == 10.23 + assert float(to_domain_int(1, 10)) == 10.23 with domain_range_scale("100"): assert to_domain_int(1, dtype=np.float16).dtype == np.float16 -class TestFromRange1(unittest.TestCase): +class TestFromRange1: """ Define :func:`colour.utilities.common.from_range_1` definition unit tests methods. @@ -1274,19 +2636,19 @@ def test_from_range_1(self) -> None: """Test :func:`colour.utilities.common.from_range_1` definition.""" with domain_range_scale("Reference"): - assert from_range_1(1) == 1 + assert float(from_range_1(1)) == 1 with domain_range_scale("1"): - assert from_range_1(1) == 1 + assert float(from_range_1(1)) == 1 with domain_range_scale("100"): - assert from_range_1(1) == 100 + assert float(from_range_1(1)) == 100 with domain_range_scale("100"): - assert from_range_1(1, np.pi) == 1 * np.pi + assert float(from_range_1(1, np.pi)) == 1 * np.pi -class TestFromRange10(unittest.TestCase): +class TestFromRange10: """ Define :func:`colour.utilities.common.from_range_10` definition unit tests methods. @@ -1296,19 +2658,19 @@ def test_from_range_10(self) -> None: """Test :func:`colour.utilities.common.from_range_10` definition.""" with domain_range_scale("Reference"): - assert from_range_10(1) == 1 + assert float(from_range_10(1)) == 1 with domain_range_scale("1"): - assert from_range_10(1) == 0.1 + assert float(from_range_10(1)) == 0.1 with domain_range_scale("100"): - assert from_range_10(1) == 10 + assert float(from_range_10(1)) == 10 with domain_range_scale("100"): - assert from_range_10(1, np.pi) == 1 * np.pi + assert float(from_range_10(1, np.pi)) == 1 * np.pi -class TestFromRange100(unittest.TestCase): +class TestFromRange100: """ Define :func:`colour.utilities.common.from_range_100` definition unit tests methods. @@ -1318,19 +2680,19 @@ def test_from_range_100(self) -> None: """Test :func:`colour.utilities.common.from_range_100` definition.""" with domain_range_scale("Reference"): - assert from_range_100(1) == 1 + assert float(from_range_100(1)) == 1 with domain_range_scale("1"): - assert from_range_100(1) == 0.01 + assert float(from_range_100(1)) == 0.01 with domain_range_scale("100"): - assert from_range_100(1) == 1 + assert float(from_range_100(1)) == 1 with domain_range_scale("1"): - assert from_range_100(1, np.pi) == 1 / np.pi + assert float(from_range_100(1, np.pi)) == 1 / np.pi -class TestFromRangeDegrees(unittest.TestCase): +class TestFromRangeDegrees: """ Define :func:`colour.utilities.common.from_range_degrees` definition unit tests methods. @@ -1340,19 +2702,19 @@ def test_from_range_degrees(self) -> None: """Test :func:`colour.utilities.common.from_range_degrees` definition.""" with domain_range_scale("Reference"): - assert from_range_degrees(1) == 1 + assert float(from_range_degrees(1)) == 1 with domain_range_scale("1"): - assert from_range_degrees(1) == 1 / 360 + assert float(from_range_degrees(1)) == 1 / 360 with domain_range_scale("100"): - assert from_range_degrees(1) == 1 / 3.6 + assert float(from_range_degrees(1)) == 1 / 3.6 with domain_range_scale("100"): - assert from_range_degrees(1, np.pi) == 1 / (np.pi / 100) + assert float(from_range_degrees(1, np.pi)) == 1 / (np.pi / 100) -class TestFromRangeInt(unittest.TestCase): +class TestFromRangeInt: """ Define :func:`colour.utilities.common.from_range_int` definition unit tests methods. @@ -1362,22 +2724,22 @@ def test_from_range_int(self) -> None: """Test :func:`colour.utilities.common.from_range_int` definition.""" with domain_range_scale("Reference"): - assert from_range_int(1) == 1 + assert float(from_range_int(1)) == 1 with domain_range_scale("1"): - assert from_range_int(1) == 1 / 255 + assert float(from_range_int(1)) == 1 / 255 with domain_range_scale("100"): - assert from_range_int(1) == 1 / 2.55 + assert float(from_range_int(1)) == 1 / 2.55 with domain_range_scale("100"): - assert from_range_int(1, 10) == 1 / (1023 / 100) + assert float(from_range_int(1, 10)) == 1 / (1023 / 100) with domain_range_scale("100"): assert from_range_int(1, dtype=np.float16).dtype == np.float16 -class TestIsNdarrayCopyEnabled(unittest.TestCase): +class TestIsNdarrayCopyEnabled: """ Define :func:`colour.utilities.array.is_ndarray_copy_enabled` definition unit tests methods. @@ -1395,27 +2757,27 @@ def test_is_ndarray_copy_enabled(self) -> None: assert not is_ndarray_copy_enabled() -class TestSetNdarrayCopyEnabled(unittest.TestCase): +class TestSetNdarrayCopyEnabled: """ - Define :func:`colour.utilities.array.set_ndarray_copy_enable` definition + Define :func:`colour.utilities.array.set_ndarray_copy_enabled` definition unit tests methods. """ - def test_set_ndarray_copy_enable(self) -> None: + def test_set_ndarray_copy_enabled(self) -> None: """ - Test :func:`colour.utilities.array.set_ndarray_copy_enable` definition. + Test :func:`colour.utilities.array.set_ndarray_copy_enabled` definition. """ with ndarray_copy_enable(is_ndarray_copy_enabled()): - set_ndarray_copy_enable(True) + set_ndarray_copy_enabled(True) assert is_ndarray_copy_enabled() with ndarray_copy_enable(is_ndarray_copy_enabled()): - set_ndarray_copy_enable(False) + set_ndarray_copy_enabled(False) assert not is_ndarray_copy_enabled() -class TestNdarrayCopyEnable(unittest.TestCase): +class TestNdarrayCopyEnable: """ Define :func:`colour.utilities.array.ndarray_copy_enable` definition unit tests methods. @@ -1449,33 +2811,33 @@ def fn_b() -> None: fn_b() -class TestNdarrayCopy(unittest.TestCase): +class TestNdarrayCopy: """ Define :func:`colour.utilities.array.ndarray_copy` definition unit tests methods. """ - def test_ndarray_copy(self) -> None: + def test_ndarray_copy(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.ndarray_copy` definition.""" - a = np.linspace(0, 1, 10) + a = xp_linspace(0, 1, num=10, xp=xp) with ndarray_copy_enable(True): - assert id(ndarray_copy(a)) != id(a) + assert id(ndarray_copy(a)) != id(a) # pyright: ignore with ndarray_copy_enable(False): - assert id(ndarray_copy(a)) == id(a) + assert id(ndarray_copy(a)) == id(a) # pyright: ignore -class TestClosestIndexes(unittest.TestCase): +class TestClosestIndexes: """ Define :func:`colour.utilities.array.closest_indexes` definition unit tests methods. """ - def test_closest_indexes(self) -> None: + def test_closest_indexes(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.closest_indexes` definition.""" - a = np.array( + a = xp_as_array( [ 24.31357115, 63.62396289, @@ -1483,31 +2845,32 @@ def test_closest_indexes(self) -> None: 62.70988028, 46.84480573, 25.40026416, - ] + ], + xp=xp, ) - assert closest_indexes(a, 63.05) == 3 + assert as_ndarray(closest_indexes(a, 63.05)).item() == 3 - assert closest_indexes(a, 51.15) == 4 + assert as_ndarray(closest_indexes(a, 51.15)).item() == 4 - assert closest_indexes(a, 24.90) == 5 + assert as_ndarray(closest_indexes(a, 24.90)).item() == 5 - np.testing.assert_array_equal( - closest_indexes(a, np.array([63.05, 51.15, 24.90])), - np.array([3, 4, 5]), + xp_assert_equal( + closest_indexes(a, xp_as_array([63.05, 51.15, 24.90], xp=xp)), + [3, 4, 5], ) -class TestClosest(unittest.TestCase): +class TestClosest: """ Define :func:`colour.utilities.array.closest` definition unit tests methods. """ - def test_closest(self) -> None: + def test_closest(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.closest` definition.""" - a = np.array( + a = xp_as_array( [ 24.31357115, 63.62396289, @@ -1515,126 +2878,142 @@ def test_closest(self) -> None: 62.70988028, 46.84480573, 25.40026416, - ] + ], + xp=xp, ) - assert closest(a, 63.05) == 62.70988028 - - assert closest(a, 51.15) == 46.84480573 - - assert closest(a, 24.90) == 25.40026416 - - np.testing.assert_allclose( - closest(a, np.array([63.05, 51.15, 24.90])), - np.array([62.70988028, 46.84480573, 25.40026416]), + xp_assert_close( + closest(a, xp_as_array([63.05, 51.15, 24.90], xp=xp)), + [62.70988028, 46.84480573, 25.40026416], atol=TOLERANCE_ABSOLUTE_TESTS, ) -class TestInterval(unittest.TestCase): +class TestInterval: """ Define :func:`colour.utilities.array.interval` definition unit tests methods. """ - def test_interval(self) -> None: + def test_interval(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.interval` definition.""" - np.testing.assert_array_equal(interval(range(0, 10, 2)), np.array([2])) + xp_assert_equal( + interval(xp.arange(0, 10, 2)), + [2], + ) - np.testing.assert_array_equal( - interval(range(0, 10, 2), False), np.array([2, 2, 2, 2]) + xp_assert_equal( + interval(xp.arange(0, 10, 2), False), + [2, 2, 2, 2], ) - np.testing.assert_allclose( - interval([1, 2, 3, 4, 6, 6.5]), - np.array([0.5, 1.0, 2.0]), + xp_assert_close( + interval(xp_as_array([1.0, 2.0, 3.0, 4.0, 6.0, 6.5], xp=xp)), + [0.5, 1.0, 2.0], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( - interval([1, 2, 3, 4, 6, 6.5], False), - np.array([1.0, 1.0, 1.0, 2.0, 0.5]), + xp_assert_close( + interval(xp_as_array([1.0, 2.0, 3.0, 4.0, 6.0, 6.5], xp=xp), False), + [1.0, 1.0, 1.0, 2.0, 0.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) -class TestIsUniform(unittest.TestCase): +class TestIsUniform: """ Define :func:`colour.utilities.array.is_uniform` definition unit tests methods. """ - def test_is_uniform(self) -> None: + def test_is_uniform(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.is_uniform` definition.""" - assert is_uniform(range(0, 10, 2)) + assert is_uniform(xp.arange(0, 10, 2)) - assert not is_uniform([1, 2, 3, 4, 6]) + assert not is_uniform(xp_as_array([1.0, 2.0, 3.0, 4.0, 6.0], xp=xp)) -class TestInArray(unittest.TestCase): +class TestInArray: """ Define :func:`colour.utilities.array.in_array` definition unit tests methods. """ - def test_in_array(self) -> None: + def test_in_array(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.in_array` definition.""" + b = xp_linspace(0, 10, num=101, xp=xp) + assert np.array_equal( - in_array(np.array([0.50, 0.60]), np.linspace(0, 10, 101)), + as_ndarray(in_array(xp_as_array([0.50, 0.60], xp=xp), b)), # pyright: ignore np.array([True, True]), ) assert not np.array_equal( - in_array(np.array([0.50, 0.61]), np.linspace(0, 10, 101)), + as_ndarray(in_array(xp_as_array([0.50, 0.61], xp=xp), b)), # pyright: ignore np.array([True, True]), ) assert np.array_equal( - in_array(np.array([[0.50], [0.60]]), np.linspace(0, 10, 101)), + as_ndarray(in_array(xp_as_array([[0.50], [0.60]], xp=xp), b)), # pyright: ignore np.array([[True], [True]]), ) - def test_n_dimensional_in_array(self) -> None: + def test_n_dimensional_in_array(self, xp: ModuleType) -> None: """ Test :func:`colour.utilities.array.in_array` definition n-dimensional support. """ - np.testing.assert_array_equal( - in_array(np.array([0.50, 0.60]), np.linspace(0, 10, 101)).shape, - np.array([2]), + b = xp_linspace(0, 10, num=101, xp=xp) + + xp_assert_equal( + in_array(xp_as_array([0.50, 0.60], xp=xp), b).shape, # pyright: ignore + [2], ) - np.testing.assert_array_equal( - in_array(np.array([[0.50, 0.60]]), np.linspace(0, 10, 101)).shape, - np.array([1, 2]), + xp_assert_equal( + in_array(xp_as_array([[0.50, 0.60]], xp=xp), b).shape, # pyright: ignore + [1, 2], ) - np.testing.assert_array_equal( - in_array(np.array([[0.50], [0.60]]), np.linspace(0, 10, 101)).shape, - np.array([2, 1]), + xp_assert_equal( + in_array(xp_as_array([[0.50], [0.60]], xp=xp), b).shape, # pyright: ignore + [2, 1], ) -class TestTstack(unittest.TestCase): +class TestTstack: """ Define :func:`colour.utilities.array.tstack` definition unit tests methods. """ - def test_tstack(self) -> None: + def test_tstack(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.tstack` definition.""" a = 0 - np.testing.assert_array_equal(tstack([a, a, a]), np.array([0, 0, 0])) + xp_assert_equal(tstack([a, a, a]), [0, 0, 0]) - a = np.arange(0, 6) - np.testing.assert_array_equal( + a = xp.arange(0, 6) + xp_assert_equal( tstack([a, a, a]), - np.array( + [ + [0, 0, 0], + [1, 1, 1], + [2, 2, 2], + [3, 3, 3], + [4, 4, 4], + [5, 5, 5], + ], + ) + + a = xp_reshape(xp.arange(0, 6), (1, 6), xp=xp) + xp_assert_equal( + tstack([a, a, a]), + [ [ [0, 0, 0], [1, 1, 1], @@ -1643,37 +3022,18 @@ def test_tstack(self) -> None: [4, 4, 4], [5, 5, 5], ] - ), - ) - - a = np.reshape(a, (1, 6)) - np.testing.assert_array_equal( - tstack([a, a, a]), - np.array( - [ - [ - [0, 0, 0], - [1, 1, 1], - [2, 2, 2], - [3, 3, 3], - [4, 4, 4], - [5, 5, 5], - ] - ] - ), + ], ) - a = np.reshape(a, (1, 2, 3)) - np.testing.assert_array_equal( + a = xp_reshape(xp.arange(0, 6), (1, 2, 3), xp=xp) + xp_assert_equal( tstack([a, a, a]), - np.array( + [ [ - [ - [[0, 0, 0], [1, 1, 1], [2, 2, 2]], - [[3, 3, 3], [4, 4, 4], [5, 5, 5]], - ] + [[0, 0, 0], [1, 1, 1], [2, 2, 2]], + [[3, 3, 3], [4, 4, 4], [5, 5, 5]], ] - ), + ], ) # Ensuring that contiguity is maintained. @@ -1683,39 +3043,37 @@ def test_tstack(self) -> None: # Ensuring that independence is maintained. a *= 2 - np.testing.assert_array_equal( + xp_assert_equal( b, - np.array( - [ - [0, 0, 0], - [1, 1, 1], - [2, 2, 2], - ], - ), + [ + [0, 0, 0], + [1, 1, 1], + [2, 2, 2], + ], ) a = np.array([0, 1, 2], dtype=DTYPE_FLOAT_DEFAULT) b = tstack([a, a, a]) b[1] *= 2 - np.testing.assert_array_equal( + xp_assert_equal( a, - np.array([0, 1, 2]), + [0, 1, 2], ) -class TestTsplit(unittest.TestCase): +class TestTsplit: """ Define :func:`colour.utilities.array.tsplit` definition unit tests methods. """ - def test_tsplit(self) -> None: + def test_tsplit(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.tsplit` definition.""" - a = np.array([0, 0, 0]) - np.testing.assert_array_equal(tsplit(a), np.array([0, 0, 0])) - a = np.array( + a = xp_as_array([0, 0, 0], xp=xp) + xp_assert_equal(tsplit(a), [0, 0, 0]) + a = xp_as_array( [ [0, 0, 0], [1, 1, 1], @@ -1723,20 +3081,19 @@ def test_tsplit(self) -> None: [3, 3, 3], [4, 4, 4], [5, 5, 5], - ] + ], + xp=xp, ) - np.testing.assert_array_equal( + xp_assert_equal( tsplit(a), - np.array( - [ - [0, 1, 2, 3, 4, 5], - [0, 1, 2, 3, 4, 5], - [0, 1, 2, 3, 4, 5], - ] - ), + [ + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5], + ], ) - a = np.array( + a = xp_as_array( [ [ [0, 0, 0], @@ -1746,36 +3103,34 @@ def test_tsplit(self) -> None: [4, 4, 4], [5, 5, 5], ], - ] + ], + xp=xp, ) - np.testing.assert_array_equal( + xp_assert_equal( tsplit(a), - np.array( - [ - [[0, 1, 2, 3, 4, 5]], - [[0, 1, 2, 3, 4, 5]], - [[0, 1, 2, 3, 4, 5]], - ] - ), + [ + [[0, 1, 2, 3, 4, 5]], + [[0, 1, 2, 3, 4, 5]], + [[0, 1, 2, 3, 4, 5]], + ], ) - a = np.array( + a = xp_as_array( [ [ [[0, 0, 0], [1, 1, 1], [2, 2, 2]], [[3, 3, 3], [4, 4, 4], [5, 5, 5]], ] - ] + ], + xp=xp, ) - np.testing.assert_array_equal( + xp_assert_equal( tsplit(a), - np.array( - [ - [[[0, 1, 2], [3, 4, 5]]], - [[[0, 1, 2], [3, 4, 5]]], - [[[0, 1, 2], [3, 4, 5]]], - ] - ), + [ + [[[0, 1, 2], [3, 4, 5]]], + [[[0, 1, 2], [3, 4, 5]]], + [[[0, 1, 2], [3, 4, 5]]], + ], ) # Ensuring that contiguity is maintained. @@ -1792,15 +3147,13 @@ def test_tsplit(self) -> None: # Ensuring that independence is maintained. a *= 2 - np.testing.assert_array_equal( + xp_assert_equal( b, - np.array( - [ - [0, 1, 2], - [0, 1, 2], - [0, 1, 2], - ] - ), + [ + [0, 1, 2], + [0, 1, 2], + [0, 1, 2], + ], ) a = np.array( @@ -1814,203 +3167,191 @@ def test_tsplit(self) -> None: b = tsplit(a) b[1] *= 2 - np.testing.assert_array_equal( + xp_assert_equal( a, - np.array( - [ - [0, 0, 0], - [1, 1, 1], - [2, 2, 2], - ] - ), + [ + [0, 0, 0], + [1, 1, 1], + [2, 2, 2], + ], ) -class TestRowAsDiagonal(unittest.TestCase): +class TestRowAsDiagonal: """ Define :func:`colour.utilities.array.row_as_diagonal` definition unit tests methods. """ - def test_row_as_diagonal(self) -> None: + def test_row_as_diagonal(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.row_as_diagonal` definition.""" - np.testing.assert_allclose( + xp_assert_close( row_as_diagonal( - np.array( + xp_as_array( [ [0.25891593, 0.07299478, 0.36586996], [0.30851087, 0.37131459, 0.16274825], [0.71061831, 0.67718718, 0.09562581], [0.71588836, 0.76772047, 0.15476079], [0.92985142, 0.22263399, 0.88027331], - ] + ], + xp=xp, ) ), - np.array( + [ [ - [ - [0.25891593, 0.00000000, 0.00000000], - [0.00000000, 0.07299478, 0.00000000], - [0.00000000, 0.00000000, 0.36586996], - ], - [ - [0.30851087, 0.00000000, 0.00000000], - [0.00000000, 0.37131459, 0.00000000], - [0.00000000, 0.00000000, 0.16274825], - ], - [ - [0.71061831, 0.00000000, 0.00000000], - [0.00000000, 0.67718718, 0.00000000], - [0.00000000, 0.00000000, 0.09562581], - ], - [ - [0.71588836, 0.00000000, 0.00000000], - [0.00000000, 0.76772047, 0.00000000], - [0.00000000, 0.00000000, 0.15476079], - ], - [ - [0.92985142, 0.00000000, 0.00000000], - [0.00000000, 0.22263399, 0.00000000], - [0.00000000, 0.00000000, 0.88027331], - ], - ] - ), + [0.25891593, 0.00000000, 0.00000000], + [0.00000000, 0.07299478, 0.00000000], + [0.00000000, 0.00000000, 0.36586996], + ], + [ + [0.30851087, 0.00000000, 0.00000000], + [0.00000000, 0.37131459, 0.00000000], + [0.00000000, 0.00000000, 0.16274825], + ], + [ + [0.71061831, 0.00000000, 0.00000000], + [0.00000000, 0.67718718, 0.00000000], + [0.00000000, 0.00000000, 0.09562581], + ], + [ + [0.71588836, 0.00000000, 0.00000000], + [0.00000000, 0.76772047, 0.00000000], + [0.00000000, 0.00000000, 0.15476079], + ], + [ + [0.92985142, 0.00000000, 0.00000000], + [0.00000000, 0.22263399, 0.00000000], + [0.00000000, 0.00000000, 0.88027331], + ], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) -class TestOrient(unittest.TestCase): +class TestOrient: """ Define :func:`colour.utilities.array.orient` definition unit tests methods. """ - def test_orient(self) -> None: + def test_orient(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.orient` definition.""" - a = np.tile(np.arange(5), (5, 1)) + a = xp.tile(xp.arange(5), (5, 1)) - np.testing.assert_array_equal( + xp_assert_equal( orient(a, "Flip"), - np.array( - [ - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - ] - ), + [ + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + ], ) - np.testing.assert_array_equal( + xp_assert_equal( orient(a, "Flop"), - np.array( - [ - [0, 1, 2, 3, 4], - [0, 1, 2, 3, 4], - [0, 1, 2, 3, 4], - [0, 1, 2, 3, 4], - [0, 1, 2, 3, 4], - ] - ), + [ + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + ], ) - np.testing.assert_array_equal( + xp_assert_equal( orient(a, "90 CW"), - np.array( - [ - [0, 0, 0, 0, 0], - [1, 1, 1, 1, 1], - [2, 2, 2, 2, 2], - [3, 3, 3, 3, 3], - [4, 4, 4, 4, 4], - ] - ), + [ + [0, 0, 0, 0, 0], + [1, 1, 1, 1, 1], + [2, 2, 2, 2, 2], + [3, 3, 3, 3, 3], + [4, 4, 4, 4, 4], + ], ) - np.testing.assert_array_equal( + xp_assert_equal( orient(a, "90 CCW"), - np.array( - [ - [4, 4, 4, 4, 4], - [3, 3, 3, 3, 3], - [2, 2, 2, 2, 2], - [1, 1, 1, 1, 1], - [0, 0, 0, 0, 0], - ] - ), + [ + [4, 4, 4, 4, 4], + [3, 3, 3, 3, 3], + [2, 2, 2, 2, 2], + [1, 1, 1, 1, 1], + [0, 0, 0, 0, 0], + ], ) - np.testing.assert_array_equal( + xp_assert_equal( orient(a, "180"), - np.array( - [ - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - [4, 3, 2, 1, 0], - ] - ), + [ + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + [4, 3, 2, 1, 0], + ], ) - np.testing.assert_array_equal(orient(a), a) + xp_assert_equal(orient(a), as_ndarray(a)) -class TestCentroid(unittest.TestCase): +class TestCentroid: """ Define :func:`colour.utilities.array.centroid` definition unit tests methods. """ - def test_centroid(self) -> None: + def test_centroid(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.centroid` definition.""" - a = np.arange(5) - np.testing.assert_array_equal(centroid(a), np.array([3])) + a = xp.arange(5) + xp_assert_equal(centroid(a), [3]) - a = np.tile(a, (5, 1)) - np.testing.assert_array_equal(centroid(a), np.array([2, 3])) + a = xp.tile(xp.arange(5), (5, 1)) + xp_assert_equal(centroid(a), [2, 3]) - a = np.tile(np.linspace(0, 1, 10), (10, 1)) - np.testing.assert_array_equal(centroid(a), np.array([4, 6])) + a = xp.tile(xp_linspace(0, 1, num=10, xp=xp), (10, 1)) + xp_assert_equal(centroid(a), [4, 6]) - a = tstack([a, a, a]) - np.testing.assert_array_equal(centroid(a), np.array([4, 6, 1])) + a_np = np.tile(np.linspace(0, 1, 10), (10, 1)) + a_3d = tstack([a_np, a_np, a_np]) + xp_assert_equal(centroid(a_3d), [4, 6, 1]) -class TestFillNan(unittest.TestCase): +class TestFillNan: """ Define :func:`colour.utilities.array.fill_nan` definition unit tests methods. """ - def test_fill_nan(self) -> None: + def test_fill_nan(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.fill_nan` definition.""" - a = np.array([0.1, 0.2, np.nan, 0.4, 0.5]) - np.testing.assert_allclose( + a = xp_as_array([0.1, 0.2, float("nan"), 0.4, 0.5], xp=xp) + xp_assert_close( fill_nan(a), - np.array([0.1, 0.2, 0.3, 0.4, 0.5]), + [0.1, 0.2, 0.3, 0.4, 0.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( fill_nan(a, method="Constant", default=8.0), - np.array([0.1, 0.2, 8.0, 0.4, 0.5]), + [0.1, 0.2, 8.0, 0.4, 0.5], atol=TOLERANCE_ABSOLUTE_TESTS, ) -class TestHasNanOnly(unittest.TestCase): +class TestHasNanOnly: """ Define :func:`colour.utilities.array.has_only_nan` definition unit tests methods. """ - def test_has_only_nan(self) -> None: + def test_has_only_nan(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.has_only_nan` definition.""" assert has_only_nan(None) # pyright: ignore @@ -2019,10 +3360,10 @@ def test_has_only_nan(self) -> None: assert not has_only_nan([True, None]) # pyright: ignore - assert not has_only_nan([0.1, np.nan, 0.3]) + assert not has_only_nan(xp_as_array([0.1, float("nan"), 0.3], xp=xp)) -class TestNdarrayWrite(unittest.TestCase): +class TestNdarrayWrite: """ Define :func:`colour.utilities.array.ndarray_write` definition unit tests methods. @@ -2041,7 +3382,7 @@ def test_ndarray_write(self) -> None: a += 1 -class TestZeros(unittest.TestCase): +class TestZeros: """ Define :func:`colour.utilities.array.zeros` definition unit tests methods. @@ -2050,10 +3391,10 @@ class TestZeros(unittest.TestCase): def test_zeros(self) -> None: """Test :func:`colour.utilities.array.zeros` definition.""" - np.testing.assert_equal(zeros(3), np.zeros(3)) + xp_assert_equal(zeros(3), np.zeros(3)) -class TestOnes(unittest.TestCase): +class TestOnes: """ Define :func:`colour.utilities.array.ones` definition unit tests methods. @@ -2062,10 +3403,10 @@ class TestOnes(unittest.TestCase): def test_ones(self) -> None: """Test :func:`colour.utilities.array.ones` definition.""" - np.testing.assert_equal(ones(3), np.ones(3)) + xp_assert_equal(ones(3), np.ones(3)) -class TestFull(unittest.TestCase): +class TestFull: """ Define :func:`colour.utilities.array.full` definition unit tests methods. @@ -2074,18 +3415,18 @@ class TestFull(unittest.TestCase): def test_full(self) -> None: """Test :func:`colour.utilities.array.full` definition.""" - np.testing.assert_equal(full(3, 0.5), np.full(3, 0.5)) + xp_assert_equal(full(3, 0.5), np.full(3, 0.5)) -class TestIndexAlongLastAxis(unittest.TestCase): +class TestIndexAlongLastAxis: """ Define :func:`colour.utilities.array.index_along_last_axis` definition unit tests methods. """ - def test_index_along_last_axis(self) -> None: + def test_index_along_last_axis(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.array.index_along_last_axis` definition.""" - a = np.array( + a = xp_as_array( [ [ [ @@ -2117,36 +3458,43 @@ def test_index_along_last_axis(self) -> None: [0.90644279, 0.09689787, 0.93483977], ], ], - ] + ], + xp=xp, ) - indexes = np.array([[[0, 1], [0, 1]], [[2, 1], [2, 1]], [[2, 1], [2, 0]]]) + indexes = xp_as_array( + [[[0, 1], [0, 1]], [[2, 1], [2, 1]], [[2, 1], [2, 0]]], xp=xp + ) - np.testing.assert_equal( + xp_assert_close( index_along_last_axis(a, indexes), - np.array( - [ - [[0.51090627, 0.80587656], [0.84085977, 0.79308353]], - [[0.20199051, 0.84189245], [0.01612045, 0.58905552]], - [[0.0716432, 0.0367514], [0.9771182, 0.90644279]], - ] - ), + [ + [[0.51090627, 0.80587656], [0.84085977, 0.79308353]], + [[0.20199051, 0.84189245], [0.01612045, 0.58905552]], + [[0.0716432, 0.0367514], [0.9771182, 0.90644279]], + ], + atol=TOLERANCE_ABSOLUTE_TESTS, ) - def test_compare_with_argmin_argmax(self) -> None: + def test_compare_with_argmin_argmax(self, xp: ModuleType) -> None: """ Test :func:`colour.utilities.array.index_along_last_axis` definition by comparison with :func:`argmin` and :func:`argmax`. """ - a = np.random.random((2, 3, 4, 5, 6, 7)) + a_np = np.random.random((2, 3, 4, 5, 6, 7)).astype(DTYPE_FLOAT_DEFAULT) + a = xp_as_array(a_np, xp=xp) - np.testing.assert_equal( - index_along_last_axis(a, np.argmin(a, axis=-1)), np.min(a, axis=-1) + xp_assert_close( + index_along_last_axis(a, xp.argmin(a, axis=-1)), + np.min(as_ndarray(a), axis=-1), + atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_equal( - index_along_last_axis(a, np.argmax(a, axis=-1)), np.max(a, axis=-1) + xp_assert_close( + index_along_last_axis(a, xp.argmax(a, axis=-1)), + np.max(as_ndarray(a), axis=-1), + atol=TOLERANCE_ABSOLUTE_TESTS, ) def test_exceptions(self) -> None: @@ -2167,13 +3515,12 @@ def test_exceptions(self) -> None: indexes = np.array([123, 456]) index_along_last_axis(a, indexes) - # Non-int indexes - with pytest.raises(IndexError): - indexes = np.array([0.0, 0.0]) - index_along_last_axis(a, indexes) + # Float indexes are now converted to int by as_int_array. + indexes = np.array([0.0, 0.0]) + index_along_last_axis(a, indexes) -class TestFormatArrayAsRow(unittest.TestCase): +class TestFormatArrayAsRow: """ Define :func:`colour.utilities.array.format_array_as_row` definition unit tests methods. @@ -2187,3 +3534,66 @@ def test_format_array_as_row(self) -> None: assert format_array_as_row([1.25, 2.5, 3.75], 3) == "1.250 2.500 3.750" assert format_array_as_row([1.25, 2.5, 3.75], 3, ", ") == "1.250, 2.500, 3.750" + + +class TestAsArrayArrayApi: + """Define :func:`colour.utilities.as_array` Array API dispatch tests.""" + + def test_as_array(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.as_array` definition.""" + + a = xp_as_array([1, 2, 3], xp=xp) + + with array_api_enable(False): + result = as_array(a) + assert isinstance(result, np.ndarray) + + with array_api_enable(True): + result = as_array(a) + assert array_namespace(result) is array_namespace(a) + + result = as_float_array(a) + assert array_namespace(result) is array_namespace(a) + + +class TestTstackArrayApi: + """Define :func:`colour.utilities.tstack` Array API dispatch tests.""" + + def test_tstack(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.tstack` definition.""" + + a = xp_as_array(np.arange(6, dtype=float), xp=xp) + + with array_api_enable(False): + result = tstack([a, a, a]) + assert result.shape == (6, 3) + + with array_api_enable(True): + result = tstack([a, a, a]) + assert result.shape == (6, 3) + assert array_namespace(result) is array_namespace(a) + + +class TestTsplitArrayApi: + """Define :func:`colour.utilities.tsplit` Array API dispatch tests.""" + + def test_tsplit(self, xp: ModuleType) -> None: + """Test :func:`colour.utilities.tsplit` definition.""" + + a = xp_as_array(np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), xp=xp) + + with array_api_enable(False): + result = tsplit(a) + assert result.shape == (3, 2) + + with array_api_enable(True): + result = tsplit(a) + assert result.shape == (3, 2) + assert array_namespace(result) is array_namespace(a) + + a = xp_as_array(np.arange(6, dtype=float), xp=xp) + stacked = tstack([a, a, a]) + split = tsplit(stacked) + xp_assert_equal(split[0], a) + xp_assert_equal(split[1], a) + xp_assert_equal(split[2], a) diff --git a/colour/utilities/tests/test_common.py b/colour/utilities/tests/test_common.py index 52caf43c79..150e024bd5 100644 --- a/colour/utilities/tests/test_common.py +++ b/colour/utilities/tests/test_common.py @@ -14,7 +14,7 @@ import pytest if typing.TYPE_CHECKING: - from colour.hints import Any, Real, Tuple + from colour.hints import Any, Tuple from colour.utilities import ( CacheRegistry, @@ -34,9 +34,8 @@ is_iterable, is_numeric, is_sibling, - multiprocessing_pool, optional, - set_caching_enable, + set_caching_enabled, slugify, validate_method, ) @@ -56,7 +55,6 @@ "TestIgnorePythonWarnings", "TestAttest", "TestBatch", - "TestMultiprocessingPool", "TestIsIterable", "TestIsNumeric", "TestIsInteger", @@ -88,19 +86,19 @@ def test_is_caching_enabled(self) -> None: class TestSetCachingEnabled: """ - Define :func:`colour.utilities.common.set_caching_enable` definition unit + Define :func:`colour.utilities.common.set_caching_enabled` definition unit tests methods. """ - def test_set_caching_enable(self) -> None: - """Test :func:`colour.utilities.common.set_caching_enable` definition.""" + def test_set_caching_enabled(self) -> None: + """Test :func:`colour.utilities.common.set_caching_enabled` definition.""" with caching_enable(is_caching_enabled()): - set_caching_enable(True) + set_caching_enabled(True) assert is_caching_enabled() with caching_enable(is_caching_enabled()): - set_caching_enable(False) + set_caching_enabled(False) assert not is_caching_enabled() @@ -265,7 +263,8 @@ def test_attest(self) -> None: assert attest(True, "") is None - pytest.raises(AssertionError, attest, False) + with pytest.raises(AssertionError): + attest(False) class TestBatch: @@ -300,55 +299,6 @@ def test_batch(self) -> None: ] -def _add(a: Real, b: Real) -> Real: - """ - Add two numbers. - - This definition is intended to be used with a multiprocessing pool for unit - testing. - - Parameters - ---------- - a - Variable :math:`a`. - b - Variable :math:`b`. - - Returns - ------- - numeric - Addition result. - """ - - # NOTE: No coverage information is available as this code is executed in - # sub-processes. - return a + b # pragma: no cover - - -class TestMultiprocessingPool: - """ - Define :func:`colour.utilities.common.multiprocessing_pool` definition - unit tests methods. - """ - - def test_multiprocessing_pool(self) -> None: - """Test :func:`colour.utilities.common.multiprocessing_pool` definition.""" - - with multiprocessing_pool() as pool: - assert pool.map(partial(_add, b=2), range(10)) == [ - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - ] - - class TestIsIterable: """ Define :func:`colour.utilities.common.is_iterable` definition unit tests @@ -560,7 +510,8 @@ def test_raise_exception_validate_method(self) -> None: exception. """ - pytest.raises(ValueError, validate_method, "Invalid", ("Valid", "Yes", "Ok")) + with pytest.raises(ValueError): + validate_method("Invalid", ("Valid", "Yes", "Ok")) class TestOptional: @@ -701,16 +652,15 @@ def test_download_url(self) -> None: ) # Wrong SHA-256 triggers re-download and raises. - pytest.raises( - ValueError, - download_url, - "https://huggingface.co/colour-science/" - "learning-munsell/resolve/main/" - "models/to_xyY/" - "multi_mlp_normalization_parameters.npz", - target, - "0" * 64, - 1, - ) + with pytest.raises(ValueError): + download_url( + "https://huggingface.co/colour-science/" + "learning-munsell/resolve/main/" + "models/to_xyY/" + "multi_mlp_normalization_parameters.npz", + target, + "0" * 64, + 1, + ) finally: shutil.rmtree(os.path.dirname(target)) diff --git a/colour/utilities/tests/test_deprecation.py b/colour/utilities/tests/test_deprecation.py index 44625148b0..0e6ddc8e84 100644 --- a/colour/utilities/tests/test_deprecation.py +++ b/colour/utilities/tests/test_deprecation.py @@ -325,7 +325,8 @@ def assert_warns() -> None: colour.utilities.tests.test_deprecated.OLD_NAME # noqa: B018 # pyright: ignore - pytest.warns(ColourUsageWarning, assert_warns) + with pytest.warns(ColourUsageWarning): + assert_warns() del sys.modules["colour.utilities.tests.test_deprecated"] @@ -337,12 +338,8 @@ def test_raise_exception__getattr__(self) -> None: import colour.utilities.tests.test_deprecated # noqa: PLC0415 - pytest.raises( - AttributeError, - getattr, - colour.utilities.tests.test_deprecated, - "REMOVED", - ) + with pytest.raises(AttributeError): + _ = colour.utilities.tests.test_deprecated.REMOVED # pyright: ignore del sys.modules["colour.utilities.tests.test_deprecated"] @@ -364,7 +361,7 @@ def test_get_attribute(self) -> None: assert get_attribute("colour.models.eotf_inverse_sRGB") is eotf_inverse_sRGB - from colour.utilities.array import as_float # noqa: PLC0415 + from colour.utilities import as_float # noqa: PLC0415 assert get_attribute("colour.utilities.array.as_float") is as_float diff --git a/colour/utilities/tests/test_metrics.py b/colour/utilities/tests/test_metrics.py index 48ac77ee60..8aebfd2e9d 100644 --- a/colour/utilities/tests/test_metrics.py +++ b/colour/utilities/tests/test_metrics.py @@ -2,10 +2,20 @@ from __future__ import annotations -import numpy as np +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import metric_mse, metric_psnr +from colour.utilities import ( + as_ndarray, + metric_mse, + metric_psnr, + xp_as_array, + xp_assert_close, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2013 Colour Developers" @@ -26,21 +36,21 @@ class TestMetricMse: methods. """ - def test_metric_mse(self) -> None: + def test_metric_mse(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.metrics.metric_mse` definition.""" - a = np.array([0.48222001, 0.31654775, 0.22070353]) - assert metric_mse(a, a) == 0 + a = xp_as_array([0.48222001, 0.31654775, 0.22070353], xp=xp) + assert as_ndarray(metric_mse(a, a)) == 0 b = a * 0.9 - np.testing.assert_allclose( + xp_assert_close( metric_mse(a, b), 0.0012714955474297446, atol=TOLERANCE_ABSOLUTE_TESTS, ) b = a * 1.1 - np.testing.assert_allclose( + xp_assert_close( metric_mse(a, b), 0.0012714955474297446, atol=TOLERANCE_ABSOLUTE_TESTS, @@ -53,21 +63,21 @@ class TestMetricPsnr: methods. """ - def test_metric_psnr(self) -> None: + def test_metric_psnr(self, xp: ModuleType) -> None: """Test :func:`colour.utilities.metrics.metric_psnr` definition.""" - a = np.array([0.48222001, 0.31654775, 0.22070353]) - assert metric_psnr(a, a) == 0 + a = xp_as_array([0.48222001, 0.31654775, 0.22070353], xp=xp) + assert as_ndarray(metric_psnr(a, a)) == 0 b = a * 0.9 - np.testing.assert_allclose( + xp_assert_close( metric_psnr(a, b), 28.956851563141299, atol=TOLERANCE_ABSOLUTE_TESTS, ) b = a * 1.1 - np.testing.assert_allclose( + xp_assert_close( metric_psnr(a, b), 28.956851563141296, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/utilities/tests/test_requirements.py b/colour/utilities/tests/test_requirements.py new file mode 100644 index 0000000000..c065d6a503 --- /dev/null +++ b/colour/utilities/tests/test_requirements.py @@ -0,0 +1,67 @@ +"""Define the unit tests for the :mod:`colour.utilities.requirements` module.""" + +from __future__ import annotations + +import sys +from unittest import mock + +import pytest + +from colour.utilities import ( + is_array_api_compat_installed, + is_array_api_extra_installed, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "TestIsArrayApiCompatInstalled", + "TestIsArrayApiExtraInstalled", +] + + +class TestIsArrayApiCompatInstalled: + """ + Define :func:`colour.utilities.is_array_api_compat_installed` definition + unit tests methods. + """ + + def test_is_array_api_compat_installed(self) -> None: + """ + Test :func:`colour.utilities.is_array_api_compat_installed` + definition. + """ + + assert is_array_api_compat_installed() + + with mock.patch.dict(sys.modules, {"array_api_compat": None}): + assert not is_array_api_compat_installed() + + with pytest.raises(ImportError): + is_array_api_compat_installed(raise_exception=True) + + +class TestIsArrayApiExtraInstalled: + """ + Define :func:`colour.utilities.is_array_api_extra_installed` definition + unit tests methods. + """ + + def test_is_array_api_extra_installed(self) -> None: + """ + Test :func:`colour.utilities.is_array_api_extra_installed` + definition. + """ + + assert is_array_api_extra_installed() + + with mock.patch.dict(sys.modules, {"array_api_extra": None}): + assert not is_array_api_extra_installed() + + with pytest.raises(ImportError): + is_array_api_extra_installed(raise_exception=True) diff --git a/colour/utilities/tests/test_structures.py b/colour/utilities/tests/test_structures.py index 34baa0796f..8083eab555 100644 --- a/colour/utilities/tests/test_structures.py +++ b/colour/utilities/tests/test_structures.py @@ -137,7 +137,8 @@ def test_raise_exception_first_key_from_value(self) -> None: Lookup.first_key_from_value` method raised exception. """ - pytest.raises(IndexError, Lookup().first_key_from_value, "John") + with pytest.raises(IndexError): + Lookup().first_key_from_value("John") class TestCanonicalMapping: @@ -344,12 +345,10 @@ def test_raise_exception__eq__(self) -> None: method raised exception. """ - pytest.raises( - TypeError, - operator.eq, - CanonicalMapping(John="Doe", Jane="Doe"), - ["John", "Doe", "Jane", "Doe"], - ) + with pytest.raises(TypeError): + operator.eq( + CanonicalMapping(John="Doe", Jane="Doe"), ["John", "Doe", "Jane", "Doe"] + ) def test__ne__(self) -> None: """ @@ -368,12 +367,10 @@ def test_raise_exception__ne__(self) -> None: method raised exception. """ - pytest.raises( - TypeError, - operator.ne, - CanonicalMapping(John="Doe", Jane="Doe"), - ["John", "Doe", "Jane", "Doe"], - ) + with pytest.raises(TypeError): + operator.ne( + CanonicalMapping(John="Doe", Jane="Doe"), ["John", "Doe", "Jane", "Doe"] + ) def test_copy(self) -> None: """ @@ -400,7 +397,8 @@ def test_lower_keys(self) -> None: mapping = CanonicalMapping(John="Doe", john="Doe") - pytest.warns(ColourUsageWarning, lambda: list(mapping.lower_keys())) + with pytest.warns(ColourUsageWarning): + list(mapping.lower_keys()) def test_lower_items(self) -> None: """ @@ -430,7 +428,8 @@ def test_slugified_keys(self) -> None: mapping = CanonicalMapping({"McCamy 1992": 1, "McCamy-1992": 2}) - pytest.warns(ColourUsageWarning, lambda: list(mapping.slugified_keys())) + with pytest.warns(ColourUsageWarning): + list(mapping.slugified_keys()) def test_slugified_items(self) -> None: """ @@ -459,7 +458,8 @@ def test_canonical_keys(self) -> None: mapping = CanonicalMapping({"McCamy_1992": 1, "McCamy-1992": 2}) - pytest.warns(ColourUsageWarning, lambda: list(mapping.canonical_keys())) + with pytest.warns(ColourUsageWarning): + list(mapping.canonical_keys()) def test_canonical_items(self) -> None: """ diff --git a/colour/utilities/tests/test_verbose.py b/colour/utilities/tests/test_verbose.py index dc050a6c42..dac1ba023e 100644 --- a/colour/utilities/tests/test_verbose.py +++ b/colour/utilities/tests/test_verbose.py @@ -247,7 +247,7 @@ def __str__(self) -> str: { "name": "_c", "label": 'List "c"', - "formatter": lambda x: "; ".join(x), + "formatter": "; ".join, }, ], ) @@ -301,9 +301,9 @@ def __repr__(self) -> str: {"name": "_b"}, { "name": "_c", - "formatter": lambda x: repr(x) - .replace("[", "(") - .replace("]", ")"), + "formatter": lambda x: ( + repr(x).replace("[", "(").replace("]", ")") + ), }, { "name": "_d", diff --git a/colour/utilities/verbose.py b/colour/utilities/verbose.py index 79f4bd0a69..9daa7d32f2 100644 --- a/colour/utilities/verbose.py +++ b/colour/utilities/verbose.py @@ -523,8 +523,8 @@ def as_bool(a: str) -> bool: os.environ.get("COLOUR_SCIENCE__FILTER_COLOUR_WARNINGS") is not None ): # pragma: no cover filter_warnings( - colour_usage_warnings=as_bool( - os.environ["COLOUR_SCIENCE__FILTER_WARNINGS"], + colour_warnings=as_bool( + os.environ["COLOUR_SCIENCE__FILTER_COLOUR_WARNINGS"], ) ) @@ -532,9 +532,7 @@ def as_bool(a: str) -> bool: os.environ.get("COLOUR_SCIENCE__FILTER_PYTHON_WARNINGS") is not None ): # pragma: no cover filter_warnings( - colour_usage_warnings=as_bool( - os.environ["COLOUR_SCIENCE__FILTER_PYTHON_WARNINGS"] - ) + python_warnings=as_bool(os.environ["COLOUR_SCIENCE__FILTER_PYTHON_WARNINGS"]) ) @@ -1142,9 +1140,9 @@ def multiline_repr( ... {"name": "_b"}, ... { ... "name": "_c", - ... "formatter": lambda x: repr(x) - ... .replace("[", "(") - ... .replace("]", ")"), + ... "formatter": lambda x: ( + ... repr(x).replace("[", "(").replace("]", ")") + ... ), ... }, ... ], ... ) diff --git a/colour/volume/macadam_limits.py b/colour/volume/macadam_limits.py index ebff7bbb5c..25fa57e8e4 100644 --- a/colour/volume/macadam_limits.py +++ b/colour/volume/macadam_limits.py @@ -9,8 +9,6 @@ import typing -import numpy as np - from colour.constants import EPSILON if typing.TYPE_CHECKING: @@ -19,6 +17,8 @@ from colour.models import xyY_to_XYZ from colour.utilities import ( CACHE_REGISTRY, + array_namespace, + as_ndarray, is_caching_enabled, required, validate_method, @@ -118,6 +118,7 @@ def is_within_macadam_limits( Examples -------- + >>> import numpy as np >>> is_within_macadam_limits(np.array([0.3205, 0.4131, 0.51]), "A") array(True) >>> a = np.array([[0.3205, 0.4131, 0.51], [0.0005, 0.0031, 0.001]]) @@ -135,6 +136,10 @@ def is_within_macadam_limits( Delaunay(optimal_colour_stimuli) ) - simplex = triangulation.find_simplex(xyY_to_XYZ(xyY), tol=tolerance) + XYZ = xyY_to_XYZ(xyY) + + xp = array_namespace(XYZ) + + simplex = triangulation.find_simplex(as_ndarray(XYZ), tol=tolerance) - return np.where(simplex >= 0, True, False) + return xp.asarray(simplex >= 0) diff --git a/colour/volume/mesh.py b/colour/volume/mesh.py index 31ce5d0a2b..fbc03519df 100644 --- a/colour/volume/mesh.py +++ b/colour/volume/mesh.py @@ -11,10 +11,15 @@ import typing -import numpy as np - from colour.constants import EPSILON -from colour.utilities import as_float_array, required +from colour.utilities import ( + CACHE_REGISTRY, + array_namespace, + as_ndarray, + int_digest, + required, + xp_as_array, +) if typing.TYPE_CHECKING: from colour.hints import ArrayLike, NDArrayFloat @@ -30,6 +35,8 @@ "is_within_mesh_volume", ] +_CACHE_DELAUNAY: dict = CACHE_REGISTRY.register_cache(f"{__name__}._CACHE_DELAUNAY") + @required("SciPy") def is_within_mesh_volume( @@ -56,6 +63,7 @@ def is_within_mesh_volume( Examples -------- + >>> import numpy as np >>> mesh = np.array( ... [ ... [-1.0, -1.0, 1.0], @@ -74,8 +82,18 @@ def is_within_mesh_volume( from scipy.spatial import Delaunay # noqa: PLC0415 - triangulation = Delaunay(as_float_array(mesh)) + xp = array_namespace(points) + + mesh_np = as_ndarray(mesh) + # ``Delaunay`` triangulation is a *scipy* host-only operation so the + # mesh is materialised to *NumPy* and content-hashed for the cache; + # ``id(mesh)`` would thrash across array copies that share content + # and could be reused after garbage collection. + cache_key = (int_digest(mesh_np.tobytes()), mesh_np.shape, mesh_np.dtype.str) + triangulation = _CACHE_DELAUNAY.get(cache_key) + if triangulation is None: + triangulation = _CACHE_DELAUNAY[cache_key] = Delaunay(mesh_np) - simplex = triangulation.find_simplex(as_float_array(points), tol=tolerance) + simplex = triangulation.find_simplex(as_ndarray(points), tol=tolerance) - return np.where(simplex >= 0, True, False) + return xp_as_array(simplex >= 0, xp=xp, like=points) diff --git a/colour/volume/rgb.py b/colour/volume/rgb.py index 7c672ee91b..030c46dfdc 100644 --- a/colour/volume/rgb.py +++ b/colour/volume/rgb.py @@ -37,7 +37,10 @@ XYZ_to_Lab, XYZ_to_RGB, ) -from colour.utilities import as_float_array, multiprocessing_pool +from colour.utilities import ( + array_namespace, + as_float_array, +) from colour.volume import is_within_pointer_gamut, is_within_visible_spectrum __author__ = "Colour Developers" @@ -57,26 +60,6 @@ ] -def _wrapper_RGB_colourspace_volume_MonteCarlo(arguments: tuple) -> int: - """ - Wrap the - :func:`colour.volume.rgb.sample_RGB_colourspace_volume_MonteCarlo` - function for parallel processing with multiple arguments. - - Parameters - ---------- - arguments - Arguments to pass to the wrapped function. - - Returns - ------- - :class:`int` - Inside *RGB* colourspace volume sample count. - """ - - return sample_RGB_colourspace_volume_MonteCarlo(*arguments) - - def sample_RGB_colourspace_volume_MonteCarlo( colourspace: RGB_Colourspace, samples: int = 1000000, @@ -147,7 +130,10 @@ def sample_RGB_colourspace_volume_MonteCarlo( illuminant_Lab, chromatic_adaptation_transform, ) - RGB_w = RGB[np.logical_and(np.min(RGB, axis=-1) >= 0, np.max(RGB, axis=-1) <= 1)] + + xp = array_namespace(RGB) + + RGB_w = RGB[xp.logical_and(xp.min(RGB, axis=-1) >= 0, xp.max(RGB, axis=-1) <= 1)] return len(RGB_w) @@ -184,19 +170,15 @@ def RGB_colourspace_limits(colourspace: RGB_Colourspace) -> NDArrayFloat: [-107.8503557..., 94.4894974...]]) """ - Lab = np.array( - [ - XYZ_to_Lab( - RGB_to_XYZ(combination, colourspace), - colourspace.whitepoint, - ) - for combination in list(itertools.product([0, 1], repeat=3)) - ] + corners = as_float_array(list(itertools.product([0, 1], repeat=3))) + Lab = XYZ_to_Lab( + RGB_to_XYZ(corners, colourspace), + colourspace.whitepoint, ) - limits = [(np.min(Lab[..., i]), np.max(Lab[..., i])) for i in np.arange(3)] + xp = array_namespace(Lab) - return np.array(limits) + return xp.stack([xp.min(Lab, axis=0), xp.max(Lab, axis=0)], axis=-1) def RGB_colourspace_volume_MonteCarlo( @@ -255,23 +237,17 @@ def RGB_colourspace_volume_MonteCarlo( Examples -------- >>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB - >>> from colour.utilities import disable_multiprocessing >>> prng = np.random.RandomState(2) - >>> with disable_multiprocessing(): - ... RGB_colourspace_volume_MonteCarlo(sRGB, 10e3, random_state=prng) + >>> RGB_colourspace_volume_MonteCarlo(sRGB, 10e3, random_state=prng) ... # doctest: +SKIP ... 8... """ - import multiprocessing # noqa: PLC0415 - - processes = multiprocessing.cpu_count() - process_samples = DTYPE_INT_DEFAULT(np.round(samples / processes)) - - arguments = ( + samples = int(DTYPE_INT_DEFAULT(samples)) + within = sample_RGB_colourspace_volume_MonteCarlo( colourspace, - process_samples, + samples, limits, illuminant_Lab, chromatic_adaptation_transform, @@ -279,15 +255,13 @@ def RGB_colourspace_volume_MonteCarlo( random_state, ) - with multiprocessing_pool() as pool: - results = pool.map( - _wrapper_RGB_colourspace_volume_MonteCarlo, - [arguments for _ in range(processes)], - ) + limits = as_float_array(limits) - Lab_volume = np.prod([np.sum(np.abs(x)) for x in as_float_array(limits)]) + xp = array_namespace(limits) - return Lab_volume * np.sum(results) / (process_samples * processes) + Lab_volume = xp.prod(xp.sum(xp.abs(limits), axis=-1)) + + return float(Lab_volume * within / samples) def RGB_colourspace_volume_coverage_MonteCarlo( @@ -338,7 +312,9 @@ def RGB_colourspace_volume_coverage_MonteCarlo( RGB = XYZ_to_RGB(XYZ_vs, colourspace) - RGB_c = RGB[np.logical_and(np.min(RGB, axis=-1) >= 0, np.max(RGB, axis=-1) <= 1)] + xp = array_namespace(RGB) + + RGB_c = RGB[xp.logical_and(xp.min(RGB, axis=-1) >= 0, xp.max(RGB, axis=-1) <= 1)] return 100 * RGB_c.size / XYZ_vs.size diff --git a/colour/volume/spectrum.py b/colour/volume/spectrum.py index aa2b45d712..62bf835f0a 100644 --- a/colour/volume/spectrum.py +++ b/colour/volume/spectrum.py @@ -24,8 +24,6 @@ import typing -import numpy as np - from colour.colorimetry import ( MultiSpectralDistributions, SpectralDistribution, @@ -43,7 +41,13 @@ NDArrayFloat, ) -from colour.utilities import CACHE_REGISTRY, is_caching_enabled, validate_method, zeros +from colour.utilities import ( + CACHE_REGISTRY, + array_namespace, + is_caching_enabled, + validate_method, + zeros, +) from colour.volume import is_within_mesh_volume __author__ = "Colour Developers" @@ -221,28 +225,34 @@ def generate_pulse_waves( ) square_waves = [] - square_waves_basis = np.tril(np.ones((bins, bins), dtype=DTYPE_FLOAT_DEFAULT))[ + + xp = array_namespace() + + square_waves_basis = xp.tril(xp.ones((bins, bins), dtype=DTYPE_FLOAT_DEFAULT))[ 0:-1, : ] if pulse_order.lower() == "bins": for square_wave_basis in square_waves_basis: for i in range(bins): - square_waves.append(np.roll(square_wave_basis, i)) # noqa: PERF401 + square_waves.append(xp.roll(square_wave_basis, i)) # noqa: PERF401 else: for i in range(bins): for j, square_wave_basis in enumerate(square_waves_basis): - square_waves.append(np.roll(square_wave_basis, i - j // 2)) + square_waves.append(xp.roll(square_wave_basis, i - j // 2)) if filter_jagged_pulses: square_waves = square_waves[::2] - return np.vstack( + square_waves_array = xp.stack(square_waves) + + return xp.concat( [ - zeros(bins), - np.vstack(square_waves), - np.ones(bins, dtype=DTYPE_FLOAT_DEFAULT), - ] + zeros((1, bins)), + square_waves_array, + xp.ones((1, bins), dtype=DTYPE_FLOAT_DEFAULT), + ], + axis=0, ) diff --git a/colour/volume/tests/test_macadam_limits.py b/colour/volume/tests/test_macadam_limits.py index c77076b077..bb0e58ed55 100644 --- a/colour/volume/tests/test_macadam_limits.py +++ b/colour/volume/tests/test_macadam_limits.py @@ -2,11 +2,24 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) from colour.volume import is_within_macadam_limits __author__ = "Colour Developers" @@ -27,21 +40,29 @@ class TestIsWithinMacadamLimits: definition unit tests methods. """ - def test_is_within_macadam_limits(self) -> None: + def test_is_within_macadam_limits(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.macadam_limits.is_within_macadam_limits` definition. """ - assert is_within_macadam_limits(np.array([0.3205, 0.4131, 0.5100]), "A") + assert is_within_macadam_limits( + xp_as_array([0.3205, 0.4131, 0.5100], xp=xp), "A" + ) - assert not is_within_macadam_limits(np.array([0.0005, 0.0031, 0.0010]), "A") + assert not is_within_macadam_limits( + xp_as_array([0.0005, 0.0031, 0.0010], xp=xp), "A" + ) - assert is_within_macadam_limits(np.array([0.4325, 0.3788, 0.1034]), "C") + assert is_within_macadam_limits( + xp_as_array([0.4325, 0.3788, 0.1034], xp=xp), "C" + ) - assert not is_within_macadam_limits(np.array([0.0025, 0.0088, 0.0340]), "C") + assert not is_within_macadam_limits( + xp_as_array([0.0025, 0.0088, 0.0340], xp=xp), "C" + ) - def test_n_dimensional_is_within_macadam_limits(self) -> None: + def test_n_dimensional_is_within_macadam_limits(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.macadam_limits.is_within_macadam_limits` definition n-dimensional arrays support. @@ -50,16 +71,24 @@ def test_n_dimensional_is_within_macadam_limits(self) -> None: if not is_scipy_installed(): # pragma: no cover return - a = np.array([0.3205, 0.4131, 0.5100]) - b = is_within_macadam_limits(a, "A") - - a = np.tile(a, (6, 1)) - b = np.tile(b, 6) - np.testing.assert_allclose(is_within_macadam_limits(a, "A"), b) - - a = np.reshape(a, (2, 3, 3)) - b = np.reshape(b, (2, 3)) - np.testing.assert_allclose(is_within_macadam_limits(a, "A"), b) + a = xp_as_array([0.3205, 0.4131, 0.5100], xp=xp) + b = as_ndarray(is_within_macadam_limits(a, "A")) + + a = xp.tile(xp_as_array(a, xp=xp), (6, 1)) + b = xp.tile(xp_as_array(b, xp=xp), (6,)) + xp_assert_close( + is_within_macadam_limits(a, "A"), + b, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 3), xp=xp) + b = xp_reshape(xp_as_array(b, xp=xp), (2, 3), xp=xp) + xp_assert_close( + is_within_macadam_limits(a, "A"), + b, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) @ignore_numpy_errors def test_nan_is_within_macadam_limits(self) -> None: diff --git a/colour/volume/tests/test_mesh.py b/colour/volume/tests/test_mesh.py index ee006a3963..be8c863c56 100644 --- a/colour/volume/tests/test_mesh.py +++ b/colour/volume/tests/test_mesh.py @@ -2,12 +2,24 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) from colour.volume import is_within_mesh_volume __author__ = "Colour Developers" @@ -41,21 +53,29 @@ def setup_method(self) -> None: ] ) - def test_is_within_mesh_volume(self) -> None: + def test_is_within_mesh_volume(self, xp: ModuleType) -> None: """Test :func:`colour.volume.mesh.is_within_mesh_volume` definition.""" if not is_scipy_installed(): # pragma: no cover return - assert is_within_mesh_volume(np.array([0.0005, 0.0031, 0.0010]), self._mesh) + assert is_within_mesh_volume( + xp_as_array([0.0005, 0.0031, 0.0010], xp=xp), self._mesh + ) - assert not is_within_mesh_volume(np.array([0.3205, 0.4131, 0.5100]), self._mesh) + assert not is_within_mesh_volume( + xp_as_array([0.3205, 0.4131, 0.5100], xp=xp), self._mesh + ) - assert is_within_mesh_volume(np.array([0.0025, 0.0088, 0.0340]), self._mesh) + assert is_within_mesh_volume( + xp_as_array([0.0025, 0.0088, 0.0340], xp=xp), self._mesh + ) - assert not is_within_mesh_volume(np.array([0.4325, 0.3788, 0.1034]), self._mesh) + assert not is_within_mesh_volume( + xp_as_array([0.4325, 0.3788, 0.1034], xp=xp), self._mesh + ) - def test_n_dimensional_is_within_mesh_volume(self) -> None: + def test_n_dimensional_is_within_mesh_volume(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.mesh.is_within_mesh_volume` definition n-dimensional arrays support. @@ -64,20 +84,20 @@ def test_n_dimensional_is_within_mesh_volume(self) -> None: if not is_scipy_installed(): # pragma: no cover return - a = np.array([0.0005, 0.0031, 0.0010]) - b = is_within_mesh_volume(a, self._mesh) + a = xp_as_array([0.0005, 0.0031, 0.0010], xp=xp) + b = as_ndarray(is_within_mesh_volume(a, self._mesh)) - a = np.tile(a, (6, 1)) - b = np.tile(b, 6) - np.testing.assert_allclose( + a = xp.tile(xp_as_array(a, xp=xp), (6, 1)) + b = xp.tile(xp_as_array(b, xp=xp), (6,)) + xp_assert_close( is_within_mesh_volume(a, self._mesh), b, atol=TOLERANCE_ABSOLUTE_TESTS, ) - a = np.reshape(a, (2, 3, 3)) - b = np.reshape(b, (2, 3)) - np.testing.assert_allclose( + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 3), xp=xp) + b = xp_reshape(xp_as_array(b, xp=xp), (2, 3), xp=xp) + xp_assert_close( is_within_mesh_volume(a, self._mesh), b, atol=TOLERANCE_ABSOLUTE_TESTS, diff --git a/colour/volume/tests/test_pointer_gamut.py b/colour/volume/tests/test_pointer_gamut.py index 416061cc36..bccc41e438 100644 --- a/colour/volume/tests/test_pointer_gamut.py +++ b/colour/volume/tests/test_pointer_gamut.py @@ -2,11 +2,24 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_reshape, +) from colour.volume import is_within_pointer_gamut __author__ = "Colour Developers" @@ -27,21 +40,21 @@ class TestIsWithinPointerGamut: definition unit tests methods. """ - def test_is_within_pointer_gamut(self) -> None: + def test_is_within_pointer_gamut(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.pointer_gamut.is_within_pointer_gamut` definition. """ - assert is_within_pointer_gamut(np.array([0.3205, 0.4131, 0.5100])) + assert is_within_pointer_gamut(xp_as_array([0.3205, 0.4131, 0.5100], xp=xp)) - assert not is_within_pointer_gamut(np.array([0.0005, 0.0031, 0.0010])) + assert not is_within_pointer_gamut(xp_as_array([0.0005, 0.0031, 0.0010], xp=xp)) - assert is_within_pointer_gamut(np.array([0.4325, 0.3788, 0.1034])) + assert is_within_pointer_gamut(xp_as_array([0.4325, 0.3788, 0.1034], xp=xp)) - assert not is_within_pointer_gamut(np.array([0.0025, 0.0088, 0.0340])) + assert not is_within_pointer_gamut(xp_as_array([0.0025, 0.0088, 0.0340], xp=xp)) - def test_n_dimensional_is_within_pointer_gamut(self) -> None: + def test_n_dimensional_is_within_pointer_gamut(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.pointer_gamut.is_within_pointer_gamut` definition n-dimensional arrays support. @@ -50,16 +63,24 @@ def test_n_dimensional_is_within_pointer_gamut(self) -> None: if not is_scipy_installed(): # pragma: no cover return - a = np.array([0.3205, 0.4131, 0.5100]) - b = is_within_pointer_gamut(a) - - a = np.tile(a, (6, 1)) - b = np.tile(b, 6) - np.testing.assert_allclose(is_within_pointer_gamut(a), b) - - a = np.reshape(a, (2, 3, 3)) - b = np.reshape(b, (2, 3)) - np.testing.assert_allclose(is_within_pointer_gamut(a), b) + a = xp_as_array([0.3205, 0.4131, 0.5100], xp=xp) + b = as_ndarray(is_within_pointer_gamut(a)) + + a = xp.tile(xp_as_array(a, xp=xp), (6, 1)) + b = xp.tile(xp_as_array(b, xp=xp), (6,)) + xp_assert_close( + is_within_pointer_gamut(a), + b, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 3), xp=xp) + b = xp_reshape(xp_as_array(b, xp=xp), (2, 3), xp=xp) + xp_assert_close( + is_within_pointer_gamut(a), + b, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) @ignore_numpy_errors def test_nan_is_within_pointer_gamut(self) -> None: diff --git a/colour/volume/tests/test_rgb.py b/colour/volume/tests/test_rgb.py index ca94b420cf..1af3fd5557 100644 --- a/colour/volume/tests/test_rgb.py +++ b/colour/volume/tests/test_rgb.py @@ -20,6 +20,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS @@ -28,7 +33,10 @@ RGB_COLOURSPACE_BT709, RGB_COLOURSPACE_BT2020, ) -from colour.utilities import disable_multiprocessing, is_scipy_installed +from colour.utilities import ( + is_scipy_installed, + xp_assert_close, +) from colour.volume import ( RGB_colourspace_limits, RGB_colourspace_pointer_gamut_coverage_MonteCarlo, @@ -63,39 +71,33 @@ class TestRGB_colourspaceLimits: def test_RGB_colourspace_limits(self) -> None: """Test :func:`colour.volume.rgb.RGB_colourspace_limits` definition.""" - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_limits(RGB_COLOURSPACE_BT709), - np.array( - [ - [0.00000000, 100.00000000], - [-86.18159689, 98.23744381], - [-107.85546554, 94.48384002], - ] - ), + [ + [0.00000000, 100.00000000], + [-86.18159689, 98.23744381], + [-107.85546554, 94.48384002], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_limits(RGB_COLOURSPACE_BT2020), - np.array( - [ - [0.00000000, 100.00000000], - [-172.32005590, 130.52657313], - [-120.27412558, 136.88564561], - ] - ), + [ + [0.00000000, 100.00000000], + [-172.32005590, 130.52657313], + [-120.27412558, 136.88564561], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_limits(RGB_COLOURSPACE_ACES2065_1), - np.array( - [ - [-65.15706201, 102.72462756], - [-380.86283223, 281.23227495], - [-284.75355519, 177.11142683], - ] - ), + [ + [-65.15706201, 102.72462756], + [-380.86283223, 281.23227495], + [-284.75355519, 177.11142683], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -110,14 +112,13 @@ class TestRGB_colourspaceVolumeMonteCarlo: :cite:`Laurent2012a` """ - @disable_multiprocessing() def test_RGB_colourspace_volume_MonteCarlo(self) -> None: """ Test :func:`colour.volume.rgb.RGB_colourspace_volume_MonteCarlo` definition. """ - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_volume_MonteCarlo( RGB_COLOURSPACE_BT709, int(10e3), @@ -125,7 +126,7 @@ def test_RGB_colourspace_volume_MonteCarlo(self) -> None: ) * 1e-6, 821700.0 * 1e-6, - atol=1, + atol=TOLERANCE_ABSOLUTE_TESTS * 10000000, ) @@ -148,7 +149,7 @@ def test_RGB_colourspace_volume_coverage_MonteCarlo(self) -> None: if not is_scipy_installed(): # pragma: no cover return - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_volume_coverage_MonteCarlo( RGB_COLOURSPACE_BT709, is_within_pointer_gamut, @@ -171,7 +172,10 @@ class TestRGB_colourspacePointerGamutCoverageMonteCarlo: :cite:`Laurent2012a` """ - def test_RGB_colourspace_pointer_gamut_coverage_MonteCarlo(self) -> None: + def test_RGB_colourspace_pointer_gamut_coverage_MonteCarlo( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.volume.rgb.\ RGB_colourspace_pointer_gamut_coverage_MonteCarlo` definition. @@ -180,7 +184,7 @@ def test_RGB_colourspace_pointer_gamut_coverage_MonteCarlo(self) -> None: if not is_scipy_installed(): # pragma: no cover return - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_pointer_gamut_coverage_MonteCarlo( RGB_COLOURSPACE_BT709, int(10e3), @@ -202,7 +206,10 @@ class TestRGB_colourspaceVisibleSpectrumCoverageMonteCarlo: :cite:`Laurent2012a` """ - def test_RGB_colourspace_visible_spectrum_coverage_MonteCarlo(self) -> None: + def test_RGB_colourspace_visible_spectrum_coverage_MonteCarlo( + self, + xp: ModuleType, # noqa: ARG002 + ) -> None: """ Test :func:`colour.volume.rgb.\ RGB_colourspace_visible_spectrum_coverage_MonteCarlo` definition. @@ -211,7 +218,7 @@ def test_RGB_colourspace_visible_spectrum_coverage_MonteCarlo(self) -> None: if not is_scipy_installed(): # pragma: no cover return - np.testing.assert_allclose( + xp_assert_close( RGB_colourspace_visible_spectrum_coverage_MonteCarlo( RGB_COLOURSPACE_BT709, int(10e3), diff --git a/colour/volume/tests/test_spectrum.py b/colour/volume/tests/test_spectrum.py index 2cf6bb28ed..e1d331b6c3 100644 --- a/colour/volume/tests/test_spectrum.py +++ b/colour/volume/tests/test_spectrum.py @@ -2,6 +2,11 @@ from __future__ import annotations +import typing + +if typing.TYPE_CHECKING: + from colour.hints import ModuleType + from itertools import product import numpy as np @@ -13,7 +18,15 @@ reshape_msds, ) from colour.constants import TOLERANCE_ABSOLUTE_TESTS -from colour.utilities import ignore_numpy_errors, is_scipy_installed +from colour.utilities import ( + as_ndarray, + ignore_numpy_errors, + is_scipy_installed, + xp_as_array, + xp_assert_close, + xp_assert_equal, + xp_reshape, +) from colour.volume import ( XYZ_outer_surface, generate_pulse_waves, @@ -46,89 +59,83 @@ def test_generate_pulse_waves(self) -> None: definition. """ - np.testing.assert_array_equal( + xp_assert_equal( generate_pulse_waves(5), - np.array( - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0], - [1.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 1.0], - [1.0, 0.0, 0.0, 0.0, 1.0], - [1.0, 1.0, 1.0, 0.0, 0.0], - [0.0, 1.0, 1.0, 1.0, 0.0], - [0.0, 0.0, 1.0, 1.0, 1.0], - [1.0, 0.0, 0.0, 1.0, 1.0], - [1.0, 1.0, 0.0, 0.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 0.0], - [0.0, 1.0, 1.0, 1.0, 1.0], - [1.0, 0.0, 1.0, 1.0, 1.0], - [1.0, 1.0, 0.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 0.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0], - ] - ), + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 1.0], + [1.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 1.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0], + ], ) - np.testing.assert_array_equal( + xp_assert_equal( generate_pulse_waves(5, "Pulse Wave Width"), - np.array( - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 0.0, 0.0, 0.0], - [1.0, 1.0, 0.0, 0.0, 1.0], - [1.0, 1.0, 1.0, 0.0, 1.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 1.0, 0.0, 0.0], - [1.0, 1.0, 1.0, 0.0, 0.0], - [1.0, 1.0, 1.0, 1.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 1.0, 0.0], - [0.0, 1.0, 1.0, 1.0, 0.0], - [0.0, 1.0, 1.0, 1.0, 1.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 1.0], - [0.0, 0.0, 1.0, 1.0, 1.0], - [1.0, 0.0, 1.0, 1.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 1.0], - [1.0, 0.0, 0.0, 0.0, 1.0], - [1.0, 0.0, 0.0, 1.0, 1.0], - [1.0, 1.0, 0.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0], - ] - ), - ) - - np.testing.assert_equal( - np.sort(generate_pulse_waves(5), axis=0), - np.sort(generate_pulse_waves(5, "Pulse Wave Width"), axis=0), - ) - - np.testing.assert_array_equal( - generate_pulse_waves(5, "Pulse Wave Width", True), - np.array( [ [0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 0.0, 1.0], [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 0.0, 0.0], [1.0, 1.0, 1.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0], - ] - ), + ], + ) + + xp_assert_equal( + np.sort(generate_pulse_waves(5), axis=0), + np.sort(generate_pulse_waves(5, "Pulse Wave Width"), axis=0), + ) + + xp_assert_equal( + generate_pulse_waves(5, "Pulse Wave Width", True), + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 0.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0], + ], ) @@ -149,44 +156,42 @@ def test_XYZ_outer_surface(self) -> None: ) cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] - np.testing.assert_allclose( + xp_assert_close( XYZ_outer_surface(reshape_msds(cmfs, shape)), - np.array( - [ - [0.00000000e00, 0.00000000e00, 0.00000000e00], - [9.63613812e-05, 2.90567768e-06, 4.49612264e-04], - [2.59105294e-01, 2.10312980e-02, 1.32074689e00], - [1.05610219e-01, 6.20382435e-01, 3.54235713e-02], - [7.26479803e-01, 3.54608696e-01, 2.10051491e-04], - [1.09718745e-02, 3.96354538e-03, 0.00000000e00], - [3.07925724e-05, 1.11197622e-05, 0.00000000e00], - [2.59201656e-01, 2.10342037e-02, 1.32119651e00], - [3.64715514e-01, 6.41413733e-01, 1.35617047e00], - [8.32090022e-01, 9.74991131e-01, 3.56336228e-02], - [7.37451677e-01, 3.58572241e-01, 2.10051491e-04], - [1.10026671e-02, 3.97466514e-03, 0.00000000e00], - [1.27153954e-04, 1.40254398e-05, 4.49612264e-04], - [3.64811875e-01, 6.41416639e-01, 1.35662008e00], - [1.09119532e00, 9.96022429e-01, 1.35638052e00], - [8.43061896e-01, 9.78954677e-01, 3.56336228e-02], - [7.37482470e-01, 3.58583361e-01, 2.10051491e-04], - [1.10990285e-02, 3.97757082e-03, 4.49612264e-04], - [2.59232448e-01, 2.10453234e-02, 1.32119651e00], - [1.09129168e00, 9.96025335e-01, 1.35683013e00], - [1.10216719e00, 9.99985975e-01, 1.35638052e00], - [8.43092689e-01, 9.78965796e-01, 3.56336228e-02], - [7.37578831e-01, 3.58586267e-01, 6.59663755e-04], - [2.70204323e-01, 2.50088688e-02, 1.32119651e00], - [3.64842668e-01, 6.41427759e-01, 1.35662008e00], - [1.10226355e00, 9.99988880e-01, 1.35683013e00], - [1.10219798e00, 9.99997094e-01, 1.35638052e00], - [8.43189050e-01, 9.78968702e-01, 3.60832350e-02], - [9.96684125e-01, 3.79617565e-01, 1.32140656e00], - [3.75814542e-01, 6.45391304e-01, 1.35662008e00], - [1.09132247e00, 9.96036455e-01, 1.35683013e00], - [1.10229434e00, 1.00000000e00, 1.35683013e00], - ] - ), + [ + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [9.63613812e-05, 2.90567768e-06, 4.49612264e-04], + [2.59105294e-01, 2.10312980e-02, 1.32074689e00], + [1.05610219e-01, 6.20382435e-01, 3.54235713e-02], + [7.26479803e-01, 3.54608696e-01, 2.10051491e-04], + [1.09718745e-02, 3.96354538e-03, 0.00000000e00], + [3.07925724e-05, 1.11197622e-05, 0.00000000e00], + [2.59201656e-01, 2.10342037e-02, 1.32119651e00], + [3.64715514e-01, 6.41413733e-01, 1.35617047e00], + [8.32090022e-01, 9.74991131e-01, 3.56336228e-02], + [7.37451677e-01, 3.58572241e-01, 2.10051491e-04], + [1.10026671e-02, 3.97466514e-03, 0.00000000e00], + [1.27153954e-04, 1.40254398e-05, 4.49612264e-04], + [3.64811875e-01, 6.41416639e-01, 1.35662008e00], + [1.09119532e00, 9.96022429e-01, 1.35638052e00], + [8.43061896e-01, 9.78954677e-01, 3.56336228e-02], + [7.37482470e-01, 3.58583361e-01, 2.10051491e-04], + [1.10990285e-02, 3.97757082e-03, 4.49612264e-04], + [2.59232448e-01, 2.10453234e-02, 1.32119651e00], + [1.09129168e00, 9.96025335e-01, 1.35683013e00], + [1.10216719e00, 9.99985975e-01, 1.35638052e00], + [8.43092689e-01, 9.78965796e-01, 3.56336228e-02], + [7.37578831e-01, 3.58586267e-01, 6.59663755e-04], + [2.70204323e-01, 2.50088688e-02, 1.32119651e00], + [3.64842668e-01, 6.41427759e-01, 1.35662008e00], + [1.10226355e00, 9.99988880e-01, 1.35683013e00], + [1.10219798e00, 9.99997094e-01, 1.35638052e00], + [8.43189050e-01, 9.78968702e-01, 3.60832350e-02], + [9.96684125e-01, 3.79617565e-01, 1.32140656e00], + [3.75814542e-01, 6.45391304e-01, 1.35662008e00], + [1.09132247e00, 9.96036455e-01, 1.35683013e00], + [1.10229434e00, 1.00000000e00, 1.35683013e00], + ], atol=TOLERANCE_ABSOLUTE_TESTS, ) @@ -197,7 +202,7 @@ class TestIsWithinVisibleSpectrum: definition unit tests methods. """ - def test_is_within_visible_spectrum(self) -> None: + def test_is_within_visible_spectrum(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.spectrum.is_within_visible_spectrum` definition. @@ -206,15 +211,19 @@ def test_is_within_visible_spectrum(self) -> None: if not is_scipy_installed(): # pragma: no cover return - assert is_within_visible_spectrum(np.array([0.3205, 0.4131, 0.5100])) + assert is_within_visible_spectrum(xp_as_array([0.3205, 0.4131, 0.5100], xp=xp)) - assert not is_within_visible_spectrum(np.array([-0.0005, 0.0031, 0.0010])) + assert not is_within_visible_spectrum( + xp_as_array([-0.0005, 0.0031, 0.0010], xp=xp) + ) - assert is_within_visible_spectrum(np.array([0.4325, 0.3788, 0.1034])) + assert is_within_visible_spectrum(xp_as_array([0.4325, 0.3788, 0.1034], xp=xp)) - assert not is_within_visible_spectrum(np.array([0.0025, 0.0088, 0.0340])) + assert not is_within_visible_spectrum( + xp_as_array([0.0025, 0.0088, 0.0340], xp=xp) + ) - def test_n_dimensional_is_within_visible_spectrum(self) -> None: + def test_n_dimensional_is_within_visible_spectrum(self, xp: ModuleType) -> None: """ Test :func:`colour.volume.spectrum.is_within_visible_spectrum` definition n-dimensional arrays support. @@ -223,16 +232,24 @@ def test_n_dimensional_is_within_visible_spectrum(self) -> None: if not is_scipy_installed(): # pragma: no cover return - a = np.array([0.3205, 0.4131, 0.5100]) - b = is_within_visible_spectrum(a) + a = xp_as_array([0.3205, 0.4131, 0.5100], xp=xp) + b = as_ndarray(is_within_visible_spectrum(a)) - a = np.tile(a, (6, 1)) - b = np.tile(b, 6) - np.testing.assert_allclose(is_within_visible_spectrum(a), b) + a = xp.tile(xp_as_array(a, xp=xp), (6, 1)) + b = xp.tile(xp_as_array(b, xp=xp), (6,)) + xp_assert_close( + is_within_visible_spectrum(a), + b, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) - a = np.reshape(a, (2, 3, 3)) - b = np.reshape(b, (2, 3)) - np.testing.assert_allclose(is_within_visible_spectrum(a), b) + a = xp_reshape(xp_as_array(a, xp=xp), (2, 3, 3), xp=xp) + b = xp_reshape(xp_as_array(b, xp=xp), (2, 3), xp=xp) + xp_assert_close( + is_within_visible_spectrum(a), + b, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) @ignore_numpy_errors def test_nan_is_within_visible_spectrum(self) -> None: diff --git a/conftest.py b/conftest.py index 43798bd772..569814e662 100644 --- a/conftest.py +++ b/conftest.py @@ -28,3 +28,19 @@ def pytest_configure(config: Any) -> None: config.option.numprocesses = 0 if getattr(config.option, "dist", None): config.option.dist = "no" + + # NOTE: The *Colour* warning filters are registered here rather than via the + # ``filterwarnings`` ini option on purpose: an + # ``ignore::colour.utilities.ColourWarning`` *ini* entry is resolved during + # pytest's initial-conftest phase, forcing ``import colour`` *before* + # *pytest-cov* starts recording in each xdist worker, which leaves all + # import-time code measured untraced (reported as ``0%`` coverage). + # ``addinivalue_line`` appends the same filters post-configure, deferring + # their category resolution (and the ``import colour`` it triggers) to test + # time, under coverage, while preserving the per-test filtering behaviour. + for warning in ( + "ignore::colour.utilities.ColourWarning", + "ignore::colour.utilities.ColourRuntimeWarning", + "ignore::colour.utilities.ColourUsageWarning", + ): + config.addinivalue_line("filterwarnings", warning) diff --git a/docs/advanced.rst b/docs/advanced.rst index 2c6545f60a..50ea1cfcf7 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -18,8 +18,15 @@ runtime: `float64` (default). Changing the float dtype might result in various **Colour** `functionality breaking entirely `__. *With great power comes great responsibility*. +- ``COLOUR_SCIENCE__DEFAULT_COMPLEX_DTYPE``: Set the complex dtype for most of + **Colour** computations. Possible values are `complex64` and `complex128` + (default). Changing the complex dtype might result in various **Colour** + functionality breaking entirely. *With great power comes great responsibility*. - ``COLOUR_SCIENCE__DISABLE_CACHING``: Disable the caches that can be disabled, useful for debugging purposes. +- ``COLOUR_SCIENCE__DOCUMENTATION_BUILD``: Signal that the documentation is + being built, equivalent to the *READTHEDOCS* environment variable, as + queried by :func:`colour.utilities.is_documentation_building`. - ``COLOUR_SCIENCE__COLOUR__IMPORT_VAAB_COLOUR``: Import `vaab/colour `__ injection into **Colour** namespace. This solves the clash with @@ -35,6 +42,9 @@ runtime: - ``COLOUR_SCIENCE__FILTER_COLOUR_WARNINGS``: Filter *Colour* warnings, this also filters *Colour* usage and runtime warnings. - ``COLOUR_SCIENCE__FILTER_PYTHON_WARNINGS``: Filter *Python* warnings. +- ``COLOUR_SCIENCE__ARRAY_API``: Enable *Python Array API Standard* dispatch, + allowing alternative backends such as *JAX*, *PyTorch*, and *CuPy*. + See `Array API Support`_ for details. JEnv File --------- @@ -81,3 +91,547 @@ cache registry object: See :class:`colour.utilities.CacheRegistry` class documentation for more information on how to manage the cache registry. + +Array API Support +----------------- + +**Colour** has opt-in support for the +`Python Array API Standard `__, +enabling alternative backends such as `JAX `__, +`PyTorch `__ (including MPS GPU), and +`CuPy `__ alongside the default *NumPy* backend. + +Enabling Array API Dispatch +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Array API dispatch is **disabled by default**. *NumPy* remains the sole +backend unless explicitly opted in: + +.. code-block:: bash + + # Environment variable (before importing Colour): + export COLOUR_SCIENCE__ARRAY_API=1 + +.. code-block:: python + + from colour.utilities import ( + array_api_enable, + is_array_api_enabled, + set_array_api_enabled, + ) + + # Programmatic toggle: + set_array_api_enabled(True) + + # Context manager (recommended for isolated use): + with array_api_enable(True): + # All Colour functions dispatch to the caller's backend. + ... + + # Check current state: + is_array_api_enabled() + +When dispatch is enabled, **Colour** inspects the input arrays to determine +which backend to use. Pass a *JAX* array and the computation runs in *JAX*; +pass a *PyTorch* tensor and it runs in *PyTorch*. + +Writing Backend-Aware Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Every function that operates on arrays should follow a similar pattern. +From :func:`colour.intermediate_lightness_function_CIE1976`: + +.. code-block:: python + + def intermediate_lightness_function_CIE1976(Y, Y_n=100): + Y = as_float_array(Y) + + xp = array_namespace(Y, Y_n) + + Y_n = xp_as_float_array(Y_n, xp=xp, like=Y) + + Y_Y_n = Y / Y_n + + f_Y_Y_n = xp.where( + Y_Y_n > (24 / 116) ** 3, + spow(Y_Y_n, 1 / 3), + (841 / 108) * Y_Y_n + 16 / 116, + ) + + return as_float(f_Y_Y_n) + +**Conventions** + +1. **Convert primary inputs first.** Call ``to_domain_*`` or + ``as_float_array`` on all primary inputs **before** ``array_namespace``. + These conversions ensure the inputs are float arrays and preserve the + caller's backend. + +2. **One** ``array_namespace`` **per function, immediately after the + conversions.** Call ``xp = array_namespace(...)`` exactly once, on the + line after the primary input conversions and **before any** ``tsplit`` + call or secondary promotion, passing every input that a caller might + provide as a backend tensor. Pass converted names only: never inline a + call (``array_namespace(as_float_array(a))``), never pass a raw name + when the converted one exists, and never call with zero arguments when + the function has an array input. Module-level constants (matrices, + lookup tables) and scalar curve parameters with library defaults (e.g. + the ``r`` exponent of *ARIB STD-B67*) do not need to be included; the + latter are promoted per convention 4 instead. + +3. **Blank line before and after** ``xp = array_namespace(...)``. + +4. **Promote secondary parameters with** ``xp_as_float_array``. After + obtaining ``xp``, convert secondary parameters (scalars, optional + arguments, module-level constants) using + ``xp_as_float_array(param, xp=xp, like=primary)`` which enforces + ``DTYPE_FLOAT_DEFAULT`` and places the array on the correct device. For + integer data, use ``xp_as_int_array``. For data that should preserve its + original dtype, use ``xp_as_array``. + +5. **Do not promote the primary input after** ``to_domain_*``. + ``to_domain_*`` functions are backend-aware and return arrays in the + correct namespace and dtype. However, **secondary parameters** that went + through ``to_domain_*`` but could be scalars (e.g. ``Y`` when the primary + is ``xy``) still need ``xp_as_float_array`` promotion because + ``to_domain_*(scalar)`` returns a *NumPy* array regardless of the target + backend. + +6. **Promote dataclass-extracted variables.** When a function receives a + dataclass (e.g. ``CAM_Specification_CIECAM02``) and extracts fields via + ``astuple`` or ``tsplit``, those fields may be *NumPy* arrays even when + other arguments are backend tensors. After ``to_domain_*`` scaling, these + variables still need ``xp_as_float_array`` promotion because the dataclass + fields do not carry backend information. + +7. **Use** ``xp.*`` **for standard operations:** ``xp.sqrt``, ``xp.exp``, + ``xp.log``, ``xp.where``, ``xp.stack``, ``xp.zeros``, ``xp.ones``, + ``xp.full``, ``xp.abs``, ``xp.sum``, ``xp.mean``, ``xp.clip``, + ``xp.squeeze``, ``xp.expand_dims``, ``xp.broadcast_to``, etc. + +8. **Use** ``float("nan")`` **and** ``float("inf")`` **instead of** + ``np.nan`` **and** ``np.inf`` as array fill values inside backend-aware + code (e.g. in ``xp.full``, ``xp.where``). Scalar constants like + ``np.pi`` are plain Python floats and are fine to use anywhere. + +9. **Return arrays in the caller's namespace.** Functions like ``tstack``, + ``from_range_100``, and ``as_float_array`` are already namespace-aware and + preserve the input backend. + +Array Conversion Helpers +^^^^^^^^^^^^^^^^^^^^^^^^ + +Two families of conversion helpers cover the two distinct conversion roles +inside a backend-aware function: + +- ``as_float_array`` / ``as_int_array`` / ``as_complex_array`` : *Auto- + detect* helpers used at function entry on the primary inputs. They are + namespace-aware: when *Array API* dispatch is enabled and the input is + a non-*NumPy* array, the result is returned in the input's native + namespace and on its device. The function does not need to know which + backend is in play; the conversion preserves it. +- ``xp_as_float_array`` / ``xp_as_int_array`` / ``xp_as_array`` -- + *Explicit-namespace* helpers used to promote secondary parameters + (scalars, lists, module-level constants) into a namespace that has + already been resolved via :func:`array_namespace`. The ``like`` + parameter matches the device of an existing primary array, which is + how a Python float or a *NumPy* matrix lands on the right *PyTorch + MPS* / *CUDA* / *JAX* device. + +The split mirrors a real semantic distinction: primary inputs *carry* +namespace information (so the helper can discover it), secondary inputs +typically *do not* (so the caller has to push it in along with a device +target). Mixing the two roles is the most common mis-use; if a value +comes in via the function signature as the load-bearing array argument, +it is a primary, use ``as_float_array``. Everything else is a secondary, +use ``xp_as_float_array`` with ``xp=xp, like=primary``. + +The explicit-namespace helpers in detail: + +- ``xp_as_float_array(a, *, xp, like=None)`` : Converts to float using + ``DTYPE_FLOAT_DEFAULT``. Use for colour values, scalars, matrices, and any + data that should be floating-point. The ``like`` parameter matches the + device of an existing array. +- ``xp_as_int_array(a, *, xp, like=None)`` : Converts to integer using + ``DTYPE_INT_DEFAULT``. Use for indices, counts, and integer data. +- ``xp_as_array(a, *, dtype=None, xp, like=None)`` : Generic conversion + with optional explicit ``dtype``. Use when the original dtype should be + preserved (e.g. boolean masks) or when a specific dtype is needed. + +When ``xp`` Is Not Needed +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many functions do not call ``array_namespace`` directly because the +infrastructure handles dispatch transparently: + +- ``tsplit`` / ``tstack``: Split and stack along the last axis. +- ``as_float_array`` / ``as_int_array`` / ``as_complex_array``: Convert to + typed arrays, namespace-aware when dispatch is enabled. +- ``to_domain_*`` / ``from_range_*``: Domain and range scaling. +- ``vecmul``: Matrix-vector multiplication. +- ``spow``: Safe power function. + +If a function only uses these utilities and standard arithmetic +(``+``, ``-``, ``*``, ``/``, ``**``), it is already backend-aware without +calling ``array_namespace`` explicitly. + +Compatibility Helpers (``xp_*``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Array API Standard does not cover every *NumPy* function. **Colour** +provides ``xp_*`` compatibility helpers for operations that either are not +in the standard or differ across backends. Most take ``xp`` as a +keyword-only parameter (``xp_function(a, ..., *, xp)``); a few take it as +an optional positional with ``None`` default (``xp_function(a, xp=None)``, +deriving the namespace from ``a``), and a few infer ``xp`` from the input +and accept no ``xp`` argument at all (e.g. ``xp_radians`` / ``xp_degrees``). +The exact signature is listed per helper below. + +**Array Creation and Conversion** + +The ``xp_as_array`` / ``xp_as_float_array`` / ``xp_as_int_array`` +explicit-namespace helpers are described in the *Helpers in detail* +section above. The remaining creation helpers are: + +- ``xp_astype(a, dtype, *, xp=None)`` : Portable ``a.astype(dtype)``. +- ``xp_linspace(start, stop, *, num, xp)`` : ``np.linspace``. + +**Shape Manipulation** + +- ``xp_atleast_1d(a, xp=None)`` : ``np.atleast_1d``. +- ``xp_atleast_2d(a, xp=None)`` : ``np.atleast_2d``. +- ``xp_reshape(a, shape, *, xp)`` : ``np.reshape``. +- ``xp_broadcast_to(a, shape, *, xp)`` : ``np.broadcast_to``. +- ``xp_pad(a, pad_width, *, xp)`` : ``np.pad``. +- ``xp_insert(a, obj, values, *, xp)`` : ``np.insert``. +- ``xp_resize(a, new_shape, *, xp)`` : ``np.resize`` (NumPy fallback). +- ``xp_squeeze(a, *, axis, xp)`` : ``np.squeeze``. When ``axis`` is + ``None``, all size-1 dimensions are squeezed (the *Array API* standard + requires an explicit ``axis``). + +**Math and Statistics** + +- ``xp_average(a, *, weights, xp)`` : ``np.average``. +- ``xp_median(a, *, xp)`` : ``np.median`` (NumPy fallback). +- ``xp_nanmean(a, *, xp)`` : ``np.nanmean``. +- ``xp_gradient(a, *, xp)`` : ``np.gradient`` (NumPy fallback). +- ``xp_trapezoid(y, *, x, xp)`` : ``np.trapezoid`` (NumPy fallback). +- ``xp_radians(a)`` / ``xp_degrees(a)`` : Angle conversion (namespace + derived from ``a``). +- ``xp_round(a, *, decimals, xp)`` : ``np.round``. +- ``xp_sinc(a, *, xp)`` : ``np.sinc``. + +**Selection and Comparison** + +- ``xp_select(condlist, choicelist, *, default, xp)`` : ``np.select`` + (native Array API implementation using ``xp.where``). +- ``xp_interp(x, xp_val, fp, *, xp)`` : ``np.interp`` (NumPy fallback). +- ``xp_nan_to_num(a, *, xp)`` : ``np.nan_to_num``. +- ``xp_isclose(a, b, *, xp)`` : ``np.isclose``. +- ``xp_isin(element, test_elements, *, xp)`` : ``np.isin``. + +**Linear Algebra** + +- ``xp_lstsq(a, b, *, xp)`` : ``np.linalg.lstsq``. +- ``xp_eig(a, *, xp)`` : ``np.linalg.eig`` (NumPy fallback for MPS). +- ``xp_eigh(a, *, xp)`` : ``np.linalg.eigh`` (NumPy fallback for MPS). +- ``xp_create_diagonal(a, *, xp)`` : ``np.diag``. + +**Set Operations** + +- ``xp_unique(a, *, xp)`` : ``np.unique``. +- ``xp_setxor1d(ar1, ar2, *, xp)`` : ``np.setxor1d``. + +**Testing** + +- ``xp_assert_close(actual, desired, atol=1e-7, rtol=1e-7, err_msg="")`` : + ``np.testing.assert_allclose`` (converts both arguments via + ``as_ndarray`` internally). +- ``xp_assert_equal(actual, desired)`` : ``np.testing.assert_array_equal``. + +See :mod:`colour.utilities.array` for the full list. + +**Usage example** from +:func:`colour.temperature.CCT_to_xy_Kang2002`: + +.. code-block:: python + + def CCT_to_xy_Kang2002(CCT): + CCT = as_float_array(CCT) + + xp = array_namespace(CCT) + + CCT_3 = CCT**3 + CCT_2 = CCT**2 + + x = xp.where( + CCT <= 4000, + -0.2661239 * 10**9 / CCT_3 + - 0.2343589 * 10**6 / CCT_2 + + 0.8776956 * 10**3 / CCT + + 0.179910, + -3.0258469 * 10**9 / CCT_3 + + 2.1070379 * 10**6 / CCT_2 + + 0.2226347 * 10**3 / CCT + + 0.24039, + ) + + x_3 = x**3 + x_2 = x**2 + + cnd_l = [CCT <= 2222, xp.logical_and(CCT > 2222, CCT <= 4000), CCT > 4000] + i = -1.1063814 * x_3 - 1.34811020 * x_2 + 2.18555832 * x - 0.20219683 + j = -0.9549476 * x_3 - 1.37418593 * x_2 + 2.09137015 * x - 0.16748867 + k = 3.0817580 * x_3 - 5.8733867 * x_2 + 3.75112997 * x - 0.37001483 + y = xp_select(cnd_l, [i, j, k], xp=xp) + + return tstack([x, y]) + +Converting NumPy Arrays for Backend Use +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*NumPy* constants (e.g. matrices defined at module level) must be promoted +into the caller's namespace inside backend-aware functions. Use +``xp_as_float_array`` with the ``like`` parameter to match the device and +enforce ``DTYPE_FLOAT_DEFAULT``. +From :func:`colour.adaptation.matrix_chromatic_adaptation_VonKries`: + +.. code-block:: python + + def matrix_chromatic_adaptation_VonKries(XYZ_w, XYZ_wr, transform="CAT02"): + XYZ_w = as_float_array(XYZ_w) + XYZ_wr = as_float_array(XYZ_wr) + + xp = array_namespace(XYZ_w, XYZ_wr) + + M = xp_as_float_array(CHROMATIC_ADAPTATION_TRANSFORMS[transform], xp=xp, like=XYZ_w) + + RGB_w = vecmul(M, XYZ_w) + RGB_wr = vecmul(M, XYZ_wr) + + with sdiv_mode(): + D = sdiv(RGB_wr, RGB_w) + + ... + +The ``like`` parameter places the constant on the correct device (e.g. MPS +GPU). ``xp_as_float_array`` enforces ``DTYPE_FLOAT_DEFAULT``, ensuring that +a ``float64`` module-level constant is correctly narrowed to ``float32`` when +the global default dtype has been changed. + +Domain/Range Functions and Promotion +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Parameters that go through ``to_domain_*`` (e.g. ``to_domain_100``, +``to_domain_1``) are already backend-aware; they preserve the input +namespace and return float arrays. These do **not** need a subsequent +``xp_as_float_array`` call. Only raw scalars and optional parameters that +did not go through ``to_domain_*`` need promotion. +From :func:`colour.adaptation.chromatic_adaptation_forward_CMCCAT2000`: + +.. code-block:: python + + def chromatic_adaptation_forward_CMCCAT2000( + XYZ, XYZ_w, XYZ_wr, L_A1, L_A2, surround=... + ): + # to_domain_* handles dtype and backend; no further promotion needed. + XYZ = to_domain_100(XYZ) + XYZ_w = to_domain_100(XYZ_w) + XYZ_wr = to_domain_100(XYZ_wr) + + xp = array_namespace(XYZ, L_A1, L_A2) + + # Only raw scalars need promotion. + L_A1 = xp_as_float_array(L_A1, xp=xp, like=XYZ) + L_A2 = xp_as_float_array(L_A2, xp=xp, like=XYZ) + + ... + +Writing Backend-Aware Tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests are parametrised across backends using the ``xp`` fixture defined in +``conftest.py``. Every test method that exercises a backend-aware function +should accept the ``xp`` parameter. +From ``colour/models/tests/test_igpgtg.py``: + +.. code-block:: python + + class TestXYZ_to_IgPgTg: + def test_XYZ_to_IgPgTg(self, xp: ModuleType) -> None: + xp_assert_close( + XYZ_to_IgPgTg(xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp)), + [0.42421258, 0.18632491, 0.10689223], + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + def test_n_dimensional_XYZ_to_IgPgTg(self, xp: ModuleType) -> None: + XYZ = xp_as_array([0.20654008, 0.12197225, 0.05136952], xp=xp) + IgPgTg = as_ndarray(XYZ_to_IgPgTg(XYZ)) + + XYZ = xp.tile(xp_as_array(XYZ, xp=xp), (6, 1)) + IgPgTg = xp.tile(xp_as_array(IgPgTg, xp=xp), (6, 1)) + xp_assert_close(XYZ_to_IgPgTg(XYZ), IgPgTg, atol=TOLERANCE_ABSOLUTE_TESTS) + + XYZ = xp_reshape(xp_as_array(XYZ, xp=xp), (2, 3, 3), xp=xp) + IgPgTg = xp_reshape(xp_as_array(IgPgTg, xp=xp), (2, 3, 3), xp=xp) + xp_assert_close(XYZ_to_IgPgTg(XYZ), IgPgTg, atol=TOLERANCE_ABSOLUTE_TESTS) + +**Conventions** + +- **Inputs**: Use ``xp_as_array([...], xp=xp)`` for all data passed to the + function under test. +- **Expected values**: Use **plain Python lists** as the second argument to + ``xp_assert_close`` / ``xp_assert_equal``. No ``np.array`` wrapping + needed : the assertion helpers convert internally. +- **Element-wise arithmetic on expected values**: For arithmetic + like ``expected * 100``, use ``xp_as_array([...], xp=xp) * 100`` since + ``[...] * 100`` is Python list repetition. +- **n-dimensional tiling**: Use ``xp.tile`` and ``xp_reshape`` for shape + tests. +- **Scalar-input tests**: If a test only passes scalars (not arrays), add + ``xp: ModuleType # noqa: ARG002`` to ensure it runs across backends even + though ``xp`` is not directly referenced. +- **NumPy-only functions**: If the function under test is inherently + *NumPy*-only (e.g. uses ``SpectralDistribution``, ``scipy.optimize``, or + in-place assignment), add ``xp: ModuleType # noqa: ARG002`` and keep + inputs as ``np.array``. + +.. note:: + + In tests, ``xp_as_array`` is preferred over ``xp_as_float_array`` + because test inputs are explicit literals whose dtype is already correct. + ``xp_as_float_array`` is for source code where ``DTYPE_FLOAT_DEFAULT`` + enforcement matters. + +What Stays NumPy-Only +~~~~~~~~~~~~~~~~~~~~~ + +Not everything needs backend support. The following are intentionally *NumPy*-only: + +- **Spectral classes**: ``SpectralDistribution``, ``MultiSpectralDistributions``, + ``Signal``, ``MultiSignals`` : these use *Pandas* internally. +- **I/O operations**: File reading/writing, image loading. +- **Scipy-dependent optimisation**: Functions using ``scipy.optimize``, + ``scipy.interpolate`` : callbacks must receive *NumPy* arrays. Use + ``as_ndarray()`` to convert before passing to *SciPy*. +- **In-place operations**: *JAX* arrays are immutable + (``a[i] = v`` raises ``TypeError``). Algorithms requiring in-place + mutation stay *NumPy*-only. + +Common Pitfalls +~~~~~~~~~~~~~~~ + +1. **List repetition vs element-wise multiplication**: ``[1, 2, 3] * 10`` + produces ``[1, 2, 3, 1, 2, 3, ...]`` (30 elements). + ``np.array([1, 2, 3]) * 10`` produces ``[10, 20, 30]``. Element-wise + arithmetic requires wrapping with ``xp_as_array`` or + ``xp_as_float_array`` first. + +2. **MPS GPU limitations**: Apple's MPS backend does not support + ``complex128`` or ``float64``. The ``xp_as_array`` helper + automatically falls back to ``float32`` / ``complex64`` and emits a + :class:`colour.utilities.runtime_warning` reporting the downcast. + The ``xp_eig`` / ``xp_eigh`` helpers fall back to *NumPy* when the + backend lacks ``linalg.eig``. The ``xp_linspace`` helper falls back + to ``float32`` on the same path, also with a warning. + +3. **Mixing namespaces**: Never multiply a *NumPy* array by a *PyTorch* + tensor directly. Use ``xp_as_float_array`` to promote constants into + the caller's namespace first. Mixing two non-*NumPy* backends in the + same call (e.g. a *JAX* array and a *PyTorch* tensor) routes through + :func:`colour.utilities.array_namespace` and raises ``TypeError``; + there is no implicit cross-backend coercion. + +4. **Scalar-promotion cache**: ``xp_as_array`` memoises promotions of + *Python* scalars and small (``<= 16`` element) *NumPy* constants + keyed on ``(value, namespace, device, dtype)``. The cache lives in + :attr:`colour.utilities.CACHE_REGISTRY` and is bypassed when the + caching context is disabled + (``with caching_enable(False): ...``). The cache eliminates repeated + CPU-to-GPU transfers of module-level matrices and tolerance scalars; + when debugging unexpected device placement, disable it first. + +5. **Module-level constants**: Matrices and lookup tables defined at module + level with ``np.array`` are fine : they stay as *NumPy* arrays. Convert + them inside functions using + ``xp_as_float_array(CONSTANT, xp=xp, like=input)``. + +6. **Choosing the right conversion helper**: Use ``xp_as_float_array`` for + colour data, scalars, and constants (enforces ``DTYPE_FLOAT_DEFAULT``). + Use ``xp_as_int_array`` for indices and counts. Use ``xp_as_array`` only + when the original dtype must be preserved (boolean masks, generic + pass-through). + +Debugging +~~~~~~~~~ + +Use :class:`colour.utilities.trace_array_namespace` to trace which array +namespace each function call resolves to: + +.. code-block:: python + + import numpy as np + import colour + from colour.utilities import trace_array_namespace + + XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) + with trace_array_namespace(): + Lab = colour.XYZ_to_Lab(XYZ) + +This prints an indented call tree showing every function invocation, the types +and shapes of array arguments, and return values: + +.. code-block:: text + + XYZ_to_Lab(XYZ: ndarray[3], illuminant: ndarray[2]) + to_domain_1(a: ndarray[3], scale_factor: int, dtype: None) + as_float_array(a: ndarray[3], dtype: type) + as_array(a: ndarray[3], dtype: type) + -> ndarray[3] + -> ndarray[3] + ndarray_copy(a: ndarray[3]) + array_namespace() + is_numpy_namespace(xp: module) + -> ndarray[3] + ... + -> ndarray[3] + tsplit(a: ndarray[3], dtype: None) + ... + -> ndarray[3] + ... + -> ndarray[3] + +When using a non-*NumPy* backend, array arguments show their backend type +and calls where multiple backends coexist are flagged as ``MIXED``: + +.. code-block:: python + + import torch + import colour + from colour.utilities import array_api_enable, trace_array_namespace + + XYZ = torch.tensor([0.20654008, 0.12197225, 0.05136952], dtype=torch.float64) + with array_api_enable(True), trace_array_namespace(): + Lab = colour.XYZ_to_Lab(XYZ) + +.. code-block:: text + + XYZ_to_Lab(XYZ: torch.Tensor[3], illuminant: ndarray[2]) [MIXED] + to_domain_1(a: torch.Tensor[3], scale_factor: int, dtype: None) + as_float_array(a: torch.Tensor[3], dtype: type) + array_namespace() + -> torch.Tensor[3] + -> torch.Tensor[3] + ndarray_copy(a: torch.Tensor[3]) + ... + -> torch.Tensor[3] + ... + -> torch.Tensor[3] + ... + -> torch.Tensor[3] + +The ``[MIXED]`` flag on the first line indicates that ``XYZ`` is a +*PyTorch* tensor while ``illuminant`` is a *NumPy* array (the default +*D65* illuminant). This is expected here : the function promotes the +illuminant internally. diff --git a/docs/basics.rst b/docs/basics.rst index 28310fe658..7982c27ced 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -594,23 +594,6 @@ scale value: [ 0.24033795 0.21156212 0.17643012] -Multiprocessing on Windows with Domain-Range Scales -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Windows does not have a `fork `__ system call, -a consequence is that child processes do not necessarily -`inherit from changes made to global variables `__. - -It has crucial `consequences `__ -as **Colour** stores the current domain-range scale into a global variable. - -The solution is to define an initialisation definition that defines the -scale upon child processes spawning. - -The :class:`colour.utilities.multiprocessing_pool` context manager conveniently -performs the required initialisation so that the domain-range scale is -propagated appropriately to child processes. - Safe Power and Division ----------------------- diff --git a/docs/colour.algebra.rst b/docs/colour.algebra.rst index 4c17b5c6c8..700f8d987e 100644 --- a/docs/colour.algebra.rst +++ b/docs/colour.algebra.rst @@ -130,7 +130,7 @@ Common sdiv sdiv_mode set_sdiv_mode - set_spow_enable + set_spow_enabled smooth smoothstep_function spow diff --git a/docs/colour.colorimetry.rst b/docs/colour.colorimetry.rst index 7cdaa10fbc..130afc162f 100644 --- a/docs/colour.colorimetry.rst +++ b/docs/colour.colorimetry.rst @@ -31,10 +31,13 @@ Spectral Data Structure .. autosummary:: :toctree: generated/ + extrapolate_signal + interpolate_signal reshape_msds reshape_sd sds_and_msds_to_msds sds_and_msds_to_sds + trim_signal Spectral Data Generation ------------------------ @@ -46,8 +49,11 @@ Spectral Data Generation .. autosummary:: :toctree: generated/ + msds_blackbody + msds_CIE_illuminant_D_series msds_constant msds_ones + msds_rayleigh_jeans msds_zeros sd_blackbody sd_CIE_illuminant_D_series @@ -71,6 +77,7 @@ Spectral Data Generation :toctree: generated/ blackbody_spectral_radiance + CIE_illuminant_D_series daylight_locus_function planck_law rayleigh_jeans_law @@ -143,6 +150,7 @@ ASTM E308-15 adjust_tristimulus_weighting_factors_ASTME308 lagrange_coefficients_ASTME2022 + msds_to_XYZ_tristimulus_weighting_factors_ASTME308 sd_to_XYZ_tristimulus_weighting_factors_ASTME308 tristimulus_weighting_factors_ASTME2022 diff --git a/docs/colour.utilities.rst b/docs/colour.utilities.rst index 1ad018c261..4b01b24220 100644 --- a/docs/colour.utilities.rst +++ b/docs/colour.utilities.rst @@ -43,6 +43,7 @@ Common :template: class.rst CacheRegistry + caching_enable .. currentmodule:: colour.utilities @@ -52,9 +53,7 @@ Common attest batch CACHE_REGISTRY - caching_enable copy_definition - disable_multiprocessing filter_kwargs filter_mapping first_item @@ -67,13 +66,12 @@ Common is_iterable is_numeric is_sibling - multiprocessing_pool optional print_numpy_errors raise_numpy_errors - set_caching_enable + set_caching_enabled slugify - url_download + download_url hash_sha256 validate_method warn_numpy_errors @@ -93,6 +91,7 @@ Array MixinDataclassArray MixinDataclassFields MixinDataclassIterable + ndarray_copy_enable .. autosummary:: :toctree: generated/ @@ -123,14 +122,14 @@ Array is_ndarray_copy_enabled is_uniform ndarray_copy - ndarray_copy_enable ndarray_write ones orient row_as_diagonal + set_default_complex_dtype set_default_float_dtype set_default_int_dtype - set_ndarray_copy_enable + set_ndarray_copy_enabled to_domain_1 to_domain_10 to_domain_100 @@ -140,6 +139,90 @@ Array tstack zeros +Array API +--------- + +``colour.utilities`` + +.. currentmodule:: colour.utilities + +**Context Managers** + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + array_api_enable + trace_array_namespace + +**Namespace and Dispatch** + +.. autosummary:: + :toctree: generated/ + + array_namespace + is_array_api_enabled + is_non_ndarray + is_numpy_namespace + set_array_api_enabled + +**Boundary Conversion** + +.. autosummary:: + :toctree: generated/ + + as_ndarray + cast_non_ndarray + xp_as_array + xp_as_float_array + xp_as_int_array + +**Assertion Helpers** + +.. autosummary:: + :toctree: generated/ + + xp_assert_close + xp_assert_equal + +**Array Operations** + +.. autosummary:: + :toctree: generated/ + + xp_ascontiguousarray + xp_astype + xp_atleast_1d + xp_atleast_2d + xp_average + xp_broadcast_to + xp_create_diagonal + xp_degrees + xp_eig + xp_eigh + xp_gradient + xp_insert + xp_interp + xp_isclose + xp_isin + xp_linspace + xp_lstsq + xp_matrix_transpose + xp_median + xp_nan_to_num + xp_nanmean + xp_pad + xp_radians + xp_reshape + xp_resize + xp_round + xp_select + xp_setxor1d + xp_sinc + xp_squeeze + xp_trapezoid + xp_unique + Data Structures --------------- @@ -195,6 +278,8 @@ Requirements .. autosummary:: :toctree: generated/ + is_array_api_compat_installed + is_array_api_extra_installed is_ctlrender_installed is_imageio_installed is_matplotlib_installed diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d5b37846d1..cc80119d38 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -379,23 +379,26 @@ the objects needed for spectral computations and many others: .. code-block:: text - ['SpectralShape', - 'SPECTRAL_SHAPE_DEFAULT', - 'SpectralDistribution', + ['SPECTRAL_SHAPE_DEFAULT', 'MultiSpectralDistributions', - 'reshape_sd', + 'SpectralDistribution', + 'SpectralShape', 'reshape_msds', - 'sds_and_msds_to_sds', + 'reshape_sd', 'sds_and_msds_to_msds', - 'planck_law', + 'sds_and_msds_to_sds', 'blackbody_spectral_radiance', - 'sd_blackbody', + 'msds_blackbody', + 'msds_rayleigh_jeans', + 'planck_law', 'rayleigh_jeans_law', + 'sd_blackbody', 'sd_rayleigh_jeans', 'LMS_ConeFundamentals', 'RGB_ColourMatchingFunctions', 'XYZ_ColourMatchingFunctions', 'CCS_ILLUMINANTS', + 'CCS_LIGHT_SOURCES', 'MSDS_CMFS', 'MSDS_CMFS_LMS', 'MSDS_CMFS_RGB', @@ -405,95 +408,98 @@ the objects needed for spectral computations and many others: 'SDS_LEFS', 'SDS_LEFS_PHOTOPIC', 'SDS_LEFS_SCOTOPIC', + 'SDS_LIGHT_SOURCES', 'TVS_ILLUMINANTS', 'TVS_ILLUMINANTS_HUNTERLAB', - 'CCS_LIGHT_SOURCES', - 'SDS_LIGHT_SOURCES', - 'sd_constant', - 'sd_zeros', - 'sd_ones', + 'SD_GAUSSIAN_METHODS', + 'SD_MULTI_LEDS_METHODS', + 'SD_SINGLE_LED_METHODS', 'msds_constant', - 'msds_zeros', 'msds_ones', - 'SD_GAUSSIAN_METHODS', + 'msds_zeros', + 'sd_constant', 'sd_gaussian', - 'sd_gaussian_normal', 'sd_gaussian_fwhm', + 'sd_gaussian_normal', 'sd_gaussian_super_clamped', - 'SD_SINGLE_LED_METHODS', - 'sd_single_led', - 'sd_single_led_Ohno2005', - 'SD_MULTI_LEDS_METHODS', 'sd_multi_leds', 'sd_multi_leds_Ohno2005', - 'SD_TO_XYZ_METHODS', + 'sd_ones', + 'sd_single_led', + 'sd_single_led_Ohno2005', + 'sd_zeros', 'MSDS_TO_XYZ_METHODS', - 'sd_to_XYZ', - 'msds_to_XYZ', + 'SD_TO_XYZ_METHODS', 'SPECTRAL_SHAPE_ASTME308', + 'adjust_tristimulus_weighting_factors_ASTME308', 'handle_spectral_arguments', 'lagrange_coefficients_ASTME2022', - 'tristimulus_weighting_factors_ASTME2022', - 'adjust_tristimulus_weighting_factors_ASTME308', + 'msds_to_XYZ', + 'msds_to_XYZ_ASTME308', + 'msds_to_XYZ_integration', + 'msds_to_XYZ_tristimulus_weighting_factors_ASTME308', + 'sd_to_XYZ', + 'sd_to_XYZ_ASTME308', 'sd_to_XYZ_integration', 'sd_to_XYZ_tristimulus_weighting_factors_ASTME308', - 'sd_to_XYZ_ASTME308', - 'msds_to_XYZ_integration', - 'msds_to_XYZ_ASTME308', + 'tristimulus_weighting_factors_integration', + 'tristimulus_weighting_factors_ASTME2022', 'wavelength_to_XYZ', 'spectral_uniformity', 'BANDPASS_CORRECTION_METHODS', 'bandpass_correction', 'bandpass_correction_Stearns1988', - 'sd_CIE_standard_illuminant_A', - 'sd_CIE_illuminant_D_series', + 'CIE_illuminant_D_series', 'daylight_locus_function', - 'sd_mesopic_luminous_efficiency_function', + 'msds_CIE_illuminant_D_series', + 'sd_CIE_illuminant_D_series', + 'sd_CIE_standard_illuminant_A', 'mesopic_weighting_function', + 'sd_mesopic_luminous_efficiency_function', 'LIGHTNESS_METHODS', + 'intermediate_lightness_function_CIE1976', 'lightness', - 'lightness_Glasser1958', - 'lightness_Wyszecki1963', + 'lightness_Abebe2017', 'lightness_CIE1976', 'lightness_Fairchild2010', 'lightness_Fairchild2011', - 'lightness_Abebe2017', - 'intermediate_lightness_function_CIE1976', + 'lightness_Glasser1958', + 'lightness_Wyszecki1963', 'LUMINANCE_METHODS', + 'intermediate_luminance_function_CIE1976', 'luminance', - 'luminance_Newhall1943', + 'luminance_Abebe2017', 'luminance_ASTMD1535', 'luminance_CIE1976', 'luminance_Fairchild2010', 'luminance_Fairchild2011', - 'luminance_Abebe2017', - 'intermediate_luminance_function_CIE1976', - 'dominant_wavelength', + 'luminance_Newhall1943', + 'colorimetric_purity', 'complementary_wavelength', + 'dominant_wavelength', 'excitation_purity', - 'colorimetric_purity', - 'luminous_flux', - 'luminous_efficiency', 'luminous_efficacy', - 'RGB_10_degree_cmfs_to_LMS_10_degree_cmfs', - 'RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs', - 'RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs', + 'luminous_efficiency', + 'luminous_flux', 'LMS_2_degree_cmfs_to_XYZ_2_degree_cmfs', 'LMS_10_degree_cmfs_to_XYZ_10_degree_cmfs', + 'RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs', + 'RGB_10_degree_cmfs_to_LMS_10_degree_cmfs', + 'RGB_10_degree_cmfs_to_XYZ_10_degree_cmfs', 'WHITENESS_METHODS', 'whiteness', - 'whiteness_Berger1959', - 'whiteness_Taube1960', - 'whiteness_Stensby1968', 'whiteness_ASTME313', - 'whiteness_Ganz1979', + 'whiteness_Berger1959', 'whiteness_CIE2004', + 'whiteness_Ganz1979', + 'whiteness_Stensby1968', + 'whiteness_Taube1960', + 'YELLOWNESS_COEFFICIENTS_ASTME313', 'YELLOWNESS_METHODS', 'yellowness', 'yellowness_ASTMD1925', - 'yellowness_ASTME313_alternative', - 'YELLOWNESS_COEFFICIENTS_ASTME313', - 'yellowness_ASTME313'] + 'yellowness_ASTME313', + 'yellowness_ASTME313_alternative'] **Colour** computations leverage a comprehensive quantity of datasets available in most sub-packages, for example the ``colour.colorimetry.datasets`` defines diff --git a/pyproject.toml b/pyproject.toml index b84b32c19f..9e206365c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,10 @@ optional = [ "trimesh>=4,<5", "xxhash>=3,<4", ] +array-api = [ + "array-api-compat>=1,<2", + "array-api-extra>=0.7,<1", +] docs = [ "biblib-simple", "pydata-sphinx-theme", @@ -132,9 +136,11 @@ addopts = "-n auto --dist=loadscope --durations=5" filterwarnings = [ "ignore::RuntimeWarning", "ignore::pytest.PytestCollectionWarning", - "ignore::colour.utilities.ColourWarning", - "ignore::colour.utilities.ColourRuntimeWarning", - "ignore::colour.utilities.ColourUsageWarning", + # NOTE: The ``colour.utilities.Colour*Warning`` filters are registered in + # the root ``conftest.py`` ``pytest_configure`` hook rather than here: + # resolving an ``ignore::colour...`` ini entry forces ``import colour`` + # during pytest's initial-conftest phase, before *pytest-cov* starts + # recording in each xdist worker, leaving all import-time code untraced. "ignore:Implicit None on return values is deprecated:DeprecationWarning", "ignore:Jupyter is migrating its paths:DeprecationWarning", "ignore:the imp module is deprecated:DeprecationWarning", @@ -146,6 +152,10 @@ filterwarnings = [ "ignore:Matplotlib is currently using agg:UserWarning", "ignore:override the edgecolor or facecolor properties:UserWarning", ] +markers = [ + "mps_tolerance_absolute: override the absolute test tolerance (TOLERANCE_ABSOLUTE_TESTS) under the torch-mps backend, e.g. mps_tolerance_absolute(1e-2).", + "mps_xfail: strict expected failure under the torch-mps backend for tests that cannot pass at float32, e.g. mps_xfail('reason').", +] [tool.ruff] target-version = "py311" diff --git a/tasks.py b/tasks.py index 0ac66eee43..73130664c0 100644 --- a/tasks.py +++ b/tasks.py @@ -237,6 +237,24 @@ def tests(ctx: Context) -> None: ) +@task +def benchmark(ctx: Context, mode: str = "quick") -> None: + """ + Run the cross-backend benchmarks. + + Parameters + ---------- + ctx + Context. + mode + Benchmark mode, either *quick* for a correctness smoke test or *full* + for *HD* timing. + """ + + message_box("Running benchmarks...") + ctx.run(f"python {os.path.join('utilities', 'benchmark.py')} --mode {mode}") + + @task def examples(ctx: Context, plots: bool = False) -> None: """ diff --git a/utilities/benchmark.py b/utilities/benchmark.py new file mode 100644 index 0000000000..de707a6556 --- /dev/null +++ b/utilities/benchmark.py @@ -0,0 +1,2647 @@ +#!/usr/bin/env python +""" +Benchmark +========= + +Cross-backend benchmarks for *Colour*, covering correctness and performance +across *Array API* backends (*NumPy*, *JAX*, *PyTorch CPU*, *PyTorch MPS*). + +Each benchmark suite is a concrete subclass of :class:`BenchmarkSuite` and +emits :class:`BenchmarkResult` records accumulated in a +:class:`BenchmarkReport`. +A :class:`BenchmarkRunner` orchestrates shared execution concerns (backend +dispatch, warmup / timed runs, memory release). + +Currently implemented suites: + +- :class:`BenchmarkSuite_ConversionGraph`, walks the array-path of the + *Colour* automatic conversion graph (iterative methods filtered out + via :attr:`BenchmarkConfiguration.iterative_graph_cases`). +- :class:`BenchmarkSuite_ConversionGraphIterative`, walks the same + graph but yields only methods backed by ``solve_CCT_Newton`` or + ``scipy.spatial.distance.cdist``; measures Python-loop overhead. +- :class:`BenchmarkSuite_Difference`, colour difference functions + (Delta E family). +- :class:`BenchmarkSuite_IntegrationArray`, spectral integration + array-path (``sd_to_XYZ`` / ``msds_to_XYZ`` with raw ``ArrayLike`` + inputs and ``method="Integration"``); dispatches through the *Array + API*. +- :class:`BenchmarkSuite_IntegrationObject`, spectral integration + object-path (``SpectralDistribution`` / ``MultiSpectralDistributions`` + inputs across all methods); numpy-bound by the SD object machinery. +- :class:`BenchmarkSuite_TransferFunction`, CCTF encoding / decoding. +- :class:`BenchmarkSuite_Adaptation`, direct chromatic adaptation + transforms. +- :class:`BenchmarkSuite_Characterisation`, colour correction matrix + methods. +- :class:`BenchmarkSuite_RecoveryArray`, vectorisable spectral + reflectance recovery (*Smits 1999*, *Gaussian*, *Mallett 2019*, + *Jakob 2019* LUT runtime). +- :class:`BenchmarkSuite_RecoveryObject`, per-pixel spectral + reflectance recovery solvers (*Jakob 2019*, *Meng 2015*, *Otsu 2018*). +- :class:`BenchmarkSuite_QualityArray`, batched + :class:`MultiSpectralDistributions` paths for *CRI* / *CFI* / *CQS*; + captures the win from computing the *Planckian* / *CIE D Series* + references via the array kernels (``planck_law``, + ``CIE_illuminant_D_series``) rather than per-entry + :class:`MultiSpectralDistributions` construction. +- :class:`BenchmarkSuite_QualityObject`, light-source quality indices + (CRI / CFI / CQS / SSI) on single :class:`SpectralDistribution` + inputs; numpy-bound by the SD object machinery. +- :class:`BenchmarkSuite_Volume`, gamut volume on ``RGB_Colourspace`` + instances (``Monte Carlo``, ``RGB_colourspace_limits``). +- :class:`BenchmarkSuite_VolumeIterative`, ``scipy.spatial.Delaunay``- + backed gamut containment checks (``is_within_visible_spectrum``, + ``is_within_macadam_limits``). +- :class:`BenchmarkSuite_Phenomena`, sky models and Rayleigh scattering. +- :class:`BenchmarkSuite_TemperatureArray`, closed-form *CIE xy* + <-> ``CCT`` methods that dispatch as one-shot *Array API* operations. +- :class:`BenchmarkSuite_TemperatureIterative`, *CIE xy* <-> ``CCT`` + methods backed by :func:`solve_CCT_Newton` / + :func:`solve_xy_Newton`; iteration cost scales with batch size. +- :class:`BenchmarkSuite_Blindness`, colour-vision deficiency models. +- :class:`BenchmarkSuite_Contrast`, contrast sensitivity functions. +- :class:`BenchmarkSuite_GeneratorsArray`, vectorised spectral + generators (``planck_law``, ``rayleigh_jeans_law``); array-path, + dispatches through the *Array API*. +- :class:`BenchmarkSuite_GeneratorsObject`, per-:class:`SpectralDistribution` + generators (``sd_blackbody``, ``sd_rayleigh_jeans``, + ``sd_CIE_illuminant_D_series``, ``msds_*`` variants); numpy-bound + by the SD object machinery. +- :class:`BenchmarkSuite_Photometry`, photometric measures over a + :class:`SpectralDistribution` (``luminous_flux``, + ``luminous_efficacy``, ``luminous_efficiency``); numpy-bound. + +Usage:: + + uv run python utilities/benchmark.py --mode quick + uv run python utilities/benchmark.py --mode full + uv run python utilities/benchmark.py --mode full \\ + --suites conversion_graph,difference + uv run python utilities/benchmark.py --mode full \\ + --json results.json --csv results.csv +""" + +from __future__ import annotations + +import argparse +import contextlib +import csv +import gc +import json +import logging +import statistics +import sys +import time +import typing +from abc import ABC, abstractmethod +from collections import Counter +from dataclasses import dataclass, field, fields, is_dataclass, replace +from functools import cached_property, partial + +import numpy as np + +from colour.adaptation import CHROMATIC_ADAPTATION_METHODS, chromatic_adaptation +from colour.algebra import vecmul +from colour.appearance import ( + CAM_Specification_CAM16, + CAM_Specification_CIECAM02, + CAM_Specification_CIECAM16, + CAM_Specification_Hellwig2022, + CAM_Specification_Kim2009, + CAM_Specification_sCAM, + CAM_Specification_ZCAM, +) +from colour.blindness import matrix_cvd_Machado2009 +from colour.characterisation import ( + MATRIX_COLOUR_CORRECTION_METHODS, + apply_matrix_colour_correction, + matrix_colour_correction, +) +from colour.colorimetry import ( + LIGHTNESS_METHODS, + LUMINANCE_METHODS, + MSDS_CMFS, + MSDS_TO_XYZ_METHODS, + SD_TO_XYZ_METHODS, + SDS_ILLUMINANTS, + SDS_LIGHT_SOURCES, + WHITENESS_METHODS, + YELLOWNESS_METHODS, + CIE_illuminant_D_series, + MultiSpectralDistributions, + SpectralDistribution, + SpectralShape, + luminous_efficacy, + luminous_efficiency, + luminous_flux, + msds_blackbody, + msds_CIE_illuminant_D_series, + msds_rayleigh_jeans, + msds_to_XYZ, + planck_law, + rayleigh_jeans_law, + sd_blackbody, + sd_CIE_illuminant_D_series, + sd_rayleigh_jeans, + sd_to_XYZ, +) +from colour.contrast import contrast_sensitivity_function +from colour.difference import DELTA_E_METHODS +from colour.graph.conversion import CONVERSION_SPECIFICATIONS_DATA, convert +from colour.models import ( + CCTF_DECODINGS, + CCTF_ENCODINGS, + RGB_COLOURSPACES, + RGB_COLOURSPACE_sRGB, +) +from colour.notation import MUNSELL_VALUE_METHODS +from colour.phenomena import ( + rayleigh_optical_depth, + sd_rayleigh_scattering, + sky_luminance_distribution_CIE2003, + sky_luminance_distribution_overcast_CIE2003, + sky_scattering_indicatrix_CIE2003, +) +from colour.quality import ( + colour_fidelity_index, + colour_quality_scale, + colour_rendering_index, + spectral_similarity_index, +) +from colour.recovery import ( + LUT3D_Jakob2019, + RGB_to_msds_Smits1999, + XYZ_to_sd, + generate_gaussian_basis, +) +from colour.recovery.jakob2019 import SPECTRAL_SHAPE_JAKOB2019 +from colour.recovery.mallett2019 import MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019 +from colour.temperature import ( + CCT_TO_UV_METHODS, + UV_TO_CCT_METHODS, + CCT_to_xy, + xy_to_CCT, +) +from colour.utilities import array_api_enable, set_default_float_dtype +from colour.volume import ( + RGB_colourspace_limits, + RGB_colourspace_volume_MonteCarlo, + is_within_macadam_limits, + is_within_visible_spectrum, +) + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Iterable, Mapping + +jax: typing.Any = None +jnp: typing.Any = None +with contextlib.suppress(ImportError): + import jax + import jax.numpy as jnp + +torch: typing.Any = None +with contextlib.suppress(ImportError): + import torch + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "BenchmarkConfiguration", + "DEFAULT_BENCHMARK_CONFIGURATION", + "BenchmarkResult", + "BackendStatistics", + "BenchmarkReport", + "write_report_json", + "write_report_csv", + "BenchmarkRunner", + "BenchmarkSuite", + "BenchmarkSuite_ConversionGraph", + "BenchmarkSuite_ConversionGraphIterative", + "BenchmarkSuite_Difference", + "BenchmarkSuite_IntegrationArray", + "BenchmarkSuite_IntegrationObject", + "BenchmarkSuite_TransferFunction", + "BenchmarkSuite_Adaptation", + "BenchmarkSuite_Characterisation", + "BenchmarkSuite_RecoveryArray", + "BenchmarkSuite_RecoveryObject", + "BenchmarkSuite_QualityArray", + "BenchmarkSuite_QualityObject", + "BenchmarkSuite_Volume", + "BenchmarkSuite_VolumeIterative", + "BenchmarkSuite_Phenomena", + "BenchmarkSuite_TemperatureArray", + "BenchmarkSuite_TemperatureIterative", + "BenchmarkSuite_Blindness", + "BenchmarkSuite_Contrast", + "BenchmarkSuite_GeneratorsArray", + "BenchmarkSuite_GeneratorsObject", + "BenchmarkSuite_Photometry", + "BENCHMARK_SUITES", + "build_argument_parser", + "main", +] + +LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class BenchmarkConfiguration: + """ + Define static configuration for the benchmark suites. + + Parameters + ---------- + backends + Canonical backend ordering for reporting. + input_resolutions + Pixel counts keyed by input size tag. + skip_sources + Conversion graph source nodes that cannot be exercised with plain + array inputs. + skip_targets + Conversion graph target nodes that produce non-array outputs. + cam_sources + Colour appearance model sources requiring dataclass inputs (i.e. an + instance of :class:`colour.appearance.CAM_Specification_*`). + reduced_size_targets + Conversion graph targets allocating ``O(N * M)`` intermediates; + reduced pixel count is used to avoid OOM errors. + reduced_size_edges + Conversion graph edges backed by iterative solvers that do not + scale to HD inputs. + skip_mps_edges + Conversion graph edges that crash the *PyTorch MPS* runtime. + exception_categories + Ordered ``(code, substrings)`` pairs used by + :meth:`BenchmarkResult.categorise_exception`. + cam_specifications + Ordered ``(source, specification)`` pairs mapping CAM source names to + their specification dataclasses. + edge_method_registries + Conversion graph edges whose ``convert(..., method=...)`` kwarg fans + out across a method registry, yielding one benchmark case per method. + rng_seed + Seed used to build deterministic :class:`numpy.random.Generator` + instances for reproducible benchmark inputs. + + Methods + ------- + - :meth:`~BenchmarkConfiguration.resolution` + - :meth:`~BenchmarkConfiguration.rng` + """ + + backends: tuple[str, ...] = ("numpy", "jax", "torch-cpu", "torch-mps") + + input_resolutions: tuple[tuple[str, int], ...] = ( + ("small", 2), + ("reduced", 10000), + ("hd", 1920 * 1080), + ) + + skip_sources: frozenset[str] = frozenset( + { + "Spectral Distribution", + "Hexadecimal", + "Munsell Colour", + "CSS Color 3", + "Wavelength", + } + ) + + skip_targets: frozenset[str] = frozenset( + { + "Hexadecimal", + "Munsell Colour", + "Spectral Distribution", + "Complementary Wavelength", + "Dominant Wavelength", + "Luminous Flux", + "Luminous Efficiency", + "Luminous Efficacy", + } + ) + + cam_sources: frozenset[str] = frozenset( + { + "CIECAM02", + "CAM16", + "CIECAM16", + "Hellwig 2022", + "Kim 2009", + "sCAM", + "ZCAM", + } + ) + + reduced_size_targets: frozenset[str] = frozenset( + { + "Colorimetric Purity", + "Excitation Purity", + } + ) + + reduced_size_edges: frozenset[tuple[str, str]] = frozenset( + { + ("OSA UCS", "CIE XYZ"), + ("CCT", "CIE UCS uv"), + ("CIE UCS uv", "CCT"), + } + ) + + skip_mps_edges: frozenset[tuple[str, str]] = frozenset( + { + ("OSA UCS", "CIE XYZ"), + } + ) + + exception_categories: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("DEV", ("device",)), + ("TYP", ("must be tensor", "must be torch")), + ("NIM", ("not implemented",)), + ("OVF", ("overflow",)), + ) + + cam_specifications: tuple[tuple[str, type], ...] = ( + ("CAM16", CAM_Specification_CAM16), + ("CIECAM02", CAM_Specification_CIECAM02), + ("CIECAM16", CAM_Specification_CIECAM16), + ("Hellwig 2022", CAM_Specification_Hellwig2022), + ("Kim 2009", CAM_Specification_Kim2009), + ("sCAM", CAM_Specification_sCAM), + ("ZCAM", CAM_Specification_ZCAM), + ) + + edge_method_registries: tuple[tuple[tuple[str, str], typing.Any], ...] = ( + (("CIE XYZ", "Whiteness"), WHITENESS_METHODS), + (("CIE XYZ", "Yellowness"), YELLOWNESS_METHODS), + (("Luminance", "Lightness"), LIGHTNESS_METHODS), + (("Lightness", "Luminance"), LUMINANCE_METHODS), + (("Luminance", "Munsell Value"), MUNSELL_VALUE_METHODS), + (("CIE UCS uv", "CCT"), UV_TO_CCT_METHODS), + (("CCT", "CIE UCS uv"), CCT_TO_UV_METHODS), + ) + + iterative_graph_cases: frozenset[tuple[str, str, str | None]] = frozenset( + { + # ``CIE UCS uv -> CCT`` methods backed by + # :func:`colour.temperature.solve_CCT_Newton`. The Newton + # iteration itself is vectorised across samples but runs + # ``newton_iterations * backtrack_iterations`` forward + # evaluations per call (each of which is a full *Planckian* + # SD generation at HD), so the cost is unrelated to the + # closed-form Array-API path. The other ``uv -> CCT`` + # methods (``Robertson 1968`` isotherm-line broadcast, + # ``Ohno 2013`` batched LUT scan) are fully vectorised and + # stay in the array path. + ("CIE UCS uv", "CCT", "Planck 1900"), + ("CIE UCS uv", "CCT", "Krystek 1985"), + # Per-sample spectral-locus search via + # ``scipy.spatial.distance.cdist`` (forced host round-trip, + # no Array-API dispatch possible). + ("CIE xy", "Colorimetric Purity", None), + ("CIE xy", "Excitation Purity", None), + } + ) + """ + ``(source, target, method_or_None)`` triples whose underlying + implementation is iterative (*solve_CCT_Newton*) or *scipy*-bound + (``scipy.spatial.distance.cdist``). The other slow rows in + ``conversion_graph`` (``Ohno 2013``, all ``Munsell Value`` methods + including ``ASTM D1535`` / ``McCamy 1987``) are fully vectorised + closed-form / batched-LUT and stay in the array path. + """ + + rng_seed: int = 16 + + def resolution(self, size: str) -> int: + """Return the pixel count for a size tag (``small``/``reduced``/``hd``).""" + + return dict(self.input_resolutions)[size] + + def rng(self) -> np.random.Generator: + """Return a fresh deterministic random generator seeded with ``rng_seed``.""" + + return np.random.default_rng(self.rng_seed) + + +DEFAULT_BENCHMARK_CONFIGURATION = BenchmarkConfiguration() +"""Default configuration shared across the benchmark suites.""" + + +@dataclass +class BenchmarkResult: + """ + Define the outcome of benchmarking a single case on a single backend. + + Parameters + ---------- + suite + Name of the suite that produced the result. + backend + Backend identifier (``"numpy"``, ``"jax"``, ``"torch-cpu"``, + ``"torch-mps"``). + status + One of ``"SUCCEEDED"``, ``"SKIPPED"``, or a short error code from + :meth:`BenchmarkResult.categorise_exception`. + label + Human-readable identifier for the case, supplied by the suite. + duration + Best (minimum) of timed runs, in seconds. + error + Truncated error message when :attr:`status` is not ``"SUCCEEDED"``. + metadata + BenchmarkSuite-specific fields (e.g., ``source`` / ``target``, ``method``). + + Methods + ------- + - :meth:`~BenchmarkResult.format_duration` + - :meth:`~BenchmarkResult.categorise_exception` + - :meth:`~BenchmarkResult.failed` + """ + + suite: str + backend: str + status: str + label: str = "" + duration: float = 0.0 + error: str = "" + metadata: dict[str, str] = field(default_factory=dict) + + @staticmethod + def format_duration(duration: float) -> str: + """ + Format a duration given in seconds with an appropriate unit. + + Picks between ``s``, ``ms``, and ``us`` based on magnitude. + """ + + if duration >= 1.0: + return f"{duration:.2f} s" + + milliseconds = duration * 1e3 + if milliseconds >= 10: + return f"{round(milliseconds)} ms" + if milliseconds >= 1: + return f"{milliseconds:.1f} ms" + if milliseconds >= 0.001: + return f"{milliseconds:.2f} ms" + + return f"{milliseconds * 1e3:.2f} us" + + @staticmethod + def categorise_exception(exception: Exception) -> str: + """ + Return a short status code characterising ``exception``. + + Codes: ``DEV`` (device mismatch), ``TYP`` (type mismatch), ``NIM`` + (not implemented), ``OVF`` (overflow), ``OTH`` (other). + """ + + message = str(exception).lower() + for code, substrings in DEFAULT_BENCHMARK_CONFIGURATION.exception_categories: + if any(s in message for s in substrings): + return code + return "OTH" + + @classmethod + def failed( + cls, + suite: str, + backend: str, + label: str, + exception: Exception, + metadata: dict[str, str], + ) -> BenchmarkResult: + """Build a failed-case :class:`BenchmarkResult` and collect memory.""" + + gc.collect() + return cls( + suite=suite, + backend=backend, + status=cls.categorise_exception(exception), + label=label, + error=str(exception)[:80], + metadata=metadata, + ) + + +@dataclass +class BackendStatistics: + """ + Define aggregated statistics for one backend within one suite. + + Parameters + ---------- + backend + Backend identifier. + succeeded + Number of cases that completed successfully. + skipped + Number of cases skipped (e.g., known MPS segfaults). + failed + Number of cases that raised an exception. + duration + Median duration of successful cases, in seconds. + speedup + Geometric mean of per-case speedups vs the *NumPy* baseline. + + Methods + ------- + - :meth:`~BackendStatistics.median` + - :meth:`~BackendStatistics.geometric_mean` + """ + + backend: str + succeeded: int + skipped: int + failed: int + duration: float + speedup: float + + @staticmethod + def median(values: list[float]) -> float: + """Return the median of ``values`` (``0`` for the empty list).""" + + return statistics.median(values) if values else 0.0 + + @staticmethod + def best(values: list[float]) -> float: + """ + Return the minimum of ``values`` (``0`` for the empty list). + + Best-of-N is the conventional aggregator for micro-benchmarks: noise + adds to the timing (cache misses, GC, OS scheduling) and never + subtracts from it, so the fastest observed run is the closest + estimator of the noise-free cost. + """ + + return min(values) if values else 0.0 + + @staticmethod + def geometric_mean(values: list[float]) -> float: + """Return the geometric mean of ``values`` (``0`` for the empty list).""" + + if not values: + return 0.0 + return statistics.geometric_mean(values) + + +@dataclass +class BenchmarkReport: + """ + Define an accumulator of :class:`BenchmarkResult` records across suites. + + Parameters + ---------- + results + Initial list of results (defaults to empty). + + Attributes + ---------- + - :attr:`~BenchmarkReport.suites` + - :attr:`~BenchmarkReport.statistics` + + Methods + ------- + - :meth:`~BenchmarkReport.extend` + - :meth:`~BenchmarkReport.results_for` + - :meth:`~BenchmarkReport.rank_speedups` + - :meth:`~BenchmarkReport.log` + - :meth:`~BenchmarkReport.log_summary` + - :meth:`~BenchmarkReport.log_errors` + - :meth:`~BenchmarkReport.log_speedup_extremes` + + See Also + -------- + - :func:`write_report_json` + - :func:`write_report_csv` + """ + + results: list[BenchmarkResult] = field(default_factory=list) + + @property + def suites(self) -> list[str]: + """Distinct suite names present in the report, in insertion order.""" + + seen: dict[str, None] = {} + for result in self.results: + seen.setdefault(result.suite, None) + return list(seen) + + @cached_property + def statistics(self) -> dict[str, list[BackendStatistics]]: + """Per-suite, per-backend aggregated statistics.""" + + return {suite: self._statistics_for(suite) for suite in self.suites} + + def extend(self, results: list[BenchmarkResult]) -> None: + """Extend the report with ``results`` and invalidate cached views.""" + + self.results.extend(results) + self.__dict__.pop("statistics", None) + + def results_for(self, suite: str) -> list[BenchmarkResult]: + """Return all results belonging to ``suite``.""" + + return [result for result in self.results if result.suite == suite] + + def _statistics_for(self, suite: str) -> list[BackendStatistics]: + """Compute per-backend statistics for ``suite``.""" + + suite_results = self.results_for(suite) + + numpy_durations = { + result.label: result.duration + for result in suite_results + if result.backend == "numpy" and result.status == "SUCCEEDED" + } + + backends = sorted( + {result.backend for result in suite_results}, + key=DEFAULT_BENCHMARK_CONFIGURATION.backends.index, + ) + + per_backend = [] + for backend in backends: + backend_results = [ + result for result in suite_results if result.backend == backend + ] + successful = [ + result + for result in backend_results + if result.status == "SUCCEEDED" and result.duration > 0 + ] + + speedups = [ + numpy_durations[result.label] / result.duration + for result in successful + if numpy_durations.get(result.label, 0) > 0 + ] + + per_backend.append( + BackendStatistics( + backend=backend, + succeeded=sum( + 1 for result in backend_results if result.status == "SUCCEEDED" + ), + skipped=sum( + 1 for result in backend_results if result.status == "SKIPPED" + ), + failed=sum( + 1 + for result in backend_results + if result.status not in ("SUCCEEDED", "SKIPPED") + ), + duration=BackendStatistics.median( + [result.duration for result in successful] + ), + speedup=BackendStatistics.geometric_mean(speedups), + ) + ) + return per_backend + + def rank_speedups( + self, + suite: str, + backend: str, + ) -> list[tuple[float, BenchmarkResult]]: + """Return per-case ``(speedup, result)`` pairs sorted descending.""" + + suite_results = self.results_for(suite) + numpy_durations = { + result.label: result.duration + for result in suite_results + if result.backend == "numpy" and result.status == "SUCCEEDED" + } + + ranked = [ + (numpy_durations[result.label] / result.duration, result) + for result in suite_results + if result.backend == backend + and result.status == "SUCCEEDED" + and result.duration > 0 + and numpy_durations.get(result.label, 0) > 0 + ] + return sorted(ranked, key=lambda x: -x[0]) + + def log(self) -> None: + """Log summary, errors, and speedup extremes for every suite.""" + + for suite in self.suites: + LOGGER.info("") + LOGGER.info("BenchmarkSuite: %s", suite) + self.log_summary(suite) + self.log_errors(suite) + self.log_speedup_extremes(suite) + + def log_summary(self, suite: str) -> None: + """Log the per-backend summary table for ``suite``.""" + + LOGGER.info( + "%-12s %9s %7s %6s %10s %10s", + "Backend", + "Succeeded", + "Skipped", + "Failed", + "Median", + "Geo-mean", + ) + LOGGER.info("-" * 58) + + for statistic in self.statistics[suite]: + duration = ( + BenchmarkResult.format_duration(statistic.duration) + if statistic.duration > 0 + else "-" + ) + speedup = f"{statistic.speedup:.1f}x" if statistic.speedup > 0 else "-" + LOGGER.info( + "%-12s %9d %7d %6d %10s %10s", + statistic.backend, + statistic.succeeded, + statistic.skipped, + statistic.failed, + duration, + speedup, + ) + + def log_errors(self, suite: str) -> None: + """Log a compact breakdown of failed cases within ``suite``.""" + + errors = [ + result + for result in self.results_for(suite) + if result.status not in ("SUCCEEDED", "SKIPPED") + ] + if not errors: + return + + LOGGER.info("Errors (%d):", len(errors)) + by_category = Counter((result.status, result.backend) for result in errors) + for (category, backend), count in by_category.most_common(): + examples = [ + result + for result in errors + if result.status == category and result.backend == backend + ][:3] + labels = ", ".join(result.label for result in examples) + LOGGER.info(" %s [%s] x%d: %s", category, backend, count, labels) + + def log_speedup_extremes(self, suite: str) -> None: + """Log the top-10 and bottom-10 speedups per backend within ``suite``.""" + + backends = sorted( + { + result.backend + for result in self.results_for(suite) + if result.backend != "numpy" + }, + key=DEFAULT_BENCHMARK_CONFIGURATION.backends.index, + ) + + def _format(entries: list[tuple[float, BenchmarkResult]]) -> None: + for speedup, result in entries: + LOGGER.info( + " %6.1fx %s (%s)", + speedup, + result.label, + BenchmarkResult.format_duration(result.duration), + ) + + for backend in backends: + ranked = self.rank_speedups(suite, backend) + if not ranked: + continue + + LOGGER.info("Top 10 speedups (%s):", backend) + _format(ranked[:10]) + + LOGGER.info("Bottom 10 (%s):", backend) + _format(ranked[-10:]) + + +def write_report_json(report: BenchmarkReport, path: str) -> None: + """Write a machine-readable JSON ``report`` to ``path`` (durations in seconds).""" + + document = { + "generated": time.strftime("%Y-%m-%dT%H:%M:%S"), + "suites": [ + { + "name": suite, + "summary": [ + { + "backend": statistic.backend, + "succeeded": statistic.succeeded, + "skipped": statistic.skipped, + "failed": statistic.failed, + "duration": statistic.duration, + "speedup": statistic.speedup, + } + for statistic in report.statistics[suite] + ], + "results": [ + { + "label": result.label, + "backend": result.backend, + "status": result.status, + "duration": result.duration, + "error": result.error, + "metadata": result.metadata, + } + for result in report.results_for(suite) + ], + } + for suite in report.suites + ], + } + + with open(path, "w", encoding="utf-8") as f: + json.dump(document, f, indent=2) + LOGGER.info("JSON report written to %s", path) + + +def write_report_csv(report: BenchmarkReport, path: str) -> None: + """Write a flat CSV of all raw ``report`` results to ``path``.""" + + with open(path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["suite", "label", "backend", "status", "duration", "error"]) + for result in report.results: + writer.writerow( + [ + result.suite, + result.label, + result.backend, + result.status, + result.duration, + result.error, + ] + ) + LOGGER.info("CSV report written to %s", path) + + +class BenchmarkRunner: + """ + Define a runner orchestrating the execution of one or more suites. + + The runner owns shared execution concerns (backend dispatch, warmup / + timed runs, memory release), while each :class:`BenchmarkSuite` defines + what to benchmark; :meth:`run` iterates over them. + + Parameters + ---------- + mode + ``"quick"`` for correctness smoke tests with small inputs, + ``"full"`` for HD timing. + backends + Backend identifiers to measure; defaults to + :attr:`BenchmarkConfiguration.backends`. Unavailable backends are + silently dropped via :meth:`available_backends`. + warmup + Untimed iterations before timing starts. + runs + Timed iterations; the best (minimum) is recorded per case. + edges_filter + Optional ``"src->tgt,src->tgt"`` filter consumed by + :class:`BenchmarkSuite_ConversionGraph`. + + Attributes + ---------- + - :attr:`~BenchmarkRunner.resolved_backends` + - :attr:`~BenchmarkRunner.size` + - :attr:`~BenchmarkRunner.resolution` + + Methods + ------- + - :meth:`~BenchmarkRunner.__init__` + - :meth:`~BenchmarkRunner.available_backends` + - :meth:`~BenchmarkRunner.convert_to_backend` + - :meth:`~BenchmarkRunner.log_progress` + - :meth:`~BenchmarkRunner.run_case` + - :meth:`~BenchmarkRunner.run_cases` + - :meth:`~BenchmarkRunner.run` + - :meth:`~BenchmarkRunner.parse_suites` + + Examples + -------- + >>> runner = BenchmarkRunner(mode="quick", backends=["numpy"]) + >>> report = runner.run([BenchmarkSuite_Difference(runner)]) # doctest: +SKIP + >>> report.log() # doctest: +SKIP + """ + + def __init__( + self, + mode: str = "quick", + backends: list[str] | None = None, + warmup: int = 1, + runs: int = 1, + edges_filter: str | None = None, + ) -> None: + self.mode = mode + self.backends = backends + self.warmup = warmup + self.runs = runs + self.edges_filter = edges_filter + + @cached_property + def resolved_backends(self) -> list[str]: + """Requested backends filtered to those available on this system.""" + + return self.available_backends( + self.backends or list(DEFAULT_BENCHMARK_CONFIGURATION.backends) + ) + + @cached_property + def size(self) -> str: + """Input-size tag (``"small"`` or ``"hd"``) matching :attr:`mode`.""" + + return "small" if self.mode == "quick" else "hd" + + @cached_property + def resolution(self) -> int: + """Pixel count associated with :attr:`size`.""" + + return DEFAULT_BENCHMARK_CONFIGURATION.resolution(self.size) + + # -- Backends ---------------------------------------------------------- + + @staticmethod + def available_backends(requested: list[str]) -> list[str]: + """Return the subset of ``requested`` backends available on this system.""" + + available = [] + for backend in requested: + if ( + backend == "numpy" + or (backend == "jax" and jax is not None) + or (backend == "torch-cpu" and torch is not None) + ): + available.append(backend) + elif backend == "torch-mps" and torch is not None: + try: + if torch.backends.mps.is_available(): + available.append(backend) + except AttributeError: + continue + return available + + @staticmethod + def convert_to_backend(data: typing.Any, backend: str) -> typing.Any: + """Promote a *NumPy* array to ``backend``, leaving other inputs unchanged.""" + + if isinstance(data, tuple): + return tuple( + BenchmarkRunner.convert_to_backend(value, backend) for value in data + ) + + if is_dataclass(data) and not isinstance(data, type): + promoted = { + f.name: BenchmarkRunner.convert_to_backend( + getattr(data, f.name), backend + ) + for f in fields(data) + } + return replace(data, **promoted) + + if not isinstance(data, np.ndarray) or backend == "numpy": + return data + + if backend == "jax" and jax is not None and jnp is not None: + jax.config.update("jax_enable_x64", True) + data = jnp.array(data) + elif backend == "torch-cpu" and torch is not None: + data = torch.from_numpy(data.copy()) + elif backend == "torch-mps" and torch is not None: + data = torch.from_numpy(data.astype(np.float32).copy()).to("mps") + + return data + + # -- Primitives -------------------------------------------------------- + + def log_progress(self, done: int, total: int, result: BenchmarkResult) -> None: + """Log a compact progress line for a finished case.""" + + status = ( + BenchmarkResult.format_duration(result.duration) + if result.status == "SUCCEEDED" + else result.status + ) + LOGGER.info( + "[%d/%d] %s: %s %s", + done, + total, + result.backend, + result.label, + status, + ) + + def run_case( + self, + suite: str, + backend: str, + label: str, + metadata: dict[str, str], + operation: Callable[[], object], + ) -> BenchmarkResult: + """Run ``operation`` ``warmup`` + ``runs`` times, recording the best.""" + + # ``torch-mps`` is the only backend with an asynchronous queue we + # need to drain to time fairly; ``numpy`` / ``jax`` / ``torch-cpu`` + # are synchronous from the host's perspective. + synchronise = backend == "torch-mps" and torch is not None + + times: list[float] = [] + try: + for _ in range(self.warmup): + with array_api_enable(backend != "numpy"): + operation() + if synchronise: + torch.mps.synchronize() + + for _ in range(self.runs): + if synchronise: + torch.mps.synchronize() + start = time.perf_counter() + with array_api_enable(backend != "numpy"): + operation() + if synchronise: + torch.mps.synchronize() + times.append(time.perf_counter() - start) + # Bench isolation: a failing case (any exception other than + # ``KeyboardInterrupt`` / ``SystemExit``) is recorded and the + # runner continues with the next case. The broad catch is + # intentional. + except Exception as exception: # noqa: BLE001 + return BenchmarkResult.failed(suite, backend, label, exception, metadata) + + gc.collect() + if synchronise: + torch.mps.empty_cache() + + return BenchmarkResult( + suite=suite, + backend=backend, + status="SUCCEEDED", + label=label, + duration=BackendStatistics.best(times), + metadata=metadata, + ) + + def run_cases(self, suite: BenchmarkSuite) -> list[BenchmarkResult]: + """Iterate backends x suite cases, tracking progress and MPS dtype.""" + + results: list[BenchmarkResult] = [] + total = suite.case_count * len(self.resolved_backends) + done = 0 + + for backend in self.resolved_backends: + if backend == "torch-mps": + set_default_float_dtype(np.float32) + + for label, metadata, operation in suite.cases(backend): + done += 1 + if operation is None: + result = BenchmarkResult( + suite=suite.NAME, + backend=backend, + status="SKIPPED", + label=label, + metadata=metadata, + ) + else: + result = self.run_case( + suite.NAME, backend, label, metadata, operation + ) + results.append(result) + self.log_progress(done, total, result) + + if backend == "torch-mps": + set_default_float_dtype(np.float64) + + return results + + # -- Orchestration ----------------------------------------------------- + + def run(self, suites: Iterable[BenchmarkSuite]) -> BenchmarkReport: + """Run ``suites`` and return an accumulated :class:`BenchmarkReport`.""" + + report = BenchmarkReport() + for suite in suites: + suite.log_header() + report.extend(self.run_cases(suite)) + return report + + @classmethod + def parse_suites(cls, value: str, runner: BenchmarkRunner) -> list[BenchmarkSuite]: + """Resolve a comma-separated suite name string to a list of instances.""" + + selected = [] + for name in value.split(","): + stripped = name.strip() + if stripped not in BENCHMARK_SUITES: + message = ( + f"Unknown suite: {stripped!r}. Available: {list(BENCHMARK_SUITES)}" + ) + raise ValueError(message) + selected.append(BENCHMARK_SUITES[stripped](runner)) + return selected + + +class BenchmarkSuite(ABC): + """ + Abstract base class for a benchmark suite. + + A suite describes WHAT to benchmark: its name, how many cases it runs, + and how to generate ``(label, metadata, operation)`` tuples per backend. + Execution is delegated to :meth:`BenchmarkRunner.run_cases`. + + Parameters + ---------- + runner + Runner providing backend dispatch, warmup / run counts, and size. + + Attributes + ---------- + - :attr:`~BenchmarkSuite.NAME` + - :attr:`~BenchmarkSuite.case_count` + + Methods + ------- + - :meth:`~BenchmarkSuite.cases` + """ + + NAME: str = "" + + def __init__(self, runner: BenchmarkRunner) -> None: + self._runner = runner + self._rng = DEFAULT_BENCHMARK_CONFIGURATION.rng() + + @property + @abstractmethod + def case_count(self) -> int: + """Total number of cases the suite will emit per backend.""" + + @abstractmethod + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """ + Yield ``(label, metadata, operation)`` tuples for ``backend``. + + An ``operation`` of ``None`` signals a preemptive skip: the runner + records a :class:`BenchmarkResult` with ``status="SKIPPED"`` instead + of executing anything. + """ + + def log_header(self) -> None: + """Log a one-line header describing the suite before execution.""" + + LOGGER.info("BenchmarkSuite: %s | Cases: %d", self.NAME, self.case_count) + + +# ----------------------------------------------------------------------------- +# Concrete suites +# ----------------------------------------------------------------------------- + + +class BenchmarkSuite_ConversionGraph(BenchmarkSuite): + """ + Walk the array-path of the *Colour* automatic conversion graph. + + Methods backed by ``solve_CCT_Newton`` or + ``scipy.spatial.distance.cdist`` are filtered out via + :attr:`BenchmarkConfiguration.iterative_graph_cases` and benched + separately in :class:`BenchmarkSuite_ConversionGraphIterative`. The + geo-mean here therefore reflects *Array API*-dispatchable closed-form + edges only. + """ + + NAME = "conversion_graph" + + @cached_property + def edges(self) -> list[tuple[str, str]]: + """``(source, target)`` edges honouring ``edges_filter`` / skip lists.""" + + filter_set: frozenset[tuple[str, str]] | None = None + if self._runner.edges_filter: + parsed = set() + for entry in self._runner.edges_filter.split(","): + parts = entry.strip().split("->") + if len(parts) == 2: + parsed.add((parts[0].strip(), parts[1].strip())) + filter_set = frozenset(parsed) + + edges: list[tuple[str, str]] = [] + for source, target, _fn in CONVERSION_SPECIFICATIONS_DATA: + if ( + source in DEFAULT_BENCHMARK_CONFIGURATION.skip_sources + or target in DEFAULT_BENCHMARK_CONFIGURATION.skip_targets + ): + continue + if filter_set is not None and (source, target) not in filter_set: + continue + edges.append((source, target)) + return edges + + @cached_property + def edge_method_registries(self) -> dict[tuple[str, str], list[str]]: + """Per-edge method lists, deduped from each registry.""" + + edges: dict[tuple[str, str], list[str]] = {} + for edge, registry in DEFAULT_BENCHMARK_CONFIGURATION.edge_method_registries: + seen: set[int] = set() + methods: list[str] = [] + for method, function in registry.items(): + key = id(function) + if key not in seen: + seen.add(key) + methods.append(method) + edges[edge] = methods + return edges + + def _is_iterative_case(self, source: str, target: str, method: str | None) -> bool: + """Return whether ``(source, target, method)`` is iterative-bound.""" + + return ( + source, + target, + method, + ) in DEFAULT_BENCHMARK_CONFIGURATION.iterative_graph_cases + + def _include_case(self, source: str, target: str, method: str | None) -> bool: + """ + Per-class filter on whether a case belongs in this suite. + + Overridden by :class:`BenchmarkSuite_ConversionGraphIterative` to + flip the filter direction. + """ + + return not self._is_iterative_case(source, target, method) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + total = 0 + for source, target in self.edges: + methods = self.edge_method_registries.get((source, target)) + if methods: + total += sum( + 1 for m in methods if self._include_case(source, target, m) + ) + elif self._include_case(source, target, None): + total += 1 + return total + + def log_header(self) -> None: + """See :class:`BenchmarkSuite`.""" + + LOGGER.info( + "BenchmarkSuite: %s | Size: %s | Cases: %d", + self.NAME, + self._runner.size, + self.case_count, + ) + + def generate_input(self, source: str, size: str) -> object: + """Generate a representative input for a graph source node. + + A fresh seeded ``Generator`` is built per call rather than reusing + ``self._rng`` so each ``(source, size)`` pair produces the same + values on every backend pass, keeping cross-backend timings + directly comparable. The trade-off is that distinct sources at + the same shape see identical random draws; that is acceptable + because each edge is timed in isolation. + """ + + n = DEFAULT_BENCHMARK_CONFIGURATION.resolution(size) + rng = DEFAULT_BENCHMARK_CONFIGURATION.rng() + + if source in DEFAULT_BENCHMARK_CONFIGURATION.cam_sources: + return dict(DEFAULT_BENCHMARK_CONFIGURATION.cam_specifications)[source]( + J=rng.random(n) * 100, + M=rng.random(n), + h=rng.random(n) * 360, + ) + + if source == "CMYK": + return rng.random((n, 4)) + + if source.startswith("CCT") or source.endswith((" xy", " uv")): + return rng.random((n, 2)) + + return rng.random((n, 3)) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + for source, target in self.edges: + base_metadata = {"source": source, "target": target} + + if ( + backend == "torch-mps" + and (source, target) in DEFAULT_BENCHMARK_CONFIGURATION.skip_mps_edges + ): + yield f"{source} -> {target}", base_metadata, None + continue + + edge_size = ( + "reduced" + if self._runner.size == "hd" + and ( + target in DEFAULT_BENCHMARK_CONFIGURATION.reduced_size_targets + or (source, target) + in DEFAULT_BENCHMARK_CONFIGURATION.reduced_size_edges + ) + else self._runner.size + ) + data = self.generate_input(source, edge_size) + if backend != "numpy": + data = BenchmarkRunner.convert_to_backend(data, backend) + + methods = self.edge_method_registries.get((source, target)) + if not methods: + if not self._include_case(source, target, None): + continue + yield ( + f"{source} -> {target}", + base_metadata, + lambda x=data, s=source, t=target: convert(x, s, t), + ) + continue + + for method in methods: + if not self._include_case(source, target, method): + continue + yield ( + f"{source} -> {target} ({method})", + {**base_metadata, "method": method}, + lambda x=data, s=source, t=target, m=method: convert( + x, s, t, method=m + ), + ) + + +class BenchmarkSuite_ConversionGraphIterative(BenchmarkSuite_ConversionGraph): + """Iterative-method counterpart to :class:`BenchmarkSuite_ConversionGraph`. + + Walks the same graph but yields only the cases flagged in + :attr:`BenchmarkConfiguration.iterative_graph_cases`: methods backed + by ``solve_CCT_Newton`` (``Planck 1900``, ``Krystek 1985``) or + ``scipy.spatial.distance.cdist`` (``Colorimetric Purity``, + ``Excitation Purity``). The geo-mean here measures Python-loop + overhead, not *Array API* dispatch; a faster backend cannot + accelerate these methods without an algorithmic restructure. + """ + + NAME = "conversion_graph_iterative" + + def _include_case(self, source: str, target: str, method: str | None) -> bool: + """Invert the parent filter: yield only iterative-bound cases.""" + + return self._is_iterative_case(source, target, method) + + +class BenchmarkSuite_Difference(BenchmarkSuite): + """Benchmark every Delta E function on an ``(N, 3)`` pair.""" + + NAME = "difference" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._a_np = self._rng.random((runner.resolution, 3)) * 100 + self._b_np = self._rng.random((runner.resolution, 3)) * 100 + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(DELTA_E_METHODS) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + a = BenchmarkRunner.convert_to_backend(self._a_np, backend) + b = BenchmarkRunner.convert_to_backend(self._b_np, backend) + for method, function in DELTA_E_METHODS.items(): + yield ( + method, + {"method": method}, + lambda function=function, x=a, y=b: function(x, y), + ) + + +class BenchmarkSuite_IntegrationArray(BenchmarkSuite): + """ + Benchmark vectorised spectral integration via the array-path of + ``sd_to_XYZ`` / ``msds_to_XYZ`` with raw ``ArrayLike`` inputs and + ``method="Integration"``. + + Both cases dispatch through the *Array API* and follow the input + backend; ``msds_to_XYZ array`` exercises an HD multi-spectral image + at 1 nm resolution and is the integration suite's headline + backend-acceleration case. + """ + + NAME = "integration_array" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._shape = SpectralShape(380, 780, 1) + self._cmfs = ( + MSDS_CMFS["CIE 1931 2 Degree Standard Observer"].copy().align(self._shape) + ) + self._illuminant = SDS_ILLUMINANTS["D65"].copy().align(self._shape) + n_wavelengths = self._shape.wavelengths.shape[0] + self._sd_values_np = self._rng.random(n_wavelengths) + self._msds_values_np = self._rng.random((runner.resolution, n_wavelengths)) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 2 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + sd_array = BenchmarkRunner.convert_to_backend(self._sd_values_np, backend) + msds_array = BenchmarkRunner.convert_to_backend(self._msds_values_np, backend) + + yield ( + "sd_to_XYZ array (Integration)", + {"operation": "sd_to_XYZ array", "method": "Integration"}, + lambda x=sd_array: sd_to_XYZ( + x, + cmfs=self._cmfs, + illuminant=self._illuminant, + method="Integration", + shape=self._shape, + ), + ) + yield ( + "msds_to_XYZ array (Integration)", + {"operation": "msds_to_XYZ array", "method": "Integration"}, + lambda x=msds_array: msds_to_XYZ( + x, + cmfs=self._cmfs, + illuminant=self._illuminant, + method="Integration", + shape=self._shape, + ), + ) + + +class BenchmarkSuite_IntegrationObject(BenchmarkSuite): + """ + Benchmark per-:class:`SpectralDistribution` integration via the + object-path of ``sd_to_XYZ`` / ``msds_to_XYZ`` across every registered + method. + + The :class:`SpectralDistribution` and + :class:`MultiSpectralDistributions` instances are constructed per + backend with backend-typed values, so the integration helpers + dispatch through the *Array API* and follow the input backend rather + than collapsing to a numpy-only baseline. + """ + + NAME = "integration_object" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._shape = SpectralShape(380, 780, 1) + self._cmfs = ( + MSDS_CMFS["CIE 1931 2 Degree Standard Observer"].copy().align(self._shape) + ) + self._illuminant = SDS_ILLUMINANTS["D65"].copy().align(self._shape) + n_wavelengths = self._shape.wavelengths.shape[0] + # Small ``MSDS`` matching a colour-checker chart in ``quick`` + # mode and a small spectral image in ``full`` mode. + n_msds = 24 if runner.mode == "quick" else 1024 + self._sd_values_np = self._rng.random(n_wavelengths) + self._msds_values_np = self._rng.random((n_wavelengths, n_msds)) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(SD_TO_XYZ_METHODS) + len(MSDS_TO_XYZ_METHODS) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + sd_values = BenchmarkRunner.convert_to_backend(self._sd_values_np, backend) + msds_values = BenchmarkRunner.convert_to_backend(self._msds_values_np, backend) + with array_api_enable(backend != "numpy"): + sd = SpectralDistribution(sd_values, self._shape.wavelengths) + msds = MultiSpectralDistributions(msds_values, self._shape.wavelengths) + + for method in SD_TO_XYZ_METHODS: + yield ( + f"sd_to_XYZ ({method})", + {"operation": "sd_to_XYZ", "method": method}, + lambda method=method, x=sd: sd_to_XYZ( + x, + cmfs=self._cmfs, + illuminant=self._illuminant, + method=method, + ), + ) + for method in MSDS_TO_XYZ_METHODS: + yield ( + f"msds_to_XYZ ({method})", + {"operation": "msds_to_XYZ", "method": method}, + lambda method=method, x=msds: msds_to_XYZ( + x, + cmfs=self._cmfs, + illuminant=self._illuminant, + method=method, + ), + ) + + +class BenchmarkSuite_TransferFunction(BenchmarkSuite): + """Benchmark every CCTF encoding and decoding across backends.""" + + NAME = "transfer_function" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._a_np = self._rng.random(runner.resolution) + self._directions: list[tuple[str, Mapping[str, Callable[..., object]]]] = [ + ("encoding", CCTF_ENCODINGS), + ("decoding", CCTF_DECODINGS), + ] + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return sum(len(mapping) for _, mapping in self._directions) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + a = BenchmarkRunner.convert_to_backend(self._a_np, backend) + for direction, mapping in self._directions: + for function_name, function in mapping.items(): + yield ( + f"{direction} {function_name}", + {"direction": direction, "function": function_name}, + lambda function=function, x=a: function(x), + ) + + +class BenchmarkSuite_Adaptation(BenchmarkSuite): + """Benchmark direct chromatic adaptation (matrix build + apply).""" + + NAME = "adaptation" + + _EXTRA_KWARGS: typing.ClassVar[dict[str, dict[str, float]]] = { + "CIE 1994": {"Y_o": 0.2, "E_o1": 1000.0, "E_o2": 1000.0}, + "CMCCAT2000": {"L_A1": 200.0, "L_A2": 200.0}, + "Fairchild 1990": {"Y_n": 200.0}, + "Li 2025": {"L_A": 200.0, "F_surround": 1.0}, + } + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._XYZ_np = self._rng.random((runner.resolution, 3)) + self._XYZ_w_np = np.array([0.95045593, 1.0, 1.08905775]) + self._XYZ_wr_np = np.array([0.96429568, 1.0, 0.82510460]) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(CHROMATIC_ADAPTATION_METHODS) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + XYZ = BenchmarkRunner.convert_to_backend(self._XYZ_np, backend) + XYZ_w = BenchmarkRunner.convert_to_backend(self._XYZ_w_np, backend) + XYZ_wr = BenchmarkRunner.convert_to_backend(self._XYZ_wr_np, backend) + for method in CHROMATIC_ADAPTATION_METHODS: + yield ( + f"CAT {method}", + {"method": method}, + partial( + chromatic_adaptation, + XYZ, + XYZ_w, + XYZ_wr, + method=method, + **self._EXTRA_KWARGS.get(method, {}), + ), + ) + + +class BenchmarkSuite_Characterisation(BenchmarkSuite): + """Benchmark colour correction matrix methods (build + apply).""" + + NAME = "characterisation" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._M_T_np = self._rng.random((24, 3)) + self._M_R_np = self._rng.random((24, 3)) + self._RGB_np = self._rng.random((runner.resolution, 3)) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(MATRIX_COLOUR_CORRECTION_METHODS) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + M_T = BenchmarkRunner.convert_to_backend(self._M_T_np, backend) + M_R = BenchmarkRunner.convert_to_backend(self._M_R_np, backend) + RGB = BenchmarkRunner.convert_to_backend(self._RGB_np, backend) + for method in MATRIX_COLOUR_CORRECTION_METHODS: + yield ( + f"CCM {method}", + {"method": method}, + lambda method=method, M_T=M_T, M_R=M_R, RGB=RGB: ( + apply_matrix_colour_correction( + RGB, + matrix_colour_correction(M_T, M_R, method=method), + method=method, + ) + ), + ) + + +class BenchmarkSuite_RecoveryArray(BenchmarkSuite): + """ + Benchmark vectorisable spectral recovery methods on an ``(N, 3)`` + *RGB* batch sized by :attr:`BenchmarkRunner.resolution`. + + Covers *Smits 1999*, *Gaussian*, *Mallett 2019* (basis-expansion + methods) and the *Jakob 2019* LUT runtime path + (:class:`LUT3D_Jakob2019.RGB_to_coefficients`), which all natively + support batched array input. The *Jakob 2019* offline solver + (:func:`XYZ_to_sd_Jakob2019`) is benched separately in + :class:`BenchmarkSuite_RecoveryObject`. + """ + + NAME = "recovery_array" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._RGB_batch_np = self._rng.random((runner.resolution, 3)) * 0.5 + self._basis_functions_mallett_np = np.asarray( + MSDS_BASIS_FUNCTIONS_sRGB_MALLETT2019.values + ) + + # ``LUT3D_Jakob2019.generate`` is offline (one optimisation per + # grid cell); a small grid at 10 nm step keeps bench-time setup + # interactive. Runtime ``RGB_to_coefficients`` cost is + # independent of grid size for HD batches. + shape = SpectralShape( + SPECTRAL_SHAPE_JAKOB2019.start, SPECTRAL_SHAPE_JAKOB2019.end, 10 + ) + cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"].copy().align(shape) + illuminant = SDS_ILLUMINANTS["D65"].copy().align(shape) + self._jakob2019_lut = LUT3D_Jakob2019() + self._jakob2019_lut.generate(RGB_COLOURSPACE_sRGB, cmfs, illuminant, size=5) + + @cached_property + def _gaussian_basis(self) -> MultiSpectralDistributions: + """Return a 10nm *Gaussian* recovery basis for ``RGB_to_msds_Gaussian``. + + The default ``MSDS_GAUSSIAN_BASIS`` ships at 1nm + (``SPECTRAL_SHAPE_DEFAULT``, 421 wavelengths) which produces a + ~7 GB float64 output at HD resolution. The basis spectra are + smooth clamped Gaussians with no features narrower than ~30nm, + so 10nm sampling is colorimetrically equivalent and an order + of magnitude cheaper in memory bandwidth; representative of + how callers actually use the recovery for image reproduction. + Built lazily so unrelated suite invocations don't pay for it. + """ + + return generate_gaussian_basis(SpectralShape(360, 780, 10)) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 4 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + RGB_batch = BenchmarkRunner.convert_to_backend(self._RGB_batch_np, backend) + basis_mallett = BenchmarkRunner.convert_to_backend( + self._basis_functions_mallett_np, backend + ) + + yield ( + "RGB_to_msds_Smits1999", + {"method": "Smits 1999"}, + lambda x=RGB_batch: RGB_to_msds_Smits1999(x, as_array=True), + ) + # ``RGB_to_msds_Gaussian`` is a thin wrapper over + # ``RGB_to_msds_Smits1999`` that pins ``MSDS_GAUSSIAN_BASIS`` at + # the 1nm module default. We call the underlying function + # directly with a 10nm basis so the bench measures algorithm + # cost rather than the 1nm memory-bandwidth wall. + yield ( + "RGB_to_msds_Gaussian (10nm basis)", + {"method": "Gaussian"}, + lambda x=RGB_batch, b=self._gaussian_basis: RGB_to_msds_Smits1999( + x, b, as_array=True + ), + ) + yield ( + "RGB_to_msds_Mallett2019", + {"method": "Mallett 2019"}, + lambda x=RGB_batch, b=basis_mallett: x @ b.T, + ) + yield ( + "LUT3D_Jakob2019.RGB_to_coefficients", + {"method": "Jakob 2019 LUT"}, + lambda lut=self._jakob2019_lut, x=RGB_batch: lut.RGB_to_coefficients(x), + ) + + +class BenchmarkSuite_RecoveryObject(BenchmarkSuite): + """ + Benchmark per-pixel spectral recovery solvers on a single + ``(3,)`` *CIE XYZ* input. + + Covers *Jakob 2019*, *Meng 2015*, and *Otsu 2018*: per-pixel + optimisers / decision-tree dispatch that do not vectorise across + samples. + """ + + NAME = "recovery_object" + + _PER_PIXEL_METHODS: typing.ClassVar[tuple[str, ...]] = ( + "Jakob 2019", + "Meng 2015", + "Otsu 2018", + ) + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._XYZ_np = np.array([0.21, 0.18, 0.08]) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(self._PER_PIXEL_METHODS) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + XYZ = BenchmarkRunner.convert_to_backend(self._XYZ_np, backend) + for method in self._PER_PIXEL_METHODS: + yield ( + f"XYZ_to_sd ({method})", + {"method": method}, + lambda method=method, x=XYZ: XYZ_to_sd(x, method=method), + ) + + +class BenchmarkSuite_QualityArray(BenchmarkSuite): + """ + Benchmark light-source quality indices on batched + :class:`MultiSpectralDistributions` inputs. + + Covers the *CRI*, *CFI* (*CIE 2017* and *ANSI/IES TM-30-18*) and + *CQS* batch paths. ``spectral_similarity_index`` accepts a single + :class:`SpectralDistribution` only and is not exercised here. The + batch path computes the *Planckian* / *CIE D Series* references via + the array kernels (``planck_law``, ``CIE_illuminant_D_series``) + rather than building an :class:`MSDS` for each batch entry, so this + suite captures the win from skipping that construction. The + :class:`MultiSpectralDistributions` values are promoted to the + requested backend in :meth:`cases` so that *Array API* dispatch + propagates through the entire pipeline. + """ + + NAME = "quality_array" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + n = 24 if runner.mode == "quick" else 1024 + self._sd_template = SDS_LIGHT_SOURCES["Cool White FL"] + # Synthetic batch: ``sd_template`` replicated and perturbed by + # a small uniform jitter so the *CCT* solver stays well-posed + # while the test exercises the full vectorised pipeline. + self._values_np = self._rng.random( + (self._sd_template.values.shape[0], n) + ) * 0.05 + (self._sd_template.values[:, None] * 0.95) + self._labels = [f"{self._sd_template.name} #{i}" for i in range(n)] + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 4 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + # The :class:`MultiSpectralDistributions` constructor calls + # ``as_float_array`` on the input values which falls back to + # ``np.asarray`` outside the *Array API* dispatch context; + # building the *MSDS* inside ``array_api_enable`` keeps backend + # tensors (notably *PyTorch MPS*) without forcing a host + # round-trip. + values = BenchmarkRunner.convert_to_backend(self._values_np, backend) + with array_api_enable(backend != "numpy"): + msds_test = MultiSpectralDistributions( + values, self._sd_template.wavelengths, labels=self._labels + ) + + yield ( + "colour_rendering_index", + {"function": "colour_rendering_index"}, + lambda: colour_rendering_index(msds_test), + ) + yield ( + "colour_fidelity_index_CIE2017", + {"function": "colour_fidelity_index_CIE2017"}, + lambda: colour_fidelity_index(msds_test), # pyright: ignore + ) + yield ( + "colour_fidelity_index_ANSIIESTM3018", + {"function": "colour_fidelity_index_ANSIIESTM3018"}, + lambda: colour_fidelity_index( + msds_test, # pyright: ignore + method="ANSI/IES TM-30-18", + ), + ) + yield ( + "colour_quality_scale", + {"function": "colour_quality_scale"}, + lambda: colour_quality_scale(msds_test), + ) + + +class BenchmarkSuite_QualityObject(BenchmarkSuite): + """ + Benchmark light-source quality indices on single + :class:`SpectralDistribution` inputs. + + Covers *CRI*, *CFI* (*CIE 2017* and *ANSI/IES TM-30-18*), *CQS* and + *SSI*. The :class:`SpectralDistribution` instances are rebuilt per + backend with backend-typed values so the underlying integration + paths dispatch through the *Array API* rather than running the same + numpy compute under every backend. + """ + + NAME = "quality_object" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._sd_test_template = SDS_LIGHT_SOURCES["Cool White FL"] + self._sd_reference_template = SDS_LIGHT_SOURCES["Daylight FL"] + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 5 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + test_values = BenchmarkRunner.convert_to_backend( + self._sd_test_template.values, backend + ) + reference_values = BenchmarkRunner.convert_to_backend( + self._sd_reference_template.values, backend + ) + with array_api_enable(backend != "numpy"): + sd_test = SpectralDistribution( + test_values, + self._sd_test_template.wavelengths, + name=self._sd_test_template.name, + ) + sd_reference = SpectralDistribution( + reference_values, + self._sd_reference_template.wavelengths, + name=self._sd_reference_template.name, + ) + + yield ( + "colour_rendering_index", + {"function": "colour_rendering_index"}, + lambda: colour_rendering_index(sd_test), + ) + yield ( + "colour_fidelity_index_CIE2017", + {"function": "colour_fidelity_index_CIE2017"}, + lambda: colour_fidelity_index(sd_test), + ) + yield ( + "colour_fidelity_index_ANSIIESTM3018", + {"function": "colour_fidelity_index_ANSIIESTM3018"}, + lambda: colour_fidelity_index(sd_test, method="ANSI/IES TM-30-18"), + ) + yield ( + "colour_quality_scale", + {"function": "colour_quality_scale"}, + lambda: colour_quality_scale(sd_test), + ) + yield ( + "spectral_similarity_index", + {"function": "spectral_similarity_index"}, + lambda: spectral_similarity_index(sd_test, sd_reference), + ) + + +class BenchmarkSuite_Volume(BenchmarkSuite): + """ + Benchmark the *Array API*-dispatchable gamut volume cases: + ``RGB_colourspace_volume_MonteCarlo`` and ``RGB_colourspace_limits``. + + The ``scipy.spatial.Delaunay``-backed containment checks live in + :class:`BenchmarkSuite_VolumeIterative` so the geo-mean here is not + polluted by host-only ``Delaunay`` triangulation. + """ + + NAME = "volume" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._colourspace = RGB_COLOURSPACES["sRGB"] + self._samples = 10_000 if runner.mode == "quick" else 250_000 + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 2 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + del backend + yield ( + "RGB_colourspace_volume_MonteCarlo", + {"function": "RGB_colourspace_volume_MonteCarlo"}, + lambda: RGB_colourspace_volume_MonteCarlo( + self._colourspace, samples=self._samples + ), + ) + yield ( + "RGB_colourspace_limits", + {"function": "RGB_colourspace_limits"}, + lambda: RGB_colourspace_limits(self._colourspace), + ) + + +class BenchmarkSuite_VolumeIterative(BenchmarkSuite): + """``scipy.spatial.Delaunay``-backed gamut containment checks. + + Counterpart to :class:`BenchmarkSuite_Volume`. The two cases here + (``is_within_visible_spectrum``, ``is_within_macadam_limits``) + forcibly round-trip through host memory for the *Delaunay* + triangulation and ``find_simplex`` query, so they don't benefit + from *Array API* dispatch. + """ + + NAME = "volume_iterative" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + n = DEFAULT_BENCHMARK_CONFIGURATION.resolution( + "small" if runner.mode == "quick" else "reduced" + ) + self._XYZ_np = self._rng.random((n, 3)) + self._xyY_np = self._rng.random((n, 3)) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 2 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + XYZ = BenchmarkRunner.convert_to_backend(self._XYZ_np, backend) + xyY = BenchmarkRunner.convert_to_backend(self._xyY_np, backend) + + yield ( + "is_within_visible_spectrum", + {"function": "is_within_visible_spectrum"}, + lambda XYZ=XYZ: is_within_visible_spectrum(XYZ), + ) + yield ( + "is_within_macadam_limits", + {"function": "is_within_macadam_limits"}, + lambda xyY=xyY: is_within_macadam_limits(xyY), + ) + + +class BenchmarkSuite_Phenomena(BenchmarkSuite): + """ + Benchmark sky models and *Rayleigh* scattering. + + ``rayleigh_optical_depth`` and the three *CIE 2003* sky distributions + take array inputs (``wavelengths``, ``zenith``, ``azimuth``) and + dispatch through the *Array API*; ``sd_rayleigh_scattering`` returns a + :class:`SpectralDistribution` and stays numpy-bound. + """ + + NAME = "phenomena" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._wavelengths_np = SpectralShape(360, 780, 1).wavelengths + self._zenith_np = self._rng.random(runner.resolution) * (np.pi / 2) + self._azimuth_np = self._rng.random(runner.resolution) * (2 * np.pi) + self._z_sun = np.pi / 4 + self._a_sun = np.pi + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 5 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + wavelengths = BenchmarkRunner.convert_to_backend(self._wavelengths_np, backend) + zenith = BenchmarkRunner.convert_to_backend(self._zenith_np, backend) + azimuth = BenchmarkRunner.convert_to_backend(self._azimuth_np, backend) + + yield ( + "rayleigh_optical_depth", + {"function": "rayleigh_optical_depth"}, + lambda w=wavelengths: rayleigh_optical_depth(w), + ) + yield ( + "sd_rayleigh_scattering", + {"function": "sd_rayleigh_scattering"}, + sd_rayleigh_scattering, + ) + yield ( + "sky_luminance_distribution_CIE2003", + {"function": "sky_luminance_distribution_CIE2003"}, + lambda z=zenith, a=azimuth, zs=self._z_sun, az=self._a_sun: ( + sky_luminance_distribution_CIE2003(1, z, a, zs, az) + ), + ) + yield ( + "sky_luminance_distribution_overcast_CIE2003", + {"function": "sky_luminance_distribution_overcast_CIE2003"}, + lambda z=zenith: sky_luminance_distribution_overcast_CIE2003(z), + ) + yield ( + "sky_scattering_indicatrix_CIE2003", + {"function": "sky_scattering_indicatrix_CIE2003"}, + lambda z=zenith, a=azimuth, zs=self._z_sun, az=self._a_sun: ( + sky_scattering_indicatrix_CIE2003(z, a, zs, az) + ), + ) + + +class BenchmarkSuite_TemperatureArray(BenchmarkSuite): + """ + Benchmark closed-form *CIE xy* <-> correlated colour temperature + conversions. + + *CIE UCS uv* and ``CCT`` and the colorimetry indices (whiteness, + yellowness, lightness, luminance, Munsell value) are exercised through + :class:`BenchmarkSuite_ConversionGraph` per-method fan-out and are + deliberately not duplicated here. + """ + + NAME = "temperature_array" + + _XY_TO_CCT: typing.ClassVar[tuple[str, ...]] = ("Hernandez 1999", "McCamy 1992") + _CCT_TO_XY: typing.ClassVar[tuple[str, ...]] = ( + "CIE Illuminant D Series", + "Kang 2002", + ) + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._xy_np = self._rng.random((runner.resolution, 2)) * 0.4 + 0.2 + self._CCT_np = self._rng.random(runner.resolution) * 9000 + 1000 + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(self._XY_TO_CCT) + len(self._CCT_TO_XY) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + xy = BenchmarkRunner.convert_to_backend(self._xy_np, backend) + CCT = BenchmarkRunner.convert_to_backend(self._CCT_np, backend) + + for method in self._XY_TO_CCT: + yield ( + f"xy_to_CCT ({method})", + {"direction": "xy_to_CCT", "method": method}, + lambda method=method, c=xy: xy_to_CCT(c, method=method), + ) + for method in self._CCT_TO_XY: + yield ( + f"CCT_to_xy ({method})", + {"direction": "CCT_to_xy", "method": method}, + lambda method=method, t=CCT: CCT_to_xy(t, method=method), + ) + + +class BenchmarkSuite_TemperatureIterative(BenchmarkSuite): + """ + Benchmark iterative *CIE xy* <-> correlated colour temperature + conversions backed by per-sample *Newton* solvers. + + The methods batch through :func:`colour.temperature.solve_CCT_Newton` + and :func:`colour.temperature.solve_xy_Newton`; each sample converges + independently so the suite is benched at ``reduced`` resolution to + keep the iteration cost tractable in ``hd`` mode. + """ + + NAME = "temperature_iterative" + + _XY_TO_CCT: typing.ClassVar[tuple[str, ...]] = ( + "CIE Illuminant D Series", + "Kang 2002", + ) + _CCT_TO_XY: typing.ClassVar[tuple[str, ...]] = ("Hernandez 1999", "McCamy 1992") + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + n = ( + DEFAULT_BENCHMARK_CONFIGURATION.resolution("reduced") + if runner.size == "hd" + else runner.resolution + ) + self._xy_np = self._rng.random((n, 2)) * 0.4 + 0.2 + self._CCT_np = self._rng.random(n) * 9000 + 1000 + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(self._XY_TO_CCT) + len(self._CCT_TO_XY) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + xy = BenchmarkRunner.convert_to_backend(self._xy_np, backend) + CCT = BenchmarkRunner.convert_to_backend(self._CCT_np, backend) + + for method in self._XY_TO_CCT: + yield ( + f"xy_to_CCT ({method})", + {"direction": "xy_to_CCT", "method": method}, + lambda method=method, c=xy: xy_to_CCT(c, method=method), + ) + for method in self._CCT_TO_XY: + yield ( + f"CCT_to_xy ({method})", + {"direction": "CCT_to_xy", "method": method}, + lambda method=method, t=CCT: CCT_to_xy(t, method=method), + ) + + +class BenchmarkSuite_Blindness(BenchmarkSuite): + """ + Benchmark *Machado (2009)* colour vision deficiency simulation. + + Each case builds the deficiency matrix and applies it to an HD *RGB* + image, mirroring a typical CVD simulation pipeline. + """ + + NAME = "blindness" + + _DEFICIENCIES: typing.ClassVar[tuple[str, ...]] = ( + "Protanomaly", + "Deuteranomaly", + "Tritanomaly", + ) + _SEVERITY: typing.ClassVar[float] = 0.5 + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._RGB_np = self._rng.random((runner.resolution, 3)) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return len(self._DEFICIENCIES) + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + RGB = BenchmarkRunner.convert_to_backend(self._RGB_np, backend) + for deficiency in self._DEFICIENCIES: + + def operation( + d: str = deficiency, + x: typing.Any = RGB, + s: float = self._SEVERITY, + ) -> object: + matrix = matrix_cvd_Machado2009(d, s) + matrix = BenchmarkRunner.convert_to_backend(matrix, backend) + return vecmul(matrix, x) + + yield ( + f"matrix_cvd_Machado2009 ({deficiency})", + {"deficiency": deficiency}, + operation, + ) + + +class BenchmarkSuite_Contrast(BenchmarkSuite): + """Benchmark *Barten (1999)* contrast sensitivity function.""" + + NAME = "contrast" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._frequencies_np = np.linspace(1.0, 30.0, runner.resolution) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 1 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + frequencies = BenchmarkRunner.convert_to_backend(self._frequencies_np, backend) + yield ( + "contrast_sensitivity_function (Barten 1999)", + {"method": "Barten 1999"}, + lambda u=frequencies: contrast_sensitivity_function( + u=u, method="Barten 1999" + ), + ) + + +class BenchmarkSuite_GeneratorsArray(BenchmarkSuite): + """ + Benchmark vectorised spectral generators with raw ``ArrayLike`` inputs + and outputs. + + ``planck_law`` and ``rayleigh_jeans_law`` operate purely on + wavelength and temperature arrays, broadcast to a + ``(n_wavelengths, n_temperatures)`` output and dispatch through the + *Array API*. ``CIE_illuminant_D_series`` evaluates the *CIE D Series* + basis at a batch of *CIE xy* chromaticities, returning shape + ``(n_wavelengths, N)``. The temperature / chromaticity batches are + taken at ``"reduced"`` resolution in ``hd`` mode so broadcasted + outputs stay inside a typical GPU memory budget. + """ + + NAME = "generators_array" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._shape = SpectralShape(380, 780, 1) + self._wavelengths_np = self._shape.wavelengths + n = ( + DEFAULT_BENCHMARK_CONFIGURATION.resolution("reduced") + if runner.size == "hd" + else runner.resolution + ) + self._temperatures_np = np.linspace(2000, 10000, n) + self._xy_np = self._rng.random((n, 2)) * 0.1 + (0.31, 0.33) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 3 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + wavelengths = BenchmarkRunner.convert_to_backend(self._wavelengths_np, backend) + temperatures = BenchmarkRunner.convert_to_backend( + self._temperatures_np, backend + ) + xy = BenchmarkRunner.convert_to_backend(self._xy_np, backend) + + yield ( + "planck_law", + {"function": "planck_law"}, + lambda w=wavelengths, t=temperatures: planck_law(w * 1e-9, t), + ) + yield ( + "rayleigh_jeans_law", + {"function": "rayleigh_jeans_law"}, + lambda w=wavelengths, t=temperatures: rayleigh_jeans_law(w * 1e-9, t), + ) + yield ( + "CIE_illuminant_D_series", + {"function": "CIE_illuminant_D_series"}, + lambda x=xy: CIE_illuminant_D_series(x, shape=self._shape), + ) + + +class BenchmarkSuite_GeneratorsObject(BenchmarkSuite): + """ + Benchmark per-:class:`SpectralDistribution` / + :class:`MultiSpectralDistributions` spectral generators. + + The ``sd_*`` cases take scalar inputs and construct a single + :class:`SpectralDistribution` per call: the path is numpy-bound and + backend-agnostic. The ``msds_*`` cases take array inputs that are + promoted to the requested backend in :meth:`cases` so the underlying + array kernels (``planck_law``, ``rayleigh_jeans_law``, + ``CIE_illuminant_D_series``) dispatch through the *Array API* before + the result is wrapped into a :class:`MultiSpectralDistributions`. + """ + + NAME = "generators_object" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._shape = SpectralShape(380, 780, 1) + # Multi-spectral fixtures match a colour-checker chart in + # ``quick`` mode and a small spectral image in ``full`` mode. + n = 24 if runner.mode == "quick" else 1024 + self._temperatures_np = np.linspace(2000, 10000, n) + self._xy_d_series_np = self._rng.random((n, 2)) * 0.1 + (0.31, 0.33) + self._xy_d_illuminant_np = np.array([0.31, 0.33]) + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 6 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + temperatures = BenchmarkRunner.convert_to_backend( + self._temperatures_np, backend + ) + xy_d_series = BenchmarkRunner.convert_to_backend(self._xy_d_series_np, backend) + + yield ( + "sd_blackbody", + {"function": "sd_blackbody"}, + lambda: sd_blackbody(6500, self._shape), + ) + yield ( + "sd_rayleigh_jeans", + {"function": "sd_rayleigh_jeans"}, + lambda: sd_rayleigh_jeans(6500, self._shape), + ) + yield ( + "sd_CIE_illuminant_D_series", + {"function": "sd_CIE_illuminant_D_series"}, + lambda x=self._xy_d_illuminant_np: sd_CIE_illuminant_D_series(x), + ) + yield ( + "msds_blackbody", + {"function": "msds_blackbody"}, + lambda t=temperatures: msds_blackbody(t, self._shape), + ) + yield ( + "msds_rayleigh_jeans", + {"function": "msds_rayleigh_jeans"}, + lambda t=temperatures: msds_rayleigh_jeans(t, self._shape), + ) + yield ( + "msds_CIE_illuminant_D_series", + {"function": "msds_CIE_illuminant_D_series"}, + lambda x=xy_d_series: msds_CIE_illuminant_D_series(x, shape=self._shape), + ) + + +class BenchmarkSuite_Photometry(BenchmarkSuite): + """ + Benchmark photometric measures over a :class:`SpectralDistribution`. + + ``luminous_flux``, ``luminous_efficacy`` and ``luminous_efficiency`` + consume a :class:`SpectralDistribution` instance (numpy-backed) and + are therefore numpy-bound regardless of the requested backend. + """ + + NAME = "photometry" + + def __init__(self, runner: BenchmarkRunner) -> None: + super().__init__(runner) + self._sd_template = SDS_LIGHT_SOURCES["Cool White FL"] + + @property + def case_count(self) -> int: + """See :class:`BenchmarkSuite`.""" + + return 3 + + def cases( + self, backend: str + ) -> Iterable[tuple[str, dict[str, str], Callable[[], object] | None]]: + """See :class:`BenchmarkSuite`.""" + + values = BenchmarkRunner.convert_to_backend(self._sd_template.values, backend) + with array_api_enable(backend != "numpy"): + sd_test = SpectralDistribution( + values, self._sd_template.wavelengths, name=self._sd_template.name + ) + + yield ( + "luminous_flux", + {"function": "luminous_flux"}, + lambda: luminous_flux(sd_test), + ) + yield ( + "luminous_efficacy", + {"function": "luminous_efficacy"}, + lambda: luminous_efficacy(sd_test), + ) + yield ( + "luminous_efficiency", + {"function": "luminous_efficiency"}, + lambda: luminous_efficiency(sd_test), + ) + + +BENCHMARK_SUITES: dict[str, type[BenchmarkSuite]] = { + suite.NAME: suite + for suite in ( + BenchmarkSuite_ConversionGraph, + BenchmarkSuite_ConversionGraphIterative, + BenchmarkSuite_Difference, + BenchmarkSuite_IntegrationArray, + BenchmarkSuite_IntegrationObject, + BenchmarkSuite_TransferFunction, + BenchmarkSuite_Adaptation, + BenchmarkSuite_Characterisation, + BenchmarkSuite_RecoveryArray, + BenchmarkSuite_RecoveryObject, + BenchmarkSuite_QualityArray, + BenchmarkSuite_QualityObject, + BenchmarkSuite_Volume, + BenchmarkSuite_VolumeIterative, + BenchmarkSuite_Phenomena, + BenchmarkSuite_TemperatureArray, + BenchmarkSuite_TemperatureIterative, + BenchmarkSuite_Blindness, + BenchmarkSuite_Contrast, + BenchmarkSuite_GeneratorsArray, + BenchmarkSuite_GeneratorsObject, + BenchmarkSuite_Photometry, + ) +} +""" +Registry mapping suite names to their :class:`BenchmarkSuite` subclasses. + +Listed explicitly: the dispatch order doubles as the suite execution order +when no ``--suites`` filter is provided. +""" + + +def build_argument_parser() -> argparse.ArgumentParser: + """Construct the command-line argument parser.""" + + parser = argparse.ArgumentParser( + description="Cross-backend benchmarks for Colour.", + ) + parser.add_argument( + "--mode", + choices=["quick", "full"], + default="quick", + help="'quick' runs a correctness smoke test; 'full' runs HD timing.", + ) + parser.add_argument( + "--suites", + default=",".join(BENCHMARK_SUITES), + help="Comma-separated suite names. Available: " + ", ".join(BENCHMARK_SUITES), + ) + parser.add_argument( + "--backends", + default=",".join(DEFAULT_BENCHMARK_CONFIGURATION.backends), + help="Comma-separated backend list (unavailable backends are skipped).", + ) + parser.add_argument( + "--warmup", + type=int, + default=1, + help="Untimed iterations before timed runs.", + ) + parser.add_argument( + "--runs", + type=int, + default=3, + help="Timed iterations; the best (minimum) is reported.", + ) + parser.add_argument( + "--edges", + default=None, + help='Conversion-graph edges filter, e.g. "CIE XYZ->CIE Lab,Src->Tgt".', + ) + parser.add_argument( + "--json", + default=None, + help="Optional JSON report path.", + ) + parser.add_argument( + "--csv", + default=None, + help="Optional CSV report path.", + ) + parser.add_argument( + "--log", + default="benchmark.log", + help="Log file path (default: CWD).", + ) + return parser + + +def main() -> None: + """Parse arguments, run the selected benchmarks, and emit reports.""" + + args = build_argument_parser().parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + handlers=[ + logging.FileHandler(args.log, mode="w"), + logging.StreamHandler(sys.stderr), + ], + ) + + runner = BenchmarkRunner( + mode=args.mode, + backends=args.backends.split(","), + warmup=args.warmup, + runs=args.runs, + edges_filter=args.edges, + ) + + suites = BenchmarkRunner.parse_suites(args.suites, runner) + + report = runner.run(suites) + report.log() + + if args.json: + write_report_json(report, args.json) + + if args.csv: + write_report_csv(report, args.csv) + + +if __name__ == "__main__": + main()