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

"""
Munsell Renotation System
=========================

Defines various objects for *Munsell Renotation System* computations:

-   :func:`munsell_value_priest1920`: *Munsell* value :math:`V` computation of
    given *luminance* :math:`Y` using *Priest et al. (1920)* method.
-   :func:`munsell_value_munsell1933`: *Munsell* value :math:`V` computation of
    given *luminance* :math:`Y` using *Munsell, Sloan, and Godlove (1933)*
    method.
-   :func:`munsell_value_moon1943`: *Munsell* value :math:`V` computation of
    given *luminance* :math:`Y` using *Moon and Spencer (1943)* method.
-   :func:`munsell_value_saunderson1944`: *Munsell* value :math:`V` computation
    of given *luminance* :math:`Y` using *Saunderson and Milner (1944)* method.
-   :func:`munsell_value_ladd1955`: *Munsell* value :math:`V` computation of
    given *luminance* :math:`Y` using *Ladd and Pinney (1955)*  method.
-   :func:`munsell_value_mccamy1987`: *Munsell* value :math:`V` computation of
    given *luminance* :math:`Y` using *McCamy (1987)*  method.
-   :func:`munsell_value_ASTM_D1535_08` [1]_ [2]_: *Munsell* value :math:`V`
    computation of given *luminance* :math:`Y` using *ASTM D1535-08e1 (2008)*
    method.
-   :func:`munsell_colour_to_xyY` [1]_ [2]_
-   :func:`xyY_to_munsell_colour` [1]_ [2]_

See Also
--------
`Munsell Renotation System IPython Notebook
<http://nbviewer.ipython.org/github/colour-science/colour-ipython/blob/master/notebooks/notation/munsell.ipynb>`_  # noqa

References
----------
.. [1]  **Paul Centore**, `Munsell and Kubelka-Munk Toolbox
        <http://www.99main.com/~centore/MunsellResources/MunsellResources.html>`_  # noqa
        (Last accessed 26 July 2014)
.. [2]  **Paul Centore**,
        http://www.99main.com/~centore/ColourSciencePapers/OpenSourceInverseRenotationArticle.pdf  # noqa
        (Last accessed 26 July 2014)
"""

from __future__ import division, unicode_literals

import math
import numpy as np
import re

try:
    from collections import OrderedDict
except ImportError:
    from ordereddict import OrderedDict

from colour.algebra import (
    Extrapolator1d,
    LinearInterpolator1d,
    cartesian_to_cylindrical,
    is_numeric,
    is_integer)
from colour.algebra.common import (
    INTEGER_THRESHOLD,
    FLOATING_POINT_NUMBER_PATTERN)
from colour.colorimetry import ILLUMINANTS, luminance_ASTM_D1535_08
from colour.models import Lab_to_LCHab, XYZ_to_Lab, XYZ_to_xy, xyY_to_XYZ
from colour.optimal import is_within_macadam_limits
from colour.notation import MUNSELL_COLOURS_ALL
from colour.utilities import CaseInsensitiveMapping, Lookup

__author__ = 'Colour Developers, Paul Centore'
__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__ = ['MUNSELL_GRAY_PATTERN',
           'MUNSELL_COLOUR_PATTERN',
           'MUNSELL_GRAY_FORMAT',
           'MUNSELL_COLOUR_FORMAT',
           'MUNSELL_GRAY_EXTENDED_FORMAT',
           'MUNSELL_COLOUR_EXTENDED_FORMAT',
           'MUNSELL_HUE_LETTER_CODES',
           'MUNSELL_DEFAULT_ILLUMINANT',
           'MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES',
           'munsell_value_priest1920',
           'munsell_value_munsell1933',
           'munsell_value_moon1943',
           'munsell_value_saunderson1944',
           'munsell_value_ladd1955',
           'munsell_value_mccamy1987',
           'munsell_value_ASTM_D1535_08',
           'MUNSELL_VALUE_METHODS',
           'munsell_value',
           'munsell_specification_to_xyY',
           'munsell_colour_to_xyY',
           'xyY_to_munsell_specification',
           'xyY_to_munsell_colour',
           'parse_munsell_colour',
           'is_grey_munsell_colour',
           'normalize_munsell_specification',
           'munsell_colour_to_munsell_specification',
           'munsell_specification_to_munsell_colour',
           'xyY_from_renotation',
           'is_specification_in_renotation',
           'bounding_hues_from_renotation',
           'hue_to_hue_angle',
           'hue_angle_to_hue',
           'hue_to_ASTM_hue',
           'interpolation_method_from_renotation_ovoid',
           'xy_from_renotation_ovoid',
           'LCHab_to_munsell_specification',
           'maximum_chroma_from_renotation',
           'munsell_specification_to_xy']

MUNSELL_GRAY_PATTERN = 'N(?P<value>{0})'.format(FLOATING_POINT_NUMBER_PATTERN)
MUNSELL_COLOUR_PATTERN = (
    '(?P<hue>{0})\s*(?P<letter>BG|GY|YR|RP|PB|B|G|Y|R|P)\s*(?P<value>{0})\s*\/\s*(?P<chroma>[-+]?{0})'.format(  # noqa
        FLOATING_POINT_NUMBER_PATTERN))

MUNSELL_GRAY_FORMAT = 'N{0}'
MUNSELL_COLOUR_FORMAT = '{0} {1}/{2}'
MUNSELL_GRAY_EXTENDED_FORMAT = 'N{0:.{1}f}'
MUNSELL_COLOUR_EXTENDED_FORMAT = '{0:.{1}f}{2} {3:.{4}f}/{5:.{6}f}'

MUNSELL_HUE_LETTER_CODES = Lookup({
    'BG': 2,
    'GY': 4,
    'YR': 6,
    'RP': 8,
    'PB': 10,
    'B': 1,
    'G': 3,
    'Y': 5,
    'R': 7,
    'P': 9})

MUNSELL_DEFAULT_ILLUMINANT = 'C'
MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES = ILLUMINANTS.get(
    'CIE 1931 2 Degree Standard Observer').get(
    MUNSELL_DEFAULT_ILLUMINANT)

_MUNSELL_SPECIFICATIONS_CACHE = None
_MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR_CACHE = None
_MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION_CACHE = None


def _munsell_specifications():
    """
    Returns the *Munsell Renotation System* specifications and caches them if
    not existing.

    The *Munsell Renotation System* data is stored in
    :attr:`colour.notation.dataset.munsell.MUNSELL_COLOURS` attribute in a 2
    columns form:

    (('2.5GY', 0.2, 2.0), (0.713, 1.414, 0.237)),
    (('5GY', 0.2, 2.0), (0.449, 1.145, 0.237)),
    (('7.5GY', 0.2, 2.0), (0.262, 0.837, 0.237)),
    ...,)

    The first column is converted from *Munsell* colour to specification using
    :def:`munsell_colour_to_munsell_specification` definition:

    ('2.5GY', 0.2, 2.0) ---> (2.5, 0.2, 2.0, 4)

    Returns
    -------
    list
        *Munsell Renotation System* specifications.
    """

    global _MUNSELL_SPECIFICATIONS_CACHE
    if _MUNSELL_SPECIFICATIONS_CACHE is None:
        _MUNSELL_SPECIFICATIONS_CACHE = [
            munsell_colour_to_munsell_specification(
                MUNSELL_COLOUR_FORMAT.format(*colour[0]))
            for colour in MUNSELL_COLOURS_ALL]
    return _MUNSELL_SPECIFICATIONS_CACHE


