Coverage for colour/colorimetry/spectrum.py: 100%

312 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Spectrum 

3======== 

4 

5Define classes and objects for handling spectral data computations. 

6 

7- :class:`colour.SPECTRAL_SHAPE_DEFAULT` 

8- :class:`colour.SpectralShape` 

9- :class:`colour.SpectralDistribution` 

10- :class:`colour.MultiSpectralDistributions` 

11- :func:`colour.colorimetry.sds_and_msds_to_sds` 

12- :func:`colour.colorimetry.sds_and_msds_to_msds` 

13- :func:`colour.colorimetry.reshape_sd` 

14- :func:`colour.colorimetry.reshape_msds` 

15 

16References 

17---------- 

18- :cite:`CIETC1-382005e` : CIE TC 1-38. (2005). 9. INTERPOLATION. In CIE 

19 167:2005 Recommended Practice for Tabulating Spectral Data for Use in 

20 Colour Computations (pp. 14-19). ISBN:978-3-901906-41-1 

21- :cite:`CIETC1-382005g` : CIE TC 1-38. (2005). EXTRAPOLATION. In CIE 

22 167:2005 Recommended Practice for Tabulating Spectral Data for Use in 

23 Colour Computations (pp. 19-20). ISBN:978-3-901906-41-1 

24- :cite:`CIETC1-482004l` : CIE TC 1-48. (2004). Extrapolation. In CIE 

25 015:2004 Colorimetry, 3rd Edition (p. 24). ISBN:978-3-901906-33-6 

26""" 

27 

28from __future__ import annotations 

29 

30import typing 

31from collections.abc import KeysView, Mapping, ValuesView 

32 

33import numpy as np 

34 

35from colour.algebra import ( 

36 CubicSplineInterpolator, 

37 Extrapolator, 

38 SpragueInterpolator, 

39 sdiv, 

40 sdiv_mode, 

41) 

42from colour.constants import DTYPE_FLOAT_DEFAULT 

43from colour.continuous import MultiSignals, Signal 

44 

45if typing.TYPE_CHECKING: 

46 from colour.hints import ( 

47 ArrayLike, 

48 DTypeFloat, 

49 Generator, 

50 List, 

51 Literal, 

52 NDArrayFloat, 

53 ProtocolExtrapolator, 

54 ProtocolInterpolator, 

55 Real, 

56 Self, 

57 Sequence, 

58 Type, 

59 TypeVar, 

60 ) 

61 

62from colour.hints import Any, TypeVar, cast 

63from colour.utilities import ( 

64 CACHE_REGISTRY, 

65 as_float_array, 

66 as_int, 

67 attest, 

68 filter_kwargs, 

69 first_item, 

70 interval, 

71 is_caching_enabled, 

72 is_iterable, 

73 is_numeric, 

74 is_pandas_installed, 

75 is_uniform, 

76 optional, 

77 runtime_warning, 

78 tstack, 

79 validate_method, 

80) 

81 

82if typing.TYPE_CHECKING or is_pandas_installed(): 

83 from pandas import DataFrame, Series # pragma: no cover 

84else: # pragma: no cover 

85 from unittest import mock 

86 

87 DataFrame = mock.MagicMock() 

88 Series = mock.MagicMock() 

89 

90__author__ = "Colour Developers" 

91__copyright__ = "Copyright 2013 Colour Developers" 

92__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

93__maintainer__ = "Colour Developers" 

94__email__ = "colour-developers@colour-science.org" 

95__status__ = "Production" 

96 

97__all__ = [ 

98 "SpectralShape", 

99 "SPECTRAL_SHAPE_DEFAULT", 

100 "SpectralDistribution", 

101 "MultiSpectralDistributions", 

102 "reshape_sd", 

103 "reshape_msds", 

104 "sds_and_msds_to_sds", 

105 "sds_and_msds_to_msds", 

106] 

107 

108_CACHE_SHAPE_RANGE: dict = CACHE_REGISTRY.register_cache( 

109 f"{__name__}._CACHE_SHAPE_RANGE" 

110) 

111 

112 

113class SpectralShape: 

114 """ 

115 Define the base object for spectral distribution shape. 

116 

117 The :class:`colour.SpectralShape` class represents the shape of spectral 

118 data by defining its wavelength range and sampling interval. It provides 

119 a structured way to handle spectral data boundaries and generate 

120 wavelength arrays for spectral computations. 

121 

122 Parameters 

123 ---------- 

124 start 

125 Wavelength :math:`\\lambda_{i}` range start in nm. 

126 end 

127 Wavelength :math:`\\lambda_{i}` range end in nm. 

128 interval 

129 Wavelength :math:`\\lambda_{i}` range interval. 

130 

131 Attributes 

132 ---------- 

133 - :attr:`~colour.SpectralShape.start` 

134 - :attr:`~colour.SpectralShape.end` 

135 - :attr:`~colour.SpectralShape.interval` 

136 - :attr:`~colour.SpectralShape.boundaries` 

137 - :attr:`~colour.SpectralShape.wavelengths` 

138 

139 Methods 

140 ------- 

141 - :meth:`~colour.SpectralShape.__init__` 

142 - :meth:`~colour.SpectralShape.__str__` 

143 - :meth:`~colour.SpectralShape.__repr__` 

144 - :meth:`~colour.SpectralShape.__hash__` 

145 - :meth:`~colour.SpectralShape.__iter__` 

146 - :meth:`~colour.SpectralShape.__contains__` 

147 - :meth:`~colour.SpectralShape.__len__` 

148 - :meth:`~colour.SpectralShape.__eq__` 

149 - :meth:`~colour.SpectralShape.__ne__` 

150 - :meth:`~colour.SpectralShape.range` 

151 

152 Examples 

153 -------- 

154 >>> SpectralShape(360, 830, 1) 

155 SpectralShape(360, 830, 1) 

156 """ 

157 

158 def __init__(self, start: Real, end: Real, interval: Real) -> None: 

159 self._start: Real = 0 

160 self._end: Real = np.inf 

161 self._interval: Real = 1 

162 self.start = start 

163 self.end = end 

164 self.interval = interval 

165 

166 @property 

167 def start(self) -> Real: 

168 """ 

169 Getter and setter for the spectral shape start. 

170 

171 Parameters 

172 ---------- 

173 value 

174 Value to set the spectral shape start wavelength with. 

175 

176 Returns 

177 ------- 

178 Real 

179 Start wavelength of the spectral shape in nanometres. 

180 """ 

181 

182 return self._start 

183 

184 @start.setter 

185 def start(self, value: Real) -> None: 

186 """Setter for the **self.start** property.""" 

187 

188 attest( 

189 is_numeric(value), 

190 f'"start" property: "{value}" is not a "number"!', 

191 ) 

192 

193 attest( 

194 bool(value < self._end), 

195 f'"start" attribute value must be strictly less than "{self._end}"!', 

196 ) 

197 

198 self._start = value 

199 

200 @property 

201 def end(self) -> Real: 

202 """ 

203 Getter and setter for the spectral shape end. 

204 

205 Parameters 

206 ---------- 

207 value 

208 Value to set the spectral shape end wavelength with. 

209 

210 Returns 

211 ------- 

212 Real 

213 End wavelength of the spectral shape in nanometres. 

214 . 

215 """ 

216 

217 return self._end 

218 

219 @end.setter 

220 def end(self, value: Real) -> None: 

221 """Setter for the **self.end** property.""" 

222 

223 attest( 

224 is_numeric(value), 

225 f'"end" property: "{value}" is not a "number"!', 

226 ) 

227 

228 attest( 

229 bool(value > self._start), 

230 f'"end" attribute value must be strictly greater than "{self._start}"!', 

231 ) 

232 

233 self._end = value 

234 

235 @property 

236 def interval(self) -> Real: 

237 """ 

238 Getter and setter for the spectral shape interval. 

239 

240 The interval defines the wavelength spacing between consecutive 

241 samples in the spectral distribution. 

242 

243 Parameters 

244 ---------- 

245 value 

246 Value to set the spectral shape interval with. 

247 

248 Returns 

249 ------- 

250 Real 

251 Spectral shape interval. 

252 """ 

253 

254 return self._interval 

255 

256 @interval.setter 

257 def interval(self, value: Real) -> None: 

258 """Setter for the **self.interval** property.""" 

259 

260 attest( 

261 is_numeric(value), 

262 f'"interval" property: "{value}" is not a "number"!', 

263 ) 

264 

265 self._interval = value 

266 

267 @property 

268 def boundaries(self) -> tuple: 

269 """ 

270 Getter and setter for the boundaries of the spectral shape. 

271 

272 The boundaries define the start and end points of the spectral 

273 range as a tuple of two values. 

274 

275 Parameters 

276 ---------- 

277 value 

278 Value to set the spectral shape boundaries with. 

279 

280 Returns 

281 ------- 

282 :class:`tuple` 

283 Spectral shape boundaries. 

284 """ 

285 

286 return self._start, self._end 

287 

288 @boundaries.setter 

289 def boundaries(self, value: ArrayLike) -> None: 

290 """Setter for the **self.boundaries** property.""" 

291 

292 value = np.asarray(value) 

293 

294 attest( 

295 value.size == 2, 

296 f'"boundaries" property: "{value}" must have exactly two elements!', 

297 ) 

298 

299 self.start, self.end = value 

300 

301 @property 

302 def wavelengths(self) -> NDArrayFloat: 

303 """ 

304 Getter for the spectral shape wavelengths. 

305 

306 Returns 

307 ------- 

308 :class:`numpy.ndarray` 

309 Spectral shape wavelengths. 

310 """ 

311 

312 return self.range() 

313 

314 def __str__(self) -> str: 

315 """ 

316 Return a formatted string representation of the spectral shape. 

317 

318 Returns 

319 ------- 

320 :class:`str` 

321 Formatted string representation. 

322 """ 

323 

324 return f"({self._start}, {self._end}, {self._interval})" 

325 

326 def __repr__(self) -> str: 

327 """ 

328 Return an evaluable string representation of the spectral shape. 

329 

330 Returns 

331 ------- 

332 :class:`str` 

333 Evaluable string representation. 

334 """ 

335 

336 return f"SpectralShape({self._start}, {self._end}, {self._interval})" 

337 

338 def __hash__(self) -> int: 

339 """ 

340 Return the hash value of the spectral shape. 

341 

342 The hash is computed based on the spectral shape's start wavelength, 

343 end wavelength, and wavelength interval. 

344 

345 Returns 

346 ------- 

347 :class:`int` 

348 Hash value of the spectral shape. 

349 """ 

350 

351 return hash((self.start, self.end, self.interval)) 

352 

353 def __iter__(self) -> Generator: 

354 """ 

355 Generate wavelengths for the spectral shape range. 

356 

357 Yields 

358 ------ 

359 Generator 

360 Wavelength values from start to end at the specified interval. 

361 

362 Examples 

363 -------- 

364 >>> shape = SpectralShape(0, 10, 1) 

365 >>> for wavelength in shape: 

366 ... print(wavelength) 

367 0.0 

368 1.0 

369 2.0 

370 3.0 

371 4.0 

372 5.0 

373 6.0 

374 7.0 

375 8.0 

376 9.0 

377 10.0 

378 """ 

379 

380 yield from self.wavelengths 

381 

382 def __contains__(self, wavelength: ArrayLike) -> bool: 

383 """ 

384 Determine if the spectral shape contains the specified wavelength 

385 :math:`\\lambda`. 

386 

387 Parameters 

388 ---------- 

389 wavelength 

390 Wavelength :math:`\\lambda` to check for containment. 

391 

392 Returns 

393 ------- 

394 :class:`bool` 

395 Whether the wavelength :math:`\\lambda` is contained within the 

396 spectral shape. 

397 

398 Examples 

399 -------- 

400 >>> 0.5 in SpectralShape(0, 10, 0.1) 

401 True 

402 >>> 0.6 in SpectralShape(0, 10, 0.1) 

403 True 

404 >>> 0.51 in SpectralShape(0, 10, 0.1) 

405 False 

406 >>> np.array([0.5, 0.6]) in SpectralShape(0, 10, 0.1) 

407 True 

408 >>> np.array([0.51, 0.6]) in SpectralShape(0, 10, 0.1) 

409 False 

410 """ 

411 

412 decimals = np.finfo(cast("Any", DTYPE_FLOAT_DEFAULT)).precision 

413 

414 return bool( 

415 np.all( 

416 np.isin( 

417 np.around( 

418 wavelength, # pyright: ignore 

419 decimals, 

420 ), 

421 np.around( 

422 self.wavelengths, 

423 decimals, 

424 ), 

425 ) 

426 ) 

427 ) 

428 

429 def __len__(self) -> int: 

430 """ 

431 Return the spectral shape wavelength :math:`\\lambda_n` count. 

432 

433 Returns 

434 ------- 

435 :class:`int` 

436 Spectral shape wavelength :math:`\\lambda_n` count. 

437 

438 Examples 

439 -------- 

440 >>> len(SpectralShape(0, 10, 0.1)) 

441 101 

442 """ 

443 

444 return len(self.wavelengths) 

445 

446 def __eq__(self, other: object) -> bool: 

447 """ 

448 Determine whether the spectral shape is equal to the specified other 

449 object. 

450 

451 Parameters 

452 ---------- 

453 other 

454 Object to determine whether it is equal to the spectral shape. 

455 

456 Returns 

457 ------- 

458 :class:`bool` 

459 Whether the specified object is equal to the spectral shape. 

460 

461 Examples 

462 -------- 

463 >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 0.1) 

464 True 

465 >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 1) 

466 False 

467 """ 

468 

469 if isinstance(other, SpectralShape): 

470 return np.array_equal(self.wavelengths, other.wavelengths) 

471 

472 return False 

473 

474 def __ne__(self, other: object) -> bool: 

475 """ 

476 Determine whether the spectral shape is not equal to the specified 

477 other object. 

478 

479 Parameters 

480 ---------- 

481 other 

482 Object to determine whether it is not equal to the spectral 

483 shape. 

484 

485 Returns 

486 ------- 

487 :class:`bool` 

488 Whether the specified object is not equal to the spectral 

489 shape. 

490 

491 Examples 

492 -------- 

493 >>> SpectralShape(0, 10, 0.1) != SpectralShape(0, 10, 0.1) 

494 False 

495 >>> SpectralShape(0, 10, 0.1) != SpectralShape(0, 10, 1) 

496 True 

497 """ 

498 

499 return not (self == other) 

500 

501 def range(self, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat: 

502 """ 

503 Return an iterable range for the spectral shape. 

504 

505 Parameters 

506 ---------- 

507 dtype 

508 Data type used to generate the range. 

509 

510 Returns 

511 ------- 

512 :class:`numpy.ndarray` 

513 Iterable range for the spectral distribution shape. 

514 

515 Examples 

516 -------- 

517 >>> SpectralShape(0, 10, 0.1).wavelengths 

518 array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 

519 0.9, 1. , 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 

520 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 

521 2.7, 2.8, 2.9, 3. , 3.1, 3.2, 3.3, 3.4, 3.5, 

522 3.6, 3.7, 3.8, 3.9, 4. , 4.1, 4.2, 4.3, 4.4, 

523 4.5, 4.6, 4.7, 4.8, 4.9, 5. , 5.1, 5.2, 5.3, 

