#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
CIECAM02 Colour Appearance Model
================================

Defines *CIECAM02* colour appearance model objects:

-   :class:`CIECAM02_InductionFactors`
-   :attr:`CIECAM02_VIEWING_CONDITIONS`
-   :class:`CIECAM02_Specification`
-   :func:`XYZ_to_CIECAM02`
-   :func:`CIECAM02_to_XYZ`

See Also
--------
`CIECAM02 Colour Appearance Model IPython Notebook
<http://nbviewer.ipython.org/github/colour-science/colour-ipython/blob/master/notebooks/appearance/ciecam02.ipynb>`_  # noqa

References
----------
.. [1]  http://en.wikipedia.org/wiki/CIECAM02
        (Last accessed 14 August 2014)
.. [2]  **Mark D. Fairchild**, *Color Appearance Models, 2nd Edition*,
        The Wiley-IS&T Series in Imaging Science and Technology,
        published 19 November 2004, ISBN-13: 978-0470012161,
        pages 265-277.
.. [3]  **Stephen Westland, Caterina Ripamonti, Vien Cheung**,
        *Computational Colour Science Using MATLAB, 2nd Edition*,
        The Wiley-IS&T Series in Imaging Science and Technology,
        published July 2012, ISBN-13: 978-0-470-66569-5, page  38.
.. [4]  `The CIECAM02 Color Appearance Model
        <http://rit-mcsl.org/fairchild/PDFs/PRO19.pdf>`_
        (Last accessed 30 July 2014)
"""

from __future__ import division, unicode_literals

import bisect

try:
    from functools import lru_cache
except ImportError:
    from backports.functools_lru_cache import lru_cache
import math
import numpy as np
from collections import namedtuple

from colour.adaptation.cat import CAT02_CAT, CAT02_INVERSE_CAT
from colour.appearance.hunt import (XYZ_TO_HPE_MATRIX,
                                    HPE_TO_XYZ_MATRIX,
                                    luminance_level_adaptation_factor)
from colour.utilities import CaseInsensitiveMapping

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013 - 2014 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'

__all__ = ['CIECAM02_InductionFactors',
           'CIECAM02_VIEWING_CONDITIONS',
           'HUE_DATA_FOR_HUE_QUADRATURE',
           'CIECAM02_Specification',
           'XYZ_to_CIECAM02',
           'CIECAM02_to_XYZ',
           'chromatic_induction_factors',
           'base_exponential_non_linearity',
           'viewing_condition_dependent_parameters',
           'degree_of_adaptation',
           'full_chromatic_adaptation_forward',
           'full_chromatic_adaptation_reverse',
           'RGB_to_rgb',
           'rgb_to_RGB',
           'post_adaptation_non_linear_response_compression_forward',
           'post_adaptation_non_linear_response_compression_reverse',
           'opponent_colour_dimensions_forward',
           'opponent_colour_dimensions_reverse',
           'hue_angle',
           'hue_quadrature',
           'eccentricity_factor',
           'achromatic_response_forward',
           'achromatic_response_reverse',
           'lightness_correlate',
           'brightness_correlate',
           'temporary_magnitude_quantity_forward',
           'temporary_magnitude_quantity_reverse',
           'chroma_correlate',
           'colourfulness_correlate',
           'saturation_correlate',
           'P',
           'post_adaptation_non_linear_response_compression_matrix']


class CIECAM02_InductionFactors(
    namedtuple('CIECAM02_InductionFactors',
               ('F', 'c', 'N_c'))):
    """
    *CIECAM02* colour appearance model induction factors.

    Parameters
    ----------
    F : numeric
        Maximum degree of adaptation :math:`F`.
    c : numeric
        Exponential non linearity :math:`c`.
    N_c : numeric
        Chromatic induction factor :math:`N_c`.
    """


CIECAM02_VIEWING_CONDITIONS = CaseInsensitiveMapping(
    {'Average': CIECAM02_InductionFactors(1, 0.69, 1),
     'Dim': CIECAM02_InductionFactors(0.9, 0.59, 0.95),
     'Dark': CIECAM02_InductionFactors(0.8, 0.525, 0.8)})
"""
Reference *CIECAM02* colour appearance model viewing conditions.

CIECAM02_VIEWING_CONDITIONS : dict
    ('Average', 'Dim', 'Dark')
