Coverage for continuous/abstract.py: 18%

1"""

2 Abstract Continuous Function

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

4

5 Define an abstract base class for continuous mathematical functions with

6 support for interpolation, extrapolation, and arithmetical operations:

7

8- :class:`colour.continuous.AbstractContinuousFunction`

9"""

10

11 from __future__ import annotations

12

13 import typing

14 from abc import ABC, abstractmethod

15 from copy import deepcopy

16

17 import numpy as np

18

19 if typing.TYPE_CHECKING:

20 from colour.hints import (

21 ArrayLike,

22 Callable,

23 DTypeFloat,

24 Generator,

25 Literal,

26 NDArrayFloat,

27 ProtocolExtrapolator,

28 ProtocolInterpolator,

29 Real,

30 Self,

31 Type,

32 )

33

34 from colour.utilities import (

35 MixinCallback,

36 as_float,

37 attest,

38 closest,

39 is_uniform,

40 optional,

41)

42

43 __author__ = "Colour Developers"

44 __copyright__ = "Copyright 2013 Colour Developers"

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

46 __maintainer__ = "Colour Developers"

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

48 __status__ = "Production"

49

50 __all__ = [

51 "AbstractContinuousFunction",

52]

53

54

55 class AbstractContinuousFunction(ABC, MixinCallback):

56 """

57 Define the base class for an abstract continuous function.

58

59 This is an :class:`ABCMeta` abstract class that must be inherited by

60 sub-classes.

61

62 The sub-classes are expected to implement the

63 :meth:`colour.continuous.AbstractContinuousFunction.function` method so

64 that evaluating the function for any independent domain variable

65 :math:`x \\in\\mathbb{R}` returns a corresponding range variable

66 :math:`y \\in\\mathbb{R}`. A conventional implementation adopts an

67 interpolating function encapsulated inside an extrapolating function.

68 The resulting function independent domain, stored as discrete values in

69 the :attr:`colour.continuous.AbstractContinuousFunction.domain` attribute,

70 corresponds with the function dependent and already known range stored in

71 the :attr:`colour.continuous.AbstractContinuousFunction.range` property.

72

73 Parameters

74 ----------

75 name

76 Continuous function name.

77

78 Attributes

79 ----------

80 - :attr:`~colour.continuous.AbstractContinuousFunction.name`

81 - :attr:`~colour.continuous.AbstractContinuousFunction.dtype`

82 - :attr:`~colour.continuous.AbstractContinuousFunction.domain`

83 - :attr:`~colour.continuous.AbstractContinuousFunction.range`

84 - :attr:`~colour.continuous.AbstractContinuousFunction.interpolator`

85 - :attr:`~colour.continuous.\

86 AbstractContinuousFunction.interpolator_kwargs`

87 - :attr:`~colour.continuous.AbstractContinuousFunction.extrapolator`

88 - :attr:`~colour.continuous.\

89 AbstractContinuousFunction.extrapolator_kwargs`

90 - :attr:`~colour.continuous.AbstractContinuousFunction.function`

91

92 Methods

93 -------

94 - :meth:`~colour.continuous.AbstractContinuousFunction.__init__`

95 - :meth:`~colour.continuous.AbstractContinuousFunction.__str__`

96 - :meth:`~colour.continuous.AbstractContinuousFunction.__repr__`

97 - :meth:`~colour.continuous.AbstractContinuousFunction.__hash__`

98 - :meth:`~colour.continuous.AbstractContinuousFunction.__getitem__`

99 - :meth:`~colour.continuous.AbstractContinuousFunction.__setitem__`

100 - :meth:`~colour.continuous.AbstractContinuousFunction.__contains__`

101 - :meth:`~colour.continuous.AbstractContinuousFunction.__iter__`

102 - :meth:`~colour.continuous.AbstractContinuousFunction.__len__`

103 - :meth:`~colour.continuous.AbstractContinuousFunction.__eq__`

104 - :meth:`~colour.continuous.AbstractContinuousFunction.__ne__`

105 - :meth:`~colour.continuous.AbstractContinuousFunction.__iadd__`

106 - :meth:`~colour.continuous.AbstractContinuousFunction.__add__`

107 - :meth:`~colour.continuous.AbstractContinuousFunction.__isub__`

108 - :meth:`~colour.continuous.AbstractContinuousFunction.__sub__`

109 - :meth:`~colour.continuous.AbstractContinuousFunction.__imul__`

110 - :meth:`~colour.continuous.AbstractContinuousFunction.__mul__`

111 - :meth:`~colour.continuous.AbstractContinuousFunction.__idiv__`

112 - :meth:`~colour.continuous.AbstractContinuousFunction.__div__`

113 - :meth:`~colour.continuous.AbstractContinuousFunction.__ipow__`

114 - :meth:`~colour.continuous.AbstractContinuousFunction.__pow__`

115 - :meth:`~colour.continuous.AbstractContinuousFunction.\

116 arithmetical_operation`

117 - :meth:`~colour.continuous.AbstractContinuousFunction.fill_nan`

118 - :meth:`~colour.continuous.AbstractContinuousFunction.domain_distance`

119 - :meth:`~colour.continuous.AbstractContinuousFunction.is_uniform`

120 - :meth:`~colour.continuous.AbstractContinuousFunction.copy`

121 """

