Repository: colour-science/colour

Issue:
Implement support for "Visible Spectrum" coverage computation.
Similar needs than #190.

Discussion:

Patch:
diff --git a/colour/volume/__init__.py b/colour/volume/__init__.py
index 58c0f7e4b..72f6c7b1e 100644
--- a/colour/volume/__init__.py
+++ b/colour/volume/__init__.py
@@ -6,16 +6,24 @@ from __future__ import absolute_import
 from .dataset import *  # noqa
 from . import dataset
 from .macadam_limits import is_within_macadam_limits
+from .mesh import is_within_mesh_volume
 from .pointer_gamut import is_within_pointer_gamut
+from .spectrum import is_within_visible_spectrum
 from .rgb import (
     RGB_colourspace_limits,
     RGB_colourspace_volume_MonteCarlo,
-    RGB_colourspace_pointer_gamut_coverage_MonteCarlo)
+    RGB_colourspace_volume_coverage_MonteCarlo,
+    RGB_colourspace_pointer_gamut_coverage_MonteCarlo,
+    RGB_colourspace_visible_spectrum_coverage_MonteCarlo)
 
 __all__ = []
 __all__ += dataset.__all__
 __all__ += ['is_within_macadam_limits']
+__all__ += ['is_within_mesh_volume']
 __all__ += ['is_within_pointer_gamut']
+__all__ += ['is_within_visible_spectrum']
 __all__ += ['RGB_colourspace_limits',
             'RGB_colourspace_volume_MonteCarlo',
-            'RGB_colourspace_pointer_gamut_coverage_MonteCarlo']
+            'RGB_colourspace_volume_coverage_MonteCarlo',
+            'RGB_colourspace_pointer_gamut_coverage_MonteCarlo',
+            'RGB_colourspace_visible_spectrum_coverage_MonteCarlo']
diff --git a/colour/volume/mesh.py b/colour/volume/mesh.py
new file mode 100644
index 000000000..e90f458a3
--- /dev/null
+++ b/colour/volume/mesh.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Mesh Volume Computations Helpers
+================================
+
+Defines helpers objects related to volume computations.
+"""
+
+from __future__ import division, unicode_literals
+
+import numpy as np
+
+from colour.utilities import is_scipy_installed
+
+__author__ = 'Colour Developers'
+__copyright__ = 'Copyright (C) 2013 - 2015 - Colour Developers'
+__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
+__maintainer__ = 'Colour Developers'
+__email__ = 'colour-science@googlegroups.com'
+__status__ = 'Production'
+
+__all__ = ['is_within_mesh_volume']
+
+
+def is_within_mesh_volume(points, mesh, tolerance=None):
+    """
+    Returns if given points are within given mesh volume using Delaunay
+    triangulation.
+
+    Parameters
+    ----------
+    points : array_like
+        Points to check if they are within `mesh` volume.
+    mesh : array_like
+        Points of the volume used to generate the Delaunay triangulation.
+    tolerance : numeric, optional
+        Tolerance allowed in the inside-triangle check.
+
+    Returns
+    -------
+    bool
+        Is within mesh volume.
+
+    Notes
+    -----
+    -   This definition requires *scipy* to be installed.
+
+    Examples
+    --------
+    >>> mesh = np.array([[-1.0, -1.0, 1.0],
+    ...                  [1.0, -1.0, 1.0],
+    ...                  [1.0, -1.0, -1.0],
+    ...                  [-1.0, -1.0, -1.0],
+    ...                  [0.0, 1.0, 0.0]])
+    >>> is_within_mesh_volume(np.array([0.0005, 0.0031, 0.0010]), mesh)
+    array(True, dtype=bool)
+    >>> a = np.array([[0.0005, 0.0031, 0.0010],
+    ...               [0.3205, 0.4131, 0.5100]])
+    >>> is_within_mesh_volume(a, mesh)
+    array([ True, False], dtype=bool)
+    """
+
+    if is_scipy_installed(raise_exception=True):
+        from scipy.spatial import Delaunay
+
+        triangulation = Delaunay(mesh)
+
+        simplex = triangulation.find_simplex(points, tol=tolerance)
+        simplex = np.where(simplex >= 0, True, False)
+
+        return simplex
diff --git a/colour/volume/pointer_gamut.py b/colour/volume/pointer_gamut.py
index 57415b898..860cbd0f0 100644
--- a/colour/volume/pointer_gamut.py
+++ b/colour/volume/pointer_gamut.py
@@ -5,7 +5,7 @@
 Pointer's Gamut Volume Computations
 ===================================
 
-Defines objects related to Pointer's Gamut volume computations
+Defines objects related to Pointer's Gamut volume computations.
 
 See Also
 --------
@@ -15,14 +15,12 @@ See Also
 
 from __future__ import division, unicode_literals
 
-import numpy as np
-
 from colour.models import (
     Lab_to_XYZ,
     LCHab_to_Lab,
     POINTER_GAMUT_DATA,
     POINTER_GAMUT_ILLUMINANT)
-from colour.utilities import is_scipy_installed
+from colour.volume import is_within_mesh_volume
 
 __author__ = 'Colour Developers'
 __copyright__ = 'Copyright (C) 2013 - 2015 - Colour Developers'
@@ -33,8 +31,6 @@ __status__ = 'Production'
 
 __all__ = ['is_within_pointer_gamut']
 
-_XYZ_POINTER_GAMUT_TRIANGULATION_CACHE = None
-
 
 def is_within_pointer_gamut(XYZ, tolerance=None):
     """
