Coverage for continuous/tests/test_signal.py: 100%

1"""Define the unit tests for the :mod:`colour.continuous.signal` module."""

2

3 from __future__ import annotations

4

5 import pickle

6 import textwrap

7

8 import numpy as np

9 import pytest

10

11 from colour.algebra import CubicSplineInterpolator, Extrapolator, KernelInterpolator

12 from colour.constants import DTYPE_FLOAT_DEFAULT, TOLERANCE_ABSOLUTE_TESTS

13 from colour.continuous import Signal

14 from colour.utilities import (

15 ColourRuntimeWarning,

16 attest,

17 is_pandas_installed,

18 is_scipy_installed,

19)

20

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"

27

28 __all__ = [

29 "TestSignal",

30]

31

32

33 class TestSignal:

34 """Define :class:`colour.continuous.signal.Signal` class unit tests methods."""

35

36 def setup_method(self) -> None:

37 """Initialise the common tests attributes."""

38

39 self._range = np.linspace(10, 100, 10)

40 self._domain = np.arange(100, 1100, 100)

41

42 self._signal = Signal(self._range)

43

44 def test_required_attributes(self) -> None:

45 """Test the presence of required attributes."""

46

47 required_attributes = (

48 "dtype",

49 "domain",

50 "range",

51 "interpolator",

52 "interpolator_kwargs",

53 "extrapolator",

54 "extrapolator_kwargs",

55 "function",

56 )

57

58 for attribute in required_attributes:

59 assert attribute in dir(Signal)

60

61 def test_required_methods(self) -> None:

62 """Test the presence of required methods."""

63

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 )

81

82 for method in required_methods:

83 assert method in dir(Signal)

84

85 def test_pickling(self) -> None:

86 """

87 Test whether the :class:``colour.continuous.signal.Signal` class can be

88 pickled.

89 """

90

91 data = pickle.dumps(self._signal)

92 data = pickle.loads(data) # noqa: S301

93 assert self._signal == data

94

95 def test_dtype(self) -> None:

96 """Test :func:`colour.continuous.signal.Signal.dtype` property."""

97

98 assert self._signal.dtype == DTYPE_FLOAT_DEFAULT

99

100 signal = self._signal.copy()

101 signal.dtype = np.float32

102 assert signal.dtype == np.float32

103

104 def test_domain(self) -> None:

105 """Test :func:`colour.continuous.signal.Signal.domain` property."""

106

107 signal = self._signal.copy()

108

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 )

114

115 signal.domain = np.arange(0, 10, 1) * 10

116

117 np.testing.assert_array_equal(signal.domain, np.arange(0, 10, 1) * 10)

118

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 )

124

125 domain = np.linspace(0, 1, 10)

126 domain[0] = -np.inf

127

128 def assert_warns() -> None:

129 """Help to test the runtime warning."""

130

131 signal.domain = domain

132

133 pytest.warns(ColourRuntimeWarning, assert_warns)

134

135 def test_range(self) -> None:

136 """Test :func:`colour.continuous.signal.Signal.range` property."""

137

138 signal = self._signal.copy()

139

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 )

145

146 signal.range = self._range * 10

147

148 np.testing.assert_array_equal(signal.range, self._range * 10)

149

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 )

155

156 def assert_warns() -> None:

157 """Help to test the runtime warning."""

158

159 signal.range = self._range * np.inf

160

161 pytest.warns(ColourRuntimeWarning, assert_warns)

162

163 def test_interpolator(self) -> None:

164 """Test :func:`colour.continuous.signal.Signal.interpolator` property."""

165

166 if not is_scipy_installed(): # pragma: no cover

167 return

168

169 signal = self._signal.copy()

170

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 )

184

185 signal.interpolator = CubicSplineInterpolator

186

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 )

192

193 def test_interpolator_kwargs(self) -> None:

194 """

195 Test :func:`colour.continuous.signal.Signal.interpolator_kwargs`

196 property.

197 """

198

199 signal = self._signal.copy()

200

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 )

214

215 signal.interpolator_kwargs = {"window": 1, "kernel_kwargs": {"a": 1}}

216

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 )

230

231 def test_extrapolator(self) -> None:

232 """Test :func:`colour.continuous.signal.Signal.extrapolator` property."""

233

234 assert isinstance(self._signal.extrapolator(), Extrapolator)

235

236 def test_extrapolator_kwargs(self) -> None:

237 """

238 Test :func:`colour.continuous.signal.Signal.extrapolator_kwargs`

239 property.

240 """

241

242 signal = self._signal.copy()

243

244 attest(np.all(np.isnan(signal[np.array([-1000, 1000])])))

245

246 signal.extrapolator_kwargs = {

247 "method": "Linear",

248 }

249

250 np.testing.assert_allclose(

251 signal[np.array([-1000, 1000])],

252 np.array([-9990.0, 10010.0]),

253 atol=TOLERANCE_ABSOLUTE_TESTS,

254 )