524 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6. , 6.1, 6.2, 

525 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 7. , 7.1, 

526 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8. , 

527 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 

528 9. , 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 

529 9.9, 10. ]) 

530 """ 

531 

532 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

533 

534 hash_key = hash((self, dtype)) 

535 

536 if is_caching_enabled() and hash_key in _CACHE_SHAPE_RANGE: 

537 return _CACHE_SHAPE_RANGE[hash_key].copy() 

538 

539 start, end, interval = ( 

540 dtype(self._start), 

541 dtype(self._end), 

542 dtype(self._interval), 

543 ) 

544 

545 samples = as_int(round((interval + end - start) / interval)) 

546 range_, interval_effective = np.linspace( 

547 start, end, samples, retstep=True, dtype=dtype 

548 ) 

549 

550 _CACHE_SHAPE_RANGE[hash_key] = range_ 

551 

552 if interval_effective != self._interval: 

553 self._interval = cast("float", interval_effective) 

554 runtime_warning( 

555 f'"{(start, end, interval)}" shape could not be honoured, ' 

556 f'using "{self}"!' 

557 ) 

558 

559 return range_ 

560 

561 

562SPECTRAL_SHAPE_DEFAULT: SpectralShape = SpectralShape(360, 780, 1) 

563"""Default spectral shape according to *ASTM E308-15* practise shape.""" 

564 

565 

566class SpectralDistribution(Signal): 

567 """ 

568 Define the spectral distribution: the base object for spectral 

569 computations. 

570 

571 Initialise spectral distribution according to *CIE 15:2004* recommendation: 

572 use the method developed by *Sprague (1880)* for interpolating functions 

573 having uniformly spaced independent variables and the *Cubic Spline* method 

574 for non-uniformly spaced independent variables. Perform extrapolation 

575 according to *CIE 167:2005* recommendation. 

576 

577 .. important:: 

578 

579 Specific documentation about getting, setting, indexing and slicing 

580 the spectral power distribution values is available in the 

581 :ref:`spectral-representation-and-continuous-signal` section. 

582 

583 Parameters 

584 ---------- 

585 data 

586 Data to be stored in the spectral distribution. 

587 domain 

588 Values to initialise the 

589 :attr:`colour.SpectralDistribution.wavelength` property with. 

590 If both ``data`` and ``domain`` arguments are defined, the latter 

591 will be used to initialise the 

592 :attr:`colour.SpectralDistribution.wavelength` property. 

593 

594 Other Parameters 

595 ---------------- 

596 extrapolator 

597 Extrapolator class type to use as extrapolating function. 

598 extrapolator_kwargs 

599 Arguments to use when instantiating the extrapolating function. 

600 interpolator 

601 Interpolator class type to use as interpolating function. 

602 interpolator_kwargs 

603 Arguments to use when instantiating the interpolating function. 

604 name 

605 Spectral distribution name. 

606 display_name 

607 Spectral distribution name for figures, default to 

608 :attr:`colour.SpectralDistribution.name` property value. 

609 

610 Warnings 

611 -------- 

612 The *Cubic Spline* method might produce unexpected results with 

613 exceptionally noisy or non-uniformly spaced data. 

614 

615 Attributes 

616 ---------- 

617 - :attr:`~colour.SpectralDistribution.display_name` 

618 - :attr:`~colour.SpectralDistribution.wavelengths` 

619 - :attr:`~colour.SpectralDistribution.values` 

620 - :attr:`~colour.SpectralDistribution.shape` 

621 

622 Methods 

623 ------- 

624 - :meth:`~colour.SpectralDistribution.__init__` 

625 - :meth:`~colour.SpectralDistribution.interpolate` 

626 - :meth:`~colour.SpectralDistribution.extrapolate` 

627 - :meth:`~colour.SpectralDistribution.align` 

628 - :meth:`~colour.SpectralDistribution.trim` 

629 - :meth:`~colour.SpectralDistribution.normalise` 

630 

631 References 

632 ---------- 