@@ -60,26 +56,16 @@ def is_within_pointer_gamut(XYZ, tolerance=None):
 
     Examples
     --------
-    >>> is_within_pointer_gamut(np.array([0.3205, 0.4131, 0.51]))
+    >>> import numpy as np
+    >>> is_within_pointer_gamut(np.array([0.3205, 0.4131, 0.5100]))
     array(True, dtype=bool)
-    >>> a = np.array([[0.3205, 0.4131, 0.51],
-    ...               [0.0005, 0.0031, 0.001]])
+    >>> a = np.array([[0.3205, 0.4131, 0.5100],
+    ...               [0.0005, 0.0031, 0.0010]])
     >>> is_within_pointer_gamut(a)
     array([ True, False], dtype=bool)
     """
 
-    if is_scipy_installed(raise_exception=True):
-        from scipy.spatial import Delaunay
-
-        global _XYZ_POINTER_GAMUT_TRIANGULATION_CACHE
-
-        triangulation = _XYZ_POINTER_GAMUT_TRIANGULATION_CACHE
-        if triangulation is None:
-            _XYZ_POINTER_GAMUT_TRIANGULATION_CACHE = triangulation = (
-                Delaunay(Lab_to_XYZ(LCHab_to_Lab(POINTER_GAMUT_DATA),
-                                    POINTER_GAMUT_ILLUMINANT)))
-
-        simplex = triangulation.find_simplex(XYZ, tol=tolerance)
-        simplex = np.where(simplex >= 0, True, False)
+    XYZ_p = Lab_to_XYZ(LCHab_to_Lab(POINTER_GAMUT_DATA),
+                       POINTER_GAMUT_ILLUMINANT)
 
-        return simplex
+    return is_within_mesh_volume(XYZ, XYZ_p, tolerance)
diff --git a/colour/volume/rgb.py b/colour/volume/rgb.py
index a09b41490..0096119e8 100644
--- a/colour/volume/rgb.py
+++ b/colour/volume/rgb.py
@@ -10,6 +10,7 @@ Defines various RGB colourspace volume computation objects:
 -   :func:`RGB_colourspace_limits`
 -   :func:`RGB_colourspace_volume_MonteCarlo`
 -   :func:`RGB_colourspace_pointer_gamut_coverage_MonteCarlo`
+-   :func:`RGB_colourspace_visible_spectrum_coverage_MonteCarlo`
 
 See Also
 --------
@@ -25,9 +26,13 @@ import numpy as np
 
 from colour.algebra import random_triplet_generator
 from colour.colorimetry import ILLUMINANTS
-from colour.models import Lab_to_XYZ, RGB_to_XYZ, XYZ_to_Lab, XYZ_to_RGB
+from colour.models import (
+    Lab_to_XYZ,
+    RGB_to_XYZ,
+    XYZ_to_Lab,
+    XYZ_to_RGB)
 from colour.utilities import is_scipy_installed
-from colour.volume import is_within_pointer_gamut
+from colour.volume import is_within_pointer_gamut, is_within_visible_spectrum
 
 __author__ = 'Colour Developers'
 __copyright__ = 'Copyright (C) 2013 - 2014 - Colour Developers'
@@ -39,7 +44,8 @@ __status__ = 'Production'
 __all__ = ['sample_RGB_colourspace_volume_MonteCarlo',
            'RGB_colourspace_limits',
            'RGB_colourspace_volume_MonteCarlo',
-           'RGB_colourspace_pointer_gamut_coverage_MonteCarlo']
+           'RGB_colourspace_pointer_gamut_coverage_MonteCarlo',
+           'RGB_colourspace_visible_spectrum_coverage_MonteCarlo']
 
 
 def _wrapper_RGB_colourspace_volume_MonteCarlo(args):
@@ -262,24 +268,25 @@ def RGB_colourspace_volume_MonteCarlo(
     return Lab_volume * np.sum(results) / (process_samples * cpu_count)
 
 
-def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
+def RGB_colourspace_volume_coverage_MonteCarlo(
         colourspace,
+        coverage_sampler,
         samples=10e6,
         random_generator=random_triplet_generator,
         random_state=None):
     """
