Coverage for colour/continuous/multi_signals.py: 100%

211 statements  

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

1""" 

2Multi-Signals 

3============= 

4 

5Define multi-continuous signal support for colour science computations. 

6 

7This module provides the :class:`colour.continuous.MultiSignals` class for 

8representing and operating on multiple continuous signals simultaneously, 

9supporting interpolation and extrapolation operations. 

10 

11- :class:`colour.continuous.MultiSignals` 

12""" 

13 

14from __future__ import annotations 

15 

16import typing 

17from collections.abc import Iterator, KeysView, Mapping, ValuesView 

18 

19import numpy as np 

20 

21from colour.constants import DTYPE_FLOAT_DEFAULT 

22from colour.continuous import AbstractContinuousFunction, Signal 

23 

24if typing.TYPE_CHECKING: 

25 from colour.hints import ( 

26 Any, 

27 Dict, 

28 DTypeFloat, 

29 List, 

30 Literal, 

31 NDArrayFloat, 

32 ProtocolExtrapolator, 

33 ProtocolInterpolator, 

34 Real, 

35 Self, 

36 Sequence, 

37 Type, 

38 ) 

39 

40from colour.hints import ArrayLike, Callable, Sequence, cast 

41from colour.utilities import ( 

42 as_float_array, 

43 attest, 

44 first_item, 

45 int_digest, 

46 is_iterable, 

47 is_pandas_installed, 

48 multiline_repr, 

49 optional, 

50 required, 

51 tsplit, 

52 tstack, 

53 validate_method, 

54) 

55from colour.utilities.documentation import is_documentation_building 

56 

57if typing.TYPE_CHECKING or is_pandas_installed(): 

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

59else: # pragma: no cover 

60 from unittest import mock 

61 

62 DataFrame = mock.MagicMock() 

63 Series = mock.MagicMock() 

64 

65__author__ = "Colour Developers" 

66__copyright__ = "Copyright 2013 Colour Developers" 

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

68__maintainer__ = "Colour Developers" 

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

70__status__ = "Production" 

71 

72__all__ = [ 

73 "MultiSignals", 

74] 

75 

76 

77class MultiSignals(AbstractContinuousFunction): 

78 """ 

79 Define the base class for multi-signals, a container for 

80 multiple :class:`colour.continuous.Signal` sub-class instances. 

81 

82 .. important:: 

83 

84 Specific documentation about getting, setting, indexing and slicing 

85 the multi-signals values is available in the 

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

87 

88 Parameters 

89 ---------- 

90 data 

91 Data to be stored in the multi-signals. 

92 domain 

93 Values to initialise the multiple :class:`colour.continuous.Signal` 

94 sub-class instances :attr:`colour.continuous.Signal.domain` 

95 attribute with. If both ``data`` and ``domain`` arguments are 

96 defined, the latter will be used to initialise the 

97 :attr:`colour.continuous.Signal.domain` attribute. 

98 labels 

99 Names to use for the :class:`colour.continuous.Signal` sub-class 

100 instances. 

101 

102 Other Parameters 

103 ---------------- 

104 dtype 

105 Floating point data type. 

106 extrapolator 

107 Extrapolator class type to use as extrapolating function for the 

108 :class:`colour.continuous.Signal` sub-class instances. 

109 extrapolator_kwargs 

110 Arguments to use when instantiating the extrapolating function of 

111 the :class:`colour.continuous.Signal` sub-class instances. 

112 interpolator 

113 Interpolator class type to use as interpolating function for the 

114 :class:`colour.continuous.Signal` sub-class instances. 

115 interpolator_kwargs 

116 Arguments to use when instantiating the interpolating function of 

117 the :class:`colour.continuous.Signal` sub-class instances. 

118 name 

119 Multi-signals name. 

120 signal_type 

121 The :class:`colour.continuous.Signal` sub-class type used for 

122 instances. 

123 

124 Attributes 

125 ---------- 

126 - :attr:`~colour.continuous.MultiSignals.dtype` 

127 - :attr:`~colour.continuous.MultiSignals.domain` 

128 - :attr:`~colour.continuous.MultiSignals.range` 

129 - :attr:`~colour.continuous.MultiSignals.interpolator` 

130 - :attr:`~colour.continuous.MultiSignals.interpolator_kwargs` 

131 - :attr:`~colour.continuous.MultiSignals.extrapolator` 

132 - :attr:`~colour.continuous.MultiSignals.extrapolator_kwargs` 

133 - :attr:`~colour.continuous.MultiSignals.function` 

134 - :attr:`~colour.continuous.MultiSignals.signals` 

135 - :attr:`~colour.continuous.MultiSignals.labels` 

136 - :attr:`~colour.continuous.MultiSignals.signal_type` 

137 

138 Methods 

139 ------- 

140 - :meth:`~colour.continuous.MultiSignals.__init__` 

141 - :meth:`~colour.continuous.MultiSignals.__str__` 

142 - :meth:`~colour.continuous.MultiSignals.__repr__` 

143 - :meth:`~colour.continuous.MultiSignals.__hash__` 

144 - :meth:`~colour.continuous.MultiSignals.__getitem__` 

145 - :meth:`~colour.continuous.MultiSignals.__setitem__` 

146 - :meth:`~colour.continuous.MultiSignals.__contains__` 

147 - :meth:`~colour.continuous.MultiSignals.__eq__` 

148 - :meth:`~colour.continuous.MultiSignals.__ne__` 

149 - :meth:`~colour.continuous.MultiSignals.arithmetical_operation` 

150 - :meth:`~colour.continuous.MultiSignals.multi_signals_unpack_data` 

151 - :meth:`~colour.continuous.MultiSignals.fill_nan` 

152 - :meth:`~colour.continuous.MultiSignals.to_dataframe` 

153 

154 Examples 

155 -------- 

156 Instantiation with implicit *domain* and a single signal: 

157 

158 >>> range_ = np.linspace(10, 100, 10) 

159 >>> print(MultiSignals(range_)) 

160 [[ 0. 10.] 

161 [ 1. 20.] 

162 [ 2. 30.] 

163 [ 3. 40.] 

164 [ 4. 50.] 

165 [ 5. 60.] 

166 [ 6. 70.] 

167 [ 7. 80.] 

168 [ 8. 90.] 

169 [ 9. 100.]] 

170 

171 Instantiation with explicit *domain* and a single signal: 

172 

173 >>> domain = np.arange(100, 1100, 100) 

174 >>> print(MultiSignals(range_, domain)) 

175 [[ 100. 10.] 

176 [ 200. 20.] 

177 [ 300. 30.] 

178 [ 400. 40.] 

179 [ 500. 50.] 

180 [ 600. 60.] 

181 [ 700. 70.] 

182 [ 800. 80.] 

183 [ 900. 90.] 

184 [ 1000. 100.]] 

185 

186 Instantiation with multiple signals: 

187 

188 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

189 >>> range_ += np.array([0, 10, 20]) 

190 >>> print(MultiSignals(range_, domain)) 

191 [[ 100. 10. 20. 30.] 

192 [ 200. 20. 30. 40.] 

193 [ 300. 30. 40. 50.] 

194 [ 400. 40. 50. 60.] 

195 [ 500. 50. 60. 70.] 

196 [ 600. 60. 70. 80.] 

197 [ 700. 70. 80. 90.] 

198 [ 800. 80. 90. 100.] 

199 [ 900. 90. 100. 110.] 

200 [ 1000. 100. 110. 120.]] 

201 

202 Instantiation with a *dict*: 

203 

204 >>> print(MultiSignals(dict(zip(domain, range_)))) 

205 [[ 100. 10. 20. 30.] 

206 [ 200. 20. 30. 40.] 

207 [ 300. 30. 40. 50.] 

208 [ 400. 40. 50. 60.] 

209 [ 500. 50. 60. 70.] 

210 [ 600. 60. 70. 80.] 

211 [ 700. 70. 80. 90.] 

212 [ 800. 80. 90. 100.] 

213 [ 900. 90. 100. 110.] 

214 [ 1000. 100. 110. 120.]] 

215 

216 Instantiation using a *Signal* sub-class: 

217 

218 >>> class NotSignal(Signal): 

219 ... pass 

220 

221 >>> multi_signals = MultiSignals(range_, domain, signal_type=NotSignal) 

222 >>> print(multi_signals) 

223 [[ 100. 10. 20. 30.] 

224 [ 200. 20. 30. 40.] 

225 [ 300. 30. 40. 50.] 

226 [ 400. 40. 50. 60.] 

227 [ 500. 50. 60. 70.] 

228 [ 600. 60. 70. 80.] 

229 [ 700. 70. 80. 90.] 

230 [ 800. 80. 90. 100.] 

231 [ 900. 90. 100. 110.] 

232 [ 1000. 100. 110. 120.]] 

233 >>> type(multi_signals.signals[0]) # doctest: +SKIP 

234 <class 'multi_signals.NotSignal'> 

235 

236 Instantiation with a *Pandas* `Series`: 

237 

238 >>> if is_pandas_installed(): 

239 ... from pandas import Series 

240 ... 

241 ... print( 

242 ... MultiSignals( # doctest: +SKIP 

243 ... Series(dict(zip(domain, np.linspace(10, 100, 10)))) 

244 ... ) 

245 ... ) 

246 [[ 100. 10.] 

247 [ 200. 20.] 

248 [ 300. 30.] 

249 [ 400. 40.] 

250 [ 500. 50.] 

251 [ 600. 60.] 

252 [ 700. 70.] 

253 [ 800. 80.] 

254 [ 900. 90.] 

255 [ 1000. 100.]] 

256 

257 Instantiation with a *Pandas* :class:`pandas.DataFrame`: 

258 

259 >>> if is_pandas_installed(): 

260 ... from pandas import DataFrame 

261 ... 

262 ... data = dict(zip(["a", "b", "c"], tsplit(range_))) 

263 ... print(MultiSignals(DataFrame(data, domain))) # doctest: +SKIP 

264 [[ 100. 10. 20. 30.] 

265 [ 200. 20. 30. 40.] 

266 [ 300. 30. 40. 50.] 

267 [ 400. 40. 50. 60.] 

268 [ 500. 50. 60. 70.] 

269 [ 600. 60. 70. 80.] 

270 [ 700. 70. 80. 90.] 

271 [ 800. 80. 90. 100.] 

272 [ 900. 90. 100. 110.] 

273 [ 1000. 100. 110. 120.]] 

274 

275 Retrieving domain *y* variable for arbitrary range *x* variable: 

276 

277 >>> x = 150 

278 >>> range_ = tstack([np.sin(np.linspace(0, 1, 10))] * 3) 

279 >>> range_ += np.array([0.0, 0.25, 0.5]) 

280 >>> MultiSignals(range_, domain)[x] # doctest: +ELLIPSIS 

281 array([ 0.0359701..., 0.2845447..., 0.5331193...]) 

282 >>> x = np.linspace(100, 1000, 3) 

283 >>> MultiSignals(range_, domain)[x] # doctest: +ELLIPSIS 

284 array([[ 4.4085384...e-20, 2.5000000...e-01, 5.0000000...e-01], 

285 [ 4.7669395...e-01, 7.2526859...e-01, 9.7384323...e-01], 

286 [ 8.4147098...e-01, 1.0914709...e+00, 1.3414709...e+00]]) 

287 

288 Using an alternative interpolating function: 

289 

290 >>> x = 150 

291 >>> from colour.algebra import CubicSplineInterpolator 

292 >>> MultiSignals(range_, domain, interpolator=CubicSplineInterpolator)[ 

293 ... x 

294 ... ] # doctest: +ELLIPSIS 

295 array([ 0.0555274..., 0.3055274..., 0.5555274...]) 

296 >>> x = np.linspace(100, 1000, 3) 

297 >>> MultiSignals(range_, domain, interpolator=CubicSplineInterpolator)[ 

298 ... x 

299 ... ] # doctest: +ELLIPSIS 

300 array([[ 0. ..., 0.25 ..., 0.5 ...], 

301 [ 0.4794253..., 0.7294253..., 0.9794253...], 

302 [ 0.8414709..., 1.0914709..., 1.3414709...]]) 

303 """ 