def _munsell_value_ASTM_D1535_08_interpolator():
    """
    Returns the *Munsell* value interpolator for *ASTM D1535-08* method and
    caches it if not existing.

    Returns
    -------
    Extrapolator1d
        *Munsell* value interpolator for *ASTM D1535-08* method.
    """

    global _MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR_CACHE
    munsell_values = np.arange(0, 10, 0.001)
    if _MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR_CACHE is None:
        _MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR_CACHE = Extrapolator1d(
            LinearInterpolator1d([luminance_ASTM_D1535_08(x)
                                  for x in munsell_values],
                                 munsell_values))

    return _MUNSELL_VALUE_ASTM_D1535_08_INTERPOLATOR_CACHE


def _munsell_maximum_chromas_from_renotation():
    """
    Returns the maximum *Munsell* chromas from *Munsell Renotation System* data
    and caches them if not existing.

    Returns
    -------
    tuple
        Maximum *Munsell* chromas.
    """

    global _MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION_CACHE
    if _MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION_CACHE is None:
        chromas = OrderedDict()
        for munsell_colour in MUNSELL_COLOURS_ALL:
            hue, value, chroma, code = munsell_colour_to_munsell_specification(
                MUNSELL_COLOUR_FORMAT.format(*munsell_colour[0]))
            index = (hue, value, code)
            if index in chromas:
                chroma = max(chromas[index], chroma)

            chromas[index] = chroma

        _MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION_CACHE = tuple(
            zip(chromas.keys(), chromas.values()))
    return _MUNSELL_MAXIMUM_CHROMAS_FROM_RENOTATION_CACHE