-    Returns given *RGB* colourspace percentage coverage of Pointer's Gamut
-    volume using *Monte Carlo* method.
+    Returns given *RGB* colourspace percentage coverage of an arbitrary volume.
 
     Parameters
     ----------
     colourspace : RGB_Colourspace
-        *RGB* colourspace to compute the Pointer's Gamut coverage percentage.
+        *RGB* colourspace to compute the volume coverage percentage.
+    coverage_sampler : object
+        Python object responsible for checking the volume coverage.
     samples : numeric, optional
         Samples count.
     random_generator : generator, optional
-        Random triplet generator providing the random samples within the *Lab*
-        colourspace volume.
+        Random triplet generator providing the random samples.
     random_state : RandomState, optional
         Mersenne Twister pseudo-random number generator to use in the random
         number generator.
@@ -287,7 +294,7 @@ def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
     Returns
     -------
     float
-        Percentage coverage of Pointer's Gamut volume.
+        Percentage coverage of volume.
 
     Notes
     -----
@@ -297,7 +304,11 @@ def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
     --------
     >>> from colour import sRGB_COLOURSPACE as sRGB
     >>> prng = np.random.RandomState(2)
-    >>> RGB_colourspace_pointer_gamut_coverage_MonteCarlo(sRGB, 10e3, random_state=prng)  # noqa  # doctest: +ELLIPSIS
+    >>> RGB_colourspace_volume_coverage_MonteCarlo(
+    ...     sRGB,
+    ...     is_within_pointer_gamut,
+    ...     10e3,
+    ...     random_state=prng)  # doctest: +ELLIPSIS
     83...
     """
 
@@ -306,11 +317,12 @@ def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
                         if random_state is not None else
                         np.random.RandomState())
 
+        # TODO: Investigate for generator yielding directly a ndarray.
         XYZ = np.asarray(list(random_generator(
             samples, random_state=random_state)))
-        XYZ_p = XYZ[is_within_pointer_gamut(XYZ)]
+        XYZ_vs = XYZ[coverage_sampler(XYZ)]
 
-        RGB = XYZ_to_RGB(XYZ_p,
+        RGB = XYZ_to_RGB(XYZ_vs,
                          colourspace.whitepoint,
                          colourspace.whitepoint,
                          colourspace.XYZ_to_RGB_matrix)
@@ -318,4 +330,102 @@ def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
         RGB_c = RGB[np.logical_and(np.min(RGB, axis=-1) >= 0,
                                    np.max(RGB, axis=-1) <= 1)]
 
-        return 100 * RGB_c.size / XYZ_p.size
+        return 100 * RGB_c.size / XYZ_vs.size
+
+
+def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
+        colourspace,
+        samples=10e6,
+        random_generator=random_triplet_generator,
+        random_state=None):
+    """
+    Returns given *RGB* colourspace percentage coverage of Pointer's Gamut
+    volume using *Monte Carlo* method.
+
+    Parameters
+    ----------
+    colourspace : RGB_Colourspace
+        *RGB* colourspace to compute the Pointer's Gamut coverage percentage.
+    samples : numeric, optional
+        Samples count.
+    random_generator : generator, optional
+        Random triplet generator providing the random samples.
+    random_state : RandomState, optional
+        Mersenne Twister pseudo-random number generator to use in the random
+        number generator.
+
+    Returns
+    -------
+    float
+        Percentage coverage of Pointer's Gamut volume.
+
+    Notes
+    -----
+    -   This definition requires *scipy* to be installed.
+
+    Examples
+    --------
+    >>> from colour import sRGB_COLOURSPACE as sRGB
+    >>> prng = np.random.RandomState(2)
+    >>> RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
+    ...     sRGB,
+    ...     10e3,
+    ...     random_state=prng)  # doctest: +ELLIPSIS
+    83...
+    """
+
+    return RGB_colourspace_volume_coverage_MonteCarlo(
+        colourspace,
+        is_within_pointer_gamut,
+        samples,
+        random_generator,
+        random_state)
+
+
+def RGB_colourspace_visible_spectrum_coverage_MonteCarlo(
+        colourspace,
+        samples=10e6,
+        random_generator=random_triplet_generator,
+        random_state=None):
+    """
+    Returns given *RGB* colourspace percentage coverage of visible spectrum
+    volume using *Monte Carlo* method.
+
+    Parameters
+    ----------
+    colourspace : RGB_Colourspace
+        *RGB* colourspace to compute the visible spectrum coverage percentage.
+    samples : numeric, optional
+        Samples count.
+    random_generator : generator, optional
+        Random triplet generator providing the random samples.
+    random_state : RandomState, optional
+        Mersenne Twister pseudo-random number generator to use in the random
+        number generator.
+
+    Returns
+    -------
+    float
+        Percentage coverage of visible spectrum volume.
+
+    Notes
+    -----
+    -   This definition requires *scipy* to be installed.
+
+    Examples
+    --------
+    >>> from colour import sRGB_COLOURSPACE as sRGB
+    >>> prng = np.random.RandomState(2)
+    >>> RGB_colourspace_visible_spectrum_coverage_MonteCarlo(
+    ...     sRGB,
+    ...     10e3,
+    ...     random_state=prng)  # doctest: +ELLIPSIS
+    36...
+    """
+
+    return RGB_colourspace_volume_coverage_MonteCarlo(
+        colourspace,
+        is_within_visible_spectrum,
+        samples,
+        random_generator,
+        random_state)
diff --git a/colour/volume/spectrum.py b/colour/volume/spectrum.py
new file mode 100644
index 000000000..5e4e64241
--- /dev/null
+++ b/colour/volume/spectrum.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Visible Spectrum Volume Computations
+====================================
+
+Defines objects related to visible spectrum volume computations.
+
+See Also
+--------
+`Spectrum IPython Notebook
+<http://nbviewer.ipython.org/github/colour-science/colour-ipython/blob/master/notebooks/colorimetry/spectrum.ipynb>`_  # noqa
+"""
+
+from __future__ import division, unicode_literals
+
+from colour.colorimetry import STANDARD_OBSERVERS_CMFS
+from colour.volume import is_within_mesh_volume
+
+__author__ = 'Colour Developers'
+__copyright__ = 'Copyright (C) 2013 - 2015 - Colour Developers'
+__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
+__maintainer__ = 'Colour Developers'
+__email__ = 'colour-science@googlegroups.com'
+__status__ = 'Production'
+
+__all__ = ['is_within_visible_spectrum']
+
+
+def is_within_visible_spectrum(XYZ,
+                               cmfs=STANDARD_OBSERVERS_CMFS.get(
+                                   'CIE 1931 2 Degree Standard Observer'),
+                               tolerance=None):
+    """
+    Returns if given *CIE XYZ* tristimulus values are within visible spectrum
+    volume / given colour matching functions volume.
+
+    Parameters
+    ----------
+    XYZ : array_like
+        *CIE XYZ* tristimulus values.
+    cmfs : XYZ_ColourMatchingFunctions
+        Standard observer colour matching functions.
+    tolerance : numeric, optional
+        Tolerance allowed in the inside-triangle check.
+
+    Returns
+    -------
+    bool
+        Is within visible spectrum.
+
+    Notes
+    -----
+    -   Input *CIE XYZ* tristimulus values are in domain [0, 1].
+    -   This definition requires *scipy* to be installed.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> is_within_visible_spectrum(np.array([0.3205, 0.4131, 0.51]))
+    array(True, dtype=bool)
+    >>> a = np.array([[0.3205, 0.4131, 0.51],
+    ...               [-0.0005, 0.0031, 0.001]])
+    >>> is_within_visible_spectrum(a)
+    array([ True, False], dtype=bool)
+    """
+
+    return is_within_mesh_volume(XYZ, cmfs.values, tolerance)