304 

305 def __init__( 

306 self, 

307 data: ( 

308 ArrayLike 

309 | DataFrame 

310 | dict 

311 | Self 

312 | Sequence 

313 | Series 

314 | Signal 

315 | ValuesView 

316 | None 

317 ) = None, 

318 domain: ArrayLike | KeysView | None = None, 

319 labels: Sequence | None = None, 

320 **kwargs: Any, 

321 ) -> None: 

322 super().__init__(kwargs.get("name")) 

323 

324 self._signal_type: Type[Signal] = kwargs.get("signal_type", Signal) 

325 

326 self._signals: Dict[str, Signal] = self.multi_signals_unpack_data( 

327 data, domain, labels, **kwargs 

328 ) 

329 

330 @property 

331 def dtype(self) -> Type[DTypeFloat]: 

332 """ 

333 Getter and setter for the multi-signals dtype. 

334 

335 Parameters 

336 ---------- 

337 value 

338 Value to set the multi-signals dtype with. 

339 

340 Returns 

341 ------- 

342 Type[DTypeFloat] 

343 Multi-signals dtype. 

344 """ 

345 

346 return first_item(self._signals.values()).dtype 

347 

348 @dtype.setter 

349 def dtype(self, value: Type[DTypeFloat]) -> None: 

350 """Setter for the **self.dtype** property.""" 

351 

352 for signal in self._signals.values(): 

353 signal.dtype = value 

354 

355 @property 

356 def domain(self) -> NDArrayFloat: 

357 """ 

358 Getter and setter for the multi-signals' independent 

359 domain variable :math:`x`. 

360 

361 Parameters 

362 ---------- 

363 value 

364 Value to set the multi-signals independent domain 

365 variable :math:`x` with. 

366 

367 Returns 

368 ------- 

369 :class:`numpy.ndarray` 

370 Multi-signals independent domain variable 

371 :math:`x`. 

372 """ 

373 

374 return first_item(self._signals.values()).domain 

375 

376 @domain.setter 

377 def domain(self, value: ArrayLike) -> None: 

378 """Setter for the **self.domain** property.""" 

379 

380 for signal in self._signals.values(): 

381 signal.domain = as_float_array(value, self.dtype) 

382 

383 @property 

384 def range(self) -> NDArrayFloat: 

385 """ 

386 Getter and setter for the multi-signals' range 

387 variable :math:`y`. 

388 

389 Parameters 

390 ---------- 

391 value 

392 Value to set the multi-signals' range variable 

393 :math:`y` with. 

394 

395 Returns 

396 ------- 

397 :class:`numpy.ndarray` 

398 Multi-signals' range variable :math:`y`. 

399 """ 

400 

401 return tstack([signal.range for signal in self._signals.values()]) 

402 

403 @range.setter 

404 def range(self, value: ArrayLike) -> None: 

405 """Setter for the **self.range** property.""" 

406 

407 value = as_float_array(value) 

408 

409 if value.ndim in (0, 1): 