255

256 def test_function(self) -> None:

257 """Test :func:`colour.continuous.signal.Signal.function` property."""

258

259 attest(callable(self._signal.function))

260

261 def test_raise_exception_function(self) -> None:

262 """

263 Test :func:`colour.continuous.signal.Signal.function` property raised

264 exception.

265 """

266

267 pytest.raises(ValueError, Signal().function, 0)

268

269 def test__init__(self) -> None:

270 """Test :func:`colour.continuous.signal.Signal.__init__` method."""

271

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)

275

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)

279

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)

283

284 signal = Signal(signal)

285 np.testing.assert_array_equal(signal.domain, self._domain)

286 np.testing.assert_array_equal(signal.range, self._range)

287

288 if is_pandas_installed():

289 from pandas import Series # noqa: PLC0415

290

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)

294

295 def test__hash__(self) -> None:

296 """Test :func:`colour.continuous.signal.Signal.__hash__` method."""

297

298 assert isinstance(hash(self._signal), int)

299

300 def test__str__(self) -> None:

301 """Test :func:`colour.continuous.signal.Signal.__str__` method."""

302

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 )

321

322 assert isinstance(str(Signal()), str)

323

324 def test__repr__(self) -> None:

325 """Test :func:`colour.continuous.signal.Signal.__repr__` method."""

326

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 )

347

348 assert isinstance(repr(Signal()), str)

349

350 def test__getitem__(self) -> None:

351 """Test :func:`colour.continuous.signal.Signal.__getitem__` method."""

352

353 assert self._signal[0] == 10.0

354

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 )

360

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 )

374

375 attest(np.all(np.isnan(self._signal[np.array([-1000, 1000])])))

376

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 )

384

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 )

393

394 def test__setitem__(self) -> None:

395 """Test :func:`colour.continuous.signal.Signal.__setitem__` method."""

396

397 signal = self._signal.copy()

398

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 )

404

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 )

411

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 )

418

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 )

462

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 )

485

486 def test__contains__(self) -> None:

487 """Test :func:`colour.continuous.signal.Signal.__contains__` method."""

488

489 assert 0 in self._signal

490 assert 0.5 in self._signal

491 assert 1000 not in self._signal

492

493 def test__iter__(self) -> None:

494 """Test :func:`colour.continuous.signal.Signal.__iter__` method."""

495

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

500

501 def test__len__(self) -> None:

502 """Test :func:`colour.continuous.signal.Signal.__len__` method."""

503

504 assert len(self._signal) == 10

505

506 def test__eq__(self) -> None:

507 """Test :func:`colour.continuous.signal.Signal.__eq__` method."""

508

509 signal_1 = self._signal.copy()

510 signal_2 = self._signal.copy()

511

512 assert signal_1 == signal_2

513

514 def test__ne__(self) -> None:

515 """Test :func:`colour.continuous.signal.Signal.__ne__` method."""

516

517 signal_1 = self._signal.copy()

518 signal_2 = self._signal.copy()

519

520 signal_2[0] = 20

521 assert signal_1 != signal_2

522

523 signal_2[0] = 10

524 assert signal_1 == signal_2

525

526 signal_2.interpolator = CubicSplineInterpolator

527 assert signal_1 != signal_2

528

529 signal_2.interpolator = KernelInterpolator

530 assert signal_1 == signal_2

531

532 signal_2.interpolator_kwargs = {"window": 1}

533 assert signal_1 != signal_2

534

535 signal_2.interpolator_kwargs = {}

536 assert signal_1 == signal_2

537

538 class NotExtrapolator(Extrapolator):

539 """Not :class:`Extrapolator` class."""

540

541 signal_2.extrapolator = NotExtrapolator

542 assert signal_1 != signal_2

543

544 signal_2.extrapolator = Extrapolator

545 assert signal_1 == signal_2

546

547 signal_2.extrapolator_kwargs = {}

548 assert signal_1 != signal_2

549

550 signal_2.extrapolator_kwargs = {

551 "method": "Constant",

552 "left": np.nan,

553 "right": np.nan,

554 }

555 assert signal_1 == signal_2

556

557 def test_arithmetical_operation(self) -> None:

558 """

559 Test :meth:`colour.continuous.signal.Signal.arithmetical_operation`

560 method.

561 """

562

563 np.testing.assert_allclose(

564 self._signal.arithmetical_operation(10, "+", False).range,

565 self._range + 10,

566 atol=TOLERANCE_ABSOLUTE_TESTS,

567 )

568

569 np.testing.assert_allclose(

570 self._signal.arithmetical_operation(10, "-", False).range,

571 self._range - 10,

572 atol=TOLERANCE_ABSOLUTE_TESTS,

573 )

574