633 :cite:`CIETC1-382005e`, :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` 

634 

635 Examples 

636 -------- 

637 Instantiating a spectral distribution with a uniformly spaced independent 

638 variable: 

639 

640 >>> from colour.utilities import numpy_print_options 

641 >>> data = { 

642 ... 500: 0.0651, 

643 ... 520: 0.0705, 

644 ... 540: 0.0772, 

645 ... 560: 0.0870, 

646 ... 580: 0.1128, 

647 ... 600: 0.1360, 

648 ... } 

649 >>> with numpy_print_options(suppress=True): 

650 ... SpectralDistribution(data) # doctest: +ELLIPSIS 

651 SpectralDistribution([[ 500. , 0.0651], 

652 [ 520. , 0.0705], 

653 [ 540. , 0.0772], 

654 [ 560. , 0.087 ], 

655 [ 580. , 0.1128], 

656 [ 600. , 0.136 ]], 

657 SpragueInterpolator, 

658 {}, 

659 Extrapolator, 

660 {'method': 'Constant', 'left': None, 'right': None}) 

661 

662 Instantiating a spectral distribution with a non-uniformly spaced 

663 independent variable: 

664 

665 >>> data[510] = 0.31416 

666 >>> with numpy_print_options(suppress=True): 

667 ... SpectralDistribution(data) # doctest: +ELLIPSIS 

668 SpectralDistribution([[ 500. , 0.0651 ], 

669 [ 510. , 0.31416], 

670 [ 520. , 0.0705 ], 

671 [ 540. , 0.0772 ], 

672 [ 560. , 0.087 ], 

673 [ 580. , 0.1128 ], 

674 [ 600. , 0.136 ]], 

675 CubicSplineInterpolator, 

676 {}, 

677 Extrapolator, 

678 {'method': 'Constant', 'left': None, 'right': None}) 

679 

680 Instantiation with a *Pandas* :class:`pandas.Series`: 

681 

682 >>> from colour.utilities import is_pandas_installed 

683 >>> if is_pandas_installed(): 

684 ... from pandas import Series 

685 ... 

686 ... print(SpectralDistribution(Series(data))) # doctest: +SKIP 

687 [[ 5.0000000...e+02 6.5100000...e-02] 

688 [ 5.2000000...e+02 7.0500000...e-02] 

689 [ 5.4000000...e+02 7.7200000...e-02] 

690 [ 5.6000000...e+02 8.7000000...e-02] 

691 [ 5.8000000...e+02 1.1280000...e-01] 

692 [ 6.0000000...e+02 1.3600000...e-01] 

693 [ 5.1000000...e+02 3.1416000...e-01]] 

694 """ 

695 

696 def __init__( 

697 self, 

698 data: ArrayLike | dict | Series | Signal | ValuesView | None = None, 

699 domain: ArrayLike | SpectralShape | KeysView | None = None, 

700 **kwargs: Any, 

701 ) -> None: 

702 domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain 

703 

704 domain_unpacked, range_unpacked = self.signal_unpack_data(data, domain) 

705 

706 # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations 

707 # defaults. 

708 kwargs["interpolator"] = kwargs.get( 

709 "interpolator", 

710 ( 

711 SpragueInterpolator 

712 if domain_unpacked.size != 0 and is_uniform(domain_unpacked) 

713 else CubicSplineInterpolator 

714 ), 

715 ) 

716 kwargs["interpolator_kwargs"] = kwargs.get("interpolator_kwargs", {}) 

717 

718 kwargs["extrapolator"] = kwargs.get("extrapolator", Extrapolator) 

719 kwargs["extrapolator_kwargs"] = kwargs.get( 

720 "extrapolator_kwargs", 

721 {"method": "Constant", "left": None, "right": None}, 

722 ) 

723 super().__init__(range_unpacked, domain_unpacked, **kwargs) 

724 

725 self._display_name: str = self.name 

726 self.display_name = kwargs.get("display_name", self._display_name) 

727 

728 self._shape: SpectralShape | None = None 

729 

730 self.register_callback("_domain", "on_domain_changed", self._on_domain_changed) 

731 

732 @staticmethod 

733 def _on_domain_changed( 

734 sd: SpectralDistribution, name: str, value: NDArrayFloat 

735 ) -> NDArrayFloat: 

736 """ 

737 Invalidate the cached spectral shape when the spectral 

738 distribution domain is modified. 

739 

740 This callback ensures that the internal *_shape* attribute is reset 

741 to *None* whenever the domain values change, maintaining consistency 

742 between the domain and its derived shape representation. 

743 

744 Parameters 

745 ---------- 

746 sd 

747 Spectral distribution instance whose domain has changed. 

748 name 

749 Name of the modified attribute (expected to be "_domain"). 

750 value 

751 New domain values that triggered the callback. 

752 

753 Returns 

754 ------- 

755 :class:`numpy.ndarray` 

756 The specified domain values, unchanged. 

757 """ 

758 

759 if name == "_domain": 

760 sd._shape = None 

761 

762 return value 

763 

764 @property 

765 def display_name(self) -> str: 

766 """ 

767 Getter and setter for the spectral distribution's display name. 

768 

769 The display name provides a human-readable identifier for the 

770 spectral distribution, used for visualization and reporting purposes. 

771 

772 Parameters 

773 ---------- 

774 value 

775 Value to set the spectral distribution's display name 

776 with. 

777 

778 Returns 

779 ------- 

780 :class:`str` 

781 Spectral distribution's display name. 

782 """ 

783 

784 return self._display_name 

785 

786 @display_name.setter 

787 def display_name(self, value: str) -> None: 

788 """Setter for the **self.display_name** property.""" 

789 

790 attest( 

791 isinstance(value, str), 

792 f'"display_name" property: "{value}" type is not "str"!', 

793 ) 

794 

795 self._display_name = value 

796 

797 @property 

798 def wavelengths(self) -> NDArrayFloat: 

799 """ 

800 Getter and setter for the spectral distribution wavelengths 

801 :math:`\\lambda_n`. 

802 

803 Parameters 

804 ---------- 

805 value 

806 Value to set the spectral distribution wavelengths 

807 :math:`\\lambda_n` with. 

808 

809 Returns 

810 ------- 

811 :class:`numpy.ndarray` 

812 Spectral distribution wavelengths :math:`\\lambda_n`. 

813 """ 

814 

815 return self.domain 

816 

817 @wavelengths.setter 

818 def wavelengths(self, value: ArrayLike) -> None: 

819 """Setter for the **self.wavelengths** property.""" 

820 

821 self.domain = as_float_array(value, self.dtype) 

822 

823 @property 

824 def values(self) -> NDArrayFloat: 

825 """ 

826 Getter and setter for the spectral distribution values. 

827 

828 Parameters 

829 ---------- 

830 value 

831 Value to set the spectral distribution wavelengths values with. 

832 

833 Returns 

834 ------- 

835 :class:`numpy.ndarray` 

836 Spectral distribution values. 

837 """ 

838 

839 return self.range 

840 

841 @values.setter 

842 def values(self, value: ArrayLike) -> None: 

843 """Setter for the **self.values** property.""" 

844 

845 self.range = as_float_array(value, self.dtype) 

846 

847 @property 

848 def shape(self) -> SpectralShape: 

849 """ 

850 Getter property for the spectral distribution shape. 

851 

852 Returns 

853 ------- 

854 :class:`colour.SpectralShape` 

855 Spectral distribution shape. 

856 

857 Notes 

858 ----- 

859 - A spectral distribution with a non-uniformly spaced independent 

860 variable have multiple intervals, in that case 

861 :attr:`colour.SpectralDistribution.shape` property returns 

862 the *minimum* interval size. 

863 

864 Examples 

865 -------- 

866 Shape of a spectral distribution with a uniformly spaced independent 

867 variable: 

868 

869 >>> data = { 

870 ... 500: 0.0651, 

871 ... 520: 0.0705, 

872 ... 540: 0.0772, 

873 ... 560: 0.0870, 

874 ... 580: 0.1128, 

875 ... 600: 0.1360, 

876 ... } 

877 >>> SpectralDistribution(data).shape 

878 SpectralShape(500.0, 600.0, 20.0) 

879 

880 Shape of a spectral distribution with a non-uniformly spaced 

881 independent variable: 

882 

883 >>> data[510] = 0.31416 

884 >>> SpectralDistribution(data).shape 

885 SpectralShape(500.0, 600.0, 10.0) 

886 """ 

887 

888 if self._shape is None: 

889 wavelengths = self.wavelengths 

890 wavelengths_interval = interval(wavelengths) 

891 if wavelengths_interval.size != 1: 

892 runtime_warning( 

893 f'"{self.name}" spectral distribution is not uniform, ' 

894 "using minimum interval!" 

895 ) 

896 

897 self._shape = SpectralShape( 

898 wavelengths[0], wavelengths[-1], min(wavelengths_interval) 

899 ) 

900 

901 return self._shape 

902 

903 def interpolate( 

904 self, 

905 shape: SpectralShape, 

906 interpolator: Type[ProtocolInterpolator] | None = None, 

907 interpolator_kwargs: dict | None = None, 

908 ) -> Self: 

909 """ 

910 Interpolate the spectral distribution in-place according to 

911 *CIE 167:2005* recommendation (if the interpolator has not been 

912 changed at instantiation time) or specified interpolation arguments. 

913 

914 The logic for choosing the interpolator class when ``interpolator`` is 

915 not specified is as follows: 

916 

917 .. code-block:: python 

918 

919 if self.interpolator not in ( 

920 SpragueInterpolator, 

921 CubicSplineInterpolator, 

922 ): 

923 interpolator = self.interpolator 

924 elif self.is_uniform(): 

925 interpolator = SpragueInterpolator 

926 else: 

927 interpolator = CubicSplineInterpolator 

928 

929 The logic for choosing the interpolator keyword arguments when 

930 ``interpolator_kwargs`` is not specified is as follows: 

931 

932 .. code-block:: python 

933 

934 if self.interpolator not in ( 

935 SpragueInterpolator, 

936 CubicSplineInterpolator, 

937 ): 

938 interpolator_kwargs = self.interpolator_kwargs 

939 else: 

940 interpolator_kwargs = {} 

941 

942 Parameters 

943 ---------- 

944 shape 

945 Spectral shape used for interpolation. 

946 interpolator 

947 Interpolator class type to use as interpolating function. 

948 interpolator_kwargs 

949 Arguments to use when instantiating the interpolating function. 

950 

951 Returns 

952 ------- 

953 :class:`colour.SpectralDistribution` 

954 Interpolated spectral distribution. 

955 

956 Notes 

957 ----- 

958 - Interpolation will be performed over boundaries range, if it is 

959 required to extend the range of the spectral distribution use the 

960 :meth:`colour.SpectralDistribution.extrapolate` or 

961 :meth:`colour.SpectralDistribution.align` methods. 

962 

963 Warnings 

964 -------- 

965 - *Cubic Spline* interpolator requires at least 3 wavelengths 

966 :math:`\\lambda_n` for interpolation. 

967 - *Sprague (1880)* interpolator requires at least 6 wavelengths 

968 :math:`\\lambda_n` for interpolation. 

969 

970 References 

971 ---------- 

972 :cite:`CIETC1-382005e` 

973 

974 Examples 

975 -------- 

976 Spectral distribution with a uniformly spaced independent variable 

977 uses *Sprague (1880)* interpolation: 

978 

979 >>> from colour.utilities import numpy_print_options 

980 >>> data = { 

981 ... 500: 0.0651, 

982 ... 520: 0.0705, 

983 ... 540: 0.0772, 

984 ... 560: 0.0870, 

985 ... 580: 0.1128, 

986 ... 600: 0.1360, 

987 ... } 

988 >>> sd = SpectralDistribution(data) 

989 >>> with numpy_print_options(suppress=True): 

990 ... print(sd.interpolate(SpectralShape(500, 600, 1))) 

991 ... # doctest: +ELLIPSIS 

992 [[ 500. 0.0651 ...] 

993 [ 501. 0.0653522...] 

994 [ 502. 0.0656105...] 

995 [ 503. 0.0658715...] 

996 [ 504. 0.0661328...] 

997 [ 505. 0.0663929...] 

998 [ 506. 0.0666509...] 

999 [ 507. 0.0669069...] 

1000 [ 508. 0.0671613...] 

1001 [ 509. 0.0674150...] 

1002 [ 510. 0.0676692...] 

1003 [ 511. 0.0679253...] 

1004 [ 512. 0.0681848...] 

1005 [ 513. 0.0684491...] 

1006 [ 514. 0.0687197...] 

1007 [ 515. 0.0689975...] 

1008 [ 516. 0.0692832...] 

1009 [ 517. 0.0695771...] 

1010 [ 518. 0.0698787...] 

1011 [ 519. 0.0701870...] 

1012 [ 520. 0.0705 ...] 

1013 [ 521. 0.0708155...] 

1014 [ 522. 0.0711336...] 

1015 [ 523. 0.0714547...] 

1016 [ 524. 0.0717789...] 

1017 [ 525. 0.0721063...] 

1018 [ 526. 0.0724367...] 

1019 [ 527. 0.0727698...] 

1020 [ 528. 0.0731051...] 

1021 [ 529. 0.0734423...] 

1022 [ 530. 0.0737808...] 

1023 [ 531. 0.0741203...] 

1024 [ 532. 0.0744603...] 

1025 [ 533. 0.0748006...] 

1026 [ 534. 0.0751409...] 

1027 [ 535. 0.0754813...] 

1028 [ 536. 0.0758220...] 

1029 [ 537. 0.0761633...] 

1030 [ 538. 0.0765060...] 

1031 [ 539. 0.0768511...] 

1032 [ 540. 0.0772 ...] 

1033 [ 541. 0.0775527...] 

1034 [ 542. 0.0779042...] 

1035 [ 543. 0.0782507...] 

1036 [ 544. 0.0785908...] 

1037 [ 545. 0.0789255...] 

1038 [ 546. 0.0792576...] 

1039 [ 547. 0.0795917...] 

1040 [ 548. 0.0799334...] 

1041 [ 549. 0.0802895...] 

1042 [ 550. 0.0806671...] 

1043 [ 551. 0.0810740...] 

1044 [ 552. 0.0815176...] 

1045 [ 553. 0.0820049...] 

1046 [ 554. 0.0825423...] 

1047 [ 555. 0.0831351...] 

1048 [ 556. 0.0837873...] 

1049 [ 557. 0.0845010...] 

1050 [ 558. 0.0852763...] 

1051 [ 559. 0.0861110...] 

1052 [ 560. 0.087 ...] 

1053 [ 561. 0.0879383...] 

1054 [ 562. 0.0889300...] 

1055 [ 563. 0.0899793...] 

1056 [ 564. 0.0910876...] 

1057 [ 565. 0.0922541...] 

1058 [ 566. 0.0934760...] 

1059 [ 567. 0.0947487...] 

1060 [ 568. 0.0960663...] 

1061 [ 569. 0.0974220...] 

1062 [ 570. 0.0988081...] 

1063 [ 571. 0.1002166...] 

1064 [ 572. 0.1016394...] 

1065 [ 573. 0.1030687...] 

1066 [ 574. 0.1044972...] 

1067 [ 575. 0.1059186...] 

1068 [ 576. 0.1073277...] 

1069 [ 577. 0.1087210...] 

1070 [ 578. 0.1100968...] 

1071 [ 579. 0.1114554...] 

1072 [ 580. 0.1128 ...] 

1073 [ 581. 0.1141333...] 

1074 [ 582. 0.1154495...] 

1075 [ 583. 0.1167424...] 

1076 [ 584. 0.1180082...] 

1077 [ 585. 0.1192452...] 

1078 [ 586. 0.1204536...] 

1079 [ 587. 0.1216348...] 

1080 [ 588. 0.1227915...] 

1081 [ 589. 0.1239274...] 

1082 [ 590. 0.1250465...] 

1083 [ 591. 0.1261531...] 

1084 [ 592. 0.1272517...] 

1085 [ 593. 0.1283460...] 

1086 [ 594. 0.1294393...] 

1087 [ 595. 0.1305340...] 

1088 [ 596. 0.1316310...] 

1089 [ 597. 0.1327297...] 

1090 [ 598. 0.1338277...] 

1091 [ 599. 0.1349201...] 

1092 [ 600. 0.136 ...]] 

1093 

1094 Spectral distribution with a non-uniformly spaced independent 

1095 variable uses *Cubic Spline* interpolation: 

1096 

1097 >>> sd = SpectralDistribution(data) 

1098 >>> sd[510] = np.pi / 10 

1099 >>> with numpy_print_options(suppress=True): 

1100 ... print(sd.interpolate(SpectralShape(500, 600, 1))) 

1101 ... # doctest: +ELLIPSIS 

1102 [[ 500. 0.0651 ...] 

1103 [ 501. 0.1365202...] 

1104 [ 502. 0.1953263...] 

1105 [ 503. 0.2423724...] 

1106 [ 504. 0.2785126...] 

1107 [ 505. 0.3046010...] 

1108 [ 506. 0.3214916...] 

1109 [ 507. 0.3300387...] 

1110 [ 508. 0.3310962...] 

1111 [ 509. 0.3255184...] 

1112 [ 510. 0.3141592...] 

1113 [ 511. 0.2978729...] 

1114 [ 512. 0.2775135...] 

1115 [ 513. 0.2539351...] 

1116 [ 514. 0.2279918...] 

1117 [ 515. 0.2005378...] 

1118 [ 516. 0.1724271...] 

1119 [ 517. 0.1445139...] 

1120 [ 518. 0.1176522...] 

1121 [ 519. 0.0926962...] 

1122 [ 520. 0.0705 ...] 

1123 [ 521. 0.0517370...] 

1124 [ 522. 0.0363589...] 

1125 [ 523. 0.0241365...] 

1126 [ 524. 0.0148407...] 

1127 [ 525. 0.0082424...] 

1128 [ 526. 0.0041126...] 

1129 [ 527. 0.0022222...] 

1130 [ 528. 0.0023421...] 

1131 [ 529. 0.0042433...] 

1132 [ 530. 0.0076966...] 

1133 [ 531. 0.0124729...] 

1134 [ 532. 0.0183432...] 

1135 [ 533. 0.0250785...] 

1136 [ 534. 0.0324496...] 

1137 [ 535. 0.0402274...] 

1138 [ 536. 0.0481829...] 

1139 [ 537. 0.0560870...] 

1140 [ 538. 0.0637106...] 

1141 [ 539. 0.0708246...] 

1142 [ 540. 0.0772 ...] 

1143 [ 541. 0.0826564...] 

1144 [ 542. 0.0872086...] 

1145 [ 543. 0.0909203...] 

1146 [ 544. 0.0938549...] 

1147 [ 545. 0.0960760...] 

1148 [ 546. 0.0976472...] 

1149 [ 547. 0.0986321...] 

1150 [ 548. 0.0990942...] 

1151 [ 549. 0.0990971...] 

1152 [ 550. 0.0987043...] 

1153 [ 551. 0.0979794...] 

1154 [ 552. 0.0969861...] 

1155 [ 553. 0.0957877...] 

1156 [ 554. 0.0944480...] 

1157 [ 555. 0.0930304...] 

1158 [ 556. 0.0915986...] 

1159 [ 557. 0.0902161...] 

1160 [ 558. 0.0889464...] 

1161 [ 559. 0.0878532...] 

1162 [ 560. 0.087 ...] 

1163 [ 561. 0.0864371...] 

1164 [ 562. 0.0861623...] 

1165 [ 563. 0.0861600...] 

1166 [ 564. 0.0864148...] 

1167 [ 565. 0.0869112...] 

1168 [ 566. 0.0876336...] 

1169 [ 567. 0.0885665...] 

1170 [ 568. 0.0896945...] 

1171 [ 569. 0.0910020...] 

1172 [ 570. 0.0924735...] 

1173 [ 571. 0.0940936...] 

1174 [ 572. 0.0958467...] 

1175 [ 573. 0.0977173...] 

1176 [ 574. 0.0996899...] 

1177 [ 575. 0.1017491...] 

1178 [ 576. 0.1038792...] 

1179 [ 577. 0.1060649...] 

1180 [ 578. 0.1082906...] 

1181 [ 579. 0.1105408...] 

1182 [ 580. 0.1128 ...] 

1183 [ 581. 0.1150526...] 

1184 [ 582. 0.1172833...] 

1185 [ 583. 0.1194765...] 

1186 [ 584. 0.1216167...] 

1187 [ 585. 0.1236884...] 

1188 [ 586. 0.1256760...] 

1189 [ 587. 0.1275641...] 

1190 [ 588. 0.1293373...] 

1191 [ 589. 0.1309798...] 

1192 [ 590. 0.1324764...] 

1193 [ 591. 0.1338114...] 

1194 [ 592. 0.1349694...] 

1195 [ 593. 0.1359349...] 

1196 [ 594. 0.1366923...] 

1197 [ 595. 0.1372262...] 

1198 [ 596. 0.1375211...] 

1199 [ 597. 0.1375614...] 

1200 [ 598. 0.1373316...] 

1201 [ 599. 0.1368163...] 

1202 [ 600. 0.136 ...]] 

1203 """ 

1204 

1205 shape_start, shape_end, shape_interval = as_float_array( 

1206 [ 

1207 self.shape.start, 

1208 self.shape.end, 

1209 self.shape.interval, 

1210 ] 

1211 ) 

1212 

1213 shape = SpectralShape( 

1214 *[ 

1215 x[0] if x[0] is not None else x[1] 

1216 for x in zip( 

1217 (shape.start, shape.end, shape.interval), 

1218 (shape_start, shape_end, shape_interval), 

1219 strict=True, 

1220 ) 

1221 ] 

1222 ) 

1223 

1224 shape.start = max([shape.start, shape_start]) 

1225 shape.end = min([shape.end, shape_end]) 

1226 

1227 if interpolator is None: 

1228 if self.interpolator not in ( 

1229 SpragueInterpolator, 

1230 CubicSplineInterpolator, 

1231 ): 

1232 interpolator = self.interpolator 

1233 elif self.is_uniform(): 

1234 interpolator = SpragueInterpolator 

1235 else: 

1236 interpolator = CubicSplineInterpolator 

1237 

1238 if interpolator_kwargs is None: 

1239 if self.interpolator not in ( 

1240 SpragueInterpolator, 

1241 CubicSplineInterpolator, 

1242 ): 

1243 interpolator_kwargs = self.interpolator_kwargs 

1244 else: 

1245 interpolator_kwargs = {} 

1246 

1247 self_interpolator, self.interpolator = self.interpolator, interpolator 

1248 self_interpolator_kwargs, self.interpolator_kwargs = ( 

1249 self.interpolator_kwargs, 

1250 interpolator_kwargs, 

1251 ) 

1252 

1253 values = self[shape.wavelengths] 

1254 

1255 self.domain = shape.wavelengths 

1256 self.values = values 

1257 

1258 self.interpolator = self_interpolator 

1259 self.interpolator_kwargs = self_interpolator_kwargs 

1260 

1261 return self 

1262 

1263 def extrapolate( 

1264 self, 

1265 shape: SpectralShape, 

1266 extrapolator: Type[ProtocolExtrapolator] | None = None, 

1267 extrapolator_kwargs: dict | None = None, 

1268 ) -> Self: 

1269 """ 

1270 Extrapolate the spectral distribution in-place according to 

1271 *CIE 15:2004* and *CIE 167:2005* recommendations or specified extrapolation 

1272 arguments. 

1273 

1274 Parameters 

1275 ---------- 

1276 shape 

1277 Spectral shape used for extrapolation. 

1278 extrapolator 

1279 Extrapolator class type to use as extrapolating function. 

1280 extrapolator_kwargs 

1281 Arguments to use when instantiating the extrapolating function. 

1282 

1283 Returns 

1284 ------- 

1285 :class:`colour.SpectralDistribution` 

1286 Extrapolated spectral distribution. 

1287 

1288 References 

1289 ---------- 

1290 :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` 

1291 

1292 Examples 

1293 -------- 

1294 >>> from colour.utilities import numpy_print_options 

1295 >>> data = { 

1296 ... 500: 0.0651, 

1297 ... 520: 0.0705, 

1298 ... 540: 0.0772, 

1299 ... 560: 0.0870, 

1300 ... 580: 0.1128, 

1301 ... 600: 0.1360, 

1302 ... } 

1303 >>> sd = SpectralDistribution(data) 

1304 >>> sd.extrapolate(SpectralShape(400, 700, 20)).shape 

1305 SpectralShape(400.0, 700.0, 20.0) 

1306 >>> with numpy_print_options(suppress=True): 

1307 ... print(sd) 

1308 [[ 400. 0.0651] 

1309 [ 420. 0.0651] 

1310 [ 440. 0.0651] 

1311 [ 460. 0.0651] 

1312 [ 480. 0.0651] 

1313 [ 500. 0.0651] 

1314 [ 520. 0.0705] 

1315 [ 540. 0.0772] 

1316 [ 560. 0.087 ] 

1317 [ 580. 0.1128] 

1318 [ 600. 0.136 ] 

1319 [ 620. 0.136 ] 

1320 [ 640. 0.136 ] 

1321 [ 660. 0.136 ] 

1322 [ 680. 0.136 ] 

1323 [ 700. 0.136 ]] 

1324 """ 

1325 

1326 shape_start, shape_end, shape_interval = as_float_array( 

1327 [ 

1328 self.shape.start, 

1329 self.shape.end, 

1330 self.shape.interval, 

1331 ] 

1332 ) 

1333 

1334 wavelengths = np.hstack( 

1335 [ 

1336 np.arange(shape.start, shape_start, shape_interval), 

1337 np.arange(shape_end, shape.end, shape_interval) + shape_interval, 

1338 ] 

1339 ) 

1340 

1341 extrapolator = optional(extrapolator, Extrapolator) 

1342 extrapolator_kwargs = optional( 

1343 extrapolator_kwargs, 

1344 {"method": "Constant", "left": None, "right": None}, 

1345 ) 

1346 

1347 self_extrapolator = self.extrapolator 

1348 self_extrapolator_kwargs = self.extrapolator_kwargs 

1349 

1350 self.extrapolator = extrapolator 

1351 self.extrapolator_kwargs = extrapolator_kwargs 

1352 

1353 # The following self-assignment is written as intended and triggers the 

1354 # extrapolation. 

1355 self[wavelengths] = self[wavelengths] 

1356 

1357 self.extrapolator = self_extrapolator 

1358 self.extrapolator_kwargs = self_extrapolator_kwargs 

1359 

1360 return self 

1361 

1362 def align( 

1363 self, 

1364 shape: SpectralShape, 

1365 interpolator: Type[ProtocolInterpolator] | None = None, 

1366 interpolator_kwargs: dict | None = None, 

1367 extrapolator: Type[ProtocolExtrapolator] | None = None, 

1368 extrapolator_kwargs: dict | None = None, 

1369 ) -> Self: 

1370 """ 