410 for signal in self._signals.values(): 

411 signal.range = value 

412 else: 

413 attest( 

414 value.shape[-1] == len(self._signals), 

415 'Corresponding "y" variable columns must have ' 

416 'same count than underlying "Signal" components!', 

417 ) 

418 

419 for signal, y in zip(self._signals.values(), tsplit(value), strict=True): 

420 signal.range = y 

421 

422 @property 

423 def interpolator(self) -> Type[ProtocolInterpolator]: 

424 """ 

425 Getter and setter for the multi-signals interpolator 

426 type. 

427 

428 Parameters 

429 ---------- 

430 value 

431 Value to set the multi-signals interpolator type 

432 with. 

433 

434 Returns 

435 ------- 

436 Type[ProtocolInterpolator] 

437 Multi-signals interpolator type. 

438 """ 

439 

440 return first_item(self._signals.values()).interpolator 

441 

442 @interpolator.setter 

443 def interpolator(self, value: Type[ProtocolInterpolator]) -> None: 

444 """Setter for the **self.interpolator** property.""" 

445 

446 if value is not None: 

447 for signal in self._signals.values(): 

448 signal.interpolator = value 

449 

450 @property 

451 def interpolator_kwargs(self) -> dict: 

452 """ 

453 Getter and setter for the interpolator instantiation time arguments. 

454 

455 Parameters 

456 ---------- 

457 value 

458 Value to set the multi-signals interpolator 

459 instantiation time arguments to. 

460 

461 Returns 

462 ------- 

463 :class:`dict` 

464 Multi-signals interpolator instantiation time 

465 arguments. 

466 """ 

467 

468 return first_item(self._signals.values()).interpolator_kwargs 

469 

470 @interpolator_kwargs.setter 

471 def interpolator_kwargs(self, value: dict) -> None: 

472 """Setter for the **self.interpolator_kwargs** property.""" 

473 

474 for signal in self._signals.values(): 

475 signal.interpolator_kwargs = value 

476 

477 @property 

478 def extrapolator(self) -> Type[ProtocolExtrapolator]: 

479 """ 

480 Getter and setter for the multi-signals extrapolator 

481 type. 

482 

483 Parameters 

484 ---------- 

485 value 

486 Value to set the multi-signals extrapolator type 

487 with. 

488 

489 Returns 

490 ------- 

491 Type[ProtocolExtrapolator] 

492 Multi-signals extrapolator type. 

493 """ 

494 

495 return first_item(self._signals.values()).extrapolator 

496 

497 @extrapolator.setter 

498 def extrapolator(self, value: Type[ProtocolExtrapolator]) -> None: 

499 """Setter for the **self.extrapolator** property.""" 

500 

501 for signal in self._signals.values(): 

502 signal.extrapolator = value 

503 

504 @property 

505 def extrapolator_kwargs(self) -> dict: 

506 """ 

507 Getter and setter for the multi-signals extrapolator 

508 instantiation time arguments. 

509 

510 Parameters 

511 ---------- 

512 value 

513 Value to set the multi-signals extrapolator 

514 instantiation time arguments to. 

515 

516 Returns 

517 ------- 

518 :class:`dict` 

519 Multi-signals extrapolator instantiation time 

520 arguments. 

521 """ 

522 

523 return first_item(self._signals.values()).extrapolator_kwargs 

524 

525 @extrapolator_kwargs.setter 

526 def extrapolator_kwargs(self, value: dict) -> None: 

527 """Setter for the **self.extrapolator_kwargs** property.""" 

528 

529 for signal in self._signals.values(): 

530 signal.extrapolator_kwargs = value 

531 

532 @property 

533 def function(self) -> Callable: 

534 """ 

535 Getter for the multi-signals callable. 

536 

537 Returns 

538 ------- 

539 Callable 

540 Multi-signals callable. 

541 """ 

542 

543 return first_item(self._signals.values()).function 

544 

545 @property 

546 def signals(self) -> Dict[str, Signal]: 

547 """ 

548 Getter and setter for the dictionary of 

549 :class:`colour.continuous.Signal` sub-class instances. 

550 

551 Parameters 

552 ---------- 

553 value 

554 Dictionary of :class:`colour.continuous.Signal` sub-class 

555 instances to set. 

556 

557 Returns 

558 ------- 

559 :class:`dict` 

560 Dictionary mapping signal names to their corresponding 

561 :class:`colour.continuous.Signal` sub-class instances. 

562 """ 

563 

564 return self._signals 

565 

566 @signals.setter 

567 def signals( 

568 self, 

569 value: ArrayLike | DataFrame | dict | Self | Series | Signal | None, 

570 ) -> None: 

571 """Setter for the **self.signals** property.""" 

572 

573 self._signals = self.multi_signals_unpack_data( 

574 value, signal_type=self._signal_type 

575 ) 

576 

577 @property 

578 def labels(self) -> List[str]: 

579 """ 

580 Getter and setter for the :class:`colour.continuous.Signal` sub-class 

581 instance names. 

582 

583 Parameters 

584 ---------- 

585 value 

586 Value to set the :class:`colour.continuous.Signal` sub-class 

587 instance names. 

588 

589 Returns 

590 ------- 

591 :class:`list` 

592 :class:`colour.continuous.Signal` sub-class instance names. 

593 """ 

594 

595 return [str(key) for key in self._signals] 

596 

597 @labels.setter 

598 def labels(self, value: Sequence) -> None: 

599 """Setter for the **self.labels** property.""" 

600 

601 attest( 

602 is_iterable(value), 

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

604 ) 

605 

606 attest( 

607 len(set(value)) == len(value), 

608 '"labels" property: values must be unique!', 

609 ) 

610 

611 attest( 

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

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

614 ) 

615 

616 self._signals = { 

617 str(value[i]): signal for i, signal in enumerate(self._signals.values()) 

618 } 

619 

620 @property 

621 def signal_type(self) -> Type[Signal]: 

622 """ 

623 Getter for the type of :class:`colour.continuous.Signal` 

624 sub-class instances. 

625 

626 Returns 

627 ------- 

628 Type[Signal] 

629 Type of :class:`colour.continuous.Signal` sub-class 

630 instances used in this multi-signal collection. 

631 """ 

632 

633 return self._signal_type 

634 

635 def __str__(self) -> str: 

636 """ 

637 Return a formatted string representation of the multi-signals. 

638 

639 Returns 

640 ------- 

641 :class:`str` 

642 Formatted string representation. 

643 

644 Examples 

645 -------- 

646 >>> domain = np.arange(0, 10, 1) 

647 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

648 >>> range_ += np.array([0, 10, 20]) 

649 >>> print(MultiSignals(range_)) 

650 [[ 0. 10. 20. 30.] 

651 [ 1. 20. 30. 40.] 

652 [ 2. 30. 40. 50.] 

653 [ 3. 40. 50. 60.] 

654 [ 4. 50. 60. 70.] 

655 [ 5. 60. 70. 80.] 

656 [ 6. 70. 80. 90.] 

657 [ 7. 80. 90. 100.] 

658 [ 8. 90. 100. 110.] 

659 [ 9. 100. 110. 120.]] 

660 """ 

661 

662 return str(np.hstack([self.domain[:, None], self.range])) 

663 

664 def __repr__(self) -> str: 