575 np.testing.assert_allclose(

576 self._signal.arithmetical_operation(10, "*", False).range,

577 self._range * 10,

578 atol=TOLERANCE_ABSOLUTE_TESTS,

579 )

580

581 np.testing.assert_allclose(

582 self._signal.arithmetical_operation(10, "/", False).range,

583 self._range / 10,

584 atol=TOLERANCE_ABSOLUTE_TESTS,

585 )

586

587 np.testing.assert_allclose(

588 self._signal.arithmetical_operation(10, "**", False).range,

589 self._range**10,

590 atol=TOLERANCE_ABSOLUTE_TESTS,

591 )

592

593 np.testing.assert_allclose(

594 (self._signal + 10).range,

595 self._range + 10,

596 atol=TOLERANCE_ABSOLUTE_TESTS,

597 )

598

599 np.testing.assert_allclose(

600 (self._signal - 10).range,

601 self._range - 10,

602 atol=TOLERANCE_ABSOLUTE_TESTS,

603 )

604

605 np.testing.assert_allclose(

606 (self._signal * 10).range,

607 self._range * 10,

608 atol=TOLERANCE_ABSOLUTE_TESTS,

609 )

610

611 np.testing.assert_allclose(

612 (self._signal / 10).range,

613 self._range / 10,

614 atol=TOLERANCE_ABSOLUTE_TESTS,

615 )

616

617 np.testing.assert_allclose(

618 (self._signal**10).range,

619 self._range**10,

620 atol=TOLERANCE_ABSOLUTE_TESTS,

621 )

622

623 signal = self._signal.copy()

624

625 np.testing.assert_allclose(

626 signal.arithmetical_operation(10, "+", True).range,

627 self._range + 10,

628 atol=TOLERANCE_ABSOLUTE_TESTS,

629 )

630

631 np.testing.assert_allclose(

632 signal.arithmetical_operation(10, "-", True).range,

633 self._range,

634 atol=TOLERANCE_ABSOLUTE_TESTS,

635 )

636

637 np.testing.assert_allclose(

638 signal.arithmetical_operation(10, "*", True).range,

639 self._range * 10,

640 atol=TOLERANCE_ABSOLUTE_TESTS,

641 )

642

643 np.testing.assert_allclose(

644 signal.arithmetical_operation(10, "/", True).range,

645 self._range,

646 atol=TOLERANCE_ABSOLUTE_TESTS,

647 )

648

649 np.testing.assert_allclose(

650 signal.arithmetical_operation(10, "**", True).range,

651 self._range**10,

652 atol=TOLERANCE_ABSOLUTE_TESTS,

653 )

654

655 signal = self._signal.copy()

656

657 np.testing.assert_allclose(

658 signal.arithmetical_operation(self._range, "+", False).range,

659 signal.range + self._range,

660 atol=TOLERANCE_ABSOLUTE_TESTS,

661 )

662

663 np.testing.assert_allclose(

664 signal.arithmetical_operation(signal, "+", False).range,

665 signal.range + signal.range,

666 atol=TOLERANCE_ABSOLUTE_TESTS,

667 )

668

669 def test_is_uniform(self) -> None:

670 """Test :func:`colour.continuous.signal.Signal.is_uniform` method."""

671

672 assert self._signal.is_uniform()

673

674 signal = self._signal.copy()

675 signal[0.5] = 1.0

676 assert not signal.is_uniform()

677

678 def test_copy(self) -> None:

679 """Test :func:`colour.continuous.signal.Signal.copy` method."""

680

681 assert self._signal is not self._signal.copy()

682 assert self._signal == self._signal.copy()

683

684 def test_signal_unpack_data(self) -> None:

685 """

686 Test :meth:`colour.continuous.signal.Signal.signal_unpack_data`

687 method.

688 """

689

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

693

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)

697

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)

702

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)

708

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)

712

713 if is_pandas_installed():

714 from pandas import Series # noqa: PLC0415

715

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)

721

722 def test_fill_nan(self) -> None:

723 """Test :func:`colour.continuous.signal.Signal.fill_nan` method."""

724

725 signal = self._signal.copy()

726

727 signal[3:7] = np.nan

728

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 )

734

735 signal[3:7] = np.nan

736

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 )

742

743 def test_domain_distance(self) -> None:

744 """Test :func:`colour.continuous.signal.Signal.domain_distance` method."""

745

746 np.testing.assert_allclose(

747 self._signal.domain_distance(0.5),

748 0.5,

749 atol=TOLERANCE_ABSOLUTE_TESTS,

750 )

751

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 )

757

758 def test_to_series(self) -> None:

759 """Test :func:`colour.continuous.signal.Signal.to_series` method."""

760

761 if is_pandas_installed():

762 from pandas import Series # noqa: PLC0415

763

764 assert (

765 Signal(self._range, self._domain).to_series().all()

766 == Series(dict(zip(self._domain, self._range, strict=True))).all()

767 )