1371 Align the spectral distribution in-place to the specified spectral 

1372 shape: Interpolate first then extrapolate to fit the specified range. 

1373 

1374 Interpolation is performed according to *CIE 167:2005* 

1375 recommendation (if the interpolator has not been changed at 

1376 instantiation time) or specified interpolation arguments. 

1377 

1378 The logic for choosing the interpolator class when ``interpolator`` 

1379 is not specified is as follows: 

1380 

1381 .. code-block:: python 

1382 

1383 if self.interpolator not in ( 

1384 SpragueInterpolator, 

1385 CubicSplineInterpolator, 

1386 ): 

1387 interpolator = self.interpolator 

1388 elif self.is_uniform(): 

1389 interpolator = SpragueInterpolator 

1390 else: 

1391 interpolator = CubicSplineInterpolator 

1392 

1393 The logic for choosing the interpolator keyword arguments when 

1394 ``interpolator_kwargs`` is not specified is as follows: 

1395 

1396 .. code-block:: python 

1397 

1398 if self.interpolator not in ( 

1399 SpragueInterpolator, 

1400 CubicSplineInterpolator, 

1401 ): 

1402 interpolator_kwargs = self.interpolator_kwargs 

1403 else: 

1404 interpolator_kwargs = {} 

1405 

1406 Parameters 

1407 ---------- 

1408 shape 

1409 Spectral shape used for alignment. 

1410 interpolator 

1411 Interpolator class type to use as interpolating function. 

1412 interpolator_kwargs 

1413 Arguments to use when instantiating the interpolating function. 

1414 extrapolator 

1415 Extrapolator class type to use as extrapolating function. 

1416 extrapolator_kwargs 

1417 Arguments to use when instantiating the extrapolating function. 

1418 

1419 Returns 

1420 ------- 

1421 :class:`colour.SpectralDistribution` 

1422 Aligned spectral distribution. 

1423 

1424 Examples 

1425 -------- 

1426 >>> from colour.utilities import numpy_print_options 

1427 >>> data = { 

1428 ... 500: 0.0651, 

1429 ... 520: 0.0705, 

1430 ... 540: 0.0772, 

1431 ... 560: 0.0870, 

1432 ... 580: 0.1128, 

1433 ... 600: 0.1360, 

1434 ... } 

1435 >>> sd = SpectralDistribution(data) 

1436 >>> with numpy_print_options(suppress=True): 

1437 ... print(sd.align(SpectralShape(505, 565, 1))) 

1438 ... # doctest: +ELLIPSIS 

1439 [[ 505. 0.0663929...] 

1440 [ 506. 0.0666509...] 

1441 [ 507. 0.0669069...] 

1442 [ 508. 0.0671613...] 

1443 [ 509. 0.0674150...] 

1444 [ 510. 0.0676692...] 

1445 [ 511. 0.0679253...] 

1446 [ 512. 0.0681848...] 

1447 [ 513. 0.0684491...] 

1448 [ 514. 0.0687197...] 

1449 [ 515. 0.0689975...] 

1450 [ 516. 0.0692832...] 

1451 [ 517. 0.0695771...] 

1452 [ 518. 0.0698787...] 

1453 [ 519. 0.0701870...] 

1454 [ 520. 0.0705 ...] 

1455 [ 521. 0.0708155...] 

1456 [ 522. 0.0711336...] 

1457 [ 523. 0.0714547...] 

1458 [ 524. 0.0717789...] 

1459 [ 525. 0.0721063...] 

1460 [ 526. 0.0724367...] 

1461 [ 527. 0.0727698...] 

1462 [ 528. 0.0731051...] 

1463 [ 529. 0.0734423...] 

1464 [ 530. 0.0737808...] 

1465 [ 531. 0.0741203...] 

1466 [ 532. 0.0744603...] 

1467 [ 533. 0.0748006...] 

1468 [ 534. 0.0751409...] 

1469 [ 535. 0.0754813...] 

1470 [ 536. 0.0758220...] 

1471 [ 537. 0.0761633...] 

1472 [ 538. 0.0765060...] 

1473 [ 539. 0.0768511...] 

1474 [ 540. 0.0772 ...] 

1475 [ 541. 0.0775527...] 

1476 [ 542. 0.0779042...] 

1477 [ 543. 0.0782507...] 

1478 [ 544. 0.0785908...] 

1479 [ 545. 0.0789255...] 

1480 [ 546. 0.0792576...] 

1481 [ 547. 0.0795917...] 

1482 [ 548. 0.0799334...] 

1483 [ 549. 0.0802895...] 

1484 [ 550. 0.0806671...] 

1485 [ 551. 0.0810740...] 

1486 [ 552. 0.0815176...] 

1487 [ 553. 0.0820049...] 

1488 [ 554. 0.0825423...] 

1489 [ 555. 0.0831351...] 

1490 [ 556. 0.0837873...] 

1491 [ 557. 0.0845010...] 

1492 [ 558. 0.0852763...] 

1493 [ 559. 0.0861110...] 

1494 [ 560. 0.087 ...] 

1495 [ 561. 0.0879383...] 

1496 [ 562. 0.0889300...] 

1497 [ 563. 0.0899793...] 

1498 [ 564. 0.0910876...] 

1499 [ 565. 0.0922541...]] 

1500 """ 

1501 

1502 self.interpolate(shape, interpolator, interpolator_kwargs) 

1503 self.extrapolate(shape, extrapolator, extrapolator_kwargs) 

1504 

1505 return self 

1506 

1507 def trim(self, shape: SpectralShape) -> Self: 

1508 """ 

1509 Trim the spectral distribution wavelengths to the specified spectral shape. 

1510 

1511 Parameters 

1512 ---------- 

1513 shape 

1514 Spectral shape used for trimming. 

1515 

1516 Returns 

1517 ------- 

1518 :class:`colour.SpectralDistribution` 

1519 Trimmed spectral distribution. 

1520 

1521 Examples 

1522 -------- 

1523 >>> from colour.utilities import numpy_print_options 

1524 >>> data = { 

1525 ... 500: 0.0651, 

1526 ... 520: 0.0705, 

1527 ... 540: 0.0772, 

1528 ... 560: 0.0870, 

1529 ... 580: 0.1128, 

1530 ... 600: 0.1360, 

1531 ... } 

1532 >>> sd = SpectralDistribution(data) 

1533 >>> sd = sd.interpolate(SpectralShape(500, 600, 1)) 

1534 >>> with numpy_print_options(suppress=True): 

1535 ... print(sd.trim(SpectralShape(520, 580, 5))) 

1536 ... # doctest: +ELLIPSIS 

1537 [[ 520. 0.0705 ...] 

1538 [ 521. 0.0708155...] 

1539 [ 522. 0.0711336...] 

1540 [ 523. 0.0714547...] 

1541 [ 524. 0.0717789...] 

1542 [ 525. 0.0721063...] 

1543 [ 526. 0.0724367...] 

1544 [ 527. 0.0727698...] 

1545 [ 528. 0.0731051...] 

1546 [ 529. 0.0734423...] 

1547 [ 530. 0.0737808...] 

1548 [ 531. 0.0741203...] 

1549 [ 532. 0.0744603...] 

1550 [ 533. 0.0748006...] 

1551 [ 534. 0.0751409...] 

1552 [ 535. 0.0754813...] 

1553 [ 536. 0.0758220...] 

1554 [ 537. 0.0761633...] 

1555 [ 538. 0.0765060...] 

1556 [ 539. 0.0768511...] 

1557 [ 540. 0.0772 ...] 

1558 [ 541. 0.0775527...] 

1559 [ 542. 0.0779042...] 

1560 [ 543. 0.0782507...] 

1561 [ 544. 0.0785908...] 

1562 [ 545. 0.0789255...] 

1563 [ 546. 0.0792576...] 

1564 [ 547. 0.0795917...] 

1565 [ 548. 0.0799334...] 

1566 [ 549. 0.0802895...] 

1567 [ 550. 0.0806671...] 

1568 [ 551. 0.0810740...] 

1569 [ 552. 0.0815176...] 

1570 [ 553. 0.0820049...] 

1571 [ 554. 0.0825423...] 

1572 [ 555. 0.0831351...] 

1573 [ 556. 0.0837873...] 

1574 [ 557. 0.0845010...] 

1575 [ 558. 0.0852763...] 

1576 [ 559. 0.0861110...] 

1577 [ 560. 0.087 ...] 

1578 [ 561. 0.0879383...] 

1579 [ 562. 0.0889300...] 

1580 [ 563. 0.0899793...] 

1581 [ 564. 0.0910876...] 

1582 [ 565. 0.0922541...] 

1583 [ 566. 0.0934760...] 

1584 [ 567. 0.0947487...] 

1585 [ 568. 0.0960663...] 

1586 [ 569. 0.0974220...] 

1587 [ 570. 0.0988081...] 

1588 [ 571. 0.1002166...] 

1589 [ 572. 0.1016394...] 

1590 [ 573. 0.1030687...] 

1591 [ 574. 0.1044972...] 

1592 [ 575. 0.1059186...] 

1593 [ 576. 0.1073277...] 

1594 [ 577. 0.1087210...] 

1595 [ 578. 0.1100968...] 

1596 [ 579. 0.1114554...] 

1597 [ 580. 0.1128 ...]] 

1598 """ 

1599 

1600 start = max([shape.start, self.shape.start]) 

1601 end = min([shape.end, self.shape.end]) 

1602 

1603 indexes = np.where(np.logical_and(self.domain >= start, self.domain <= end)) 

1604 

1605 wavelengths = self.wavelengths[indexes] 

1606 values = self.values[indexes] 

1607 

1608 self.wavelengths = wavelengths 

1609 self.values = values 

1610 

1611 if self.shape.boundaries != shape.boundaries: 

1612 runtime_warning( 

1613 f'"{shape}" shape could not be honoured, using "{self.shape}"!' 

1614 ) 

1615 

1616 return self 

1617 

1618 def normalise(self, factor: Real = 1) -> Self: 

1619 """ 

1620 Normalise the spectral distribution with the specified normalization 

1621 factor. 

1622 

1623 Parameters 

1624 ---------- 

1625 factor 

1626 Normalisation factor. 

1627 

1628 Returns 

1629 ------- 

1630 :class:`colour.SpectralDistribution` 

1631 Normalised spectral distribution. 

1632 

1633 Examples 

1634 -------- 

1635 >>> from colour.utilities import numpy_print_options 

1636 >>> data = { 

1637 ... 500: 0.0651, 

1638 ... 520: 0.0705, 

1639 ... 540: 0.0772, 

1640 ... 560: 0.0870, 

1641 ... 580: 0.1128, 

1642 ... 600: 0.1360, 

1643 ... } 

1644 >>> sd = SpectralDistribution(data) 

1645 >>> with numpy_print_options(suppress=True): 

1646 ... print(sd.normalise()) # doctest: +ELLIPSIS 

1647 [[ 500. 0.4786764...] 

1648 [ 520. 0.5183823...] 

1649 [ 540. 0.5676470...] 

1650 [ 560. 0.6397058...] 

1651 [ 580. 0.8294117...] 

1652 [ 600. 1. ...]] 

1653 """ 

1654 

1655 with sdiv_mode(): 

1656 self *= sdiv(1, max(self.values)) * factor 

1657 

1658 return self 

1659 

1660 

1661class MultiSpectralDistributions(MultiSignals): 

1662 """ 

1663 Define multi-spectral distributions: the base object for multi-spectral 

1664 computations. Model colour matching functions, display primaries, camera 

1665 sensitivities, and related spectral data sets. 

1666 

1667 Initialise multi-spectral distributions according to *CIE 15:2004* 

1668 recommendation: use the method developed by *Sprague (1880)* for 

1669 interpolating functions having uniformly spaced independent variables 

1670 and the *Cubic Spline* method for non-uniformly spaced independent 

1671 variables. Perform extrapolation according to *CIE 167:2005* 

1672 recommendation. 

1673 

1674 .. important:: 

1675 

1676 Specific documentation about getting, setting, indexing and slicing 

1677 the multi-spectral power distributions values is available in the 

1678 :ref:`spectral-representation-and-continuous-signal` section. 

1679 

1680 Parameters 

1681 ---------- 

1682 data 

1683 Data to be stored in the multi-spectral distributions. 

1684 domain 

1685 Values to initialise the multiple :class:`colour.SpectralDistribution` 

1686 class instances :attr:`colour.continuous.Signal.wavelengths` attribute 

1687 with. If both ``data`` and ``domain`` arguments are defined, the 

1688 latter will be used to initialise the 

1689 :attr:`colour.continuous.Signal.wavelengths` property. 

1690 labels 

1691 Names to use for the :class:`colour.SpectralDistribution` class 

1692 instances. 

1693 

1694 Other Parameters 

1695 ---------------- 

1696 extrapolator 

1697 Extrapolator class type to use as extrapolating function for the 

1698 :class:`colour.SpectralDistribution` class instances. 

1699 extrapolator_kwargs 

1700 Arguments to use when instantiating the extrapolating function of the 

1701 :class:`colour.SpectralDistribution` class instances. 

1702 interpolator 

1703 Interpolator class type to use as interpolating function for the 

1704 :class:`colour.SpectralDistribution` class instances. 

1705 interpolator_kwargs 

1706 Arguments to use when instantiating the interpolating function of the 

1707 :class:`colour.SpectralDistribution` class instances. 

1708 name 

1709 Multi-spectral distributions name. 

1710 display_labels 

1711 Multi-spectral distributions labels for figures, default to 

1712 :attr:`colour.MultiSpectralDistributions.labels` property value. 

1713 

1714 Warnings 

1715 -------- 

1716 The *Cubic Spline* method might produce unexpected results with 

1717 exceptionally noisy or non-uniformly spaced data. 

1718 

1719 Attributes 

1720 ---------- 

1721 - :attr:`~colour.MultiSpectralDistributions.display_name` 

1722 - :attr:`~colour.MultiSpectralDistributions.display_labels` 

1723 - :attr:`~colour.MultiSpectralDistributions.wavelengths` 

1724 - :attr:`~colour.MultiSpectralDistributions.values` 

1725 - :attr:`~colour.MultiSpectralDistributions.shape` 

1726 

1727 Methods 

1728 ------- 

1729 - :meth:`~colour.MultiSpectralDistributions.__init__` 

1730 - :meth:`~colour.MultiSpectralDistributions.interpolate` 

1731 - :meth:`~colour.MultiSpectralDistributions.extrapolate` 

1732 - :meth:`~colour.MultiSpectralDistributions.align` 

1733 - :meth:`~colour.MultiSpectralDistributions.trim` 

1734 - :meth:`~colour.MultiSpectralDistributions.normalise` 