665 """ 

666 Return an evaluable string representation of the multi-signals. 

667 

668 Returns 

669 ------- 

670 :class:`str` 

671 Evaluable string representation. 

672 

673 Examples 

674 -------- 

675 >>> domain = np.arange(0, 10, 1) 

676 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

677 >>> range_ += np.array([0, 10, 20]) 

678 >>> MultiSignals(range_) 

679 MultiSignals([[ 0., 10., 20., 30.], 

680 [ 1., 20., 30., 40.], 

681 [ 2., 30., 40., 50.], 

682 [ 3., 40., 50., 60.], 

683 [ 4., 50., 60., 70.], 

684 [ 5., 60., 70., 80.], 

685 [ 6., 70., 80., 90.], 

686 [ 7., 80., 90., 100.], 

687 [ 8., 90., 100., 110.], 

688 [ 9., 100., 110., 120.]], 

689 ['0', '1', '2'], 

690 KernelInterpolator, 

691 {}, 

692 Extrapolator, 

693 {'method': 'Constant', 'left': nan, 'right': nan}) 

694 """ 

695 

696 if is_documentation_building(): # pragma: no cover 

697 return f"{self.__class__.__name__}(name='{self.name}', ...)" 

698 

699 return multiline_repr( 

700 self, 

701 [ 

702 { 

703 "formatter": lambda x: repr( # noqa: ARG005 

704 np.hstack([self.domain[:, None], self.range]) 

705 ), 

706 }, 

707 {"name": "labels"}, 

708 { 

709 "name": "interpolator", 

710 "formatter": lambda x: ( # noqa: ARG005 

711 self.interpolator.__name__ 

712 ), 

713 }, 

714 {"name": "interpolator_kwargs"}, 

715 { 

716 "name": "extrapolator", 

717 "formatter": lambda x: ( # noqa: ARG005 

718 self.extrapolator.__name__ 

719 ), 

720 }, 

721 {"name": "extrapolator_kwargs"}, 

722 ], 

723 ) 

724 

725 def __hash__(self) -> int: 

726 """ 

727 Compute the hash of the multi-signals. 

728 

729 Returns 

730 ------- 

731 :class:`int` 

732 Object hash. 

733 """ 

734 

735 return hash( 

736 ( 

737 int_digest(self.domain.tobytes()), 

738 *[hash(signal) for signal in self._signals.values()], 

739 self.interpolator.__name__, 

740 repr(self.interpolator_kwargs), 

741 self.extrapolator.__name__, 

742 repr(self.extrapolator_kwargs), 

743 ) 

744 ) 

745 

746 def __getitem__(self, x: ArrayLike | slice) -> NDArrayFloat: 

747 """ 

748 Return the corresponding range variable :math:`y` for the specified 

749 independent domain variable :math:`x`. 

750 

751 Parameters 

752 ---------- 

753 x 

754 Independent domain variable :math:`x`. 

755 

756 Returns 

757 ------- 

758 :class:`numpy.ndarray` 

759 Variable :math:`y` range value. 

760 

761 Examples 

762 -------- 

763 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

764 >>> range_ += np.array([0, 10, 20]) 

765 >>> multi_signals = MultiSignals(range_) 

766 >>> print(multi_signals) 

767 [[ 0. 10. 20. 30.] 

768 [ 1. 20. 30. 40.] 

769 [ 2. 30. 40. 50.] 

770 [ 3. 40. 50. 60.] 

771 [ 4. 50. 60. 70.] 

772 [ 5. 60. 70. 80.] 

773 [ 6. 70. 80. 90.] 

774 [ 7. 80. 90. 100.] 

775 [ 8. 90. 100. 110.] 

776 [ 9. 100. 110. 120.]] 

777 >>> multi_signals[0] 

778 array([ 10., 20., 30.]) 

779 >>> multi_signals[np.array([0, 1, 2])] 

780 array([[ 10., 20., 30.], 

781 [ 20., 30., 40.], 

782 [ 30., 40., 50.]]) 

783 >>> multi_signals[np.linspace(0, 5, 5)] # doctest: +ELLIPSIS 

784 array([[ 10. ..., 20. ..., 30. ...], 

785 [ 22.8348902..., 32.8046056..., 42.774321 ...], 

786 [ 34.8004492..., 44.7434347..., 54.6864201...], 

787 [ 47.5535392..., 57.5232546..., 67.4929700...], 

788 [ 60. ..., 70. ..., 80. ...]]) 

789 >>> multi_signals[0:3] 

790 array([[ 10., 20., 30.], 

791 [ 20., 30., 40.], 

792 [ 30., 40., 50.]]) 

793 >>> multi_signals[:, 0:2] 

794 array([[ 10., 20.], 

795 [ 20., 30.], 

796 [ 30., 40.], 

797 [ 40., 50.], 

798 [ 50., 60.], 

799 [ 60., 70.], 

800 [ 70., 80.], 

801 [ 80., 90.], 

802 [ 90., 100.], 

803 [ 100., 110.]]) 

804 """ 

805 

806 x_r, x_c = (x[0], x[1]) if isinstance(x, tuple) else (x, slice(None)) 

807 

808 values = tstack([signal[x_r] for signal in self._signals.values()]) 

809 

810 return values[..., x_c] # pyright: ignore 

811 

812 def __setitem__(self, x: ArrayLike | slice, y: ArrayLike) -> None: 

813 """ 

814 Set the corresponding range variable :math:`y` for the specified 

815 independent domain variable :math:`x`. 

816 

817 Parameters 

818 ---------- 

819 x 

820 Independent domain variable :math:`x`. 

821 y 

822 Corresponding range variable :math:`y`. 

823 

824 Examples 

825 -------- 

826 >>> domain = np.arange(0, 10, 1) 

827 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

828 >>> range_ += np.array([0, 10, 20]) 

829 >>> multi_signals = MultiSignals(range_) 

830 >>> print(multi_signals) 

831 [[ 0. 10. 20. 30.] 

832 [ 1. 20. 30. 40.] 

833 [ 2. 30. 40. 50.] 

834 [ 3. 40. 50. 60.] 

835 [ 4. 50. 60. 70.] 

836 [ 5. 60. 70. 80.] 

837 [ 6. 70. 80. 90.] 

838 [ 7. 80. 90. 100.] 

839 [ 8. 90. 100. 110.] 

840 [ 9. 100. 110. 120.]] 

841 >>> multi_signals[0] = 20 

842 >>> multi_signals[0] 

843 array([ 20., 20., 20.]) 

844 >>> multi_signals[np.array([0, 1, 2])] = 30 

845 >>> multi_signals[np.array([0, 1, 2])] 

846 array([[ 30., 30., 30.], 

847 [ 30., 30., 30.], 

848 [ 30., 30., 30.]]) 

849 >>> multi_signals[np.linspace(0, 5, 5)] = 50 

850 >>> print(multi_signals) 

851 [[ 0. 50. 50. 50. ] 

852 [ 1. 30. 30. 30. ] 

853 [ 1.25 50. 50. 50. ] 

854 [ 2. 30. 30. 30. ] 

855 [ 2.5 50. 50. 50. ] 

856 [ 3. 40. 50. 60. ] 

857 [ 3.75 50. 50. 50. ] 

858 [ 4. 50. 60. 70. ] 

859 [ 5. 50. 50. 50. ] 

860 [ 6. 70. 80. 90. ] 

861 [ 7. 80. 90. 100. ] 

862 [ 8. 90. 100. 110. ] 

863 [ 9. 100. 110. 120. ]] 

864 >>> multi_signals[np.array([0, 1, 2])] = np.array([10, 20, 30]) 

865 >>> print(multi_signals) 

866 [[ 0. 10. 20. 30. ] 

867 [ 1. 10. 20. 30. ] 

868 [ 1.25 50. 50. 50. ] 

869 [ 2. 10. 20. 30. ] 

870 [ 2.5 50. 50. 50. ] 

871 [ 3. 40. 50. 60. ] 

872 [ 3.75 50. 50. 50. ] 

873 [ 4. 50. 60. 70. ] 

874 [ 5. 50. 50. 50. ] 

875 [ 6. 70. 80. 90. ] 

876 [ 7. 80. 90. 100. ] 

877 [ 8. 90. 100. 110. ] 

878 [ 9. 100. 110. 120. ]] 

879 >>> y = np.reshape(np.arange(1, 10, 1), (3, 3)) 

880 >>> multi_signals[np.array([0, 1, 2])] = y 

881 >>> print(multi_signals) 

882 [[ 0. 1. 2. 3. ] 

883 [ 1. 4. 5. 6. ] 

884 [ 1.25 50. 50. 50. ] 

885 [ 2. 7. 8. 9. ] 

886 [ 2.5 50. 50. 50. ] 

887 [ 3. 40. 50. 60. ] 

888 [ 3.75 50. 50. 50. ] 

889 [ 4. 50. 60. 70. ] 

890 [ 5. 50. 50. 50. ] 

891 [ 6. 70. 80. 90. ] 

892 [ 7. 80. 90. 100. ] 

893 [ 8. 90. 100. 110. ] 

894 [ 9. 100. 110. 120. ]] 

895 >>> multi_signals[0:3] = 40 

896 >>> multi_signals[0:3] 

897 array([[ 40., 40., 40.], 

898 [ 40., 40., 40.], 

899 [ 40., 40., 40.]]) 

900 >>> multi_signals[:, 0:2] = 50 

901 >>> print(multi_signals) 

902 [[ 0. 50. 50. 40. ] 

903 [ 1. 50. 50. 40. ] 

904 [ 1.25 50. 50. 40. ] 

905 [ 2. 50. 50. 9. ] 

906 [ 2.5 50. 50. 50. ] 

907 [ 3. 50. 50. 60. ] 

908 [ 3.75 50. 50. 50. ] 

909 [ 4. 50. 50. 70. ] 

910 [ 5. 50. 50. 50. ] 

911 [ 6. 50. 50. 90. ] 

912 [ 7. 50. 50. 100. ] 

913 [ 8. 50. 50. 110. ] 

914 [ 9. 50. 50. 120. ]] 

915 """ 