"""

HUE_DATA_FOR_HUE_QUADRATURE = {
    'h_i': np.array([20.14, 90.00, 164.25, 237.53, 380.14]),
    'e_i': np.array([0.8, 0.7, 1.0, 1.2, 0.8]),
    'H_i': np.array([0.0, 100.0, 200.0, 300.0, 400.0])}


class CIECAM02_Specification(
    namedtuple('CIECAM02_Specification',
               ('J', 'C', 'h', 's', 'Q', 'M', 'H', 'HC'))):
    """
    Defines the *CIECAM02* colour appearance model specification.

    Parameters
    ----------
    J : numeric
        Correlate of *Lightness* :math:`J`.
    C : numeric
        Correlate of *chroma* :math:`C`.
    h : numeric
        *Hue* angle :math:`h` in degrees.
    s : numeric
        Correlate of *saturation* :math:`s`.
    Q : numeric
        Correlate of *brightness* :math:`Q`.
    M : numeric
        Correlate of *colourfulness* :math:`M`.
    H : numeric
        *Hue* :math:`h` quadrature :math:`H`.
    HC : numeric
        *Hue* :math:`h` composition :math:`H^C`.
    """


def XYZ_to_CIECAM02(XYZ,
                    XYZ_w,
                    L_A,
                    Y_b,
                    surround=CIECAM02_VIEWING_CONDITIONS.get('Average'),
                    discount_illuminant=False):
    """
    Computes the *CIECAM02* colour appearance model correlates from given
    *CIE XYZ* colourspace matrix.

    This is the *forward* implementation.

    Parameters
    ----------
    XYZ : array_like, (3,)
        *CIE XYZ* colourspace matrix of test sample / stimulus in domain
        [0, 100].
    XYZ_w : array_like, (3,)
        *CIE XYZ* colourspace matrix of reference white in domain [0, 100].
    L_A : numeric
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
    Y_b : numeric
        Adapting field *Y* tristimulus value :math:`Y_b`.
    surround : CIECAM02_InductionFactors, optional
        Surround viewing conditions induction factors.
    discount_illuminant : bool, optional
        Truth value indicating if the illuminant should be discounted.

    Returns
    -------
    CIECAM02_Specification
        *CIECAM02* colour appearance model specification.

    Warning
    -------
    The input domain of that definition is non standard!

    Notes
    -----
    -   Input *CIE XYZ* colourspace matrix is in domain [0, 100].
    -   Input *CIE XYZ_w* colourspace matrix is in domain [0, 100].

    Examples
    --------
    >>> 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 = CIECAM02_VIEWING_CONDITIONS['Average']
    >>> XYZ_to_CIECAM02(XYZ, XYZ_w, L_A, Y_b, surround)  # doctest: +ELLIPSIS
    CIECAM02_Specification(J=41.7310911..., C=0.1047077..., h=219.0484326..., s=2.3603053..., Q=195.3713259..., M=0.1088421..., H=278.0607358..., HC=None)
    """

    XYZ = np.array(XYZ).reshape((3, 1))
    XYZ_w = np.array(XYZ_w).reshape((3, 1))
    X_w, Y_w, Z_w = np.ravel(XYZ_w)

    n, F_L, N_bb, N_cb, z = viewing_condition_dependent_parameters(Y_b,
                                                                   Y_w,
                                                                   L_A)

    # Converting *CIE XYZ* colourspace matrices to *CMCCAT2000* transform
    # sharpened *RGB* values.
    RGB = np.dot(CAT02_CAT, XYZ)
    RGB_w = np.dot(CAT02_CAT, XYZ_w)

    # Computing degree of adaptation :math:`D`.
    D = degree_of_adaptation(surround.F,
                             L_A) if not discount_illuminant else 1

    # 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 = opponent_colour_dimensions_forward(RGB_a)

    # -------------------------------------------------------------------------
    # Computing the *hue* angle :math:`h`.
    h = hue_angle(a, b)
    # -------------------------------------------------------------------------
    # Computing hue :math:`h` quadrature :math:`H`.
    H = hue_quadrature(h)
    # TODO: Compute hue composition.

    # Computing eccentricity factor *e_t*.
    e_t = eccentricity_factor(h)

    # 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 the correlate of *Lightness* :math:`J`.
    # -------------------------------------------------------------------------
    J = lightness_correlate(A, A_w, surround.c, z)

    # -------------------------------------------------------------------------
    # Computing the correlate of *brightness* :math:`Q`.
    # -------------------------------------------------------------------------
    Q = brightness_correlate(surround.c, J, A_w, F_L)

    # -------------------------------------------------------------------------
    # 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)

    return CIECAM02_Specification(J, C, h, s, Q, M, H, None)


def CIECAM02_to_XYZ(J, C, h,
                    XYZ_w,
                    L_A,
                    Y_b,
                    surround=CIECAM02_VIEWING_CONDITIONS.get(
                        'Average'),
                    discount_illuminant=False):
    """
    Converts *CIECAM02* specification to *CIE XYZ* colourspace matrix.

    This is the *reverse* implementation.

    Parameters
    ----------
    CIECAM02_Specification : CIECAM02_Specification
        *CIECAM02* specification.
    XYZ_w : array_like
        *CIE XYZ* colourspace matrix of reference white.
    L_A : numeric
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
    Y_b : numeric
        Adapting field *Y* tristimulus value :math:`Y_b`.
    surround : CIECAM02_Surround, optional
        Surround viewing conditions.
    discount_illuminant : bool, optional
        Discount the illuminant.

    Returns
    -------
    XYZ : ndarray
        *CIE XYZ* colourspace matrix.

    Warning
    -------
    The output domain of that definition is non standard!

    Notes
    -----
    -   Input *CIE XYZ_w* colourspace matrix is in domain [0, 100].
    -   Output *CIE XYZ* colourspace matrix is in domain [0, 100].

    Examples
    --------
    >>> J = 41.731091132513917
    >>> C = 0.1047077571711053
    >>> h = 219.0484326582719
    >>> XYZ_w = np.array([95.05, 100.00, 108.88])
    >>> L_A = 318.31
    >>> Y_b = 20.0
    >>> CIECAM02_to_XYZ(J, C, h, XYZ_w, L_A, Y_b)  # doctest: +ELLIPSIS
    array([ 19.01...,  20...  ,  21.78...])
    """

    XYZ_w = np.array(XYZ_w).reshape((3, 1))
    X_w, Y_w, Zw = np.ravel(XYZ_w)

    n, F_L, N_bb, N_cb, z = viewing_condition_dependent_parameters(Y_b,
                                                                   Y_w,
                                                                   L_A)

    # Converting *CIE XYZ* colourspace matrices to *CMCCAT2000* transform
    # sharpened *RGB* values.
    RGB_w = np.dot(CAT02_CAT, XYZ_w)

    # Computing degree of adaptation :math:`D`.
    D = degree_of_adaptation(surround.F,
                             L_A) if not discount_illuminant else 1

    # Computation full chromatic adaptation.
    RGB_wc = full_chromatic_adaptation_forward(RGB_w, RGB_w, Y_w, D)

    # Converting to *Hunt-Pointer-Estevez* colourspace.
    RGB_pw = RGB_to_rgb(RGB_wc)

    # Applying post-adaptation non linear response compression.
    RGB_aw = post_adaptation_non_linear_response_compression_forward(
        RGB_pw, F_L)

    # Computing achromatic responses for the stimulus and the whitepoint.
    A_w = achromatic_response_forward(RGB_aw, N_bb)

    # Computing temporary magnitude quantity :math:`t`.
    t = temporary_magnitude_quantity_reverse(C, J, n)

    # Computing eccentricity factor *e_t*.
    e_t = eccentricity_factor(h)

    # Computing achromatic response :math:`A` for the stimulus.
    A = achromatic_response_reverse(A_w, J, surround.c, z)

    # Computing *P_1* to *P_3*.
    P_1, P_2, P_3 = P(surround.N_c, N_cb, e_t, t, A, N_bb)

    # Computing opponent colour dimensions :math:`a` and :math:`b`.
    a, b = opponent_colour_dimensions_reverse((P_1, P_2, P_3), h)

    # Computing post-adaptation non linear response compression matrix.
    RGB_a = post_adaptation_non_linear_response_compression_matrix(P_2, a,
                                                                   b)

    # Applying reverse post-adaptation non linear response compression.
    RGB_p = post_adaptation_non_linear_response_compression_reverse(RGB_a,
                                                                    F_L)

    # Converting to *Hunt-Pointer-Estevez* colourspace.
    RGB_c = rgb_to_RGB(RGB_p)

    # Applying reverse full chromatic adaptation.
    RGB = full_chromatic_adaptation_reverse(RGB_c, RGB_w, Y_w, D)

    # Converting *CMCCAT2000* transform sharpened *RGB* values to *CIE XYZ*
    # colourspace matrices.
    XYZ = np.dot(CAT02_INVERSE_CAT, RGB)

    return XYZ


def chromatic_induction_factors(n):
    """
    Returns the chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`.

    Parameters
    ----------
    n : numeric
        Function of the luminance factor of the background :math:`n`.

    Returns
    -------
    tuple
        Chromatic induction factors :math:`N_{bb}` and :math:`N_{cb}`.

    Examples
    --------
    >>> chromatic_induction_factors(0.2)  # doctest: +ELLIPSIS
    (1.0003040..., 1.0003040...)
    """

    N_bb = N_cb = 0.725 * (1 / n) ** 0.2
    return N_bb, N_cb


def base_exponential_non_linearity(n):
    """
    Returns the base exponential non linearity :math:`n`.

    Parameters
    ----------
    n : numeric
        Function of the luminance factor of the background :math:`n`.

    Returns
    -------
    numeric
        Base exponential non linearity :math:`z`.

    Examples
    --------
    >>> base_exponential_non_linearity(0.2)  # doctest: +ELLIPSIS
    1.9272135...
    """

    z = 1.48 + math.sqrt(n)
    return z


@lru_cache(maxsize=8192)
def viewing_condition_dependent_parameters(Y_b, Y_w, L_A):
    """
    Returns the viewing condition dependent parameters.

    Parameters
    ----------
    Y_b : numeric
        Adapting field *Y* tristimulus value :math:`Y_b`.
    Y_w : numeric
        Whitepoint *Y* tristimulus value :math:`Y_w`.
    L_A : numeric
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.

    Returns
    -------
    tuple
        Viewing condition dependent parameters.

    Examples
    --------
    >>> viewing_condition_dependent_parameters(20.0, 100.0, 318.31)  # noqa  # doctest: +ELLIPSIS
    (0.2000000..., 1.1675444..., 1.0003040..., 1.0003040..., 1.9272135...)
    """

    n = Y_b / Y_w

    F_L = luminance_level_adaptation_factor(L_A)
    N_bb, N_cb = chromatic_induction_factors(n)
    z = base_exponential_non_linearity(n)

    return n, F_L, N_bb, N_cb, z


def degree_of_adaptation(F, L_A):
    """
    Returns the degree of adaptation :math:`D` from given surround maximum
    degree of adaptation :math:`F` and Adapting field *luminance* :math:`L_A`
    in :math:`cd/m^2`.

    Parameters
    ----------
    F : numeric
        Surround maximum degree of adaptation :math:`F`.
    L_A : numeric
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.

    Returns
    -------
    numeric
        Degree of adaptation :math:`D`.

    Examples
    --------
    >>> degree_of_adaptation(1.0, 318.31)  # doctest: +ELLIPSIS
    0.9944687...
    """

    D = F * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92))
    return D


def full_chromatic_adaptation_forward(RGB, RGB_w, Y_w, D):
    """
    Applies full chromatic adaptation to given *CMCCAT2000* transform sharpened
    *RGB* matrix using given *CMCCAT2000* transform sharpened whitepoint
    *RGB_w* matrix.

    Parameters
    ----------
    RGB : array_like
        *CMCCAT2000* transform sharpened *RGB* matrix.
    RGB_w : array_like
        *CMCCAT2000* transform sharpened whitepoint *RGB_w* matrix.
    Y_w : numeric
        Whitepoint *Y* tristimulus value :math:`Y_w`.
    D : numeric
        Degree of adaptation :math:`D`.

    Returns
    -------
    ndarray, (3,)
        Adapted *RGB* matrix.

    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)  # noqa  # doctest: +ELLIPSIS
    array([ 19.9937078...,  20.0039363...,  20.0132638...])
    """

    R, G, B = np.ravel(RGB)
    R_w, G_w, B_w = np.ravel(RGB_w)

    equation = lambda x, y: ((Y_w * D / y) + 1 - D) * x

    R_c = equation(R, R_w)
    G_c = equation(G, G_w)
    B_c = equation(B, B_w)

    return np.array([R_c, G_c, B_c])


def full_chromatic_adaptation_reverse(RGB, RGB_w, Y_w, D):
    """
    Reverts full chromatic adaptation of given *CMCCAT2000* transform sharpened
    *RGB* matrix using given *CMCCAT2000* transform sharpened whitepoint
    *RGB_w* matrix.

    Parameters
    ----------
    RGB : array_like
        *CMCCAT2000* transform sharpened *RGB* matrix.
    RGB_w : array_like
        *CMCCAT2000* transform sharpened whitepoint *RGB_w* matrix.
    Y_w : numeric
        Whitepoint *Y* tristimulus value :math:`Y_w`.
    D : numeric
        Degree of adaptation :math:`D`.

    Returns
    -------
    ndarray, (3,)
        Adapted *RGB* matrix.

    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_reverse(RGB, RGB_w, Y_w, D)
    array([ 18.985456,  20.707422,  21.747482])
    """

    R, G, B = np.ravel(RGB)
    R_w, G_w, B_w = np.ravel(RGB_w)

    equation = lambda x, y: x / (Y_w * (D / y) + 1 - D)

    R_c = equation(R, R_w)
    G_c = equation(G, G_w)
    B_c = equation(B, B_w)

    return np.array([R_c, G_c, B_c])


def RGB_to_rgb(RGB):
    """
    Converts given *RGB* matrix to *Hunt-Pointer-Estevez*
    :math:`\\rho\gamma\\beta` colourspace.

    Parameters
    ----------
    RGB : array_like, (3,)
        *RGB* matrix.

    Returns
    -------
    ndarray, (3,)
        *Hunt-Pointer-Estevez* :math:`\\rho\gamma\\beta` colourspace matrix.

    Examples
    --------
    >>> RGB = np.array([19.99370783, 20.00393634, 20.01326387])
    >>> RGB_to_rgb(RGB)  # doctest: +ELLIPSIS
    array([ 19.9969397...,  20.0018612...,  20.0135053...])
    """

    rgb = np.dot(np.dot(XYZ_TO_HPE_MATRIX, CAT02_INVERSE_CAT), RGB)
    return rgb


def rgb_to_RGB(rgb):
    """
    Converts given *Hunt-Pointer-Estevez* :math:`\\rho\gamma\\beta` colourspace
    matrix to *RGB* matrix.

    Parameters
    ----------
    rgb : array_like, (3,)
        *Hunt-Pointer-Estevez* :math:`\\rho\gamma\\beta` colourspace matrix.

    Returns
    -------
    ndarray, (3,)
        *RGB* matrix.

    Examples
    --------
    >>> rgb = np.array([19.99693975, 20.00186123, 20.0135053])
    >>> rgb_to_RGB(rgb)  # doctest: +ELLIPSIS
    array([ 19.9937078...,  20.0039363...,  20.0132638...])
    """

    RGB = np.dot(np.dot(CAT02_CAT, HPE_TO_XYZ_MATRIX), rgb)
    return RGB


def post_adaptation_non_linear_response_compression_forward(RGB, F_L):
    """
    Returns given *CMCCAT2000* transform sharpened *RGB* matrix with post
    adaptation non linear response compression.

    Parameters
    ----------
    RGB : array_like
        *CMCCAT2000* transform sharpened *RGB* matrix.

    Returns
    -------
    ndarray, (3,)
        Compressed *CMCCAT2000* transform sharpened *RGB* matrix.

    Examples
    --------
    >>> RGB = np.array([19.99693975, 20.00186123, 20.0135053])
    >>> F_L = 1.16754446415
    >>> post_adaptation_non_linear_response_compression_forward(RGB, F_L)  # noqa  # doctest: +ELLIPSIS
    array([ 7.9463202...,  7.9471152...,  7.9489959...])
    """

    # TODO: Check for negative values and their handling.
    RGB_c = ((((400 * (F_L * RGB / 100) ** 0.42) /
               (27.13 + (F_L * RGB / 100) ** 0.42))) + 0.1)
    return RGB_c


def post_adaptation_non_linear_response_compression_reverse(RGB, F_L):
    """
    Returns given *CMCCAT2000* transform sharpened *RGB* matrix without post
    adaptation non linear response compression.

    Parameters
    ----------
    RGB : array_like
        *CMCCAT2000* transform sharpened *RGB* matrix.

    Returns
    -------
    ndarray, (3,)
        Uncompressed *CMCCAT2000* transform sharpened *RGB* matrix.

    Examples
    --------
    >>> RGB = np.array([7.9463202, 7.94711528, 7.94899595])
    >>> F_L = 1.16754446415
    >>> post_adaptation_non_linear_response_compression_reverse(RGB, F_L)  # noqa  # doctest: +ELLIPSIS
    array([ 19.9969397...,  20.0018612...,  20.0135052...])
    """

    RGB_p = ((np.sign(RGB - 0.1) *
              (100 / F_L) * ((27.13 * np.abs(RGB - 0.1)) /
                             (400 - np.abs(RGB - 0.1))) ** (1 / 0.42)))
    return RGB_p


def opponent_colour_dimensions_forward(RGB):
    """
    Returns opponent colour dimensions from given compressed *CMCCAT2000*
    transform sharpened *RGB* matrix for forward *CIECAM02* implementation

    Parameters
    ----------
    RGB : array_like
        Compressed *CMCCAT2000* transform sharpened *RGB* matrix.

    Returns
    -------
    tuple
        Opponent colour dimensions.

    Examples
    --------
    >>> RGB = np.array([7.9463202, 7.94711528, 7.94899595])
    >>> opponent_colour_dimensions_forward(RGB)  # doctest: +ELLIPSIS
    (-0.0006241..., -0.0005062...)
    """

    R, G, B = np.ravel(RGB)

    a = R - 12 * G / 11 + B / 11
    b = (R + G - 2 * B) / 9

    return a, b


def opponent_colour_dimensions_reverse(P, h):
    """
    Returns opponent colour dimensions from given points :math:`P` and hue
    :math:`h` in degrees for reverse *CIECAM02* implementation.

    Parameters
    ----------
    p : array_like
        Points :math:`P`.
    h : numeric
        Hue :math:`h` in degrees.

    Returns
    -------
    tuple
        Opponent colour dimensions.

    Examples
    --------
    >>> p = (30162.890815335879, 24.237205467134817, 1.05)
    >>> h = -140.9515673417281
    >>> opponent_colour_dimensions_reverse(p, h)  # doctest: +ELLIPSIS
    (-0.0006241..., -0.0005062...)
    """

    P_1, P_2, P_3 = P
    hr = math.radians(h)

    sin_hr, cos_hr = math.sin(hr), math.cos(hr)
    P_4 = P_1 / sin_hr
    P_5 = P_1 / cos_hr
    n = P_2 * (2 + P_3) * (460 / 1403)

    if abs(sin_hr) >= abs(cos_hr):
        b = n / (P_4 + (2 + P_3) * (220 / 1403) * (cos_hr / sin_hr) - (
            27 / 1403) + P_3 * (6300 / 1403))
        a = b * (cos_hr / sin_hr)
    else:
        a = n / (P_5 + (2 + P_3) * (220 / 1403) - (
            (27 / 1403) - P_3 * (6300 / 1403)) * (sin_hr / cos_hr))
        b = a * (sin_hr / cos_hr)

    return a, b


def hue_angle(a, b):
    """
    Returns the *hue* angle :math:`h` in degrees.

    Parameters
    ----------
    a : numeric
        Opponent colour dimension :math:`a`.
    b : numeric
        Opponent colour dimension :math:`b`.

    Returns
    -------
    numeric
        *Hue* angle :math:`h` in degrees.

    Examples
    --------
    >>> a = -0.0006241120682426434
    >>> b = -0.0005062701067729668
    >>> hue_angle(a, b)  # doctest: +ELLIPSIS
    219.0484326...
    """

    h = math.degrees(np.arctan2(b, a)) % 360
    return h


def hue_quadrature(h):
    """
    Returns the hue quadrature from given hue :math:`h` angle in degrees.

    Parameters
    ----------
    h : numeric
        Hue :math:`h` angle in degrees.

    Returns
    -------
    numeric
        Hue quadrature.

    Examples
    --------
    >>> hue_quadrature(219.0484326582719)  # doctest: +ELLIPSIS
    278.0607358...
    """

    h_i = HUE_DATA_FOR_HUE_QUADRATURE.get('h_i')
    e_i = HUE_DATA_FOR_HUE_QUADRATURE.get('e_i')
    H_i = HUE_DATA_FOR_HUE_QUADRATURE.get('H_i')

    i = bisect.bisect_left(h_i, h) - 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]

    if h < 20.14:
        H = 385.9
        H += (14.1 * h / 0.856) / (h / 0.856 + (20.14 - h) / 0.8)
    elif h >= 237.53:
        H = H_ii
        H += ((85.9 * (h - h_ii) / e_ii) /
              ((h - h_ii) / e_ii + (360 - h) / 0.856))
    else:
        H = H_ii
        H += ((100 * (h - h_ii) / e_ii) /
              ((h - h_ii) / e_ii + (h_ii1 - h) / e_ii1))
    return H


def eccentricity_factor(h):
    """
    Returns the eccentricity factor :math:`e_t` from given hue :math:`h` angle
    for forward *CIECAM02* implementation.

    Parameters
    ----------
    h : numeric
        Hue :math:`h` angle in degrees.

    Returns
    -------
    numeric
        Eccentricity factor :math:`e_t`.

    Examples
    --------
    >>> eccentricity_factor(-140.951567342)  # doctest: +ELLIPSIS
    1.1740054...
    """

    e_t = 1 / 4 * (math.cos(2 + h * math.pi / 180) + 3.8)
    return e_t


def achromatic_response_forward(RGB, N_bb):
    """
    Returns the achromatic response :math:`A` from given compressed
    *CMCCAT2000* transform sharpened *RGB* matrix and :math:`N_{bb}` chromatic
    induction factor for forward *CIECAM02* implementation.

    Parameters
    ----------
    RGB : array_like
        Compressed *CMCCAT2000* transform sharpened *RGB* matrix.
    N_bb : numeric
        Chromatic induction factor :math:`N_{bb}`.

    Returns
    -------
    numeric
        Achromatic response :math:`A`.

    Examples
    --------
    >>> RGB = np.array([7.9463202, 7.94711528, 7.94899595])
    >>> N_bb = 1.0003040045593807
    >>> achromatic_response_forward(RGB, N_bb)  # doctest: +ELLIPSIS
    23.9394809...
    """

    R, G, B = np.ravel(RGB)

    A = (2 * R + G + (1 / 20) * B - 0.305) * N_bb
    return A


def achromatic_response_reverse(A_w, J, c, z):
    """
    Returns the achromatic response :math:`A` from given 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 reverse *CIECAM02* implementation.

    Parameters
    ----------
    A_w : numeric
        Achromatic response :math:`A_w` for the whitepoint.
    J : numeric
        *Lightness* correlate :math:`J`.
    c : numeric
        Surround exponential non linearity :math:`c`.
    z : numeric
        Base exponential non linearity :math:`z`.

    Returns
    -------
    numeric
        Achromatic response :math:`A`.

    Examples
    --------
    >>> A_w = 46.1882087914
    >>> J = 41.73109113251392
    >>> c = 0.69
    >>> z = 1.9272135954999579
    >>> achromatic_response_reverse(A_w, J, c, z)  # doctest: +ELLIPSIS
    23.9394809...
    """

    A = A_w * (J / 100) ** (1 / (c * z))
    return A


def lightness_correlate(A, A_w, c, z):
    """
    Returns the *Lightness* correlate :math:`J`.

    Parameters
    ----------
    A : numeric
        Achromatic response :math:`A` for the stimulus.
    A_w : numeric
        Achromatic response :math:`A_w` for the whitepoint.
    c : numeric
        Surround exponential non linearity :math:`c`.
    z : numeric
        Base exponential non linearity :math:`z`.

    Returns
    -------
    numeric
        *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
    41.7310911...
    """

    J = 100 * (A / A_w) ** (c * z)
    return J


def brightness_correlate(c, J, A_w, F_L):
    """
    Returns the *brightness* correlate :math:`Q`.

    Parameters
    ----------
    c : numeric
        Surround exponential non linearity :math:`c`.
    J : numeric
        *Lightness* correlate :math:`J`.
    A_w : numeric
        Achromatic response :math:`A_w` for the whitepoint.
    F_L : numeric
        *Luminance* level adaptation factor :math:`F_L`.

    Returns
    -------
    numeric
        *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
    195.3713259...
    """

    Q = (4 / c) * math.sqrt(J / 100) * (A_w + 4) * F_L ** 0.25
    return Q


def temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a):
    """
    Returns the temporary magnitude quantity :math:`t`. for forward *CIECAM02*
    implementation.

    Parameters
    ----------
    N_c : numeric
        Surround chromatic induction factor :math:`N_{c}`.
    N_cb : numeric
        Chromatic induction factor :math:`N_{cb}`.
    e_t : numeric
        Eccentricity factor :math:`e_t`.
    a : numeric
        Opponent colour dimension :math:`a`.
    b : numeric
        Opponent colour dimension :math:`b`.
    RGB_a : array_like
        Compressed stimulus *CMCCAT2000* transform sharpened *RGB* matrix.

    Returns
    -------
    numeric
         Temporary magnitude quantity :math:`t`.

    Examples
    --------
    >>> N_c = 1.0
    >>> N_cb = 1.00030400456
    >>> e_t = 1.1740054728519145
    >>> a = -0.000624112068243
    >>> b = -0.000506270106773
    >>> RGB_a = np.array([7.9463202, 7.94711528, 7.94899595])
    >>> temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a)  # noqa  # doctest: +ELLIPSIS
    0.1497462...
    """

    Ra, Ga, Ba = np.ravel(RGB_a)
    t = ((50000 / 13) * N_c * N_cb) * (e_t * (a ** 2 + b ** 2) ** 0.5) / (
        Ra + Ga + 21 * Ba / 20)
    return t


def temporary_magnitude_quantity_reverse(C, J, n):
    """
    Returns the temporary magnitude quantity :math:`t`. for reverse *CIECAM02*
    implementation.

    Parameters
    ----------
    C : numeric
        *Chroma* correlate :math:`C`.
    J : numeric
        *Lightness* correlate :math:`J`.
    n : numeric
        Function of the luminance factor of the background :math:`n`.

    Returns
    -------
    numeric
         Temporary magnitude quantity :math:`t`.

    Examples
    --------
    >>> C = 68.8364136888275
    >>> J = 41.749268505999
    >>> n = 0.2
    >>> temporary_magnitude_quantity_reverse(C, J, n)  # doctest: +ELLIPSIS
    202.3873619...
   """

    t = (C / (math.sqrt(J / 100) * (1.64 - 0.29 ** n) ** 0.73)) ** (1 / 0.9)
    return t


def chroma_correlate(J, n, N_c, N_cb, e_t, a, b, RGB_a):
    """
    Returns the *chroma* correlate :math:`C`.

    Parameters
    ----------
    J : numeric
        *Lightness* correlate :math:`J`.
    n : numeric
        Function of the luminance factor of the background :math:`n`.
    N_c : numeric
        Surround chromatic induction factor :math:`N_{c}`.
    N_cb : numeric
        Chromatic induction factor :math:`N_{cb}`.
    e_t : numeric
        Eccentricity factor :math:`e_t`.
    a : numeric
        Opponent colour dimension :math:`a`.
    b : numeric
        Opponent colour dimension :math:`b`.
    RGB_a : array_like
        Compressed stimulus *CMCCAT2000* transform sharpened *RGB* matrix.

    Returns
    -------
    numeric
        *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.9463202, 7.94711528, 7.94899595])
    >>> chroma_correlate(J, n, N_c, N_cb, e_t, a, b, RGB_a)  # noqa  # doctest: +ELLIPSIS
    0.1047077...
    """

    t = temporary_magnitude_quantity_forward(N_c, N_cb, e_t, a, b, RGB_a)
    C = t ** 0.9 * (J / 100) ** 0.5 * (1.64 - 0.29 ** n) ** 0.73

    return C


def colourfulness_correlate(C, F_L):
    """
    Returns the *colourfulness* correlate :math:`M`.

    Parameters
    ----------
    C : numeric
        *Chroma* correlate :math:`C`.
    F_L : numeric
        *Luminance* level adaptation factor :math:`F_L`.

    Returns
    -------
    numeric
        *Colourfulness* correlate :math:`M`.

    Examples
    --------
    >>> C = 0.104707757171
    >>> F_L = 1.16754446415
    >>> colourfulness_correlate(C, F_L)  # doctest: +ELLIPSIS
    0.1088421...
    """

    M = C * F_L ** 0.25
    return M


def saturation_correlate(M, Q):
    """
    Returns the *saturation* correlate :math:`s`.

    Parameters
    ----------
    M : numeric
        *Colourfulness* correlate :math:`M`.
    Q : numeric
        *Brightness* correlate :math:`C`.

    Returns
    -------
    numeric
        *Saturation* correlate :math:`s`.

    Examples
    --------
    >>> M = 0.108842175669
    >>> Q = 195.371325966
    >>> saturation_correlate(M, Q)  # doctest: +ELLIPSIS
    2.3603053...
    """

    s = 100 * (M / Q) ** 0.5
    return s


def P(N_c, N_cb, e_t, t, A, N_bb):
    """
    Returns the points :math:`P_1`, :math:`P_2` and :math:`P_3`.

    Parameters
    ----------
    N_c : numeric
        Surround chromatic induction factor :math:`N_{c}`.
    N_cb : numeric
        Chromatic induction factor :math:`N_{cb}`.
    e_t : numeric
        Eccentricity factor :math:`e_t`.
    t : numeric
        Temporary magnitude quantity :math:`t`.
    A : numeric
        Achromatic response  :math:`A` for the stimulus.
    N_bb : numeric
        Chromatic induction factor :math:`N_{bb}`.

    Returns
    -------
    tuple
        Points :math:`P`.

    Examples
    --------
    >>> N_c = 1.0
    >>> N_cb = 1.00030400456
    >>> e_t = 1.1740054728519145
    >>> t = 0.149746202921
    >>> A = 23.9394809667
    >>> N_bb = 1.00030400456
    >>> P(N_c, N_cb, e_t, t, A, N_bb)  # doctest: +ELLIPSIS
    (30162.8908154..., 24.2372054..., 1.05)
    """

    P_1 = ((50000 / 13) * N_c * N_cb * e_t) / t
    P_2 = A / N_bb + 0.305
    P_3 = 21 / 20

    return P_1, P_2, P_3


def post_adaptation_non_linear_response_compression_matrix(P_2, a, b):
    """
    Returns the post adaptation non linear response compression matrix.

    Parameters
    ----------
    P_2 : numeric
        Point :math:`P_2`.
    a : numeric
        Opponent colour dimension :math:`a`.
    b : numeric
        Opponent colour dimension :math:`b`.

    Returns
    -------
    ndarray, (3,)
        Points :math:`P`.

    Examples
    --------
    >>> P_2 = 24.2372054671
    >>> a = -0.000624112068243
    >>> b = -0.000506270106773
    >>> post_adaptation_non_linear_response_compression_matrix(P_2, a, b)  # noqa  # doctest: +ELLIPSIS
    array([ 7.9463202...,  7.9471152...,  7.9489959...])
    """

    R_a = (460 * P_2 + 451 * a + 288 * b) / 1403
    G_a = (460 * P_2 - 891 * a - 261 * b) / 1403
    B_a = (460 * P_2 - 220 * a - 6300 * b) / 1403

    return np.array([R_a, G_a, B_a])