122

123 def __init__(self, name: str | None = None) -> None:

124 super().__init__()

125

126 self._name: str = f"{self.__class__.__name__} ({id(self)})"

127 self.name = optional(name, self._name)

128

129 @property

130 def name(self) -> str:

131 """

132 Getter and setter for the abstract continuous function name.

133

134 Parameters

135 ----------

136 value

137 Value to set the abstract continuous function name with.

138

139 Returns

140 -------

141 :class:`str`

142 Abstract continuous function name.

143 """

144

145 return self._name

146

147 @name.setter

148 def name(self, value: str) -> None:

149 """Setter for the **self.name** property."""

150

151 attest(

152 isinstance(value, str),

153 f'"name" property: "{value}" type is not "str"!',

154 )

155

156 self._name = value

157

158 @property

159 @abstractmethod

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

161 """

162 Getter and setter for the abstract continuous function dtype.

163

164 This property must be reimplemented by sub-classes.

165

166 Parameters

167 ----------

168 value

169 Value to set the abstract continuous function dtype with.

170

171 Returns

172 -------

173 Type[DTypeFloat]

174 Abstract continuous function dtype.

175 """

176

177 ... # pragma: no cover

178

179 @dtype.setter

180 @abstractmethod

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

182 """

183 Setter for the **self.dtype** property, must be reimplemented by

184 sub-classes.

185 """

186

187 ... # pragma: no cover

188

189 @property

190 @abstractmethod

191 def domain(self) -> NDArrayFloat:

192 """

193 Getter and setter for the abstract continuous function's independent

194 domain variable :math:`x`.

195

196 This property must be reimplemented by sub-classes.

197

198 Parameters

199 ----------

200 value

201 Value to set the abstract continuous function independent domain

202 variable :math:`x` with.

203

204 Returns

205 -------

206 :class:`numpy.ndarray`

207 Abstract continuous function independent domain variable

208 :math:`x`.

209 """

210

211 ... # pragma: no cover

212

213 @domain.setter

214 @abstractmethod

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

216 """

217 Setter for the **self.domain** property, must be reimplemented by

218 sub-classes.

219 """

220

221 ... # pragma: no cover

222

223 @property

224 @abstractmethod

225 def range(self) -> NDArrayFloat:

226 """

227 Getter and setter for the abstract continuous function's range

228 variable :math:`y`.

229

230 This property must be reimplemented by sub-classes.

231

232 Parameters

233 ----------

234 value

235 Value to set the abstract continuous function's range variable

236 :math:`y` with.

237

238 Returns

239 -------

240 :class:`numpy.ndarray`

241 Abstract continuous function's range variable :math:`y`.

242 """

243

244 ... # pragma: no cover

245

246 @range.setter

247 @abstractmethod

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

249 """

250 Setter for the **self.range** property, must be reimplemented by

251 sub-classes.

252 """

253

254 ... # pragma: no cover

255

256 @property

257 @abstractmethod

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

259 """

260 Getter and setter for the abstract continuous function interpolator

261 type.

262

263 This property must be reimplemented by sub-classes.

264

265 Parameters

266 ----------

267 value

268 Value to set the abstract continuous function interpolator type

269 with.

270

271 Returns

272 -------

273 Type[ProtocolInterpolator]

274 Abstract continuous function interpolator type.

275 """

276

277 ... # pragma: no cover

278

279 @interpolator.setter

280 @abstractmethod

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