Test patch:
diff --git a/colour/volume/tests/tests_mesh.py b/colour/volume/tests/tests_mesh.py
new file mode 100644
index 000000000..2ab1296d9
--- /dev/null
+++ b/colour/volume/tests/tests_mesh.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Defines unit tests for :mod:`colour.volume.mesh` module.
+"""
+
+from __future__ import division, unicode_literals
+
+import numpy as np
+import sys
+
+if sys.version_info[:2] <= (2, 6):
+    import unittest2 as unittest
+else:
+    import unittest
+from itertools import permutations
+
+from colour.volume import is_within_mesh_volume
+from colour.utilities import ignore_numpy_errors
+
+__author__ = 'Colour Developers'
+__copyright__ = 'Copyright (C) 2013 - 2015 - Colour Developers'
+__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
+__maintainer__ = 'Colour Developers'
+__email__ = 'colour-science@googlegroups.com'
+__status__ = 'Production'
+
+__all__ = ['TestIsWithinMeshVolume']
+
+
+class TestIsWithinMeshVolume(unittest.TestCase):
+    """
+    Defines :func:`colour.volume.mesh.is_within_mesh_volume` definition unit
+    tests methods.
+    """
+
+    def setUp(self):
+        """
+        Initialises common tests attributes.
+        """
+
+        self.__mesh = np.array([[-1.0, -1.0, 1.0],
+                                [1.0, -1.0, 1.0],
+                                [1.0, -1.0, -1.0],
+                                [-1.0, -1.0, -1.0],
+                                [0.0, 1.0, 0.0]])
+
+    def test_is_within_mesh_volume(self):
+        """
+        Tests :func:`colour.volume.mesh.is_within_mesh_volume` definition.
+        """
+
+        self.assertTrue(
+            is_within_mesh_volume(np.array([0.0005, 0.0031, 0.0010]),
+                                  self.__mesh))
+
+        self.assertFalse(
+            is_within_mesh_volume(np.array([0.3205, 0.4131, 0.5100]),
+                                  self.__mesh))
+
+        self.assertTrue(
+            is_within_mesh_volume(np.array([0.0025, 0.0088, 0.0340]),
+                                  self.__mesh))
+
+        self.assertFalse(
+            is_within_mesh_volume(np.array([0.4325, 0.3788, 0.1034]),
+                                  self.__mesh))
+
+    def test_n_dimensional_is_within_mesh_volume(self):
+        """
+        Tests :func:`colour.volume.mesh.is_within_mesh_volume` definition
+        n-dimensional arrays support.
+        """
+
+        a = np.array([0.0005, 0.0031, 0.0010])
+        b = np.array([True])
+        np.testing.assert_almost_equal(
+            is_within_mesh_volume(a, self.__mesh),
+            b)
+
+        a = np.tile(a, (6, 1))
+        b = np.tile(b, 6)
+        np.testing.assert_almost_equal(
+            is_within_mesh_volume(a, self.__mesh),
+            b)
+
+        a = np.reshape(a, (2, 3, 3))
+        b = np.reshape(b, (2, 3))
+        np.testing.assert_almost_equal(
+            is_within_mesh_volume(a, self.__mesh),
+            b)
+
+    @ignore_numpy_errors
+    def test_nan_is_within_mesh_volume(self):
+        """
+        Tests :func:`colour.volume.mesh.is_within_mesh_volume` definition nan
+        support.
+        """
+
+        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
+        cases = set(permutations(cases * 3, r=3))
+        for case in cases:
+            is_within_mesh_volume(case, self.__mesh)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/colour/volume/tests/tests_rgb.py b/colour/volume/tests/tests_rgb.py
index 04e7509f7..04d5b636d 100644
--- a/colour/volume/tests/tests_rgb.py
+++ b/colour/volume/tests/tests_rgb.py
@@ -3,6 +3,18 @@
 
 """
 Defines unit tests for :mod:`colour.volume.rgb` module.