916 

917 y = as_float_array(y) 

918 

919 x_r, x_c = (x[0], x[1]) if isinstance(x, tuple) else (x, slice(None)) 

920 

921 attest( 

922 y.ndim in range(3), 

923 'Corresponding "y" variable must be a numeric or a 1-dimensional ' 

924 "or 2-dimensional array!", 

925 ) 

926 

927 if y.ndim == 0: 

928 y = np.tile(y, len(self._signals)) 

929 elif y.ndim == 1: 

930 y = y[None, :] 

931 

932 attest( 

933 y.shape[-1] == len(self._signals), 

934 'Corresponding "y" variable columns must have same count than ' 

935 'underlying "Signal" components!', 

936 ) 

937 

938 values = list(zip(self._signals.values(), tsplit(y), strict=True)) 

939 

940 for signal, y in values[x_c]: # pyright: ignore 

941 signal[x_r] = y 

942 

943 def __contains__(self, x: ArrayLike | slice) -> bool: 

944 """ 

945 Determine whether the multi-signals contains the 

946 specified independent domain variable :math:`x`. 

947 

948 Parameters 

949 ---------- 

950 x 

951 Independent domain variable :math:`x`. 

952 

953 Returns 

954 ------- 

955 :class:`bool` 

956 Whether :math:`x` domain value is contained. 

957 

958 Examples 

959 -------- 

960 >>> range_ = np.linspace(10, 100, 10) 

961 >>> multi_signals = MultiSignals(range_) 

962 >>> 0 in multi_signals 

963 True 

964 >>> 0.5 in multi_signals 

965 True 

966 >>> 1000 in multi_signals 

967 False 

968 """ 

969 

970 return x in first_item(self._signals.values()) 

971 

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

973 """ 

974 Determine whether the multi-signals equals the specified 

975 object. 

976 

977 Parameters 

978 ---------- 

979 other 

980 Object to determine for equality with the multi-signals. 

981 

982 Returns 

983 ------- 

984 :class:`bool` 

985 Whether the specified object is equal to the multi-signals. 

986 

987 Examples 

988 -------- 

989 >>> range_ = np.linspace(10, 100, 10) 

990 >>> multi_signals_1 = MultiSignals(range_) 

991 >>> multi_signals_2 = MultiSignals(range_) 

992 >>> multi_signals_1 == multi_signals_2 

993 True 

994 >>> multi_signals_2[0] = 20 

995 >>> multi_signals_1 == multi_signals_2 

996 False 

997 >>> multi_signals_2[0] = 10 

998 >>> multi_signals_1 == multi_signals_2 

999 True 

1000 >>> from colour.algebra import CubicSplineInterpolator 

1001 >>> multi_signals_2.interpolator = CubicSplineInterpolator 

1002 >>> multi_signals_1 == multi_signals_2 

1003 False 

1004 """ 

1005 

1006 # NOTE: Comparing "interpolator_kwargs" and "extrapolator_kwargs" using 

1007 # their string representation because of presence of NaNs. 

1008 if isinstance(other, MultiSignals): 

1009 return all( 

1010 [ 

1011 np.array_equal(self.domain, other.domain), 

1012 np.array_equal(self.range, other.range), 

1013 self.interpolator is other.interpolator, 

1014 str(self.interpolator_kwargs) == str(other.interpolator_kwargs), 

1015 self.extrapolator is other.extrapolator, 

1016 str(self.extrapolator_kwargs) == str(other.extrapolator_kwargs), 

1017 self.labels == other.labels, 

1018 ] 

1019 ) 

1020 

1021 return False 

1022 

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

1024 """ 

1025 Determine whether the multi-signals is not equal to the 

1026 specified object. 

1027 

1028 Parameters 

1029 ---------- 

1030 other 

1031 Object to test whether it is not equal to the multi-signals. 

1032 

1033 Returns 

1034 ------- 

1035 :class:`bool` 

1036 Whether the specified object is not equal to the multi-signals. 

1037 

1038 Examples 

1039 -------- 

1040 >>> range_ = np.linspace(10, 100, 10) 

1041 >>> multi_signals_1 = MultiSignals(range_) 

1042 >>> multi_signals_2 = MultiSignals(range_) 

1043 >>> multi_signals_1 != multi_signals_2 

1044 False 

1045 >>> multi_signals_2[0] = 20 

1046 >>> multi_signals_1 != multi_signals_2 

1047 True 

1048 >>> multi_signals_2[0] = 10 

1049 >>> multi_signals_1 != multi_signals_2 

1050 False 

1051 >>> from colour.algebra import CubicSplineInterpolator 

1052 >>> multi_signals_2.interpolator = CubicSplineInterpolator 

1053 >>> multi_signals_1 != multi_signals_2 

1054 True 

1055 """ 

1056 

1057 return not (self == other) 

1058 

1059 def arithmetical_operation( 

1060 self, 

1061 a: ArrayLike | AbstractContinuousFunction, 

1062 operation: Literal["+", "-", "*", "/", "**"], 

1063 in_place: bool = False, 

1064 ) -> MultiSignals: 