282 """

283 Setter for the **self.interpolator** property, must be reimplemented by

284 sub-classes.

285 """

286

287 ... # pragma: no cover

288

289 @property

290 @abstractmethod

291 def interpolator_kwargs(self) -> dict:

292 """

293 Getter and setter for the interpolator instantiation time arguments.

294

295 This property must be reimplemented by sub-classes.

296

297 Parameters

298 ----------

299 value

300 Value to set the abstract continuous function interpolator

301 instantiation time arguments to.

302

303 Returns

304 -------

305 :class:`dict`

306 Abstract continuous function interpolator instantiation time

307 arguments.

308 """

309

310 ... # pragma: no cover

311

312 @interpolator_kwargs.setter

313 @abstractmethod

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

315 """

316 Setter for the **self.interpolator_kwargs** property, must be

317 reimplemented by sub-classes.

318 """

319

320 ... # pragma: no cover

321

322 @property

323 @abstractmethod

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

325 """

326 Getter and setter for the abstract continuous function extrapolator

327 type.

328

329 This property must be reimplemented by sub-classes.

330

331 Parameters

332 ----------

333 value

334 Value to set the abstract continuous function extrapolator type

335 with.

336

337 Returns

338 -------

339 Type[ProtocolExtrapolator]

340 Abstract continuous function extrapolator type.

341 """

342

343 ... # pragma: no cover

344

345 @extrapolator.setter

346 @abstractmethod

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

348 """

349 Setter for the **self.extrapolator** property, must be reimplemented by

350 sub-classes.

351 """

352

353 ... # pragma: no cover

354

355 @property

356 @abstractmethod

357 def extrapolator_kwargs(self) -> dict:

358 """

359 Getter and setter for the abstract continuous function extrapolator

360 instantiation time arguments.

361

362 This property must be reimplemented by sub-classes.

363

364 Parameters

365 ----------

366 value

367 Value to set the abstract continuous function extrapolator

368 instantiation time arguments to.

369

370 Returns

371 -------

372 :class:`dict`

373 Abstract continuous function extrapolator instantiation time

374 arguments.

375 """

376

377 ... # pragma: no cover

378

379 @extrapolator_kwargs.setter

380 @abstractmethod

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

382 """

383 Setter for the **self.extrapolator_kwargs** property, must be

384 reimplemented by sub-classes.

385 """

386

387 ... # pragma: no cover

388

389 @property

390 @abstractmethod

391 def function(self) -> Callable:

392 """

393 Getter for the abstract continuous function callable.

394

395 This property must be reimplemented by sub-classes.

396

397 Returns

398 -------

399 Callable

400 Abstract continuous function callable.

401 """

402

403 ... # pragma: no cover

404

405 @abstractmethod

406 def __str__(self) -> str:

407 """

408 Return a formatted string representation of the abstract continuous

409 function.

410

411 This method must be reimplemented by sub-classes.

412

413 Returns

414 -------

415 :class:`str`

416 Formatted string representation.

417 """

418

419 return super().__repr__() # pragma: no cover

420

421 @abstractmethod

422 def __repr__(self) -> str:

423 """

424 Return an evaluable string representation of the abstract continuous

425 function, must be reimplemented by sub-classes.

426

427 Returns

428 -------

429 :class:`str`

430 Evaluable string representation.

431 """

432

433 return super().__repr__() # pragma: no cover

434

435 @abstractmethod

436 def __hash__(self) -> int:

437 """

438 Compute the hash of the abstract continuous function.

439

440 Returns

441 -------

442 :class:`int`

443 Object hash.

444 """

445

446 ... # pragma: no cover

447

448 @abstractmethod

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

450 """

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

452 independent domain variable :math:`x`.

453

454 This abstract method must be reimplemented by sub-classes.

455

456 Parameters

457 ----------

458 x

459 Independent domain variable :math:`x`.

460

461 Returns

462 -------

463 :class:`numpy.ndarray`

464 Variable :math:`y` range value.

465 """

466

467 ... # pragma: no cover

468

469 @abstractmethod

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

471 """

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

473 independent domain variable :math:`x`.

474

475 This abstract method must be reimplemented by sub-classes.

476

477 Parameters

478 ----------

479 x

480 Independent domain variable :math:`x`.

481 y

482 Corresponding range variable :math:`y`.

483 """

484

485 ... # pragma: no cover

486

487 @abstractmethod

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