1735 - :meth:`~colour.MultiSpectralDistributions.to_sds` 

1736 

1737 References 

1738 ---------- 

1739 :cite:`CIETC1-382005e`, :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` 

1740 

1741 Examples 

1742 -------- 

1743 Instantiating the multi-spectral distributions with a uniformly spaced 

1744 independent variable: 

1745 

1746 >>> from colour.utilities import numpy_print_options 

1747 >>> data = { 

1748 ... 500: (0.004900, 0.323000, 0.272000), 

1749 ... 510: (0.009300, 0.503000, 0.158200), 

1750 ... 520: (0.063270, 0.710000, 0.078250), 

1751 ... 530: (0.165500, 0.862000, 0.042160), 

1752 ... 540: (0.290400, 0.954000, 0.020300), 

1753 ... 550: (0.433450, 0.994950, 0.008750), 

1754 ... 560: (0.594500, 0.995000, 0.003900), 

1755 ... } 

1756 >>> labels = ("x_bar", "y_bar", "z_bar") 

1757 >>> with numpy_print_options(suppress=True): 

1758 ... MultiSpectralDistributions(data, labels=labels) 

1759 ... # doctest: +ELLIPSIS 

1760 ... 

1761 MultiSpectral...([[ 500. , 0.0049 , 0.323 , 0.272 ], 

1762 ... [ 510. , 0.0093 , 0.503 , 0.1582 ], 

1763 ... [ 520. , 0.06327, 0.71 , 0.07825], 

1764 ... [ 530. , 0.1655 , 0.862 , 0.04216], 

1765 ... [ 540. , 0.2904 , 0.954 , 0.0203 ], 

1766 ... [ 550. , 0.43345, 0.99495, 0.00875], 

1767 ... [ 560. , 0.5945 , 0.995 , 0.0039 ]], 

1768 ... [...'x_bar', ...'y_bar', ...'z_bar'], 

1769 ... SpragueInterpolator, 

1770 ... {}, 

1771 ... Extrapolator, 

1772 ... {'method': 'Constant', 'left': None, 'right': None}) 

1773 

1774 Instantiating a spectral distribution with a non-uniformly spaced 

1775 independent variable: 

1776 

1777 >>> data[511] = (0.00314, 0.31416, 0.03142) 

1778 >>> with numpy_print_options(suppress=True): 

1779 ... MultiSpectralDistributions(data, labels=labels) 

1780 ... # doctest: +ELLIPSIS 

1781 ... 

1782 MultiSpectral...([[ 500. , 0.0049 , 0.323 , 0.272 ], 

1783 ... [ 510. , 0.0093 , 0.503 , 0.1582 ], 

1784 ... [ 511. , 0.00314, 0.31416, 0.03142], 

1785 ... [ 520. , 0.06327, 0.71 , 0.07825], 

1786 ... [ 530. , 0.1655 , 0.862 , 0.04216], 

1787 ... [ 540. , 0.2904 , 0.954 , 0.0203 ], 

1788 ... [ 550. , 0.43345, 0.99495, 0.00875], 

1789 ... [ 560. , 0.5945 , 0.995 , 0.0039 ]], 

1790 ... [...'x_bar', ...'y_bar', ...'z_bar'], 

1791 ... CubicSplineInterpolator, 

1792 ... {}, 

1793 ... Extrapolator, 

1794 ... {'method': 'Constant', 'left': None, 'right': None}) 

1795 

1796 Instantiation with a *Pandas* `DataFrame`: 

1797 

1798 >>> from colour.utilities import is_pandas_installed 

1799 >>> if is_pandas_installed(): 

1800 ... from pandas import DataFrame 

1801 ... 

1802 ... x_bar = [data[key][0] for key in sorted(data.keys())] 

1803 ... y_bar = [data[key][1] for key in sorted(data.keys())] 

1804 ... z_bar = [data[key][2] for key in sorted(data.keys())] 

1805 ... print( 

1806 ... MultiSignals( # doctest: +SKIP 

1807 ... DataFrame( 

1808 ... dict(zip(labels, [x_bar, y_bar, z_bar])), data.keys() 

1809 ... ) 

1810 ... ) 

1811 ... ) 

1812 ... 

1813 [[ 5.0000000...e+02 4.9000000...e-03 3.2300000...e-01 \ 

18142.7200000...e-01] 

1815 [ 5.1000000...e+02 9.3000000...e-03 5.0300000...e-01 \ 

18161.5820000...e-01] 

1817 [ 5.2000000...e+02 3.1400000...e-03 3.1416000...e-01 \ 

18183.1420000...e-02] 

1819 [ 5.3000000...e+02 6.3270000...e-02 7.1000000...e-01 \ 

18207.8250000...e-02] 

1821 [ 5.4000000...e+02 1.6550000...e-01 8.6200000...e-01 \ 

18224.2160000...e-02] 

1823 [ 5.5000000...e+02 2.9040000...e-01 9.5400000...e-01 \ 

18242.0300000...e-02] 

1825 [ 5.6000000...e+02 4.3345000...e-01 9.9495000...e-01 \ 

18268.7500000...e-03] 

1827 [ 5.1100000...e+02 5.9450000...e-01 9.9500000...e-01 \ 

18283.9000000...e-03]] 

1829 """ 

1830 

1831 def __init__( 

1832 self, 

1833 data: ( 

1834 ArrayLike 

1835 | DataFrame 

1836 | dict 

1837 | MultiSignals 

1838 | Sequence 

1839 | Series 

1840 | Signal 

1841 | SpectralDistribution 

1842 | ValuesView 

1843 | None 

1844 ) = None, 

1845 domain: ArrayLike | SpectralShape | KeysView | None = None, 

1846 labels: Sequence | None = None, 

1847 **kwargs: Any, 

1848 ) -> None: 

1849 domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain 

1850 signals = self.multi_signals_unpack_data(data, domain, labels) 

1851 

1852 domain = signals[next(iter(signals.keys()))].domain if signals else None 

1853 uniform = is_uniform(domain) if domain is not None and len(domain) > 0 else True 

1854 

1855 # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations 

1856 # defaults. 

1857 kwargs["interpolator"] = kwargs.get( 

1858 "interpolator", 

1859 SpragueInterpolator if uniform else CubicSplineInterpolator, 

1860 ) 

1861 kwargs["interpolator_kwargs"] = kwargs.get("interpolator_kwargs", {}) 

1862 

1863 kwargs["extrapolator"] = kwargs.get("extrapolator", Extrapolator) 

1864 kwargs["extrapolator_kwargs"] = kwargs.get( 

1865 "extrapolator_kwargs", 

1866 {"method": "Constant", "left": None, "right": None}, 

1867 ) 

1868 

1869 super().__init__(signals, domain, signal_type=SpectralDistribution, **kwargs) 

1870 

1871 self._display_name: str = self.name 

1872 self.display_name = kwargs.get("display_name", self._display_name) 

1873 self._display_labels: list = list(self.signals.keys()) 

1874 self.display_labels = kwargs.get("display_labels", self._display_labels) 

1875 

1876 @property 

1877 def display_name(self) -> str: 

1878 """ 

1879 Getter and setter for the multi-spectral distributions' display name. 

1880 

1881 The display name provides a human-readable identifier for the 

1882 multi-spectral distribution collection, used for visualization 

1883 and reporting purposes. 

1884 

1885 Parameters 

1886 ---------- 

1887 value 

1888 Value to set the multi-spectral distributions' display name 

1889 with. 

1890 

1891 Returns 

1892 ------- 

1893 :class:`str` 

1894 Multi-spectral distributions' display name. 

1895 """ 

1896 

1897 return self._display_name 

1898 

1899 @display_name.setter 

1900 def display_name(self, value: str) -> None: 

1901 """Setter for the **self.display_name** property.""" 

1902 

1903 attest( 

1904 isinstance(value, str), 

1905 f'"display_name" property: "{value}" type is not "str"!', 

1906 ) 

1907 

1908 self._display_name = value 

1909 

1910 @property 

1911 def display_labels(self) -> List[str]: 

1912 """ 

1913 Getter and setter for the display labels of the multi-spectral 

1914 distributions. 

1915 

1916 The display labels provide human-readable identifiers for each spectral 

1917 distribution in the multi-spectral collection, facilitating data 

1918 visualization and interpretation. 

1919 

1920 Parameters 

1921 ---------- 

1922 value 

1923 Value to set the multi-spectral distributions display labels with. 

1924 

1925 Returns 

1926 ------- 

1927 :class:`list` 

1928 Multi-spectral distributions display labels. 

1929 """ 

1930 

1931 return self._display_labels 

1932 

1933 @display_labels.setter 

1934 def display_labels(self, value: Sequence) -> None: 

1935 """Setter for the **self.display_labels** property.""" 

1936 

1937 attest( 

1938 is_iterable(value), 

1939 f'"display_labels" property: "{value}" is not an "iterable" like object!', 

1940 ) 

1941 

1942 attest( 

1943 len(set(value)) == len(value), 

1944 '"display_labels" property: values must be unique!', 

1945 ) 

1946 

1947 attest( 

1948 len(value) == len(self.labels), 

1949 f'"display_labels" property: length must be "{len(self.labels)}"!', 

1950 ) 

1951 

1952 self._display_labels = [str(label) for label in value] 

1953 for i, signal in enumerate(self.signals.values()): 

1954 cast("SpectralDistribution", signal).display_name = self._display_labels[i] 

1955 

1956 @property 

1957 def wavelengths(self) -> NDArrayFloat: 

1958 """ 

1959 Getter and setter for the multi-spectral distributions 

1960 wavelengths :math:`\\lambda_n`. 

1961 

1962 Parameters 

1963 ---------- 

1964 value 

1965 Value to set the multi-spectral distributions wavelengths 

1966 :math:`\\lambda_n` with. 

1967 

1968 Returns 

1969 ------- 

1970 :class:`numpy.ndarray` 

1971 Multi-spectral distributions wavelengths :math:`\\lambda_n`. 

1972 """ 

1973 

1974 return self.domain 

1975 

1976 @wavelengths.setter 

1977 def wavelengths(self, value: ArrayLike) -> None: 

1978 """Setter for the **self.wavelengths** property.""" 

1979 

1980 self.domain = as_float_array(value, self.dtype) 

1981 

1982 @property 

1983 def values(self) -> NDArrayFloat: 

1984 """ 

1985 Getter and setter for the multi-spectral distributions values. 

1986 

1987 Parameters 

1988 ---------- 

1989 value 

1990 Value to set the multi-spectral distributions wavelengths values 

1991 with. 

1992 

1993 Returns 

1994 ------- 

1995 :class:`numpy.ndarray` 

1996 Multi-spectral distributions values. 

1997 """ 

1998 

1999 return self.range 

2000 

2001 @values.setter 

2002 def values(self, value: ArrayLike) -> None: 

2003 """Setter for the **self.values** property.""" 

2004 

2005 self.range = as_float_array(value, self.dtype) 

2006 

2007 @property 

2008 def shape(self) -> SpectralShape: 

2009 """ 

2010 Getter property for the multi-spectral distributions shape. 

2011 

2012 Returns 

2013 ------- 

2014 :class:`colour.SpectralShape` 

2015 Multi-spectral distributions shape. 

2016 

2017 Notes 

2018 ----- 

2019 - Multi-spectral distributions with a non-uniformly spaced 

2020 independent variable have multiple intervals, in that case 

2021 :attr:`colour.MultiSpectralDistributions.shape` property returns 

2022 the *minimum* interval size. 

2023 

2024 Examples 

2025 -------- 

2026 Shape of the multi-spectral distributions with a uniformly spaced 

2027 independent variable: 

2028 

2029 >>> from colour.utilities import numpy_print_options 

2030 >>> data = { 

2031 ... 500: (0.004900, 0.323000, 0.272000), 

2032 ... 510: (0.009300, 0.503000, 0.158200), 

2033 ... 520: (0.063270, 0.710000, 0.078250), 

2034 ... 530: (0.165500, 0.862000, 0.042160), 

2035 ... 540: (0.290400, 0.954000, 0.020300), 

2036 ... 550: (0.433450, 0.994950, 0.008750), 

2037 ... 560: (0.594500, 0.995000, 0.003900), 

2038 ... } 

2039 >>> MultiSpectralDistributions(data).shape 

2040 SpectralShape(500.0, 560.0, 10.0) 

2041 

2042 Shape of the multi-spectral distributions with a non-uniformly spaced 

2043 independent variable: 

2044 

2045 >>> data[511] = (0.00314, 0.31416, 0.03142) 

2046 >>> MultiSpectralDistributions(data).shape 

2047 SpectralShape(500.0, 560.0, 1.0) 

2048 """ 

2049 

2050 return first_item(self._signals.values()).shape 

2051 

2052 def interpolate( 

2053 self, 

2054 shape: SpectralShape, 

2055 interpolator: Type[ProtocolInterpolator] | None = None, 

2056 interpolator_kwargs: dict | None = None, 

2057 ) -> Self: 

2058 """ 

2059 Interpolate the multi-spectral distributions in-place according to 

2060 *CIE 167:2005* recommendation (if the interpolator has not been changed 

2061 at instantiation time) or specified interpolation arguments. 

2062 

2063 The logic for choosing the interpolator class when ``interpolator`` is 

2064 not specified is as follows: 

2065 

2066 .. code-block:: python 

2067 

2068 if self.interpolator not in ( 

2069 SpragueInterpolator, 

2070 CubicSplineInterpolator, 

2071 ): 

2072 interpolator = self.interpolator 

2073 elif self.is_uniform(): 

2074 interpolator = SpragueInterpolator 

2075 else: 

2076 interpolator = CubicSplineInterpolator 

2077 

2078 The logic for choosing the interpolator keyword arguments when 

2079 ``interpolator_kwargs`` is not specified is as follows: 

2080 

2081 .. code-block:: python 

2082 

2083 if self.interpolator not in ( 

2084 SpragueInterpolator, 

2085 CubicSplineInterpolator, 

2086 ): 

2087 interpolator_kwargs = self.interpolator_kwargs 

2088 else: 

2089 interpolator_kwargs = {} 

2090 

2091 Parameters 

2092 ---------- 

2093 shape 

2094 Spectral shape used for interpolation. 

2095 interpolator 

2096 Interpolator class type to use as interpolating function. 

2097 interpolator_kwargs 

2098 Arguments to use when instantiating the interpolating function. 

2099 

2100 Returns 

2101 ------- 

2102 :class:`colour.MultiSpectralDistributions` 

2103 Interpolated multi-spectral distributions. 

2104 

2105 Notes 

2106 ----- 

2107 - See :meth:`colour.SpectralDistribution.interpolate` method notes 

2108 section. 

2109 

2110 Warnings 

2111 -------- 

2112 See :meth:`colour.SpectralDistribution.interpolate` method warning 

2113 section. 

2114 

2115 References 

2116 ---------- 

2117 :cite:`CIETC1-382005e` 

2118 

2119 Examples 

2120 -------- 

2121 Multi-spectral distributions with a uniformly spaced independent 

2122 variable uses *Sprague (1880)* interpolation: 

2123 

2124 >>> from colour.utilities import numpy_print_options 

2125 >>> data = { 

2126 ... 500: (0.004900, 0.323000, 0.272000), 

2127 ... 510: (0.009300, 0.503000, 0.158200), 

2128 ... 520: (0.063270, 0.710000, 0.078250), 

2129 ... 530: (0.165500, 0.862000, 0.042160), 

2130 ... 540: (0.290400, 0.954000, 0.020300), 

2131 ... 550: (0.433450, 0.994950, 0.008750), 

2132 ... 560: (0.594500, 0.995000, 0.003900), 

2133 ... } 

2134 >>> msds = MultiSpectralDistributions(data) 

2135 >>> with numpy_print_options(suppress=True): 

2136 ... print(msds.interpolate(SpectralShape(500, 560, 1))) 

2137 ... # doctest: +ELLIPSIS 

2138 [[ 500. 0.0049 ... 0.323 ... 0.272 ...] 

2139 [ 501. 0.0043252... 0.3400642... 0.2599848...] 

2140 [ 502. 0.0037950... 0.3572165... 0.2479849...] 

2141 [ 503. 0.0033761... 0.3744030... 0.2360688...] 

2142 [ 504. 0.0031397... 0.3916650... 0.2242878...] 

2143 [ 505. 0.0031582... 0.4091067... 0.2126801...] 

2144 [ 506. 0.0035019... 0.4268629... 0.2012748...] 

2145 [ 507. 0.0042365... 0.4450668... 0.1900968...] 

2146 [ 508. 0.0054192... 0.4638181... 0.1791709...] 

2147 [ 509. 0.0070965... 0.4831505... 0.1685260...] 

2148 [ 510. 0.0093 ... 0.503 ... 0.1582 ...] 

2149 [ 511. 0.0120562... 0.5232543... 0.1482365...] 

2150 [ 512. 0.0154137... 0.5439717... 0.1386625...] 

2151 [ 513. 0.0193991... 0.565139 ... 0.1294993...] 

2152 [ 514. 0.0240112... 0.5866255... 0.1207676...] 

2153 [ 515. 0.0292289... 0.6082226... 0.1124864...] 

2154 [ 516. 0.0350192... 0.6296821... 0.1046717...] 

2155 [ 517. 0.0413448... 0.6507558... 0.0973361...] 

2156 [ 518. 0.0481727... 0.6712346... 0.0904871...] 

2157 [ 519. 0.0554816... 0.6909873... 0.0841267...] 

2158 [ 520. 0.06327 ... 0.71 ... 0.07825 ...] 

2159 [ 521. 0.0715642... 0.7283456... 0.0728614...] 

2160 [ 522. 0.0803970... 0.7459679... 0.0680051...] 

2161 [ 523. 0.0897629... 0.7628184... 0.0636823...] 

2162 [ 524. 0.0996227... 0.7789004... 0.0598449...] 

2163 [ 525. 0.1099142... 0.7942533... 0.0564111...] 

2164 [ 526. 0.1205637... 0.8089368... 0.0532822...] 

2165 [ 527. 0.1314973... 0.8230153... 0.0503588...] 

2166 [ 528. 0.1426523... 0.8365417... 0.0475571...] 

2167 [ 529. 0.1539887... 0.8495422... 0.0448253...] 

2168 [ 530. 0.1655 ... 0.862 ... 0.04216 ...] 

2169 [ 531. 0.1772055... 0.8738585... 0.0395936...] 

2170 [ 532. 0.1890877... 0.8850940... 0.0371046...] 

2171 [ 533. 0.2011304... 0.8957073... 0.0346733...] 

2172 [ 534. 0.2133310... 0.9057092... 0.0323006...] 

2173 [ 535. 0.2256968... 0.9151181... 0.0300011...] 

2174 [ 536. 0.2382403... 0.9239560... 0.0277974...] 

2175 [ 537. 0.2509754... 0.9322459... 0.0257131...] 

2176 [ 538. 0.2639130... 0.9400080... 0.0237668...] 

2177 [ 539. 0.2770569... 0.9472574... 0.0219659...] 

2178 [ 540. 0.2904 ... 0.954 ... 0.0203 ...] 

2179 [ 541. 0.3039194... 0.9602409... 0.0187414...] 

2180 [ 542. 0.3175893... 0.9660106... 0.0172748...] 

2181 [ 543. 0.3314022... 0.9713260... 0.0158947...] 

2182 [ 544. 0.3453666... 0.9761850... 0.0146001...] 

2183 [ 545. 0.3595019... 0.9805731... 0.0133933...] 

2184 [ 546. 0.3738324... 0.9844703... 0.0122777...] 

2185 [ 547. 0.3883818... 0.9878583... 0.0112562...] 

2186 [ 548. 0.4031674... 0.9907270... 0.0103302...] 

2187 [ 549. 0.4181943... 0.9930817... 0.0094972...] 

2188 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] 

2189 [ 551. 0.4489082... 0.9963738... 0.0080748...] 

2190 [ 552. 0.4645599... 0.9973682... 0.0074580...] 

2191 [ 553. 0.4803950... 0.9979568... 0.0068902...] 

2192 [ 554. 0.4963962... 0.9981802... 0.0063660...] 

2193 [ 555. 0.5125410... 0.9980910... 0.0058818...] 

2194 [ 556. 0.5288034... 0.9977488... 0.0054349...] 

2195 [ 557. 0.5451560... 0.9972150... 0.0050216...] 

2196 [ 558. 0.5615719... 0.9965479... 0.0046357...] 

2197 [ 559. 0.5780267... 0.9957974... 0.0042671...] 

2198 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]] 

2199 

2200 Multi-spectral distributions with a non-uniformly spaced independent 

2201 variable uses *Cubic Spline* interpolation: 

2202 

2203 >>> data[511] = (0.00314, 0.31416, 0.03142) 

2204 >>> msds = MultiSpectralDistributions(data) 

2205 >>> with numpy_print_options(suppress=True): 

2206 ... print(msds.interpolate(SpectralShape(500, 560, 1))) 

2207 ... # doctest: +ELLIPSIS 

2208 [[ 500. 0.0049 ... 0.323 ... 0.272 ...] 

2209 [ 501. 0.0300110... 0.9455153... 0.5985102...] 

2210 [ 502. 0.0462136... 1.3563103... 0.8066498...] 

2211 [ 503. 0.0547925... 1.5844039... 0.9126502...] 

2212 [ 504. 0.0570325... 1.6588148... 0.9327429...] 

2213 [ 505. 0.0542183... 1.6085619... 0.8831594...] 

2214 [ 506. 0.0476346... 1.4626640... 0.7801312...] 

2215 [ 507. 0.0385662... 1.2501401... 0.6398896...] 

2216 [ 508. 0.0282978... 1.0000089... 0.4786663...] 

2217 [ 509. 0.0181142... 0.7412892... 0.3126925...] 

2218 [ 510. 0.0093 ... 0.503 ... 0.1582 ...] 

2219 [ 511. 0.00314 ... 0.31416 ... 0.03142 ...] 

2220 [ 512. 0.0006228... 0.1970419... -0.0551709...] 

2221 [ 513. 0.0015528... 0.1469341... -0.1041165...] 

2222 [ 514. 0.0054381... 0.1523785... -0.1217152...] 

2223 [ 515. 0.0117869... 0.2019173... -0.1142659...] 

2224 [ 516. 0.0201073... 0.2840925... -0.0880670...] 

2225 [ 517. 0.0299077... 0.3874463... -0.0494174...] 

2226 [ 518. 0.0406961... 0.5005208... -0.0046156...] 

2227 [ 519. 0.0519808... 0.6118579... 0.0400397...] 

2228 [ 520. 0.06327 ... 0.71 ... 0.07825 ...] 

2229 [ 521. 0.0741690... 0.7859059... 0.1050384...] 

2230 [ 522. 0.0846726... 0.8402033... 0.1207164...] 

2231 [ 523. 0.0948728... 0.8759363... 0.1269173...] 

2232 [ 524. 0.1048614... 0.8961496... 0.1252743...] 

2233 [ 525. 0.1147305... 0.9038874... 0.1174207...] 

2234 [ 526. 0.1245719... 0.9021942... 0.1049899...] 

2235 [ 527. 0.1344776... 0.8941145... 0.0896151...] 

2236 [ 528. 0.1445395... 0.8826926... 0.0729296...] 

2237 [ 529. 0.1548497... 0.8709729... 0.0565668...] 

2238 [ 530. 0.1655 ... 0.862 ... 0.04216 ...] 

2239 [ 531. 0.1765618... 0.858179 ... 0.0309976...] 

2240 [ 532. 0.1880244... 0.8593588... 0.0229897...] 

2241 [ 533. 0.1998566... 0.8647493... 0.0177013...] 

2242 [ 534. 0.2120269... 0.8735601... 0.0146975...] 

2243 [ 535. 0.2245042... 0.8850011... 0.0135435...] 

2244 [ 536. 0.2372572... 0.8982820... 0.0138044...] 

2245 [ 537. 0.2502546... 0.9126126... 0.0150454...] 

2246 [ 538. 0.2634650... 0.9272026... 0.0168315...] 

2247 [ 539. 0.2768572... 0.9412618... 0.0187280...] 

2248 [ 540. 0.2904 ... 0.954 ... 0.0203 ...] 

2249 [ 541. 0.3040682... 0.9647869... 0.0211987...] 

2250 [ 542. 0.3178617... 0.9736329... 0.0214207...] 

2251 [ 543. 0.3317865... 0.9807080... 0.0210486...] 

2252 [ 544. 0.3458489... 0.9861825... 0.0201650...] 

2253 [ 545. 0.3600548... 0.9902267... 0.0188525...] 

2254 [ 546. 0.3744103... 0.9930107... 0.0171939...] 

2255 [ 547. 0.3889215... 0.9947048... 0.0152716...] 

2256 [ 548. 0.4035944... 0.9954792... 0.0131685...] 

2257 [ 549. 0.4184352... 0.9955042... 0.0109670...] 

2258 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] 

2259 [ 551. 0.4486447... 0.9939867... 0.0065999...] 

2260 [ 552. 0.4640255... 0.9927847... 0.0045994...] 

2261 [ 553. 0.4795984... 0.9915141... 0.0028313...] 

2262 [ 554. 0.4953696... 0.9903452... 0.0013781...] 

2263 [ 555. 0.5113451... 0.9894483... 0.0003224...] 

2264 [ 556. 0.5275310... 0.9889934... -0.0002530...] 

2265 [ 557. 0.5439334... 0.9891509... -0.0002656...] 

2266 [ 558. 0.5605583... 0.9900910... 0.0003672...] 

2267 [ 559. 0.5774118... 0.9919840... 0.0017282...] 

2268 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]] 

2269 """ 

2270 

2271 for signal in self.signals.values(): 

2272 cast("SpectralDistribution", signal).interpolate( 

2273 shape, interpolator, interpolator_kwargs 

2274 ) 

2275 

2276 return self 

2277 

2278 def extrapolate( 

2279 self, 

2280 shape: SpectralShape, 

2281 extrapolator: Type[ProtocolExtrapolator] | None = None, 

2282 extrapolator_kwargs: dict | None = None, 

2283 ) -> Self: 

2284 """ 

2285 Extrapolate the multi-spectral distributions in-place according to 

2286 *CIE 15:2004* and *CIE 167:2005* recommendations or specified extrapolation 

2287 arguments. 

2288 

2289 Parameters 

2290 ---------- 

2291 shape 

2292 Spectral shape used for extrapolation. 

2293 extrapolator 

2294 Extrapolator class type to use as extrapolating function. 

2295 extrapolator_kwargs 

2296 Arguments to use when instantiating the extrapolating function. 

2297 

2298 Returns 

2299 ------- 

2300 :class:`colour.MultiSpectralDistributions` 

2301 Extrapolated multi-spectral distributions. 

2302 

2303 References 

2304 ---------- 

2305 :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` 