def munsell_value_priest1920(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    *Priest et al. (1920)* method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    References
    ----------
    .. [14] http://en.wikipedia.org/wiki/Lightness
            (Last accessed 13 April 2014)

    Examples
    --------
    >>> munsell_value_priest1920(10.08)  # doctest: +ELLIPSIS
    3.1749015...
    """

    Y /= 100
    V = 10 * math.sqrt(Y)

    return V


def munsell_value_munsell1933(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    *Munsell, Sloan, and Godlove (1933)* method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    References
    ----------
    .. [15] http://en.wikipedia.org/wiki/Lightness
            (Last accessed 13 April 2014)

    Examples
    --------
    >>> munsell_value_munsell1933(10.08)  # doctest: +ELLIPSIS
    3.7918355...
    """

    V = math.sqrt(1.4742 * Y - 0.004743 * (Y * Y))

    return V


def munsell_value_moon1943(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    *Moon and Spencer (1943)* method.


    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    References
    ----------
    .. [16] http://en.wikipedia.org/wiki/Lightness
            (Last accessed 13 April 2014)

    Examples
    --------
    >>> munsell_value_moon1943(10.08)  # doctest: +ELLIPSIS
    3.7462971...
    """

    V = 1.4 * Y ** 0.426

    return V


def munsell_value_saunderson1944(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    *Saunderson and Milner (1944)* method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    References
    ----------
    .. [17] http://en.wikipedia.org/wiki/Lightness
            (Last accessed 13 April 2014)

    Examples
    --------
    >>> munsell_value_saunderson1944(10.08)  # doctest: +ELLIPSIS
    3.6865080...
    """

    V = 2.357 * (Y ** 0.343) - 1.52

    return V


def munsell_value_ladd1955(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    *Ladd and Pinney (1955)*  method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    References
    ----------
    .. [18] http://en.wikipedia.org/wiki/Lightness
            (Last accessed 13 April 2014)

    Examples
    --------
    >>> munsell_value_ladd1955(10.08)  # doctest: +ELLIPSIS
    3.6952862...
    """

    V = 2.468 * (Y ** (1 / 3)) - 1.636

    return V


def munsell_value_mccamy1987(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    *McCamy (1987)*  method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    References
    ----------
    .. [19] `Standard Test Method for Specifying Color by the Munsell System -
            ASTM-D1535-1989
            <https://law.resource.org/pub/us/cfr/ibr/003/astm.d1535.1989.pdf>`_

    Examples
    --------
    >>> munsell_value_mccamy1987(10.08)  # doctest: +ELLIPSIS
    3.7347235...
    """

    if Y <= 0.9:
        V = 0.87445 * (Y ** 0.9967)
    else:
        V = (2.49268 * (Y ** (1 / 3)) - 1.5614 -
             (0.985 / (((0.1073 * Y - 3.084) ** 2) + 7.54)) +
             (0.0133 / (Y ** 2.3)) +
             0.0084 * math.sin(4.1 * (Y ** (1 / 3)) + 1) +
             (0.0221 / Y) * math.sin(0.39 * (Y - 2)) -
             (0.0037 / (0.44 * Y)) * math.sin(1.28 * (Y - 0.53)))
    return V


def munsell_value_ASTM_D1535_08(Y):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    a reverse lookup table from *ASTM D1535-08e1 (2008)* method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`

    Returns
    -------
    numeric
        *Munsell* value :math:`V`..

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    Examples
    --------
    >>> munsell_value_ASTM_D1535_08(10.1488096782)  # doctest: +ELLIPSIS
    3.7462971...
    """

    V = _munsell_value_ASTM_D1535_08_interpolator()(Y)

    return V


MUNSELL_VALUE_METHODS = CaseInsensitiveMapping(
    {'Priest 1920': munsell_value_priest1920,
     'Munsell 1933': munsell_value_munsell1933,
     'Moon 1943': munsell_value_moon1943,
     'Saunderson 1944': munsell_value_saunderson1944,
     'Ladd 1955': munsell_value_ladd1955,
     'McCamy 1987': munsell_value_mccamy1987,
     'ASTM D1535-08': munsell_value_ASTM_D1535_08})
"""
Supported *Munsell* value computations methods.

MUNSELL_VALUE_METHODS : dict
    ('Priest 1920', 'Munsell 1933', 'Moon 1943', 'Saunderson 1944',
    'Ladd 1955', 'McCamy 1987', 'ASTM D1535-08')

Aliases:

-   'astm2008': 'ASTM D1535-08'
"""
MUNSELL_VALUE_METHODS['astm2008'] = (
    MUNSELL_VALUE_METHODS['ASTM D1535-08'])


def munsell_value(Y, method='ASTM D1535-08'):
    """
    Returns the *Munsell* value :math:`V` of given *luminance* :math:`Y` using
    given method.

    Parameters
    ----------
    Y : numeric
        *luminance* :math:`Y`.
    method : unicode, optional
        ('Priest 1920', 'Munsell 1933', 'Moon 1943', 'Saunderson 1944',
        'Ladd 1955', 'McCamy 1987', 'ASTM D1535-08')
        Computation method.

    Returns
    -------
    numeric
        *Munsell* value :math:`V`.

    Notes
    -----
    -   Input *Y* is in domain [0, 100].
    -   Output *V* is in domain [0, 10].

    Examples
    --------
    >>> munsell_value(10.08)  # doctest: +ELLIPSIS
    3.7344764...
    >>> munsell_value(10.08, method='Priest 1920')  # doctest: +ELLIPSIS
    3.1749015...
    >>> munsell_value(10.08, method='Munsell 1933')  # doctest: +ELLIPSIS
    3.7918355...
    >>> munsell_value(10.08, method='Moon 1943')  # doctest: +ELLIPSIS
    3.7462971...
    >>> munsell_value(10.08, method='Saunderson 1944')  # doctest: +ELLIPSIS
    3.6865080...
    >>> munsell_value(10.08, method='Ladd 1955')  # doctest: +ELLIPSIS
    3.6952862...
    >>> munsell_value(10.08, method='McCamy 1987')  # doctest: +ELLIPSIS
    3.7347235...
    """

    return MUNSELL_VALUE_METHODS.get(method)(Y)


def munsell_specification_to_xyY(specification):
    """
    Converts given *Munsell* *Colorlab* specification to *CIE xyY* colourspace.


    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    ndarray, (3,)
        *CIE xyY* colourspace matrix.

    Notes
    -----
    -   Input *Munsell* *Colorlab* specification hue must be in domain [0, 10].
    -   Input *Munsell* *Colorlab* specification value must be in domain
        [0, 10].
    -   Output *CIE xyY* colourspace matrix is in domain [0, 1].

    References
    ----------
    .. [12] **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/MunsellToxyY.m*

    Examples
    --------
    >>> spc = (2.1, 8.0, 17.9, 4)
    >>> munsell_specification_to_xyY(spc)  # doctest: +ELLIPSIS
    array([ 0.4400632...,  0.5522428...,  0.5761962...])
    >>> munsell_specification_to_xyY(8.9)  # doctest: +ELLIPSIS
    array([ 0.31006  ,  0.31616  ,  0.746134...])
    """

    if is_grey_munsell_colour(specification):
        value = specification
    else:
        hue, value, chroma, code = specification

        assert 0 <= hue <= 10, (
            '"{0}" specification hue must be in domain [0, 10]!'.format(
                specification))
        assert 0 <= value <= 10, (
            '"{0}" specification value must be in domain [0, 10]!'.format(
                specification))

    Y = luminance_ASTM_D1535_08(value)

    if is_integer(value):
        value_minus = value_plus = round(value)
    else:
        value_minus = math.floor(value)
        value_plus = value_minus + 1

    specification_minus = (value_minus
                           if is_grey_munsell_colour(specification) else
                           (hue, value_minus, chroma, code))
    x_minus, y_minus = munsell_specification_to_xy(specification_minus)

    plus_specification = (value_plus
                          if (is_grey_munsell_colour(specification) or
                              value_plus == 10) else
                          (hue, value_plus, chroma, code))
    x_plus, y_plus = munsell_specification_to_xy(plus_specification)

    if value_minus == value_plus:
        x = x_minus
        y = y_minus
    else:
        Y_minus = luminance_ASTM_D1535_08(value_minus)
        Y_plus = luminance_ASTM_D1535_08(value_plus)
        x = LinearInterpolator1d([Y_minus, Y_plus], [x_minus, x_plus])(Y)
        y = LinearInterpolator1d([Y_minus, Y_plus], [y_minus, y_plus])(Y)

    return np.array([x, y, Y / 100])


def munsell_colour_to_xyY(munsell_colour):
    """
    Converts given *Munsell* colour to *CIE xyY* colourspace.

    Parameters
    ----------
    munsell_colour : unicode
        *Munsell* colour.

    Returns
    -------
    ndarray, (3,)
        *CIE xyY* colourspace matrix.

    Notes
    -----
    -   Output *CIE xyY* colourspace matrix is in domain [0, 1].

    Examples
    --------
    >>> munsell_colour_to_xyY('4.2YR 8.1/5.3')  # doctest: +ELLIPSIS
    array([ 0.3873694...,  0.3575165...,  0.59362   ])
    >>> munsell_colour_to_xyY('N8.9')  # doctest: +ELLIPSIS
    array([ 0.31006  ,  0.31616  ,  0.746134...])
    """

    specification = munsell_colour_to_munsell_specification(munsell_colour)
    return munsell_specification_to_xyY(specification)


def xyY_to_munsell_specification(xyY):
    """
    Converts from *CIE xyY* colourspace to *Munsell* *Colorlab* specification.

    Parameters
    ----------
    xyY : array_like, (3,)
        *CIE xyY* colourspace matrix.

    Returns
    -------
    numeric or tuple
        *Munsell* *Colorlab* specification.

    Raises
    ------
    ValueError
        If the given *CIE xyY* colourspace matrix is not within *MacAdam*
        limits.
    RuntimeError
        If the maximum iterations count has been reached without converging to
        a result.

    Notes
    -----
    -   Input *CIE xyY* colourspace matrix is in domain [0, 1].

    References
    ----------
    .. [13] **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/xyYtoMunsell.m*

    Examples
    --------
    >>> xyY = np.array([0.38736945, 0.35751656, 0.59362])
    >>> xyY_to_munsell_specification(xyY)  # doctest: +ELLIPSIS
    (4.1742530..., 8.0999999..., 5.3044360..., 6)
    """

    if not is_within_macadam_limits(xyY, MUNSELL_DEFAULT_ILLUMINANT):
        raise ValueError(
            ('"{0}" is not within "MacAdam" limits for illuminant '
             '"{1}"!').format(xyY, MUNSELL_DEFAULT_ILLUMINANT))

    x, y, Y = np.ravel(xyY)

    # Scaling *Y* for algorithm needs.
    value = munsell_value_ASTM_D1535_08(Y * 100)
    if is_integer(value):
        value = round(value)

    x_center, y_center, Y_center = np.ravel(
        munsell_specification_to_xyY(value))
    z_input, theta_input, rho_input = cartesian_to_cylindrical((x - x_center,
                                                                y - y_center,
                                                                Y_center))
    theta_input = math.degrees(theta_input)

    grey_threshold = 0.001
    if rho_input < grey_threshold:
        return value

    X, Y, Z = np.ravel(xyY_to_XYZ((x, y, Y)))
    xi, yi = MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES
    Xr, Yr, Zr = np.ravel(xyY_to_XYZ((xi, yi, Y)))

    XYZ = np.array((X, Y, Z))
    XYZr = np.array(((1 / Yr) * Xr, 1, (1 / Yr) * Zr))

    Lab = XYZ_to_Lab(XYZ, XYZ_to_xy(XYZr))
    LCHab = Lab_to_LCHab(Lab)
    hue_initial, value_initial, chroma_initial, code_initial = (
        LCHab_to_munsell_specification(LCHab))
    specification_current = [hue_initial,
                             value,
                             (5 / 5.5) * chroma_initial,
                             code_initial]

    convergence_threshold = 0.0001
    iterations_maximum = 64
    iterations = 0

    while iterations <= iterations_maximum:
        iterations += 1

        hue_current, value_current, chroma_current, code_current = (
            specification_current)
        hue_angle_current = hue_to_hue_angle(hue_current, code_current)

        chroma_maximum = maximum_chroma_from_renotation(hue_current,
                                                        value,
                                                        code_current)
        if chroma_current > chroma_maximum:
            chroma_current = specification_current[2] = chroma_maximum

        x_current, y_current, Y_current = np.ravel(
            munsell_specification_to_xyY(specification_current))

        z_current, theta_current, rho_current = cartesian_to_cylindrical(
            (x_current - x_center,
             y_current - y_center,
             Y_center))
        theta_current = math.degrees(theta_current)
        theta_current_difference = (360 - theta_input + theta_current) % 360
        if theta_current_difference > 180:
            theta_current_difference -= 360

        theta_differences = [theta_current_difference]
        hue_angles = [hue_angle_current]
        hue_angles_differences = [0]

        iterations_maximum_inner = 16
        iterations_inner = 0
        extrapolate = False

        while np.sign(min(theta_differences)) == np.sign(
                max(theta_differences)) and extrapolate is False:
            iterations_inner += 1

            if iterations_inner > iterations_maximum_inner:
                raise RuntimeError(('Maximum inner iterations count reached '
                                    'without convergence!'))

            hue_angle_inner = ((hue_angle_current + iterations_inner *
                                (theta_input - theta_current)) % 360)
            hue_angle_difference_inner = (iterations_inner *
                                          (theta_input - theta_current) % 360)
            if hue_angle_difference_inner > 180:
                hue_angle_difference_inner -= 360

            hue_inner, code_inner = hue_angle_to_hue(hue_angle_inner)
            x_inner, y_inner, Y_inner = np.ravel(
                munsell_specification_to_xyY((hue_inner,
                                              value,
                                              chroma_current,
                                              code_inner)))

            if len(theta_differences) >= 2:
                extrapolate = True

            if extrapolate is False:
                z_inner, theta_inner, rho_inner = cartesian_to_cylindrical(
                    (x_inner - x_center,
                     y_inner - y_center,
                     Y_center))
                theta_inner = math.degrees(theta_inner)
                theta_inner_difference = (
                    (360 - theta_input + theta_inner) % 360)
                if theta_inner_difference > 180:
                    theta_inner_difference -= 360

                theta_differences.append(theta_inner_difference)
                hue_angles.append(hue_angle_inner)
                hue_angles_differences.append(hue_angle_difference_inner)

        theta_differences = np.array(theta_differences)
        hue_angles_differences = np.array(hue_angles_differences)

        theta_differences_indexes = theta_differences.argsort()

        theta_differences = theta_differences[theta_differences_indexes]
        hue_angles_differences = hue_angles_differences[
            theta_differences_indexes]

        hue_angle_difference_new = Extrapolator1d(
            LinearInterpolator1d(
                theta_differences,
                hue_angles_differences))(0) % 360
        hue_angle_new = (hue_angle_current + hue_angle_difference_new) % 360

        hue_new, code_new = hue_angle_to_hue(hue_angle_new)
        specification_current = [hue_new, value, chroma_current, code_new]

        x_current, y_current, Y_current = np.ravel(
            munsell_specification_to_xyY(specification_current))
        difference = np.linalg.norm(
            np.array((x, y)) - np.array((x_current, y_current)))
        if difference < convergence_threshold:
            return tuple(specification_current)

        # TODO: Consider refactoring implementation.
        hue_current, value_current, chroma_current, code_current = (
            specification_current)
        chroma_maximum = maximum_chroma_from_renotation(hue_current,
                                                        value,
                                                        code_current)
        if chroma_current > chroma_maximum:
            chroma_current = specification_current[2] = chroma_maximum

        x_current, y_current, Y_current = np.ravel(
            munsell_specification_to_xyY(specification_current))

        z_current, theta_current, rho_current = cartesian_to_cylindrical(
            (x_current - x_center,
             y_current - y_center,
             Y_center))

        rho_bounds = [rho_current]
        chroma_bounds = [chroma_current]

        iterations_maximum_inner = 16
        iterations_inner = 0
        while rho_input < min(rho_bounds) or rho_input > max(rho_bounds):
            iterations_inner += 1

            if iterations_inner > iterations_maximum_inner:
                raise RuntimeError(('Maximum inner iterations count reached '
                                    'without convergence!'))

            chroma_inner = (((rho_input / rho_current) ** iterations_inner) *
                            chroma_current)
            if chroma_inner > chroma_maximum:
                chroma_inner = specification_current[2] = chroma_maximum

            specification_inner = (
                hue_current, value, chroma_inner, code_current)
            x_inner, y_inner, Y_inner = np.ravel(
                munsell_specification_to_xyY(specification_inner))

            z_inner, theta_inner, rho_inner = cartesian_to_cylindrical(
                (x_inner - x_center,
                 y_inner - y_center,
                 Y_center))

            rho_bounds.append(rho_inner)
            chroma_bounds.append(chroma_inner)

        rho_bounds = np.array(rho_bounds)
        chroma_bounds = np.array(chroma_bounds)

        rhos_bounds_indexes = rho_bounds.argsort()

        rho_bounds = rho_bounds[rhos_bounds_indexes]
        chroma_bounds = chroma_bounds[rhos_bounds_indexes]
        chroma_new = LinearInterpolator1d(rho_bounds, chroma_bounds)(rho_input)

        specification_current = [hue_current, value, chroma_new, code_current]
        x_current, y_current, Y_current = np.ravel(
            munsell_specification_to_xyY(specification_current))
        difference = np.linalg.norm(
            np.array((x, y)) - np.array((x_current, y_current)))
        if difference < convergence_threshold:
            return tuple(specification_current)

    raise RuntimeError(
        'Maximum outside iterations count reached without convergence!')


def xyY_to_munsell_colour(xyY,
                          hue_decimals=1,
                          value_decimals=1,
                          chroma_decimals=1):
    """
    Converts from *CIE xyY* colourspace to *Munsell* colour.

    Parameters
    ----------
    xyY : array_like, (3,)
        *CIE xyY* colourspace matrix.
    hue_decimals : int
        Hue formatting decimals.
    value_decimals : int
        Value formatting decimals.
    chroma_decimals : int
        Chroma formatting decimals.

    Returns
    -------
    unicode
        *Munsell* colour.

    Notes
    -----
    -   Input *CIE xyY* colourspace matrix is in domain [0, 1].

    Examples
    --------
    >>> xyY = np.array([0.38736945, 0.35751656, 0.59362])
    >>> # Doctests skip for Python 2.x compatibility.
    >>> xyY_to_munsell_colour(xyY)  # doctest: +SKIP
    '4.2YR 8.1/5.3'
    """

    specification = xyY_to_munsell_specification(xyY)
    return munsell_specification_to_munsell_colour(specification,
                                                   hue_decimals,
                                                   value_decimals,
                                                   chroma_decimals)


def parse_munsell_colour(munsell_colour):
    """
    Parses given *Munsell* colour and returns an intermediate *Munsell*
    *Colorlab* specification.

    Parameters
    ----------
    munsell_colour : unicode
        *Munsell* colour.

    Returns
    -------
    float or tuple
        Intermediate *Munsell* *Colorlab* specification.

    Raises
    ------
    ValueError
        If the given specification is not a valid *Munsell Renotation System*
        colour specification.

    Examples
    --------
    >>> parse_munsell_colour('N5.2')  # doctest: +ELLIPSIS
    5.2...
    >>> parse_munsell_colour('0YR 2.0/4.0')
    (0.0, 2.0, 4.0, 6)
    """

    match = re.match(MUNSELL_GRAY_PATTERN,
                     munsell_colour,
                     flags=re.IGNORECASE)
    if match:
        return float(match.group('value'))
    match = re.match(MUNSELL_COLOUR_PATTERN,
                     munsell_colour,
                     flags=re.IGNORECASE)
    if match:
        return (float(match.group('hue')),
                float(match.group('value')),
                float(match.group('chroma')),
                MUNSELL_HUE_LETTER_CODES.get(match.group('letter').upper()))

    raise ValueError(
        ('"{0}" is not a valid "Munsell Renotation System" colour '
         'specification!').format(munsell_colour))


def is_grey_munsell_colour(specification):
    """
    Returns if given *Munsell* *Colorlab* specification is a single number form
    used for grey colour.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    bool
        Is specification a grey colour.

    Examples
    --------
    >>> is_grey_munsell_colour((0.0, 2.0, 4.0, 6))
    False
    >>> is_grey_munsell_colour(0.5)
    True
    """

    return is_numeric(specification)


def normalize_munsell_specification(specification):
    """
    Normalises given *Munsell* *Colorlab* specification.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    numeric or tuple
        Normalised *Munsell* *Colorlab* specification.

    Examples
    --------
    >>> normalize_munsell_specification((0.0, 2.0, 4.0, 6))
    (10, 2.0, 4.0, 7)
    """

    if is_grey_munsell_colour(specification):
        return specification
    else:
        hue, value, chroma, code = specification
        if hue == 0:
            # 0YR is equivalent to 10R.
            hue, code = 10, (code + 1) % 10
        return value if chroma == 0 else (hue, value, chroma, code)


def munsell_colour_to_munsell_specification(munsell_colour):
    """
    Convenient definition to retrieve a normalised *Munsell* *Colorlab*
    specification from given *Munsell* colour.

    Parameters
    ----------
    munsell_colour : unicode
        *Munsell* colour.

    Returns
    -------
    numeric or tuple
        Normalised *Munsell* *Colorlab* specification.

    Examples
    --------
    >>> munsell_colour_to_munsell_specification('N5.2')  # doctest: +ELLIPSIS
    5.2...
    >>> munsell_colour_to_munsell_specification('0YR 2.0/4.0')
    (10, 2.0, 4.0, 7)
    """

    return normalize_munsell_specification(
        parse_munsell_colour(munsell_colour))


def munsell_specification_to_munsell_colour(specification,
                                            hue_decimals=1,
                                            value_decimals=1,
                                            chroma_decimals=1):
    """
    Converts from *Munsell* *Colorlab* specification to given *Munsell* colour.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.
    hue_decimals : int, optional
        Hue formatting decimals.
    value_decimals : int, optional
        Value formatting decimals.
    chroma_decimals : int, optional
        Chroma formatting decimals.

    Returns
    -------
    unicode
        *Munsell* colour.

    Examples
    --------
    >>> # Doctests skip for Python 2.x compatibility.
    >>> munsell_specification_to_munsell_colour(5.2)  # doctest: +SKIP
    'N5.2'
    >>> # Doctests skip for Python 2.x compatibility.
    >>> spc = (10, 2.0, 4.0, 7)
    >>> munsell_specification_to_munsell_colour(spc)  # doctest: +SKIP
    '10.0R 2.0/4.0'
    """

    if is_grey_munsell_colour(specification):
        return MUNSELL_GRAY_EXTENDED_FORMAT.format(
            specification, value_decimals)
    else:
        hue, value, chroma, code = specification
        code_values = MUNSELL_HUE_LETTER_CODES.values()

        assert 0 <= hue <= 10, (
            '"{0}" specification hue must be in domain [0, 10]!'.format(
                specification))
        assert 0 <= value <= 10, (
            '"{0}" specification value must be in domain [0, 10]!'.format(
                specification))
        assert 2 <= chroma <= 50, (
            '"{0}" specification chroma must be in domain [2, 50]!'.format(
                specification))
        assert code in code_values, (
            '"{0}" specification code must one of "{1}"!'.format(
                specification, code_values))

        if hue == 0:
            hue, code = 10, (code + 1) % 10

        if value == 0:
            return MUNSELL_GRAY_EXTENDED_FORMAT.format(
                specification, value_decimals)
        else:
            hue_letter = MUNSELL_HUE_LETTER_CODES.first_key_from_value(
                code)
            return MUNSELL_COLOUR_EXTENDED_FORMAT.format(hue,
                                                         hue_decimals,
                                                         hue_letter,
                                                         value,
                                                         value_decimals,
                                                         chroma,
                                                         chroma_decimals)


def xyY_from_renotation(specification):
    """
    Returns given existing *Munsell* *Colorlab* specification *CIE xyY*
    colourspace vector from *Munsell Renotation System* data.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    tuple
        *CIE xyY* colourspace vector.

    Raises
    ------
    ValueError
        If the given specification doesn't exist in *Munsell Renotation System*
        data.

    Examples
    --------
    >>> xyY_from_renotation((2.5, 0.2, 2.0, 4))  # doctest: +ELLIPSIS
    (0.71..., 1.41..., 0.23...)
    """

    specifications = _munsell_specifications()
    try:
        return MUNSELL_COLOURS_ALL[specifications.index(specification)][1]
    except ValueError:
        # TODO: Should raise KeyError, need to check the tests.
        raise ValueError(
            ('"{0}" specification does not exists in '
             '"Munsell Renotation System" data!').format(specification))


def is_specification_in_renotation(specification):
    """
    Returns if given *Munsell* *Colorlab* specification is in
    *Munsell Renotation System* data.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    bool
        Is specification in *Munsell Renotation System* data.

    Examples
    --------
    >>> is_specification_in_renotation((2.5, 0.2, 2.0, 4))
    True
    >>> is_specification_in_renotation((64, 0.2, 2.0, 4))
    False
    """

    try:
        xyY_from_renotation(specification)
        return True
    except ValueError:
        return False


def bounding_hues_from_renotation(hue, code):
    """
    Returns for a given hue the two bounding hues from
    *Munsell Renotation System* data.

    Parameters
    ----------
    hue : numeric
        *Munsell* *Colorlab* specification hue.
    code : numeric
        *Munsell* *Colorlab* specification code.

    Returns
    -------
    tuple
        Bounding hues.

    References
    ----------
    .. [3]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellSystemRoutines/BoundingRenotationHues.m*

    Examples
    --------
    >>> bounding_hues_from_renotation(3.2, 4)
    ((2.5, 4), (5.0, 4))
    """

    if hue % 2.5 == 0:
        if hue == 0:
            hue_cw = 10
            code_cw = (code + 1) % 10
        else:
            hue_cw = hue
            code_cw = code
        hue_ccw = hue_cw
        code_ccw = code_cw
    else:
        hue_cw = 2.5 * math.floor(hue / 2.5)
        hue_ccw = (hue_cw + 2.5) % 10
        if hue_ccw == 0:
            hue_ccw = 10
        code_ccw = code

        if hue_cw == 0:
            hue_cw = 10
            code_cw = (code + 1) % 10
            if code_cw == 0:
                code_cw = 10
        else:
            code_cw = code
        code_ccw = code

    return (hue_cw, code_cw), (hue_ccw, code_ccw)


def hue_to_hue_angle(hue, code):
    """
    Converts from the *Munsell* *Colorlab* specification hue to hue angle in
    degrees.

    Parameters
    ----------
    hue : numeric
        *Munsell* *Colorlab* specification hue.
    code : numeric
        *Munsell* *Colorlab* specification code.

    Returns
    -------
    numeric
        Hue angle in degrees.

    References
    ----------
    .. [4]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/MunsellHueToChromDiagHueAngle.m*

    Examples
    --------
    >>> hue_to_hue_angle(3.2, 4)
    65.5
    """

    single_hue = ((17 - code) % 10 + (hue / 10) - 0.5) % 10
    return LinearInterpolator1d(
        [0, 2, 3, 4, 5, 6, 8, 9, 10],
        [0, 45, 70, 135, 160, 225, 255, 315, 360])(single_hue)


def hue_angle_to_hue(hue_angle):
    """
    Converts from hue angle in degrees to the *Munsell* *Colorlab*
    specification hue.

    Parameters
    ----------
    hue_angle : numeric
        Hue angle in degrees.

    Returns
    -------
    tuple
        (*Munsell* *Colorlab* specification hue, *Munsell* *Colorlab*
        specification code).

    References
    ----------
    .. [5]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/ChromDiagHueAngleToMunsellHue.m*

    Examples
    --------
    >>> hue_angle_to_hue(65.54)  # doctest: +ELLIPSIS
    (3.2160000..., 4)
    """

    single_hue = LinearInterpolator1d(
        [0, 45, 70, 135, 160, 225, 255, 315, 360],
        [0, 2, 3, 4, 5, 6, 8, 9, 10])(hue_angle)

    if single_hue <= 0.5:
        code = 7
    elif single_hue <= 1.5:
        code = 6
    elif single_hue <= 2.5:
        code = 5
    elif single_hue <= 3.5:
        code = 4
    elif single_hue <= 4.5:
        code = 3
    elif single_hue <= 5.5:
        code = 2
    elif single_hue <= 6.5:
        code = 1
    elif single_hue <= 7.5:
        code = 10
    elif single_hue <= 8.5:
        code = 9
    elif single_hue <= 9.5:
        code = 8
    else:
        code = 7

    hue = (10 * (single_hue % 1) + 5) % 10
    if hue == 0:
        hue = 10
    return hue, code


def hue_to_ASTM_hue(hue, code):
    """
    Converts from the *Munsell* *Colorlab* specification hue to *ASTM* hue
    number in domain [0, 100].

    Parameters
    ----------
    hue : numeric
        *Munsell* *Colorlab* specification hue.
    code : numeric
        *Munsell* *Colorlab* specification code.

    Returns
    -------
    numeric
        *ASM* hue number.

    References
    ----------
    .. [6]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/MunsellHueToASTMHue.m*

    Examples
    --------
    >>> hue_to_ASTM_hue(3.2, 4)  # doctest: +ELLIPSIS
    33.2...
    """

    ASTM_hue = 10 * ((7 - code) % 10) + hue
    return 100 if ASTM_hue == 0 else ASTM_hue


def interpolation_method_from_renotation_ovoid(specification):
    """
    Returns whether to use linear or radial interpolation when drawing ovoids
    through data points in the *Munsell Renotation System* data from given
    specification.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    unicode or None ('Linear', 'Radial', None)
        Interpolation method.

    Notes
    -----
    -   Input *Munsell* *Colorlab* specification value must be an integer in
        domain [0, 10].
    -   Input *Munsell* *Colorlab* specification chroma must be an integer and
        a multiple of 2 in domain [2, 50].

    References
    ----------
    .. [7]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellSystemRoutines/LinearVsRadialInterpOnRenotationOvoid.m*

    Examples
    --------
    >>> spc = (2.5, 5.0, 12.0, 4)
    >>> # Doctests skip for Python 2.x compatibility.
    >>> interpolation_method_from_renotation_ovoid()  # doctest: +SKIP
    'Radial'
    """

    interpolation_methods = {0: None,
                             1: 'Linear',
                             2: 'Radial'}
    interpolation_method = 0
    if is_grey_munsell_colour(specification):
        # No interpolation needed for grey colours.
        interpolation_method = 0
    else:
        hue, value, chroma, code = specification

        assert 0 <= value <= 10, (
            '"{0}" specification value must be in domain [0, 10]!'.format(
                specification))
        assert is_integer(value), (
            '"{0}" specification value must be an integer!'.format(
                specification))

        value = round(value)

        # Ideal white, no interpolation needed.
        if value == 10:
            interpolation_method = 0

        assert 2 <= chroma <= 50, (
            '"{0}" specification chroma must be in domain [2, 50]!'.format(
                specification))
        assert abs(
            2 * (chroma / 2 - round(chroma / 2))) <= INTEGER_THRESHOLD, (
            ('"{0}" specification chroma must be an integer and '
             'multiple of 2!').format(specification))

        chroma = 2 * round(chroma / 2)

        # Standard Munsell Renotation System hue, no interpolation needed.
        if hue % 2.5 == 0:
            interpolation_method = 0

        ASTM_hue = hue_to_ASTM_hue(hue, code)

        if value == 1:
            if chroma == 2:
                if 15 < ASTM_hue < 30 or 60 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 4:
                if 12.5 < ASTM_hue < 27.5 or 57.5 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 6:
                if 55 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 8:
                if 67.5 < ASTM_hue < 77.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 10:
                if 72.5 < ASTM_hue < 77.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 2:
            if chroma == 2:
                if 15 < ASTM_hue < 27.5 or 77.5 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 4:
                if 12.5 < ASTM_hue < 30 or 62.5 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 6:
                if 7.5 < ASTM_hue < 22.5 or 62.5 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 8:
                if 7.5 < ASTM_hue < 15 or 60 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 10:
                if 65 < ASTM_hue < 77.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 3:
            if chroma == 2:
                if 10 < ASTM_hue < 37.5 or 65 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 4:
                if 5 < ASTM_hue < 37.5 or 55 < ASTM_hue < 72.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 6 or chroma == 8 or chroma == 10:
                if 7.5 < ASTM_hue < 37.5 or 57.5 < ASTM_hue < 82.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 12:
                if 7.5 < ASTM_hue < 42.5 or 57.5 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 4:
            if chroma == 2 or chroma == 4:
                if 7.5 < ASTM_hue < 42.5 or 57.5 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 6 or chroma == 8:
                if 7.5 < ASTM_hue < 40 or 57.5 < ASTM_hue < 82.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 10:
                if 7.5 < ASTM_hue < 40 or 57.5 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 5:
            if chroma == 2:
                if 5 < ASTM_hue < 37.5 or 55 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 4 or chroma == 6 or chroma == 8:
                if 2.5 < ASTM_hue < 42.5 or 55 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 10:
                if 2.5 < ASTM_hue < 42.5 or 55 < ASTM_hue < 82.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 6:
            if chroma == 2 or chroma == 4:
                if 5 < ASTM_hue < 37.5 or 55 < ASTM_hue < 87.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 6:
                if 5 < ASTM_hue < 42.5 or 57.5 < ASTM_hue < 87.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 8 or chroma == 10:
                if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 12 or chroma == 14:
                if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 82.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 16:
                if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 7:
            if chroma == 2 or chroma == 4 or chroma == 6:
                if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 8:
                if 5 < ASTM_hue < 42.5 or 60 < ASTM_hue < 82.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 10:
                if (30 < ASTM_hue < 42.5 or
                                5 < ASTM_hue < 25 or
                                60 < ASTM_hue < 82.5):
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma == 12:
                if (30 < ASTM_hue < 42.5 or
                                7.5 < ASTM_hue < 27.5 or
                                80 < ASTM_hue < 82.5):
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 14:
                if (32.5 < ASTM_hue < 40 or
                                7.5 < ASTM_hue < 15 or
                                80 < ASTM_hue < 82.5):
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 8:
            if (chroma == 2 or
                        chroma == 4 or
                        chroma == 6 or
                        chroma == 8 or
                        chroma == 10 or
                        chroma == 12):
                if 5 < ASTM_hue < 40 or 60 < ASTM_hue < 85:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 14:
                if (32.5 < ASTM_hue < 40 or
                                5 < ASTM_hue < 15 or
                                60 < ASTM_hue < 85):
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1
        elif value == 9:
            if chroma == 2 or chroma == 4:
                if 5 < ASTM_hue < 40 or 55 < ASTM_hue < 80:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif (chroma == 6 or
                          chroma == 8 or
                          chroma == 10 or
                          chroma == 12 or
                          chroma == 14):
                if 5 < ASTM_hue < 42.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            elif chroma >= 16:
                if 35 < ASTM_hue < 42.5:
                    interpolation_method = 2
                else:
                    interpolation_method = 1
            else:
                interpolation_method = 1

    return interpolation_methods.get(interpolation_method)


def xy_from_renotation_ovoid(specification):
    """
    Converts given *Munsell* *Colorlab* specification to *xy* chromaticity
    coordinates on *Munsell Renotation System* ovoid.
    The *xy* point will be on the ovoid about the achromatic point,
    corresponding to the *Munsell* *Colorlab* specification
    value and chroma.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    tuple
        *xy* chromaticity coordinates.

    Raises
    ------
    ValueError
        If an invalid interpolation method is retrieved from internal
        computations.

    Notes
    -----
    -   Input *Munsell* *Colorlab* specification value must be an integer in
        domain [1, 9].
    -   Input *Munsell* *Colorlab* specification chroma must be an integer and
        a multiple of 2 in domain [2, 50].

    References
    ----------
    .. [8]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/FindHueOnRenotationOvoid.m*

    Examples
    --------
    >>> xy_from_renotation_ovoid((2.5, 5.0, 12.0, 4))  # doctest: +ELLIPSIS
    (0.4333..., 0.5602...)
    >>> xy_from_renotation_ovoid(8)
    (0.31006, 0.31616)
    """

    if is_grey_munsell_colour(specification):
        return MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES
    else:
        hue, value, chroma, code = specification

        assert 1 <= value <= 9, (
            '"{0}" specification value must be in domain [1, 9]!'.format(
                specification))
        assert is_integer(value), (
            '"{0}" specification value must be an integer!'.format(
                specification))

        value = round(value)

        assert 2 <= chroma <= 50, (
            '"{0}" specification chroma must be in domain [2, 50]!'.format(
                specification))
        assert abs(
            2 * (chroma / 2 - round(chroma / 2))) <= INTEGER_THRESHOLD, (
            ('"{0}" specification chroma must be an integer and '
             'multiple of 2!').format(specification))

        chroma = 2 * round(chroma / 2)

        # Checking if renotation data is available without interpolation using
        # given threshold.
        threshold = 0.001
        if (abs(hue) < threshold or
                    abs(hue - 2.5) < threshold or
                    abs(hue - 5) < threshold or
                    abs(hue - 7.5) < threshold or
                    abs(hue - 10) < threshold):
            hue = 2.5 * round(hue / 2.5)
            x, y, Y = xyY_from_renotation((hue, value, chroma, code))
            return x, y

        hue_cw, hue_ccw = bounding_hues_from_renotation(hue, code)
        hue_minus, code_minus = hue_cw
        hue_plus, code_plus = hue_ccw

        x_grey, y_grey = MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES

        specification_minus = (hue_minus, value, chroma, code_minus)
        x_minus, y_minus, Y_minus = xyY_from_renotation(
            specification_minus)
        z_minus, theta_minus, rho_minus = cartesian_to_cylindrical(
            (x_minus - x_grey, y_minus - y_grey, Y_minus))
        theta_minus = math.degrees(theta_minus)

        specification_plus = (hue_plus, value, chroma, code_plus)
        x_plus, y_plus, Y_plus = xyY_from_renotation(specification_plus)
        z_plus, theta_plus, rho_plus = cartesian_to_cylindrical(
            (x_plus - x_grey, y_plus - y_grey, Y_plus))
        theta_plus = math.degrees(theta_plus)

        lower_hue_angle = hue_to_hue_angle(hue_minus, code_minus)
        hue_angle = hue_to_hue_angle(hue, code)
        upper_hue_angle = hue_to_hue_angle(hue_plus, code_plus)

        if theta_minus - theta_plus > 180:
            theta_plus += +360

        if lower_hue_angle == 0:
            lower_hue_angle = 360

        if lower_hue_angle > upper_hue_angle:
            if lower_hue_angle > hue_angle:
                lower_hue_angle -= 360
            else:
                lower_hue_angle -= 360
                hue_angle -= 360

        interpolation_method = interpolation_method_from_renotation_ovoid(
            specification).lower()

        if interpolation_method == 'linear':
            x = LinearInterpolator1d([lower_hue_angle, upper_hue_angle],
                                     [x_minus, x_plus])(hue_angle)
            y = LinearInterpolator1d([lower_hue_angle, upper_hue_angle],
                                     [y_minus, y_plus])(hue_angle)
        elif interpolation_method == 'radial':
            theta = LinearInterpolator1d([lower_hue_angle, upper_hue_angle],
                                         [theta_minus, theta_plus])(hue_angle)
            rho = LinearInterpolator1d([lower_hue_angle, upper_hue_angle],
                                       [rho_minus, rho_plus])(hue_angle)

            x = rho * math.cos(math.radians(theta)) + x_grey
            y = rho * math.sin(math.radians(theta)) + y_grey
        else:
            raise ValueError(
                'Invalid interpolation method: "{0}"'.format(
                    interpolation_method))

        return x, y


def LCHab_to_munsell_specification(LCHab):
    """
    Converts from *CIE LCHab* colourspace to approximate *Munsell* *Colorlab*
    specification.

    Parameters
    ----------
    LCHab : array_like, (3,)
        *CIE LCHab* colourspace matrix.

    Returns
    -------
    tuple
        *Munsell* *Colorlab* specification.

    Notes
    -----
    -   Input :math:`L^*` is in domain [0, 100].

    References
    ----------
    .. [9]  **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *GeneralRoutines/CIELABtoApproxMunsellSpec.m*

    Examples
    --------
    >>> LCHab = np.array([100, 17.50664796, 244.93046842])
    >>> LCHab_to_munsell_specification(LCHab)  # doctest: +ELLIPSIS
    (8.0362412..., 10.0, 3.5013295..., 1)
    """

    L, C, Hab = np.ravel(LCHab)

    if Hab == 0:
        code = 8
    elif Hab <= 36:
        code = 7
    elif Hab <= 72:
        code = 6
    elif Hab <= 108:
        code = 5
    elif Hab <= 144:
        code = 4
    elif Hab <= 180:
        code = 3
    elif Hab <= 216:
        code = 2
    elif Hab <= 252:
        code = 1
    elif Hab <= 288:
        code = 10
    elif Hab <= 324:
        code = 9
    else:
        code = 8

    hue = LinearInterpolator1d([0, 36], [0, 10])(Hab % 36)
    if hue == 0:
        hue = 10

    value = L / 10
    chroma = C / 5

    return hue, value, chroma, code


def maximum_chroma_from_renotation(hue, value, code):
    """
    Returns the maximum *Munsell* chroma from *Munsell Renotation System* data
    using given *Munsell* *Colorlab* specification hue, *Munsell* *Colorlab*
    specification value and *Munsell* *Colorlab* specification code.

    Parameters
    ----------
    hue : numeric
        *Munsell* *Colorlab* specification hue.
    value : numeric
        *Munsell* value code.
    code : numeric
        *Munsell* *Colorlab* specification code.

    Returns
    -------
    numeric
        Maximum chroma.

    References
    ----------
    .. [10] **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/MaxChromaForExtrapolatedRenotation.m*

    Examples
    --------
    >>> maximum_chroma_from_renotation(2.5, 5, 5)
    14.0
    """

    # Ideal white, no chroma.
    if value >= 9.99:
        return 0

    assert 1 <= value <= 10, (
        '"{0}" value must be in domain [1, 10]!'.format(value))

    if value % 1 == 0:
        value_minus = value
        value_plus = value
    else:
        value_minus = math.floor(value)
        value_plus = value_minus + 1

    hue_cw, hue_ccw = bounding_hues_from_renotation(hue, code)
    hue_cw, code_cw = hue_cw
    hue_ccw, code_ccw = hue_ccw

    maximum_chromas = _munsell_maximum_chromas_from_renotation()
    spc_for_indexes = [chroma[0] for chroma in maximum_chromas]

    ma_limit_mcw = maximum_chromas[
        spc_for_indexes.index((hue_cw, value_minus, code_cw))][1]
    ma_limit_mccw = maximum_chromas[
        spc_for_indexes.index((hue_ccw, value_minus, code_ccw))][1]

    if value_plus <= 9:
        ma_limit_pcw = maximum_chromas[
            spc_for_indexes.index((hue_cw, value_plus, code_cw))][1]
        ma_limit_pccw = maximum_chromas[
            spc_for_indexes.index((hue_ccw, value_plus, code_ccw))][1]
        max_chroma = min(ma_limit_mcw,
                         ma_limit_mccw,
                         ma_limit_pcw,
                         ma_limit_pccw)
    else:
        L = luminance_ASTM_D1535_08(value)
        L9 = luminance_ASTM_D1535_08(9)
        L10 = luminance_ASTM_D1535_08(10)

        max_chroma = min(LinearInterpolator1d([L9, L10], [ma_limit_mcw, 0])(L),
                         LinearInterpolator1d([L9, L10], [ma_limit_mccw, 0])(
                             L))
    return max_chroma


def munsell_specification_to_xy(specification):
    """
    Converts given *Munsell* *Colorlab* specification to *xy* chromaticity
    coordinates by interpolating over *Munsell Renotation System* data.

    Parameters
    ----------
    specification : numeric or tuple
        *Munsell* *Colorlab* specification.

    Returns
    -------
    tuple
        *xy* chromaticity coordinates.

    Notes
    -----
    -   Input *Munsell* *Colorlab* specification value must be an integer in
        domain [0, 10].
    -   Output *xy* chromaticity coordinates are in domain [0, 1].

    References
    ----------
    .. [11] **The Munsell and Kubelka-Munk Toolbox**:
            *MunsellAndKubelkaMunkToolboxApr2014*:
            *MunsellRenotationRoutines/MunsellToxyForIntegerMunsellValue.m*

    Examples
    --------
    >>> # Doctests ellipsis for Python 2.x compatibility.
    >>> munsell_specification_to_xy((2.1, 8.0, 17.9, 4))  # doctest: +ELLIPSIS
    (0.440063..., 0.552242...)
    >>> munsell_specification_to_xy(8)  # doctest: +ELLIPSIS
    (0.31006..., 0.31616...)
    """

    if is_grey_munsell_colour(specification):
        return MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES
    else:
        hue, value, chroma, code = specification

        assert 0 <= value <= 10, (
            '"{0}" specification value must be in domain [0, 10]!'.format(
                specification))
        assert is_integer(value), (
            '"{0}" specification value must be an integer!'.format(
                specification))

        value = round(value)

        if chroma % 2 == 0:
            chroma_minus = chroma_plus = chroma
        else:
            chroma_minus = 2 * math.floor(chroma / 2)
            chroma_plus = chroma_minus + 2

        if chroma_minus == 0:
            # Smallest chroma ovoid collapses to illuminant chromaticity
            # coordinates.
            x_minus, y_minus = (
                MUNSELL_DEFAULT_ILLUMINANT_CHROMATICITY_COORDINATES)
        else:
            x_minus, y_minus = xy_from_renotation_ovoid(
                (hue, value, chroma_minus, code))

        x_plus, y_plus = xy_from_renotation_ovoid(
            (hue, value, chroma_plus, code))

        if chroma_minus == chroma_plus:
            x = x_minus
            y = y_minus
        else:
            x = LinearInterpolator1d([chroma_minus, chroma_plus],
                                     [x_minus, x_plus])(chroma)
            y = LinearInterpolator1d([chroma_minus, chroma_plus],
                                     [y_minus, y_plus])(chroma)

        return x, y