489 """

490 Determine whether the abstract continuous function contains the

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

492

493 This abstract method must be reimplemented by sub-classes.

494

495 Parameters

496 ----------

497 x

498 Independent domain variable :math:`x`.

499

500 Returns

501 -------

502 :class:`bool`

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

504 """

505

506 ... # pragma: no cover

507

508 def __iter__(self) -> Generator:

509 """

510 Return a generator for the abstract continuous function.

511

512 Yields

513 ------

514 Generator

515 Abstract continuous function generator.

516 """

517

518 yield from np.column_stack([self.domain, self.range])

519

520 def __len__(self) -> int:

521 """

522 Return the number of elements in the abstract continuous function's

523 independent domain variable :math:`x`.

524

525 Returns

526 -------

527 :class:`int`

528 Number of elements in the independent domain variable :math:`x`.

529 """

530

531 return len(self.domain)

532

533 @abstractmethod

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

535 """

536 Determine whether the abstract continuous function equals the specified

537 object.

538

539 This abstract method must be reimplemented by sub-classes.

540

541 Parameters

542 ----------

543 other

544 Object to determine for equality with the abstract continuous function.

545

546 Returns

547 -------

548 :class:`bool`

549 Whether the specified object is equal to the abstract continuous

550 function.

551 """

552

553 ... # pragma: no cover

554

555 @abstractmethod

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

557 """

558 Determine whether the abstract continuous function is not equal to the

559 specified object.

560

561 This method must be reimplemented by sub-classes.

562

563 Parameters

564 ----------

565 other

566 Object to determine whether it is not equal to the abstract continuous

567 function.

568

569 Returns

570 -------

571 :class:`bool`

572 Whether the specified object is not equal to the abstract

573 continuous function.

574 """

575

576 ... # pragma: no cover

577

578 def __add__(self, a: ArrayLike | Self) -> Self:

579 """

580 Implement support for addition.

581

582 Parameters

583 ----------

584 a

585 Variable :math:`a` to add to the continuous function.

586

587 Returns

588 -------

589 :class:`colour.continuous.AbstractContinuousFunction`

590 Abstract continuous function with the specified variable

591 added.

592 """

593

594 return self.arithmetical_operation(a, "+")

595

596 def __iadd__(self, a: ArrayLike | Self) -> Self:

597 """

598 Implement support for in-place addition.

599

600 Parameters

601 ----------

602 a

603 Variable :math:`a` to add in-place to the abstract continuous

604 function.

605

606 Returns

607 -------

608 :class:`colour.continuous.AbstractContinuousFunction`

609 Abstract continuous function with in-place addition applied.

610 """

611

612 return self.arithmetical_operation(a, "+", True)

613

614 def __sub__(self, a: ArrayLike | Self) -> Self:

615 """

616 Implement support for subtraction.

617

618 Parameters

619 ----------

620 a

621 Variable :math:`a` to subtract from the continuous function.

622

623 Returns

624 -------

625 :class:`colour.continuous.AbstractContinuousFunction`

626 Abstract continuous function with the specified variable

627 subtracted.

628 """

629

630 return self.arithmetical_operation(a, "-")

631

632 def __isub__(self, a: ArrayLike | Self) -> Self:

633 """

634 Implement support for in-place subtraction.

635

636 Parameters

637 ----------

638 a

639 Variable :math:`a` to subtract in-place from the abstract continuous

640 function.

641

642 Returns

643 -------

644 :class:`colour.continuous.AbstractContinuousFunction`

645 Abstract continuous function with in-place subtraction applied.

646 """

647

648 return self.arithmetical_operation(a, "-", True)

649

650 def __mul__(self, a: ArrayLike | Self) -> Self:

651 """

652 Implement support for multiplication.

653

654 Parameters

655 ----------

656 a

657 Variable :math:`a` to multiply the continuous function by.

658

659 Returns

660 -------

661 :class:`colour.continuous.AbstractContinuousFunction`

662 Abstract continuous function with the specified variable

663 multiplied.

664 """

665

666 return self.arithmetical_operation(a, "*")

667

668 def __imul__(self, a: ArrayLike | Self) -> Self:

669 """

670 Implement support for in-place multiplication.

671

672 Parameters

673 ----------

674 a

675 Variable :math:`a` to multiply in-place by the abstract continuous

676 function.

677

678 Returns

679 -------

680 :class:`colour.continuous.AbstractContinuousFunction`

681 Abstract continuous function with in-place multiplication applied.

682 """