2306 

2307 Examples 

2308 -------- 

2309 >>> from colour.utilities import numpy_print_options 

2310 >>> data = { 

2311 ... 500: (0.004900, 0.323000, 0.272000), 

2312 ... 510: (0.009300, 0.503000, 0.158200), 

2313 ... 520: (0.063270, 0.710000, 0.078250), 

2314 ... 530: (0.165500, 0.862000, 0.042160), 

2315 ... 540: (0.290400, 0.954000, 0.020300), 

2316 ... 550: (0.433450, 0.994950, 0.008750), 

2317 ... 560: (0.594500, 0.995000, 0.003900), 

2318 ... } 

2319 >>> msds = MultiSpectralDistributions(data) 

2320 >>> msds.extrapolate(SpectralShape(400, 700, 10)).shape 

2321 SpectralShape(400.0, 700.0, 10.0) 

2322 >>> with numpy_print_options(suppress=True): 

2323 ... print(msds) 

2324 [[ 400. 0.0049 0.323 0.272 ] 

2325 [ 410. 0.0049 0.323 0.272 ] 

2326 [ 420. 0.0049 0.323 0.272 ] 

2327 [ 430. 0.0049 0.323 0.272 ] 

2328 [ 440. 0.0049 0.323 0.272 ] 

2329 [ 450. 0.0049 0.323 0.272 ] 

2330 [ 460. 0.0049 0.323 0.272 ] 

2331 [ 470. 0.0049 0.323 0.272 ] 

2332 [ 480. 0.0049 0.323 0.272 ] 

2333 [ 490. 0.0049 0.323 0.272 ] 

2334 [ 500. 0.0049 0.323 0.272 ] 

2335 [ 510. 0.0093 0.503 0.1582 ] 

2336 [ 520. 0.06327 0.71 0.07825] 

2337 [ 530. 0.1655 0.862 0.04216] 

2338 [ 540. 0.2904 0.954 0.0203 ] 

2339 [ 550. 0.43345 0.99495 0.00875] 

2340 [ 560. 0.5945 0.995 0.0039 ] 

2341 [ 570. 0.5945 0.995 0.0039 ] 

2342 [ 580. 0.5945 0.995 0.0039 ] 

2343 [ 590. 0.5945 0.995 0.0039 ] 

2344 [ 600. 0.5945 0.995 0.0039 ] 

2345 [ 610. 0.5945 0.995 0.0039 ] 

2346 [ 620. 0.5945 0.995 0.0039 ] 

2347 [ 630. 0.5945 0.995 0.0039 ] 

2348 [ 640. 0.5945 0.995 0.0039 ] 

2349 [ 650. 0.5945 0.995 0.0039 ] 

2350 [ 660. 0.5945 0.995 0.0039 ] 

2351 [ 670. 0.5945 0.995 0.0039 ] 

2352 [ 680. 0.5945 0.995 0.0039 ] 

2353 [ 690. 0.5945 0.995 0.0039 ] 

2354 [ 700. 0.5945 0.995 0.0039 ]] 

2355 """ 

2356 

2357 for signal in self.signals.values(): 

2358 cast("SpectralDistribution", signal).extrapolate( 

2359 shape, extrapolator, extrapolator_kwargs 

2360 ) 

2361 

2362 return self 

2363 

2364 def align( 

2365 self, 

2366 shape: SpectralShape, 

2367 interpolator: Type[ProtocolInterpolator] | None = None, 

2368 interpolator_kwargs: dict | None = None, 

2369 extrapolator: Type[ProtocolExtrapolator] | None = None, 

2370 extrapolator_kwargs: dict | None = None, 

2371 ) -> Self: 

2372 """ 

