Coverage for continuous/tests/test_signal.py: 100%
228 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""Define the unit tests for the :mod:`colour.continuous.signal` module."""
3from __future__ import annotations
5import pickle
6import textwrap
8import numpy as np
9import pytest
11from colour.algebra import CubicSplineInterpolator, Extrapolator, KernelInterpolator
12from colour.constants import DTYPE_FLOAT_DEFAULT, TOLERANCE_ABSOLUTE_TESTS
13from colour.continuous import Signal
14from colour.utilities import (
15 ColourRuntimeWarning,
16 attest,
17 is_pandas_installed,
18 is_scipy_installed,
19)
21__author__ = "Colour Developers"
22__copyright__ = "Copyright 2013 Colour Developers"
23__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
24__maintainer__ = "Colour Developers"
25__email__ = "colour-developers@colour-science.org"
26__status__ = "Production"
28__all__ = [
29 "TestSignal",
30]
33class TestSignal:
34 """Define :class:`colour.continuous.signal.Signal` class unit tests methods."""
36 def setup_method(self) -> None:
37 """Initialise the common tests attributes."""
39 self._range = np.linspace(10, 100, 10)
40 self._domain = np.arange(100, 1100, 100)
42 self._signal = Signal(self._range)
44 def test_required_attributes(self) -> None:
45 """Test the presence of required attributes."""
47 required_attributes = (
48 "dtype",
49 "domain",
50 "range",
51 "interpolator",
52 "interpolator_kwargs",
53 "extrapolator",
54 "extrapolator_kwargs",
55 "function",
56 )
58 for attribute in required_attributes:
59 assert attribute in dir(Signal)
61 def test_required_methods(self) -> None:
62 """Test the presence of required methods."""
64 required_methods = (
65 "__init__",
66 "__str__",
67 "__repr__",
68 "__hash__",
69 "__getitem__",
70 "__setitem__",
71 "__contains__",
72 "__iter__",
73 "__eq__",
74 "__ne__",
75 "arithmetical_operation",
76 "signal_unpack_data",
77 "fill_nan",
78 "domain_distance",
79 "to_series",
80 )
82 for method in required_methods:
83 assert method in dir(Signal)
85 def test_pickling(self) -> None:
86 """
87 Test whether the :class:``colour.continuous.signal.Signal` class can be
88 pickled.
89 """
91 data = pickle.dumps(self._signal)
92 data = pickle.loads(data) # noqa: S301
93 assert self._signal == data
95 def test_dtype(self) -> None:
96 """Test :func:`colour.continuous.signal.Signal.dtype` property."""
98 assert self._signal.dtype == DTYPE_FLOAT_DEFAULT
100 signal = self._signal.copy()
101 signal.dtype = np.float32
102 assert signal.dtype == np.float32
104 def test_domain(self) -> None:
105 """Test :func:`colour.continuous.signal.Signal.domain` property."""
107 signal = self._signal.copy()
109 np.testing.assert_allclose(
110 signal[np.array([0, 1, 2])],
111 np.array([10.0, 20.0, 30.0]),
112 atol=TOLERANCE_ABSOLUTE_TESTS,
113 )
115 signal.domain = np.arange(0, 10, 1) * 10
117 np.testing.assert_array_equal(signal.domain, np.arange(0, 10, 1) * 10)
119 np.testing.assert_allclose(
120 signal[np.array([0, 1, 2]) * 10],
121 np.array([10.0, 20.0, 30.0]),
122 atol=TOLERANCE_ABSOLUTE_TESTS,
123 )
125 domain = np.linspace(0, 1, 10)
126 domain[0] = -np.inf
128 def assert_warns() -> None:
129 """Help to test the runtime warning."""
131 signal.domain = domain
133 pytest.warns(ColourRuntimeWarning, assert_warns)
135 def test_range(self) -> None:
136 """Test :func:`colour.continuous.signal.Signal.range` property."""
138 signal = self._signal.copy()
140 np.testing.assert_allclose(
141 signal[np.array([0, 1, 2])],
142 np.array([10.0, 20.0, 30.0]),
143 atol=TOLERANCE_ABSOLUTE_TESTS,
144 )
146 signal.range = self._range * 10
148 np.testing.assert_array_equal(signal.range, self._range * 10)
150 np.testing.assert_allclose(
151 signal[np.array([0, 1, 2])],
152 np.array([10.0, 20.0, 30.0]) * 10,
153 atol=TOLERANCE_ABSOLUTE_TESTS,
154 )
156 def assert_warns() -> None:
157 """Help to test the runtime warning."""
159 signal.range = self._range * np.inf
161 pytest.warns(ColourRuntimeWarning, assert_warns)
163 def test_interpolator(self) -> None:
164 """Test :func:`colour.continuous.signal.Signal.interpolator` property."""
166 if not is_scipy_installed(): # pragma: no cover
167 return
169 signal = self._signal.copy()
171 np.testing.assert_allclose(
172 signal[np.linspace(0, 5, 5)],
173 np.array(
174 [
175 10.00000000,
176 22.83489024,
177 34.80044921,
178 47.55353925,
179 60.00000000,
180 ]
181 ),
182 atol=TOLERANCE_ABSOLUTE_TESTS,
183 )
185 signal.interpolator = CubicSplineInterpolator
187 np.testing.assert_allclose(
188 signal[np.linspace(0, 5, 5)],
189 np.array([10.0, 22.5, 35.0, 47.5, 60.0]),
190 atol=TOLERANCE_ABSOLUTE_TESTS,
191 )
193 def test_interpolator_kwargs(self) -> None:
194 """
195 Test :func:`colour.continuous.signal.Signal.interpolator_kwargs`
196 property.
197 """
199 signal = self._signal.copy()
201 np.testing.assert_allclose(
202 signal[np.linspace(0, 5, 5)],
203 np.array(
204 [
205 10.00000000,
206 22.83489024,
207 34.80044921,
208 47.55353925,
209 60.00000000,
210 ]
211 ),
212 atol=TOLERANCE_ABSOLUTE_TESTS,
213 )
215 signal.interpolator_kwargs = {"window": 1, "kernel_kwargs": {"a": 1}}
217 np.testing.assert_allclose(
218 signal[np.linspace(0, 5, 5)],
219 np.array(
220 [
221 10.00000000,
222 18.91328761,
223 28.36993142,
224 44.13100443,
225 60.00000000,
226 ]
227 ),
228 atol=TOLERANCE_ABSOLUTE_TESTS,
229 )
231 def test_extrapolator(self) -> None:
232 """Test :func:`colour.continuous.signal.Signal.extrapolator` property."""
234 assert isinstance(self._signal.extrapolator(), Extrapolator)
236 def test_extrapolator_kwargs(self) -> None:
237 """
238 Test :func:`colour.continuous.signal.Signal.extrapolator_kwargs`
239 property.
240 """
242 signal = self._signal.copy()
244 attest(np.all(np.isnan(signal[np.array([-1000, 1000])])))
246 signal.extrapolator_kwargs = {
247 "method": "Linear",
248 }
250 np.testing.assert_allclose(
251 signal[np.array([-1000, 1000])],
252 np.array([-9990.0, 10010.0]),
253 atol=TOLERANCE_ABSOLUTE_TESTS,
254 )
256 def test_function(self) -> None:
257 """Test :func:`colour.continuous.signal.Signal.function` property."""
259 attest(callable(self._signal.function))
261 def test_raise_exception_function(self) -> None:
262 """
263 Test :func:`colour.continuous.signal.Signal.function` property raised
264 exception.
265 """
267 pytest.raises(ValueError, Signal().function, 0)
269 def test__init__(self) -> None:
270 """Test :func:`colour.continuous.signal.Signal.__init__` method."""
272 signal = Signal(self._range)
273 np.testing.assert_array_equal(signal.domain, np.arange(0, 10, 1))
274 np.testing.assert_array_equal(signal.range, self._range)
276 signal = Signal(self._range, self._domain)
277 np.testing.assert_array_equal(signal.domain, self._domain)
278 np.testing.assert_array_equal(signal.range, self._range)
280 signal = Signal(dict(zip(self._domain, self._range, strict=True)))
281 np.testing.assert_array_equal(signal.domain, self._domain)
282 np.testing.assert_array_equal(signal.range, self._range)
284 signal = Signal(signal)
285 np.testing.assert_array_equal(signal.domain, self._domain)
286 np.testing.assert_array_equal(signal.range, self._range)
288 if is_pandas_installed():
289 from pandas import Series # noqa: PLC0415
291 signal = Signal(Series(dict(zip(self._domain, self._range, strict=True))))
292 np.testing.assert_array_equal(signal.domain, self._domain)
293 np.testing.assert_array_equal(signal.range, self._range)
295 def test__hash__(self) -> None:
296 """Test :func:`colour.continuous.signal.Signal.__hash__` method."""
298 assert isinstance(hash(self._signal), int)
300 def test__str__(self) -> None:
301 """Test :func:`colour.continuous.signal.Signal.__str__` method."""
303 assert (
304 str(self._signal)
305 == (
306 textwrap.dedent(
307 """
308 [[ 0. 10.]
309 [ 1. 20.]
310 [ 2. 30.]
311 [ 3. 40.]
312 [ 4. 50.]
313 [ 5. 60.]
314 [ 6. 70.]
315 [ 7. 80.]
316 [ 8. 90.]
317 [ 9. 100.]]"""
318 )[1:]
319 )
320 )
322 assert isinstance(str(Signal()), str)
324 def test__repr__(self) -> None:
325 """Test :func:`colour.continuous.signal.Signal.__repr__` method."""
327 assert repr(self._signal) == (
328 textwrap.dedent(
329 """
330 Signal([[ 0., 10.],
331 [ 1., 20.],
332 [ 2., 30.],
333 [ 3., 40.],
334 [ 4., 50.],
335 [ 5., 60.],
336 [ 6., 70.],
337 [ 7., 80.],
338 [ 8., 90.],
339 [ 9., 100.]],
340 KernelInterpolator,
341 {},
342 Extrapolator,
343 {'method': 'Constant', 'left': nan, 'right': nan})
344 """
345 ).strip()
346 )
348 assert isinstance(repr(Signal()), str)
350 def test__getitem__(self) -> None:
351 """Test :func:`colour.continuous.signal.Signal.__getitem__` method."""
353 assert self._signal[0] == 10.0
355 np.testing.assert_allclose(
356 self._signal[np.array([0, 1, 2])],
357 np.array([10.0, 20.0, 30.0]),
358 atol=TOLERANCE_ABSOLUTE_TESTS,
359 )
361 np.testing.assert_allclose(
362 self._signal[np.linspace(0, 5, 5)],
363 np.array(
364 [
365 10.00000000,
366 22.83489024,
367 34.80044921,
368 47.55353925,
369 60.00000000,
370 ]
371 ),
372 atol=TOLERANCE_ABSOLUTE_TESTS,
373 )
375 attest(np.all(np.isnan(self._signal[np.array([-1000, 1000])])))
377 signal = self._signal.copy()
378 signal.extrapolator_kwargs = {
379 "method": "Linear",
380 }
381 np.testing.assert_array_equal(
382 signal[np.array([-1000, 1000])], np.array([-9990.0, 10010.0])
383 )
385 signal.extrapolator_kwargs = {
386 "method": "Constant",
387 "left": 0,
388 "right": 1,
389 }
390 np.testing.assert_array_equal(
391 signal[np.array([-1000, 1000])], np.array([0.0, 1.0])
392 )
394 def test__setitem__(self) -> None:
395 """Test :func:`colour.continuous.signal.Signal.__setitem__` method."""
397 signal = self._signal.copy()
399 signal[0] = 20
400 np.testing.assert_allclose(
401 signal.range,
402 np.array([20.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]),
403 )
405 signal[np.array([0, 1, 2])] = 30
406 np.testing.assert_allclose(
407 signal.range,
408 np.array([30.0, 30.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]),
409 atol=TOLERANCE_ABSOLUTE_TESTS,
410 )
412 signal[0:3] = 40
413 np.testing.assert_allclose(
414 signal.range,
415 np.array([40.0, 40.0, 40.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]),
416 atol=TOLERANCE_ABSOLUTE_TESTS,
417 )
419 signal[np.linspace(0, 5, 5)] = 50
420 np.testing.assert_allclose(
421 signal.domain,
422 np.array(
423 [
424 0.00,
425 1.00,
426 1.25,
427 2.00,
428 2.50,
429 3.00,
430 3.75,
431 4.00,
432 5.00,
433 6.00,
434 7.00,
435 8.00,
436 9.00,
437 ]
438 ),
439 atol=TOLERANCE_ABSOLUTE_TESTS,
440 )
441 np.testing.assert_allclose(
442 signal.range,
443 np.array(
444 [
445 50.0,
446 40.0,
447 50.0,
448 40.0,
449 50.0,
450 40.0,
451 50.0,
452 50.0,
453 50.0,
454 70.0,
455 80.0,
456 90.0,
457 100.0,
458 ]
459 ),
460 atol=TOLERANCE_ABSOLUTE_TESTS,
461 )
463 signal[np.array([0, 1, 2])] = np.array([10, 20, 30])
464 np.testing.assert_allclose(
465 signal.range,
466 np.array(
467 [
468 10.0,
469 20.0,
470 50.0,
471 30.0,
472 50.0,
473 40.0,
474 50.0,
475 50.0,
476 50.0,
477 70.0,
478 80.0,
479 90.0,
480 100.0,
481 ]
482 ),
483 atol=TOLERANCE_ABSOLUTE_TESTS,
484 )
486 def test__contains__(self) -> None:
487 """Test :func:`colour.continuous.signal.Signal.__contains__` method."""
489 assert 0 in self._signal
490 assert 0.5 in self._signal
491 assert 1000 not in self._signal
493 def test__iter__(self) -> None:
494 """Test :func:`colour.continuous.signal.Signal.__iter__` method."""
496 domain = np.arange(0, 10)
497 for i, (domain_value, range_value) in enumerate(self._signal):
498 np.testing.assert_array_equal(domain_value, domain[i])
499 np.testing.assert_array_equal(range_value, self._range[i])
501 def test__len__(self) -> None:
502 """Test :func:`colour.continuous.signal.Signal.__len__` method."""
504 assert len(self._signal) == 10
506 def test__eq__(self) -> None:
507 """Test :func:`colour.continuous.signal.Signal.__eq__` method."""
509 signal_1 = self._signal.copy()
510 signal_2 = self._signal.copy()
512 assert signal_1 == signal_2
514 def test__ne__(self) -> None:
515 """Test :func:`colour.continuous.signal.Signal.__ne__` method."""
517 signal_1 = self._signal.copy()
518 signal_2 = self._signal.copy()
520 signal_2[0] = 20
521 assert signal_1 != signal_2
523 signal_2[0] = 10
524 assert signal_1 == signal_2
526 signal_2.interpolator = CubicSplineInterpolator
527 assert signal_1 != signal_2
529 signal_2.interpolator = KernelInterpolator
530 assert signal_1 == signal_2
532 signal_2.interpolator_kwargs = {"window": 1}
533 assert signal_1 != signal_2
535 signal_2.interpolator_kwargs = {}
536 assert signal_1 == signal_2
538 class NotExtrapolator(Extrapolator):
539 """Not :class:`Extrapolator` class."""
541 signal_2.extrapolator = NotExtrapolator
542 assert signal_1 != signal_2
544 signal_2.extrapolator = Extrapolator
545 assert signal_1 == signal_2
547 signal_2.extrapolator_kwargs = {}
548 assert signal_1 != signal_2
550 signal_2.extrapolator_kwargs = {
551 "method": "Constant",
552 "left": np.nan,
553 "right": np.nan,
554 }
555 assert signal_1 == signal_2
557 def test_arithmetical_operation(self) -> None:
558 """
559 Test :meth:`colour.continuous.signal.Signal.arithmetical_operation`
560 method.
561 """
563 np.testing.assert_allclose(
564 self._signal.arithmetical_operation(10, "+", False).range,
565 self._range + 10,
566 atol=TOLERANCE_ABSOLUTE_TESTS,
567 )
569 np.testing.assert_allclose(
570 self._signal.arithmetical_operation(10, "-", False).range,
571 self._range - 10,
572 atol=TOLERANCE_ABSOLUTE_TESTS,
573 )
575 np.testing.assert_allclose(
576 self._signal.arithmetical_operation(10, "*", False).range,
577 self._range * 10,
578 atol=TOLERANCE_ABSOLUTE_TESTS,
579 )
581 np.testing.assert_allclose(
582 self._signal.arithmetical_operation(10, "/", False).range,
583 self._range / 10,
584 atol=TOLERANCE_ABSOLUTE_TESTS,
585 )
587 np.testing.assert_allclose(
588 self._signal.arithmetical_operation(10, "**", False).range,
589 self._range**10,
590 atol=TOLERANCE_ABSOLUTE_TESTS,
591 )
593 np.testing.assert_allclose(
594 (self._signal + 10).range,
595 self._range + 10,
596 atol=TOLERANCE_ABSOLUTE_TESTS,
597 )
599 np.testing.assert_allclose(
600 (self._signal - 10).range,
601 self._range - 10,
602 atol=TOLERANCE_ABSOLUTE_TESTS,
603 )
605 np.testing.assert_allclose(
606 (self._signal * 10).range,
607 self._range * 10,
608 atol=TOLERANCE_ABSOLUTE_TESTS,
609 )
611 np.testing.assert_allclose(
612 (self._signal / 10).range,
613 self._range / 10,
614 atol=TOLERANCE_ABSOLUTE_TESTS,
615 )
617 np.testing.assert_allclose(
618 (self._signal**10).range,
619 self._range**10,
620 atol=TOLERANCE_ABSOLUTE_TESTS,
621 )
623 signal = self._signal.copy()
625 np.testing.assert_allclose(
626 signal.arithmetical_operation(10, "+", True).range,
627 self._range + 10,
628 atol=TOLERANCE_ABSOLUTE_TESTS,
629 )
631 np.testing.assert_allclose(
632 signal.arithmetical_operation(10, "-", True).range,
633 self._range,
634 atol=TOLERANCE_ABSOLUTE_TESTS,
635 )
637 np.testing.assert_allclose(
638 signal.arithmetical_operation(10, "*", True).range,
639 self._range * 10,
640 atol=TOLERANCE_ABSOLUTE_TESTS,
641 )
643 np.testing.assert_allclose(
644 signal.arithmetical_operation(10, "/", True).range,
645 self._range,
646 atol=TOLERANCE_ABSOLUTE_TESTS,
647 )
649 np.testing.assert_allclose(
650 signal.arithmetical_operation(10, "**", True).range,
651 self._range**10,
652 atol=TOLERANCE_ABSOLUTE_TESTS,
653 )
655 signal = self._signal.copy()
657 np.testing.assert_allclose(
658 signal.arithmetical_operation(self._range, "+", False).range,
659 signal.range + self._range,
660 atol=TOLERANCE_ABSOLUTE_TESTS,
661 )
663 np.testing.assert_allclose(
664 signal.arithmetical_operation(signal, "+", False).range,
665 signal.range + signal.range,
666 atol=TOLERANCE_ABSOLUTE_TESTS,
667 )
669 def test_is_uniform(self) -> None:
670 """Test :func:`colour.continuous.signal.Signal.is_uniform` method."""
672 assert self._signal.is_uniform()
674 signal = self._signal.copy()
675 signal[0.5] = 1.0
676 assert not signal.is_uniform()
678 def test_copy(self) -> None:
679 """Test :func:`colour.continuous.signal.Signal.copy` method."""
681 assert self._signal is not self._signal.copy()
682 assert self._signal == self._signal.copy()
684 def test_signal_unpack_data(self) -> None:
685 """
686 Test :meth:`colour.continuous.signal.Signal.signal_unpack_data`
687 method.
688 """
690 domain, range_ = Signal.signal_unpack_data(self._range)
691 np.testing.assert_array_equal(range_, self._range)
692 np.testing.assert_array_equal(domain, np.arange(0, 10, 1))
694 domain, range_ = Signal.signal_unpack_data(self._range, self._domain)
695 np.testing.assert_array_equal(range_, self._range)
696 np.testing.assert_array_equal(domain, self._domain)
698 domain, range_ = Signal.signal_unpack_data(
699 self._range, dict(zip(self._domain, self._range, strict=True)).keys()
700 )
701 np.testing.assert_array_equal(domain, self._domain)
703 domain, range_ = Signal.signal_unpack_data(
704 dict(zip(self._domain, self._range, strict=True))
705 )
706 np.testing.assert_array_equal(range_, self._range)
707 np.testing.assert_array_equal(domain, self._domain)
709 domain, range_ = Signal.signal_unpack_data(Signal(self._range, self._domain))
710 np.testing.assert_array_equal(range_, self._range)
711 np.testing.assert_array_equal(domain, self._domain)
713 if is_pandas_installed():
714 from pandas import Series # noqa: PLC0415
716 domain, range_ = Signal.signal_unpack_data(
717 Series(dict(zip(self._domain, self._range, strict=True)))
718 )
719 np.testing.assert_array_equal(range_, self._range)
720 np.testing.assert_array_equal(domain, self._domain)
722 def test_fill_nan(self) -> None:
723 """Test :func:`colour.continuous.signal.Signal.fill_nan` method."""
725 signal = self._signal.copy()
727 signal[3:7] = np.nan
729 np.testing.assert_allclose(
730 signal.fill_nan().range,
731 np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]),
732 atol=TOLERANCE_ABSOLUTE_TESTS,
733 )
735 signal[3:7] = np.nan
737 np.testing.assert_allclose(
738 signal.fill_nan(method="Constant").range,
739 np.array([10.0, 20.0, 30.0, 0.0, 0.0, 0.0, 0.0, 80.0, 90.0, 100.0]),
740 atol=TOLERANCE_ABSOLUTE_TESTS,
741 )
743 def test_domain_distance(self) -> None:
744 """Test :func:`colour.continuous.signal.Signal.domain_distance` method."""
746 np.testing.assert_allclose(
747 self._signal.domain_distance(0.5),
748 0.5,
749 atol=TOLERANCE_ABSOLUTE_TESTS,
750 )
752 np.testing.assert_allclose(
753 self._signal.domain_distance(np.linspace(0, 9, 10) + 0.5),
754 np.array([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]),
755 atol=TOLERANCE_ABSOLUTE_TESTS,
756 )
758 def test_to_series(self) -> None:
759 """Test :func:`colour.continuous.signal.Signal.to_series` method."""
761 if is_pandas_installed():
762 from pandas import Series # noqa: PLC0415
764 assert (
765 Signal(self._range, self._domain).to_series().all()
766 == Series(dict(zip(self._domain, self._range, strict=True))).all()
767 )