1065 """ 

1066 Perform the specified arithmetical operation with operand :math:`a`, 

1067 either on a copy or in-place. 

1068 

1069 Parameters 

1070 ---------- 

1071 a 

1072 Operand :math:`a`. Can be a numeric value, array-like object, or 

1073 another continuous function instance. 

1074 operation 

1075 Operation to perform. 

1076 in_place 

1077 Operation happens in place. 

1078 

1079 Returns 

1080 ------- 

1081 :class:`colour.continuous.MultiSignals` 

1082 Multi-signals. 

1083 

1084 Examples 

1085 -------- 

1086 Adding a single *numeric* variable: 

1087 

1088 >>> domain = np.arange(0, 10, 1) 

1089 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

1090 >>> range_ += np.array([0, 10, 20]) 

1091 >>> multi_signals_1 = MultiSignals(range_) 

1092 >>> print(multi_signals_1) 

1093 [[ 0. 10. 20. 30.] 

1094 [ 1. 20. 30. 40.] 

1095 [ 2. 30. 40. 50.] 

1096 [ 3. 40. 50. 60.] 

1097 [ 4. 50. 60. 70.] 

1098 [ 5. 60. 70. 80.] 

1099 [ 6. 70. 80. 90.] 

1100 [ 7. 80. 90. 100.] 

1101 [ 8. 90. 100. 110.] 

1102 [ 9. 100. 110. 120.]] 

1103 >>> print(multi_signals_1.arithmetical_operation(10, "+", True)) 

1104 [[ 0. 20. 30. 40.] 

1105 [ 1. 30. 40. 50.] 

1106 [ 2. 40. 50. 60.] 

1107 [ 3. 50. 60. 70.] 

1108 [ 4. 60. 70. 80.] 

1109 [ 5. 70. 80. 90.] 

1110 [ 6. 80. 90. 100.] 

1111 [ 7. 90. 100. 110.] 

1112 [ 8. 100. 110. 120.] 

1113 [ 9. 110. 120. 130.]] 

1114 

1115 Adding an `ArrayLike` variable: 

1116 

1117 >>> a = np.linspace(10, 100, 10) 

1118 >>> print(multi_signals_1.arithmetical_operation(a, "+", True)) 

1119 [[ 0. 30. 40. 50.] 

1120 [ 1. 50. 60. 70.] 

1121 [ 2. 70. 80. 90.] 

1122 [ 3. 90. 100. 110.] 

1123 [ 4. 110. 120. 130.] 

1124 [ 5. 130. 140. 150.] 

1125 [ 6. 150. 160. 170.] 

1126 [ 7. 170. 180. 190.] 

1127 [ 8. 190. 200. 210.] 

1128 [ 9. 210. 220. 230.]] 

1129 

1130 >>> a = np.array([[10, 20, 30]]) 

1131 >>> print(multi_signals_1.arithmetical_operation(a, "+", True)) 

1132 [[ 0. 40. 60. 80.] 

1133 [ 1. 60. 80. 100.] 

1134 [ 2. 80. 100. 120.] 

1135 [ 3. 100. 120. 140.] 

1136 [ 4. 120. 140. 160.] 

1137 [ 5. 140. 160. 180.] 

1138 [ 6. 160. 180. 200.] 

1139 [ 7. 180. 200. 220.] 

1140 [ 8. 200. 220. 240.] 

1141 [ 9. 220. 240. 260.]] 

1142 

1143 >>> a = np.reshape(np.arange(0, 30, 1), (10, 3)) 

1144 >>> print(multi_signals_1.arithmetical_operation(a, "+", True)) 

1145 [[ 0. 40. 61. 82.] 

1146 [ 1. 63. 84. 105.] 

1147 [ 2. 86. 107. 128.] 

1148 [ 3. 109. 130. 151.] 

1149 [ 4. 132. 153. 174.] 

1150 [ 5. 155. 176. 197.] 

1151 [ 6. 178. 199. 220.] 

1152 [ 7. 201. 222. 243.] 

1153 [ 8. 224. 245. 266.] 

1154 [ 9. 247. 268. 289.]] 

1155 

1156 Adding a :class:`colour.continuous.Signal` sub-class: 

1157 

1158 >>> multi_signals_2 = MultiSignals(range_) 

1159 >>> print(multi_signals_1.arithmetical_operation(multi_signals_2, "+", True)) 

1160 [[ 0. 50. 81. 112.] 

1161 [ 1. 83. 114. 145.] 

1162 [ 2. 116. 147. 178.] 

1163 [ 3. 149. 180. 211.] 

1164 [ 4. 182. 213. 244.] 

1165 [ 5. 215. 246. 277.] 

1166 [ 6. 248. 279. 310.] 

1167 [ 7. 281. 312. 343.] 

1168 [ 8. 314. 345. 376.] 

1169 [ 9. 347. 378. 409.]] 

1170 """ 

1171 

1172 multi_signals = self if in_place else self.copy() 

1173 

1174 if isinstance(a, MultiSignals): 

1175 attest( 

1176 len(self.signals) == len(a.signals), 

1177 '"MultiSignals" operands must have same count than ' 

1178 'underlying "Signal" components!', 

1179 ) 

1180 

1181 for signal_a, signal_b in zip( 

1182 multi_signals.signals.values(), a.signals.values(), strict=True 

1183 ): 

1184 signal_a.arithmetical_operation(signal_b, operation, True) 

1185 else: 

1186 a = as_float_array(cast("ArrayLike", a)) 

1187 

1188 attest( 

1189 a.ndim in range(3), 

1190 'Operand "a" variable must be a numeric or a 1-dimensional or ' 

1191 "2-dimensional array!", 

1192 ) 

1193 

1194 if a.ndim in (0, 1): 

1195 for signal in multi_signals.signals.values(): 

1196 signal.arithmetical_operation(a, operation, True) 

1197 else: 

1198 attest( 

1199 a.shape[-1] == len(multi_signals.signals), 

1200 'Operand "a" variable columns must have same count than ' 

1201 'underlying "Signal" components!', 

1202 ) 

1203 

1204 for signal, y in zip( 

1205 multi_signals.signals.values(), tsplit(a), strict=True 

1206 ): 

1207 signal.arithmetical_operation(y, operation, True) 

1208 

1209 return multi_signals 

1210 

1211 @staticmethod 

1212 def multi_signals_unpack_data( 

1213 data: ( 

1214 ArrayLike 

1215 | DataFrame 

1216 | dict 

1217 | MultiSignals 

1218 | Sequence 

1219 | Series 

1220 | Signal 

1221 | ValuesView 

1222 | None 

1223 ) = None, 

1224 domain: ArrayLike | KeysView | None = None, 

1225 labels: Sequence | None = None, 

1226 dtype: Type[DTypeFloat] | None = None, 

1227 signal_type: Type[Signal] = Signal, 

1228 **kwargs: Any, 

1229 ) -> Dict[str, Signal]: 