2373 Align the multi-spectral distributions in-place to the specified spectral 

2374 shape: Interpolates first then extrapolates to fit the specified range. 

2375 

2376 Interpolation is performed according to *CIE 167:2005* recommendation 

2377 (if the interpolator has not been changed at instantiation time) or 

2378 specified interpolation arguments. 

2379 

2380 The logic for choosing the interpolator class when ``interpolator`` is 

2381 not specified is as follows: 

2382 

2383 .. code-block:: python 

2384 

2385 if self.interpolator not in ( 

2386 SpragueInterpolator, 

2387 CubicSplineInterpolator, 

2388 ): 

2389 interpolator = self.interpolator 

2390 elif self.is_uniform(): 

2391 interpolator = SpragueInterpolator 

2392 else: 

2393 interpolator = CubicSplineInterpolator 

2394 

2395 The logic for choosing the interpolator keyword arguments when 

2396 ``interpolator_kwargs`` is not specified is as follows: 

2397 

2398 .. code-block:: python 

2399 

2400 if self.interpolator not in ( 

2401 SpragueInterpolator, 

2402 CubicSplineInterpolator, 

2403 ): 

2404 interpolator_kwargs = self.interpolator_kwargs 

2405 else: 

2406 interpolator_kwargs = {} 

2407 

2408 Parameters 

2409 ---------- 

2410 shape 

2411 Spectral shape used for alignment. 

2412 interpolator 

2413 Interpolator class type to use as interpolating function. 

2414 interpolator_kwargs 

2415 Arguments to use when instantiating the interpolating function. 

2416 extrapolator 

2417 Extrapolator class type to use as extrapolating function. 

2418 extrapolator_kwargs 

2419 Arguments to use when instantiating the extrapolating function. 

2420 

2421 Returns 

2422 ------- 

2423 :class:`colour.MultiSpectralDistributions` 

2424 Aligned multi-spectral distributions. 

2425 

2426 Examples 

2427 -------- 

2428 >>> from colour.utilities import numpy_print_options 

2429 >>> data = { 

2430 ... 500: (0.004900, 0.323000, 0.272000), 

2431 ... 510: (0.009300, 0.503000, 0.158200), 

2432 ... 520: (0.063270, 0.710000, 0.078250), 

2433 ... 530: (0.165500, 0.862000, 0.042160), 

2434 ... 540: (0.290400, 0.954000, 0.020300), 

2435 ... 550: (0.433450, 0.994950, 0.008750), 

2436 ... 560: (0.594500, 0.995000, 0.003900), 

2437 ... } 

2438 >>> msds = MultiSpectralDistributions(data) 

2439 >>> with numpy_print_options(suppress=True): 

2440 ... print(msds.align(SpectralShape(505, 565, 1))) 

2441 ... # doctest: +ELLIPSIS 

2442 [[ 505. 0.0031582... 0.4091067... 0.2126801...] 

2443 [ 506. 0.0035019... 0.4268629... 0.2012748...] 

2444 [ 507. 0.0042365... 0.4450668... 0.1900968...] 

2445 [ 508. 0.0054192... 0.4638181... 0.1791709...] 

2446 [ 509. 0.0070965... 0.4831505... 0.1685260...] 

2447 [ 510. 0.0093 ... 0.503 ... 0.1582 ...] 

2448 [ 511. 0.0120562... 0.5232543... 0.1482365...] 

2449 [ 512. 0.0154137... 0.5439717... 0.1386625...] 

2450 [ 513. 0.0193991... 0.565139 ... 0.1294993...] 

2451 [ 514. 0.0240112... 0.5866255... 0.1207676...] 

2452 [ 515. 0.0292289... 0.6082226... 0.1124864...] 

2453 [ 516. 0.0350192... 0.6296821... 0.1046717...] 

2454 [ 517. 0.0413448... 0.6507558... 0.0973361...] 

2455 [ 518. 0.0481727... 0.6712346... 0.0904871...] 

2456 [ 519. 0.0554816... 0.6909873... 0.0841267...] 

2457 [ 520. 0.06327 ... 0.71 ... 0.07825 ...] 

2458 [ 521. 0.0715642... 0.7283456... 0.0728614...] 

2459 [ 522. 0.0803970... 0.7459679... 0.0680051...] 

2460 [ 523. 0.0897629... 0.7628184... 0.0636823...] 

2461 [ 524. 0.0996227... 0.7789004... 0.0598449...] 

2462 [ 525. 0.1099142... 0.7942533... 0.0564111...] 

2463 [ 526. 0.1205637... 0.8089368... 0.0532822...] 

2464 [ 527. 0.1314973... 0.8230153... 0.0503588...] 

2465 [ 528. 0.1426523... 0.8365417... 0.0475571...] 

2466 [ 529. 0.1539887... 0.8495422... 0.0448253...] 

2467 [ 530. 0.1655 ... 0.862 ... 0.04216 ...] 

2468 [ 531. 0.1772055... 0.8738585... 0.0395936...] 

2469 [ 532. 0.1890877... 0.8850940... 0.0371046...] 

2470 [ 533. 0.2011304... 0.8957073... 0.0346733...] 

2471 [ 534. 0.2133310... 0.9057092... 0.0323006...] 

2472 [ 535. 0.2256968... 0.9151181... 0.0300011...] 

2473 [ 536. 0.2382403... 0.9239560... 0.0277974...] 

2474 [ 537. 0.2509754... 0.9322459... 0.0257131...] 

2475 [ 538. 0.2639130... 0.9400080... 0.0237668...] 

2476 [ 539. 0.2770569... 0.9472574... 0.0219659...] 

2477 [ 540. 0.2904 ... 0.954 ... 0.0203 ...] 

2478 [ 541. 0.3039194... 0.9602409... 0.0187414...] 

2479 [ 542. 0.3175893... 0.9660106... 0.0172748...] 

2480 [ 543. 0.3314022... 0.9713260... 0.0158947...] 

2481 [ 544. 0.3453666... 0.9761850... 0.0146001...] 

2482 [ 545. 0.3595019... 0.9805731... 0.0133933...] 

2483 [ 546. 0.3738324... 0.9844703... 0.0122777...] 

2484 [ 547. 0.3883818... 0.9878583... 0.0112562...] 

2485 [ 548. 0.4031674... 0.9907270... 0.0103302...] 

2486 [ 549. 0.4181943... 0.9930817... 0.0094972...] 

2487 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] 

2488 [ 551. 0.4489082... 0.9963738... 0.0080748...] 

2489 [ 552. 0.4645599... 0.9973682... 0.0074580...] 

2490 [ 553. 0.4803950... 0.9979568... 0.0068902...] 

2491 [ 554. 0.4963962... 0.9981802... 0.0063660...] 

2492 [ 555. 0.5125410... 0.9980910... 0.0058818...] 

2493 [ 556. 0.5288034... 0.9977488... 0.0054349...] 

2494 [ 557. 0.5451560... 0.9972150... 0.0050216...] 

2495 [ 558. 0.5615719... 0.9965479... 0.0046357...] 

2496 [ 559. 0.5780267... 0.9957974... 0.0042671...] 

2497 [ 560. 0.5945 ... 0.995 ... 0.0039 ...] 

2498 [ 561. 0.5945 ... 0.995 ... 0.0039 ...] 

2499 [ 562. 0.5945 ... 0.995 ... 0.0039 ...] 

2500 [ 563. 0.5945 ... 0.995 ... 0.0039 ...] 

2501 [ 564. 0.5945 ... 0.995 ... 0.0039 ...] 

2502 [ 565. 0.5945 ... 0.995 ... 0.0039 ...]] 

2503 """ 

2504 

2505 for signal in self.signals.values(): 

2506 cast("SpectralDistribution", signal).align( 

2507 shape, 

2508 interpolator, 

2509 interpolator_kwargs, 

2510 extrapolator, 

2511 extrapolator_kwargs, 

2512 ) 

2513 

2514 return self 

2515 

2516 def trim(self, shape: SpectralShape) -> Self: 

2517 """ 

2518 Trim the multi-spectral distributions wavelengths to the specified shape. 

2519 

2520 Parameters 

2521 ---------- 

2522 shape 

2523 Spectral shape used for trimming. 

2524 

2525 Returns 

2526 ------- 

2527 :class:`colour.MultiSpectralDistributions` 

2528 Trimmed multi-spectral distributions. 

2529 

2530 Examples 

2531 -------- 

2532 >>> from colour.utilities import numpy_print_options 

2533 >>> data = { 

2534 ... 500: (0.004900, 0.323000, 0.272000), 

2535 ... 510: (0.009300, 0.503000, 0.158200), 

2536 ... 520: (0.063270, 0.710000, 0.078250), 

2537 ... 530: (0.165500, 0.862000, 0.042160), 

2538 ... 540: (0.290400, 0.954000, 0.020300), 

2539 ... 550: (0.433450, 0.994950, 0.008750), 

2540 ... 560: (0.594500, 0.995000, 0.003900), 

2541 ... } 

2542 >>> msds = MultiSpectralDistributions(data) 

2543 >>> msds = msds.interpolate(SpectralShape(500, 560, 1)) 

2544 >>> with numpy_print_options(suppress=True): 

2545 ... print(msds.trim(SpectralShape(520, 580, 5))) 

2546 ... # doctest: +ELLIPSIS 

2547 [[ 520. 0.06327 ... 0.71 ... 0.07825 ...] 

2548 [ 521. 0.0715642... 0.7283456... 0.0728614...] 

2549 [ 522. 0.0803970... 0.7459679... 0.0680051...] 

2550 [ 523. 0.0897629... 0.7628184... 0.0636823...] 

2551 [ 524. 0.0996227... 0.7789004... 0.0598449...] 

2552 [ 525. 0.1099142... 0.7942533... 0.0564111...] 

2553 [ 526. 0.1205637... 0.8089368... 0.0532822...] 

2554 [ 527. 0.1314973... 0.8230153... 0.0503588...] 

2555 [ 528. 0.1426523... 0.8365417... 0.0475571...] 

2556 [ 529. 0.1539887... 0.8495422... 0.0448253...] 

2557 [ 530. 0.1655 ... 0.862 ... 0.04216 ...] 

2558 [ 531. 0.1772055... 0.8738585... 0.0395936...] 

2559 [ 532. 0.1890877... 0.8850940... 0.0371046...] 

2560 [ 533. 0.2011304... 0.8957073... 0.0346733...] 

2561 [ 534. 0.2133310... 0.9057092... 0.0323006...] 

2562 [ 535. 0.2256968... 0.9151181... 0.0300011...] 

2563 [ 536. 0.2382403... 0.9239560... 0.0277974...] 

2564 [ 537. 0.2509754... 0.9322459... 0.0257131...] 

2565 [ 538. 0.2639130... 0.9400080... 0.0237668...] 

2566 [ 539. 0.2770569... 0.9472574... 0.0219659...] 

2567 [ 540. 0.2904 ... 0.954 ... 0.0203 ...] 

2568 [ 541. 0.3039194... 0.9602409... 0.0187414...] 

2569 [ 542. 0.3175893... 0.9660106... 0.0172748...] 

2570 [ 543. 0.3314022... 0.9713260... 0.0158947...] 

2571 [ 544. 0.3453666... 0.9761850... 0.0146001...] 

2572 [ 545. 0.3595019... 0.9805731... 0.0133933...] 

2573 [ 546. 0.3738324... 0.9844703... 0.0122777...] 

2574 [ 547. 0.3883818... 0.9878583... 0.0112562...] 

2575 [ 548. 0.4031674... 0.9907270... 0.0103302...] 

2576 [ 549. 0.4181943... 0.9930817... 0.0094972...] 

2577 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] 

2578 [ 551. 0.4489082... 0.9963738... 0.0080748...] 

2579 [ 552. 0.4645599... 0.9973682... 0.0074580...] 

2580 [ 553. 0.4803950... 0.9979568... 0.0068902...] 

2581 [ 554. 0.4963962... 0.9981802... 0.0063660...] 

2582 [ 555. 0.5125410... 0.9980910... 0.0058818...] 

2583 [ 556. 0.5288034... 0.9977488... 0.0054349...] 

2584 [ 557. 0.5451560... 0.9972150... 0.0050216...] 

2585 [ 558. 0.5615719... 0.9965479... 0.0046357...] 

2586 [ 559. 0.5780267... 0.9957974... 0.0042671...] 

2587 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]] 

2588 """ 

2589 

2590 for signal in self.signals.values(): 

2591 cast("SpectralDistribution", signal).trim(shape) 

2592 

2593 return self 

2594 

2595 def normalise(self, factor: Real = 1) -> Self: 

2596 """ 

2597 Normalise the multi-spectral distributions with the specified normalization 

2598 factor. 

2599 

2600 Parameters 

2601 ---------- 

2602 factor 

2603 Normalization factor. 

2604 

2605 Returns 

2606 ------- 

2607 :class:`colour.MultiSpectralDistributions` 

2608 Normalised multi- spectral distribution. 

2609 

2610 Notes 

2611 ----- 

2612 - The implementation uses the maximum value for each 

2613 :class:`colour.SpectralDistribution` class instances. 

2614 

2615 Examples 

2616 -------- 

2617 >>> from colour.utilities import numpy_print_options 

2618 >>> data = { 

2619 ... 500: (0.004900, 0.323000, 0.272000), 

2620 ... 510: (0.009300, 0.503000, 0.158200), 

2621 ... 520: (0.063270, 0.710000, 0.078250), 

2622 ... 530: (0.165500, 0.862000, 0.042160), 

2623 ... 540: (0.290400, 0.954000, 0.020300), 

2624 ... 550: (0.433450, 0.994950, 0.008750), 

2625 ... 560: (0.594500, 0.995000, 0.003900), 

2626 ... } 

2627 >>> msds = MultiSpectralDistributions(data) 

2628 >>> with numpy_print_options(suppress=True): 

2629 ... print(msds.normalise()) # doctest: +ELLIPSIS 

2630 [[ 500. 0.0082422... 0.3246231... 1. ...] 

2631 [ 510. 0.0156434... 0.5055276... 0.5816176...] 

2632 [ 520. 0.1064255... 0.7135678... 0.2876838...] 

2633 [ 530. 0.2783852... 0.8663316... 0.155 ...] 

2634 [ 540. 0.4884777... 0.9587939... 0.0746323...] 

2635 [ 550. 0.7291000... 0.9999497... 0.0321691...] 

2636 [ 560. 1. ... 1. ... 0.0143382...]] 

2637 """ 

2638 

2639 for signal in self.signals.values(): 

2640 cast("SpectralDistribution", signal).normalise(factor) 

2641 

2642 return self 

2643 

2644 def to_sds(self) -> List[SpectralDistribution]: 

2645 """ 

2646 Convert the multi-spectral distributions to a list of spectral 

2647 distributions. 

2648 

2649 Returns 

2650 ------- 

2651 :class:`list` 

2652 List of spectral distributions. 

2653 

2654 Examples 

2655 -------- 

2656 >>> from colour.utilities import numpy_print_options 