+
+Notes
+-----
+The MonteCarlo sampling based unit tests are assuming that
+:func:`np.random.RandomState` definition will return the same sequence no
+matter which *OS* or *Python* version is used. There is however no formal
+promise about the *prng* sequence reproducibility of either *Python or *Numpy*
+implementations:
+
+Laurent. (2012). Reproducibility of python pseudo-random numbers across systems
+and versions? Retrieved January 20, 2015, from
+http://stackoverflow.com/questions/8786084/reproducibility-of-python-pseudo-random-numbers-across-systems-and-versions  # noqa
 """
 
 from __future__ import division, unicode_literals
@@ -22,7 +34,10 @@ from colour.models import (
 from colour.volume import (
     RGB_colourspace_limits,
     RGB_colourspace_volume_MonteCarlo,
-    RGB_colourspace_pointer_gamut_coverage_MonteCarlo)
+    RGB_colourspace_volume_coverage_MonteCarlo,
+    RGB_colourspace_pointer_gamut_coverage_MonteCarlo,
+    RGB_colourspace_visible_spectrum_coverage_MonteCarlo,
+    is_within_pointer_gamut)
 
 __author__ = 'Colour Developers'
 __copyright__ = 'Copyright (C) 2013 - 2014 - Colour Developers'
@@ -33,7 +48,9 @@ __status__ = 'Production'
 
 __all__ = ['TestRGB_colourspaceLimits',
            'TestRGB_colourspaceVolumeMonteCarlo',
-           'TestRGB_colourspacePointerGamutCoverageMonteCarlo']
+           'TestRGB_colourspace_volume_coverage_MonteCarlo',
+           'TestRGB_colourspacePointerGamutCoverageMonteCarlo',
+           'TestRGB_colourspaceVisibleSpectrumCoverageMonteCarlo']
 
 
 class TestRGB_colourspaceLimits(unittest.TestCase):
@@ -79,16 +96,6 @@ class TestRGB_colourspaceVolumeMonteCarlo(unittest.TestCase):
         """
         Tests :func:`colour.volume.rgb.RGB_colourspace_volume_MonteCarlo`
         definition.