1230 """ 

1231 Unpack specified data for multi-signals instantiation. 

1232 

1233 Parameters 

1234 ---------- 

1235 data 

1236 Data to unpack for multi-signals instantiation. 

1237 domain 

1238 Values to initialise the multiple :class:`colour.continuous.Signal` 

1239 sub-class instances :attr:`colour.continuous.Signal.domain` 

1240 attribute with. If both ``data`` and ``domain`` arguments are 

1241 defined, the latter will be used to initialise the 

1242 :attr:`colour.continuous.Signal.domain` property. 

1243 labels 

1244 Names to use for the :class:`colour.continuous.Signal` sub-class 

1245 instances. 

1246 dtype 

1247 Floating point data type. 

1248 signal_type 

1249 A :class:`colour.continuous.Signal` sub-class type. 

1250 

1251 Other Parameters 

1252 ---------------- 

1253 extrapolator 

1254 Extrapolator class type to use as extrapolating function for the 

1255 :class:`colour.continuous.Signal` sub-class instances. 

1256 extrapolator_kwargs 

1257 Arguments to use when instantiating the extrapolating function 

1258 of the :class:`colour.continuous.Signal` sub-class instances. 

1259 interpolator 

1260 Interpolator class type to use as interpolating function for the 

1261 :class:`colour.continuous.Signal` sub-class instances. 

1262 interpolator_kwargs 

1263 Arguments to use when instantiating the interpolating function 

1264 of the :class:`colour.continuous.Signal` sub-class instances. 

1265 name 

1266 Multi-signals name. 

1267 

1268 Returns 

1269 ------- 

1270 :class:`dict` 

1271 Mapping of labeled :class:`colour.continuous.Signal` sub-class 

1272 instances. 

1273 

1274 Examples 

1275 -------- 

1276 Unpacking using implicit *domain* and data for a single signal: 

1277 

1278 >>> range_ = np.linspace(10, 100, 10) 

1279 >>> signals = MultiSignals.multi_signals_unpack_data(range_) 

1280 >>> list(signals.keys()) 

1281 ['0'] 

1282 >>> print(signals["0"]) 

1283 [[ 0. 10.] 

1284 [ 1. 20.] 

1285 [ 2. 30.] 

1286 [ 3. 40.] 

1287 [ 4. 50.] 

1288 [ 5. 60.] 

1289 [ 6. 70.] 

1290 [ 7. 80.] 

1291 [ 8. 90.] 

1292 [ 9. 100.]] 

1293 

1294 Unpacking using explicit *domain* and data for a single signal: 

1295 

1296 >>> domain = np.arange(100, 1100, 100) 

1297 >>> signals = MultiSignals.multi_signals_unpack_data(range_, domain) 

1298 >>> list(signals.keys()) 

1299 ['0'] 

1300 >>> print(signals["0"]) 

1301 [[ 100. 10.] 

1302 [ 200. 20.] 

1303 [ 300. 30.] 

1304 [ 400. 40.] 

1305 [ 500. 50.] 

1306 [ 600. 60.] 

1307 [ 700. 70.] 

1308 [ 800. 80.] 

1309 [ 900. 90.] 

1310 [ 1000. 100.]] 

1311 

1312 Unpacking using data for multiple signals: 

1313 

1314 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

1315 >>> range_ += np.array([0, 10, 20]) 

1316 >>> signals = MultiSignals.multi_signals_unpack_data(range_, domain) 

1317 >>> list(signals.keys()) 

1318 ['0', '1', '2'] 

1319 >>> print(signals["2"]) 

1320 [[ 100. 30.] 

1321 [ 200. 40.] 

1322 [ 300. 50.] 

1323 [ 400. 60.] 

1324 [ 500. 70.] 

1325 [ 600. 80.] 

1326 [ 700. 90.] 

1327 [ 800. 100.] 

1328 [ 900. 110.] 

1329 [ 1000. 120.]] 

1330 

1331 Unpacking using a *dict*: 

1332 

1333 >>> signals = MultiSignals.multi_signals_unpack_data(dict(zip(domain, range_))) 

1334 >>> list(signals.keys()) 

1335 ['0', '1', '2'] 

1336 >>> print(signals["2"]) 

1337 [[ 100. 30.] 

1338 [ 200. 40.] 

1339 [ 300. 50.] 

1340 [ 400. 60.] 

1341 [ 500. 70.] 

1342 [ 600. 80.] 

1343 [ 700. 90.] 

1344 [ 800. 100.] 

1345 [ 900. 110.] 

1346 [ 1000. 120.]] 

1347 

1348 Unpacking using a sequence of *Signal* instances, note how the keys 

1349 are :class:`str` instances because the *Signal* names are used: 

1350 

1351 >>> signals = MultiSignals.multi_signals_unpack_data( 

1352 ... dict(zip(domain, range_)) 

1353 ... ).values() 

1354 >>> signals = MultiSignals.multi_signals_unpack_data(signals) 

1355 >>> list(signals.keys()) 

1356 ['0', '1', '2'] 

1357 >>> print(signals["2"]) 

1358 [[ 100. 30.] 

1359 [ 200. 40.] 

1360 [ 300. 50.] 

1361 [ 400. 60.] 

1362 [ 500. 70.] 

1363 [ 600. 80.] 

1364 [ 700. 90.] 

1365 [ 800. 100.] 

1366 [ 900. 110.] 

1367 [ 1000. 120.]] 

1368 

1369 Unpacking using *MultiSignals.multi_signals_unpack_data* method output: 

1370 

1371 >>> signals = MultiSignals.multi_signals_unpack_data(dict(zip(domain, range_))) 

1372 >>> signals = MultiSignals.multi_signals_unpack_data(signals) 

1373 >>> list(signals.keys()) 

1374 ['0', '1', '2'] 

1375 >>> print(signals["2"]) 

1376 [[ 100. 30.] 

1377 [ 200. 40.] 

1378 [ 300. 50.] 

1379 [ 400. 60.] 

1380 [ 500. 70.] 

1381 [ 600. 80.] 

1382 [ 700. 90.] 

1383 [ 800. 100.] 

1384 [ 900. 110.] 

1385 [ 1000. 120.]] 

1386 

1387 Unpacking using a *Pandas* `Series`: 

1388 

1389 >>> if is_pandas_installed(): 

1390 ... from pandas import Series 

1391 ... 

1392 ... signals = MultiSignals.multi_signals_unpack_data( 

1393 ... Series(dict(zip(domain, np.linspace(10, 100, 10)))) 

1394 ... ) 

1395 ... print(signals[0]) # doctest: +SKIP 

1396 [[ 100. 10.] 

1397 [ 200. 20.] 

1398 [ 300. 30.] 

1399 [ 400. 40.] 

1400 [ 500. 50.] 

1401 [ 600. 60.] 

1402 [ 700. 70.] 

1403 [ 800. 80.] 

1404 [ 900. 90.] 

1405 [ 1000. 100.]] 

1406 

1407 Unpacking using a *Pandas* :class:`pandas.DataFrame`: 

1408 

1409 >>> if is_pandas_installed(): 

1410 ... from pandas import DataFrame 

1411 ... 

1412 ... data = dict(zip(["a", "b", "c"], tsplit(range_))) 

1413 ... signals = MultiSignals.multi_signals_unpack_data( 

1414 ... DataFrame(data, domain) 

1415 ... ) 

1416 ... print(signals["c"]) # doctest: +SKIP 

1417 [[ 100. 30.] 

1418 [ 200. 40.] 

1419 [ 300. 50.] 

1420 [ 400. 60.] 

1421 [ 500. 70.] 

1422 [ 600. 80.] 

1423 [ 700. 90.] 

1424 [ 800. 100.] 

1425 [ 900. 110.] 

1426 [ 1000. 120.]] 

1427 """ 

1428 

1429 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1430 

1431 settings = {} 

1432 settings.update(kwargs) 

1433 settings.update({"dtype": dtype}) 

1434 

1435 # domain_unpacked, range_unpacked, signals = ( 

1436 # np.array([]), np.array([]), {}) 

1437 

1438 signals = {} 

1439 

1440 if isinstance(data, Signal): 

1441 signals[data.name] = data 

1442 elif isinstance(data, MultiSignals): 

1443 signals = data.signals 

1444 elif issubclass(type(data), Sequence) or isinstance( 

1445 data, (tuple, list, np.ndarray, Iterator, ValuesView) 

1446 ): 

1447 data_sequence = list(cast("Sequence", data)) 

1448 

1449 is_signal = True 

1450 for i in data_sequence: 

1451 if not isinstance(i, Signal): 

1452 is_signal = False 

1453 break 

1454 

1455 if is_signal: 