2657 >>> data = { 

2658 ... 500: (0.004900, 0.323000, 0.272000), 

2659 ... 510: (0.009300, 0.503000, 0.158200), 

2660 ... 520: (0.063270, 0.710000, 0.078250), 

2661 ... 530: (0.165500, 0.862000, 0.042160), 

2662 ... 540: (0.290400, 0.954000, 0.020300), 

2663 ... 550: (0.433450, 0.994950, 0.008750), 

2664 ... 560: (0.594500, 0.995000, 0.003900), 

2665 ... } 

2666 >>> msds = MultiSpectralDistributions(data) 

2667 >>> with numpy_print_options(suppress=True): 

2668 ... for sd in msds.to_sds(): 

2669 ... print(sd) # doctest: +ELLIPSIS 

2670 [[ 500. 0.0049 ...] 

2671 [ 510. 0.0093 ...] 

2672 [ 520. 0.06327...] 

2673 [ 530. 0.1655 ...] 

2674 [ 540. 0.2904 ...] 

2675 [ 550. 0.43345...] 

2676 [ 560. 0.5945 ...]] 

2677 [[ 500. 0.323 ...] 

2678 [ 510. 0.503 ...] 

2679 [ 520. 0.71 ...] 

2680 [ 530. 0.862 ...] 

2681 [ 540. 0.954 ...] 

2682 [ 550. 0.99495...] 

2683 [ 560. 0.995 ...]] 

2684 [[ 500. 0.272 ...] 

2685 [ 510. 0.1582 ...] 

2686 [ 520. 0.07825...] 

2687 [ 530. 0.04216...] 

2688 [ 540. 0.0203 ...] 

2689 [ 550. 0.00875...] 

2690 [ 560. 0.0039 ...]] 

2691 """ 

2692 

2693 return [ 

2694 cast("SpectralDistribution", signal.copy()) 

2695 for signal in self.signals.values() 

2696 ] 

2697 

2698 

2699_CACHE_RESHAPED_SDS_AND_MSDS: dict = CACHE_REGISTRY.register_cache( 

2700 f"{__name__}._CACHE_RESHAPED_SDS_AND_MSDS" 

2701) 

2702 

2703TypeSpectralDistribution = TypeVar( 

2704 "TypeSpectralDistribution", bound="SpectralDistribution" 

2705) 

2706 

2707 

2708def reshape_sd( 

2709 sd: TypeSpectralDistribution, 

2710 shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, 

2711 method: (Literal["Align", "Extrapolate", "Interpolate", "Trim"] | str) = "Align", 

2712 copy: bool = True, 

2713 **kwargs: Any, 

2714) -> TypeSpectralDistribution: 

2715 """ 

2716 Reshape the specified spectral distribution to match the specified spectral 

2717 shape. 

2718 

2719 The reshaped object is cached, thus another call to the definition with 

2720 the same arguments will yield the cached object immediately. 

2721 

2722 Parameters 

2723 ---------- 

2724 sd 

2725 Spectral distribution to reshape. 

2726 shape 

2727 Target spectral shape for reshaping the spectral distribution. 

2728 method 

2729 Method to use for reshaping. 

2730 copy 

2731 Whether to return a copy of the cached spectral distribution. 

2732 Default is *True*. 

2733 

2734 Other Parameters 

2735 ---------------- 

2736 kwargs 

2737 {:meth:`colour.SpectralDistribution.align`, 

2738 :meth:`colour.SpectralDistribution.extrapolate`, 

2739 :meth:`colour.SpectralDistribution.interpolate`, 

2740 :meth:`colour.SpectralDistribution.trim`}, 

2741 See the documentation of the previously listed methods. 

2742 

2743 Returns 

2744 ------- 

2745 :class:`colour.SpectralDistribution` 

2746 

2747 Warnings 

2748 -------- 

2749 Contrary to *Numpy*, reshaping a spectral distribution alters its data! 

2750 """ 

2751 

2752 method = validate_method( 

2753 method, valid_methods=("Align", "Extrapolate", "Interpolate", "Trim") 

2754 ) 

2755 

2756 # Handling dict-like keyword arguments. 

2757 kwargs_items = list(kwargs.items()) 

2758 for i, (keyword, value) in enumerate(kwargs_items): 

2759 if isinstance(value, Mapping): 

2760 kwargs_items[i] = (keyword, tuple(value.items())) 

2761 

2762 hash_key = hash((sd, shape, method, tuple(kwargs_items))) 

2763 

2764 if is_caching_enabled() and hash_key in _CACHE_RESHAPED_SDS_AND_MSDS: 

2765 reshaped_sd = _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] 

2766 

2767 return reshaped_sd.copy() if copy else reshaped_sd 

2768 

2769 function = getattr(sd, method) 

2770 

2771 reshaped_sd = getattr(sd.copy(), method)(shape, **filter_kwargs(function, **kwargs)) 

2772 

2773 _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] = reshaped_sd 

2774 

2775 return reshaped_sd 

2776 

2777 

2778TypeMultiSpectralDistributions = TypeVar( 

2779 "TypeMultiSpectralDistributions", bound="MultiSpectralDistributions" 

2780) 

2781 

2782 

2783def reshape_msds( 

2784 msds: TypeMultiSpectralDistributions, 

2785 shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, 

2786 method: (Literal["Align", "Extrapolate", "Interpolate", "Trim"] | str) = "Align", 

2787 copy: bool = True, 

2788 **kwargs: Any, 

2789) -> TypeMultiSpectralDistributions: 

2790 """ 

2791 Reshape the specified multi-spectral distributions to match the specified 

2792 spectral shape. 

2793 

2794 The reshaped object is cached, thus another call to the definition with 

2795 the same arguments will yield the cached object immediately. 

2796 

2797 Parameters 

2798 ---------- 

2799 msds 

2800 Multi-spectral distributions to reshape. 

2801 shape 

2802 Target spectral shape for reshaping the multi-spectral distributions. 

2803 method 

2804 Method to use for reshaping. 

2805 copy 

2806 Whether to return a copy of the cached multi-spectral distributions. 

2807 Default is *True*. 

2808 

2809 Other Parameters 

2810 ---------------- 

2811 kwargs 

2812 {:meth:`colour.MultiSpectralDistributions.align`, 

2813 :meth:`colour.MultiSpectralDistributions.extrapolate`, 

2814 :meth:`colour.MultiSpectralDistributions.interpolate`, 

2815 :meth:`colour.MultiSpectralDistributions.trim`}, 

2816 See the documentation of the previously listed methods. 

2817 

2818 Returns 

2819 ------- 

2820 :class:`colour.MultiSpectralDistributions` 

2821 

2822 Warnings 

2823 -------- 

2824 Contrary to *Numpy*, reshaping multi-spectral distributions alters their 

2825 data! 

2826 """ 

2827 

2828 return reshape_sd(msds, shape, method, copy, **kwargs) # pyright: ignore 

2829 

2830 

2831def sds_and_msds_to_sds( 

2832 sds: ( 

2833 Sequence[SpectralDistribution | MultiSpectralDistributions] 

2834 | SpectralDistribution 

2835 | MultiSpectralDistributions 

2836 | ValuesView 

2837 ), 

2838) -> List[SpectralDistribution]: 

2839 """ 

2840 Convert specified spectral and multi-spectral distributions to a list of 

2841 spectral distributions. 

2842 

2843 Parameters 

2844 ---------- 

2845 sds 

2846 Spectral and multi-spectral distributions to convert to a list of 

2847 spectral distributions. Each multi-spectral distribution is expanded 

2848 into its constituent spectral distributions. 

2849 

2850 Returns 

2851 ------- 

2852 :class:`list` 

2853 List of spectral distributions where multi-spectral distributions 

2854 have been expanded into individual spectral distributions. 

2855 

2856 Examples 

2857 -------- 

2858 >>> data = { 

2859 ... 500: 0.0651, 

2860 ... 520: 0.0705, 

2861 ... 540: 0.0772, 

2862 ... 560: 0.0870, 

2863 ... 580: 0.1128, 

2864 ... 600: 0.1360, 

2865 ... } 

2866 >>> sd_1 = SpectralDistribution(data) 

2867 >>> sd_2 = SpectralDistribution(data) 

2868 >>> data = { 

2869 ... 500: (0.004900, 0.323000, 0.272000), 

2870 ... 510: (0.009300, 0.503000, 0.158200), 

2871 ... 520: (0.063270, 0.710000, 0.078250), 

2872 ... 530: (0.165500, 0.862000, 0.042160), 

2873 ... 540: (0.290400, 0.954000, 0.020300), 

2874 ... 550: (0.433450, 0.994950, 0.008750), 

2875 ... 560: (0.594500, 0.995000, 0.003900), 

2876 ... } 

2877 >>> multi_sds_1 = MultiSpectralDistributions(data) 

2878 >>> multi_sds_2 = MultiSpectralDistributions(data) 

2879 >>> len(sds_and_msds_to_sds([sd_1, sd_2, multi_sds_1, multi_sds_2])) 

2880 8 

2881 """ 

2882 

2883 if isinstance(sds, SpectralDistribution): 

2884 return sds_and_msds_to_sds([sds]) 

2885 

2886 if isinstance(sds, MultiSpectralDistributions): 

2887 sds_converted = sds.to_sds() 

2888 else: 

2889 sds_converted = [] 

2890 

2891 for sd in sds: 

2892 sds_converted += ( 

2893 sd.to_sds() if isinstance(sd, MultiSpectralDistributions) else [sd] 

2894 ) 

2895 

2896 return sds_converted 

2897 

2898 

2899def sds_and_msds_to_msds( 

2900 sds: ( 

2901 Sequence[SpectralDistribution | MultiSpectralDistributions] 

2902 | SpectralDistribution 

2903 | MultiSpectralDistributions 

2904 | ValuesView 

2905 ), 

2906) -> MultiSpectralDistributions: 

2907 """ 

2908 Convert specified spectral and multi-spectral distributions to 

2909 multi-spectral distributions. 

2910 

2911 The spectral and multi-spectral distributions will be aligned to the 

2912 intersection of their spectral shapes. 

2913 

2914 Parameters 

2915 ---------- 

2916 sds 

2917 Spectral and multi-spectral distributions to convert to 

2918 multi-spectral distributions. 

2919 

2920 Returns 

2921 ------- 

2922 :class:`colour.MultiSpectralDistributions` 

2923 Multi-spectral distributions. 

2924 

2925 Examples 

2926 -------- 

2927 >>> data = { 

2928 ... 500: 0.0651, 

2929 ... 520: 0.0705, 

2930 ... 540: 0.0772, 

2931 ... 560: 0.0870, 

2932 ... 580: 0.1128, 

2933 ... 600: 0.1360, 

2934 ... } 

2935 >>> sd_1 = SpectralDistribution(data) 

2936 >>> sd_2 = SpectralDistribution(data) 

2937 >>> data = { 

2938 ... 500: (0.004900, 0.323000, 0.272000), 

2939 ... 510: (0.009300, 0.503000, 0.158200), 

2940 ... 520: (0.063270, 0.710000, 0.078250), 

2941 ... 530: (0.165500, 0.862000, 0.042160), 

2942 ... 540: (0.290400, 0.954000, 0.020300), 

2943 ... 550: (0.433450, 0.994950, 0.008750), 

2944 ... 560: (0.594500, 0.995000, 0.003900), 

2945 ... } 

2946 >>> multi_sds_1 = MultiSpectralDistributions(data) 

2947 >>> multi_sds_2 = MultiSpectralDistributions(data) 

2948 >>> from colour.utilities import numpy_print_options 

2949 >>> with numpy_print_options(suppress=True, linewidth=160): 

2950 ... sds_and_msds_to_msds( # doctest: +SKIP 

2951 ... [sd_1, sd_2, multi_sds_1, multi_sds_2] 

2952 ... ) 

2953 ... 

2954 MultiSpectralDistributions([[ 500. , 0.0651 ...,\ 

29550.0651 ..., 0.0049 ..., 0.323 ..., 0.272 ...,\ 

29560.0049 ..., 0.323 ..., 0.272 ...], 

2957 [ 510. , 0.0676692...,\ 

29580.0676692..., 0.0093 ..., 0.503 ..., 0.1582 ...,\ 

29590.0093 ..., 0.503 ..., 0.1582 ...], 

2960 [ 520. , 0.0705 ...,\ 

29610.0705 ..., 0.06327 ..., 0.71 ..., 0.07825 ...,\ 

29620.06327 ..., 0.71 ..., 0.07825 ...], 

2963 [ 530. , 0.0737808...,\ 

29640.0737808..., 0.1655 ..., 0.862 ..., 0.04216 ...,\ 

29650.1655 ..., 0.862 ..., 0.04216 ...], 

2966 [ 540. , 0.0772 ...,\ 

29670.0772 ..., 0.2904 ..., 0.954 ..., 0.0203 ...,\ 

29680.2904 ..., 0.954 ..., 0.0203 ...], 

2969 [ 550. , 0.0806671...,\ 

29700.0806671..., 0.43345 ..., 0.99495 ..., 0.00875 ...,\ 

29710.43345 ..., 0.99495 ..., 0.00875 ...], 

2972 [ 560. , 0.087 ...,\ 

29730.087 ..., 0.5945 ..., 0.995 ..., 0.0039 ...,\ 

29740.5945 ..., 0.995 ..., 0.0039 ...]], 

2975 labels=['SpectralDistribution (...)', \ 

2976'SpectralDistribution (...)', '0 - SpectralDistribution (...)', \ 

2977'1 - SpectralDistribution (...)', '2 - SpectralDistribution (...)', \ 

2978'0 - SpectralDistribution (...)', '1 - SpectralDistribution (...)', \ 

2979'2 - SpectralDistribution (...)'], 

2980 interpolator=SpragueInterpolator, 

2981 interpolator_kwargs={}, 

2982 extrapolator=Extrapolator, 

2983 extrapolator_kwargs={...}) 

2984 """ 

2985 

2986 if isinstance(sds, SpectralDistribution): 

2987 return sds_and_msds_to_msds([sds]) 

2988 

2989 if isinstance(sds, MultiSpectralDistributions): 

2990 msds_converted = sds 

2991 else: 

2992 sds_converted = sds_and_msds_to_sds(sds) 

2993 

2994 shapes = tuple({sd.shape for sd in sds_converted}) 

2995 shape = SpectralShape( 

2996 max(shape.start for shape in shapes), 

2997 min(shape.end for shape in shapes), 

2998 min(shape.interval for shape in shapes), 

2999 ) 

3000 

3001 values = [] 

3002 labels = [] 

3003 display_labels = [] 

3004 for sd in sds_converted: 

3005 if sd.shape != shape: 

3006 sd = sd.copy().align(shape) # noqa: PLW2901 

3007 

3008 values.append(sd.values) 

3009 labels.append(sd.name if sd.name not in labels else f"{sd.name} ({id(sd)})") 

3010 display_labels.append( 

3011 sd.display_name 

3012 if sd.display_name not in display_labels 

3013 else f"{sd.display_name} ({id(sd)})" 

3014 ) 

3015 

3016 msds_converted = MultiSpectralDistributions( 

3017 tstack(values), 

3018 shape.wavelengths, 

3019 labels, 

3020 display_labels=display_labels, 

3021 ) 

3022 

3023 return msds_converted