Coverage for continuous/multi_signals.py: 65%

1"""

2 Multi-Signals

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

4

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

6

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

8 representing and operating on multiple continuous signals simultaneously,

9 supporting interpolation and extrapolation operations.

10

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

12"""

13

14 from __future__ import annotations

15

16 import typing

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

18

19 import numpy as np

20

21 from colour.constants import DTYPE_FLOAT_DEFAULT

22 from colour.continuous import AbstractContinuousFunction, Signal

23

24 if 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

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

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

55 from colour.utilities.documentation import is_documentation_building

56

57 if typing.TYPE_CHECKING or is_pandas_installed():

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

59 else: # 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

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