-
-        Notes
-        -----
-        The test is assuming that :func:`np.random.RandomState` definition will
-        return the same sequence no matter which *OS* or *Python* version is
-        used. There is however no formal promise about the *prng* sequence
-        reproducibility of either *Python or *Numpy* implementations: Laurent.
-        (2012). Reproducibility of python pseudo-random numbers across systems
-        and versions? Retrieved January 20, 2015, from
-        http://stackoverflow.com/questions/8786084/reproducibility-of-python-pseudo-random-numbers-across-systems-and-versions  # noqa
         """
 
         self.assertEquals(
@@ -100,6 +107,30 @@ class TestRGB_colourspaceVolumeMonteCarlo(unittest.TestCase):
             859500.0)
 
 
+class TestRGB_colourspace_volume_coverage_MonteCarlo(unittest.TestCase):
+    """
+    Defines
+    :func:`colour.volume.rgb.RGB_colourspace_volume_coverage_MonteCarlo`
+    definition unit tests methods.
+    """
+
+    def test_RGB_colourspace_volume_coverage_MonteCarlo(self):
+        """
+        Tests
+        :func:`colour.volume.rgb.RGB_colourspace_volume_coverage_MonteCarlo`  # noqa
+        definition.
+        """
+
+        np.testing.assert_almost_equal(
+            RGB_colourspace_volume_coverage_MonteCarlo(
+                REC_709_COLOURSPACE,
+                is_within_pointer_gamut,
+                10e3,
+                random_state=np.random.RandomState(2)),
+            83.02013422818791,
+            decimal=7)
+
+
 class TestRGB_colourspacePointerGamutCoverageMonteCarlo(unittest.TestCase):
     """
     Defines
@@ -112,16 +143,6 @@ class TestRGB_colourspacePointerGamutCoverageMonteCarlo(unittest.TestCase):
         Tests
         :func:`colour.volume.rgb.RGB_colourspace_pointer_gamut_coverage_MonteCarlo`  # noqa
         definition.