1456 for signal in data_sequence: 

1457 signals[signal.name] = signal_type( 

1458 signal.range, signal.domain, **settings 

1459 ) 

1460 else: 

1461 data_array = tsplit(data_sequence) 

1462 attest( 

1463 data_array.ndim in (1, 2), 

1464 'User "data" must be 1-dimensional or 2-dimensional!', 

1465 ) 

1466 

1467 if data_array.ndim == 1: 

1468 data_array = data_array[None, :] 

1469 

1470 for i, range_unpacked in enumerate(data_array): 

1471 signals[str(i)] = signal_type(range_unpacked, domain, **settings) 

1472 elif issubclass(type(data), Mapping) or isinstance(data, dict): 

1473 data_mapping = dict(cast("Mapping", data)) 

1474 

1475 is_signal = all(isinstance(i, Signal) for i in data_mapping.values()) 

1476 

1477 if is_signal: 

1478 for label, signal in data_mapping.items(): 

1479 signals[label] = signal_type( 

1480 signal.range, signal.domain, **settings 

1481 ) 

1482 else: 

1483 domain_unpacked, range_unpacked = zip( 

1484 *sorted(data_mapping.items()), strict=True 

1485 ) 

1486 for i, values_unpacked in enumerate(tsplit(range_unpacked)): 

1487 signals[str(i)] = signal_type( 

1488 values_unpacked, domain_unpacked, **settings 

1489 ) 

1490 elif is_pandas_installed(): 

1491 if isinstance(data, Series): 

1492 signals["0"] = signal_type(data, **settings) 

1493 elif isinstance(data, DataFrame): 

1494 domain_unpacked = as_float_array(data.index.values, dtype) # pyright: ignore 

1495 signals = { 

1496 label: signal_type( 

1497 data[label], 

1498 domain_unpacked, 

1499 **settings, 

1500 ) 

1501 for label in data 

1502 } 

1503 

1504 if domain is not None: 

1505 if isinstance(domain, KeysView): 

1506 domain = list(domain) 

1507 

1508 domain_array = as_float_array(domain, dtype) 

1509 

1510 for signal in signals.values(): 

1511 attest( 

1512 len(domain_array) == len(signal.domain), 

1513 'User "domain" length is not compatible with unpacked "signals"!', 

1514 ) 

1515 

1516 signal.domain = domain_array 

1517 

1518 signals = {str(label): signal for label, signal in signals.items()} 

1519 

1520 if labels is not None: 

1521 attest( 

1522 len(labels) == len(signals), 

1523 'User "labels" length is not compatible with unpacked "signals"!', 

1524 ) 

1525 

1526 if len(labels) != len(set(labels)): 

1527 labels = [f"{label} - {i}" for i, label in enumerate(labels)] 

1528 

1529 signals = { 

1530 str(labels[i]): signal for i, signal in enumerate(signals.values()) 

1531 } 

1532 

1533 for label in signals: 

1534 signals[label].name = label 

1535 

1536 if not signals: 

1537 signals = {"Undefined": Signal(name="Undefined")} 

1538 

1539 return signals 

1540 

1541 def fill_nan( 

1542 self, 

1543 method: Literal["Constant", "Interpolation"] | str = "Interpolation", 

1544 default: Real = 0, 

1545 ) -> MultiSignals: 

1546 """ 

1547 Fill NaNs in independent domain variable :math:`x` and corresponding 

1548 range variable :math:`y` using the specified method. 

1549 

1550 Parameters 

1551 ---------- 

1552 method 

1553 *Interpolation* method linearly interpolates through the NaNs, 

1554 *Constant* method replaces NaNs with ``default``. 

1555 default 

1556 Value to use with the *Constant* method. 

1557 

1558 Returns 

1559 ------- 

1560 :class:`colour.continuous.MultiSignals` 

1561 Multi-signals with NaN values filled. 

1562 

1563 Examples 

1564 -------- 

1565 >>> domain = np.arange(0, 10, 1) 

1566 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3) 

1567 >>> range_ += np.array([0, 10, 20]) 

1568 >>> multi_signals = MultiSignals(range_) 

1569 >>> multi_signals[3:7] = np.nan 

1570 >>> print(multi_signals) 

1571 [[ 0. 10. 20. 30.] 

1572 [ 1. 20. 30. 40.] 

1573 [ 2. 30. 40. 50.] 

1574 [ 3. nan nan nan] 

1575 [ 4. nan nan nan] 

1576 [ 5. nan nan nan] 

1577 [ 6. nan nan nan] 

1578 [ 7. 80. 90. 100.] 

1579 [ 8. 90. 100. 110.] 

1580 [ 9. 100. 110. 120.]] 

1581 >>> print(multi_signals.fill_nan()) 

1582 [[ 0. 10. 20. 30.] 

1583 [ 1. 20. 30. 40.] 

1584 [ 2. 30. 40. 50.] 

1585 [ 3. 40. 50. 60.] 

1586 [ 4. 50. 60. 70.] 

1587 [ 5. 60. 70. 80.] 

1588 [ 6. 70. 80. 90.] 

1589 [ 7. 80. 90. 100.] 

1590 [ 8. 90. 100. 110.] 

1591 [ 9. 100. 110. 120.]] 

1592 >>> multi_signals[3:7] = np.nan 

1593 >>> print(multi_signals.fill_nan(method="Constant")) 

1594 [[ 0. 10. 20. 30.] 

1595 [ 1. 20. 30. 40.] 

1596 [ 2. 30. 40. 50.] 

1597 [ 3. 0. 0. 0.] 

1598 [ 4. 0. 0. 0.] 

1599 [ 5. 0. 0. 0.] 

1600 [ 6. 0. 0. 0.] 

1601 [ 7. 80. 90. 100.] 

1602 [ 8. 90. 100. 110.] 

1603 [ 9. 100. 110. 120.]] 

1604 """ 

1605 

1606 method = validate_method(method, ("Interpolation", "Constant")) 

1607 

1608 for signal in self._signals.values(): 

1609 signal.fill_nan(method, default) 

1610 

1611 return self 

1612 

1613 @required("Pandas") 

1614 def to_dataframe(self) -> DataFrame: 

1615 """ 

1616 Convert the continuous signal to a *Pandas* :class:`pandas.DataFrame` 

1617 class instance. 

1618 

1619 Returns 

1620 ------- 

1621 :class:`pandas.DataFrame` 

1622 Continuous signal as a *Pandas* :class:`pandas.DataFrame` class 

1623 instance. 

1624 

1625 Examples 

1626 -------- 

1627 >>> if is_pandas_installed(): 

1628 ... domain = np.arange(0, 10, 1) 

1629 ... range_ = tstack([np.linspace(10, 100, 10)] * 3) 

1630 ... range_ += np.array([0, 10, 20]) 

1631 ... multi_signals = MultiSignals(range_) 

1632 ... print(multi_signals.to_dataframe()) # doctest: +SKIP 

1633 0 1 2 

1634 0.0 10.0 20.0 30.0 

1635 1.0 20.0 30.0 40.0 

1636 2.0 30.0 40.0 50.0 

1637 3.0 40.0 50.0 60.0 

1638 4.0 50.0 60.0 70.0 

1639 5.0 60.0 70.0 80.0 

1640 6.0 70.0 80.0 90.0 

1641 7.0 80.0 90.0 100.0 

1642 8.0 90.0 100.0 110.0 

1643 9.0 100.0 110.0 120.0 

1644 """ 

1645 

1646 return DataFrame( 

1647 data=self.range, 

1648 index=self.domain, 

1649 columns=self.labels, # pyright: ignore 

1650 )