683

684 return self.arithmetical_operation(a, "*", True)

685

686 def __div__(self, a: ArrayLike | Self) -> Self:

687 """

688 Implement support for division.

689

690 Parameters

691 ----------

692 a

693 Variable :math:`a` to divide the continuous function by.

694

695 Returns

696 -------

697 :class:`colour.continuous.AbstractContinuousFunction`

698 Abstract continuous function with the specified variable

699 divided.

700 """

701

702 return self.arithmetical_operation(a, "/")

703

704 def __idiv__(self, a: ArrayLike | Self) -> Self:

705 """

706 Implement support for in-place division.

707

708 Parameters

709 ----------

710 a

711 Variable :math:`a` to divide in-place by the abstract continuous

712 function.

713

714 Returns

715 -------

716 :class:`colour.continuous.AbstractContinuousFunction`

717 Abstract continuous function with in-place division applied.

718 """

719

720 return self.arithmetical_operation(a, "/", True)

721

722 __itruediv__ = __idiv__

723 __truediv__ = __div__

724

725 def __pow__(self, a: ArrayLike | Self) -> Self:

726 """

727 Implement support for exponentiation.

728

729 Parameters

730 ----------

731 a

732 Variable :math:`a` to raise the continuous function to the power of.

733

734 Returns

735 -------

736 :class:`colour.continuous.AbstractContinuousFunction`

737 Abstract continuous function with the specified variable

738 exponentiated.

739 """

740

741 return self.arithmetical_operation(a, "**")

742

743 def __ipow__(self, a: ArrayLike | Self) -> Self:

744 """

745 Implement support for in-place exponentiation.

746

747 Parameters

748 ----------

749 a

750 Variable :math:`a` to raise in-place the abstract continuous

751 function to the power of.

752

753 Returns

754 -------

755 :class:`colour.continuous.AbstractContinuousFunction`

756 Abstract continuous function with in-place exponentiation applied.

757 """

758

759 return self.arithmetical_operation(a, "**", True)

760

761 @abstractmethod

762 def arithmetical_operation(

763 self,

764 a: ArrayLike | Self,

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

766 in_place: bool = False,

767 ) -> Self:

768 """

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

770 either on a copy or in-place.

771

772 This method must be reimplemented by sub-classes.

773

774 Parameters

775 ----------

776 a

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

778 another continuous function instance.

779 operation

780 Operation to perform.

781 in_place

782 Operation happens in place.

783

784 Returns

785 -------

786 :class:`colour.continuous.AbstractContinuousFunction`

787 Abstract continuous function.

788 """

789

790 ... # pragma: no cover

791

792 @abstractmethod

793 def fill_nan(

794 self,

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

796 default: Real = 0,

797 ) -> Self:

798 """

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

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

801

802 This abstract method must be reimplemented by sub-classes.

803

804 Parameters

805 ----------

806 method

807 *Interpolation* method linearly interpolates through the NaNs,

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

809 default

810 Value to use with the *Constant* method.

811

812 Returns

813 -------

814 :class:`colour.continuous.AbstractContinuousFunction`

815 NaNs filled abstract continuous function.

816 """

817

818 ... # pragma: no cover

819

820 def domain_distance(self, a: ArrayLike) -> NDArrayFloat:

821 """

822 Return the Euclidean distance between specified array and the closest

823 element of the independent domain :math:`x`.

824

825 Parameters

826 ----------

827 a

828 Variable :math:`a` to compute the Euclidean distance with the

829 independent domain variable :math:`x`.

830

831 Returns

832 -------

833 :class:`numpy.ndarray`

834 Euclidean distance between independent domain variable :math:`x`

835 and specified variable :math:`a`.

836 """

837

838 n = closest(self.domain, a)

839

840 return as_float(np.abs(a - n))

841

842 def is_uniform(self) -> bool:

843 """

844 Return whether the independent domain variable :math:`x` is uniform.

845

846 Returns

847 -------

848 :class:`bool`

849 Whether the independent domain variable :math:`x` is uniform.

850 """

851

852 return is_uniform(self.domain)

853

854 def copy(self) -> Self:

855 """

856 Return a copy of the sub-class instance.

857

858 Returns

859 -------

860 :class:`colour.continuous.AbstractContinuousFunction`

861 Copy of the abstract continuous function.

862 """

863

864 return deepcopy(self)