-
-        Notes
-        -----
-        The test is assuming that :func:`np.random.RandomState` definition will
-        return the same sequence no matter which *OS* or *Python* version is
-        used. There is however no formal promise about the *prng* sequence
-        reproducibility of either *Python or *Numpy* implementations: Laurent.
-        (2012). Reproducibility of python pseudo-random numbers across systems
-        and versions? Retrieved January 20, 2015, from
-        http://stackoverflow.com/questions/8786084/reproducibility-of-python-pseudo-random-numbers-across-systems-and-versions  # noqa
         """
 
         np.testing.assert_almost_equal(
@@ -133,5 +154,28 @@ class TestRGB_colourspacePointerGamutCoverageMonteCarlo(unittest.TestCase):
             decimal=7)
 
 
+class TestRGB_colourspaceVisibleSpectrumCoverageMonteCarlo(unittest.TestCase):
+    """
+    Defines
+    :func:`colour.volume.rgb.RGB_colourspace_visible_spectrum_coverage_MonteCarlo`  # noqa
+    definition unit tests methods.
+    """
+
+    def test_RGB_colourspace_visible_spectrum_coverage_MonteCarlo(self):
+        """
+        Tests
+        :func:`colour.volume.rgb.RGB_colourspace_visible_spectrum_coverage_MonteCarlo`  # noqa
+        definition.
+        """
+
+        np.testing.assert_almost_equal(
+            RGB_colourspace_visible_spectrum_coverage_MonteCarlo(
+                REC_709_COLOURSPACE,
+                10e3,
+                random_state=np.random.RandomState(2)),
+            36.48383937316356,
+            decimal=7)
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/colour/volume/tests/tests_spectrum.py b/colour/volume/tests/tests_spectrum.py
new file mode 100644
index 000000000..8e90ceda6
--- /dev/null
+++ b/colour/volume/tests/tests_spectrum.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Defines unit tests for :mod:`colour.volume.spectrum` module.
+"""
+
+from __future__ import division, unicode_literals
+
+import numpy as np
+import sys
+
+if sys.version_info[:2] <= (2, 6):
+    import unittest2 as unittest
+else:
+    import unittest
+from itertools import permutations
+
+from colour.volume import is_within_visible_spectrum
+from colour.utilities import ignore_numpy_errors
+
+__author__ = 'Colour Developers'
+__copyright__ = 'Copyright (C) 2013 - 2015 - Colour Developers'
+__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
+__maintainer__ = 'Colour Developers'
+__email__ = 'colour-science@googlegroups.com'
+__status__ = 'Production'
+
+__all__ = ['TestIsWithinVisibleSpectrum']
+
+
+class TestIsWithinVisibleSpectrum(unittest.TestCase):
+    """
+    Defines :func:`colour.volume.spectrum.is_within_visible_spectrum`
+    definition unit tests methods.
+    """
+
+    def test_is_within_visible_spectrum(self):
+        """
+        Tests :func:`colour.volume.spectrum.is_within_visible_spectrum`
+        definition.
+        """
+
+        self.assertTrue(
+            is_within_visible_spectrum(np.array([0.3205, 0.4131, 0.5100])))
+
+        self.assertFalse(
+            is_within_visible_spectrum(np.array([-0.0005, 0.0031, 0.0010])))
+
+        self.assertTrue(
+            is_within_visible_spectrum(np.array([0.4325, 0.3788, 0.1034])))
+
+        self.assertFalse(
+            is_within_visible_spectrum(np.array([0.0025, 0.0088, 0.0340])))
+
+    def test_n_dimensional_is_within_visible_spectrum(self):
+        """
+        Tests :func:`colour.volume.spectrum.is_within_visible_spectrum`
+        definition n-dimensional arrays support.
+        """
+
+        a = np.array([0.3205, 0.4131, 0.5100])
+        b = np.array([True])
+        np.testing.assert_almost_equal(
+            is_within_visible_spectrum(a),
+            b)
+
+        a = np.tile(a, (6, 1))
+        b = np.tile(b, 6)
+        np.testing.assert_almost_equal(
+            is_within_visible_spectrum(a),
+            b)
+
+        a = np.reshape(a, (2, 3, 3))
+        b = np.reshape(b, (2, 3))
+        np.testing.assert_almost_equal(
+            is_within_visible_spectrum(a),
+            b)
+
+    @ignore_numpy_errors
+    def test_nan_is_within_visible_spectrum(self):
+        """
+        Tests :func:`colour.volume.spectrum.is_within_visible_spectrum`
+        definition nan support.
+        """
+
+        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
+        cases = set(permutations(cases * 3, r=3))
+        for case in cases:
+            is_within_visible_spectrum(case)
+
+
+if __name__ == '__main__':
+    unittest.main()