Coverage for colour/algebra/common.py: 100%

156 statements  

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

1""" 

2Common Utilities 

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

4 

5Define common algebra utility objects that do not fall within any specific 

6category. 

7 

8The *Common* sub-package provides general-purpose mathematical and 

9computational utilities used throughout the colour science library. 

10""" 

11 

12from __future__ import annotations 

13 

14import functools 

15import typing 

16 

17import numpy as np 

18 

19if typing.TYPE_CHECKING: 

20 from colour.hints import ( 

21 Any, 

22 ArrayLike, 

23 Callable, 

24 DTypeFloat, 

25 NDArray, 

26 NDArrayFloat, 

27 Self, 

28 Tuple, 

29 ) 

30 

31from colour.constants import EPSILON 

32from colour.hints import Literal, cast 

33from colour.utilities import ( 

34 as_float, 

35 as_float_array, 

36 optional, 

37 runtime_warning, 

38 tsplit, 

39 validate_method, 

40) 

41 

42__author__ = "Colour Developers" 

43__copyright__ = "Copyright 2013 Colour Developers" 

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

45__maintainer__ = "Colour Developers" 

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

47__status__ = "Production" 

48 

49__all__ = [ 

50 "get_sdiv_mode", 

51 "set_sdiv_mode", 

52 "sdiv_mode", 

53 "sdiv", 

54 "is_spow_enabled", 

55 "set_spow_enable", 

56 "spow_enable", 

57 "spow", 

58 "normalise_vector", 

59 "normalise_maximum", 

60 "vecmul", 

61 "euclidean_distance", 

62 "manhattan_distance", 

63 "linear_conversion", 

64 "linstep_function", 

65 "lerp", 

66 "smoothstep_function", 

67 "smooth", 

68 "is_identity", 

69 "eigen_decomposition", 

70] 

71 

72_SDIV_MODE: Literal[ 

73 "Numpy", 

74 "Ignore", 

75 "Warning", 

76 "Raise", 

77 "Ignore Zero Conversion", 

78 "Warning Zero Conversion", 

79 "Ignore Limit Conversion", 

80 "Warning Limit Conversion", 

81 "Replace With Epsilon", 

82 "Warning Replace With Epsilon", 

83] = "Ignore Zero Conversion" 

84""" 

85Global variable storing the current *Colour* safe division function mode. 

86""" 

87 

88 

89def get_sdiv_mode() -> Literal[ 

90 "Numpy", 

91 "Ignore", 

92 "Warning", 

93 "Raise", 

94 "Ignore Zero Conversion", 

95 "Warning Zero Conversion", 

96 "Ignore Limit Conversion", 

97 "Warning Limit Conversion", 

98 "Replace With Epsilon", 

99 "Warning Replace With Epsilon", 

100]: 

101 """ 

102 Return the current *Colour* safe division mode. 

103 

104 Returns 

105 ------- 

106 :class:`str` 

107 Current *Colour* safe division mode. See 

108 :func:`colour.algebra.sdiv` definition for an explanation of 

109 the possible modes. 

110 

111 Examples 

112 -------- 

113 >>> with sdiv_mode("Numpy"): 

114 ... get_sdiv_mode() 

115 'numpy' 

116 >>> with sdiv_mode("Ignore Zero Conversion"): 

117 ... get_sdiv_mode() 

118 'ignore zero conversion' 

119 """ 

120 

121 return _SDIV_MODE 

122 

123 

124def set_sdiv_mode( 

125 mode: ( 

126 Literal[ 

127 "Numpy", 

128 "Ignore", 

129 "Warning", 

130 "Raise", 

131 "Ignore Zero Conversion", 

132 "Warning Zero Conversion", 

133 "Ignore Limit Conversion", 

134 "Warning Limit Conversion", 

135 "Replace With Epsilon", 

136 "Warning Replace With Epsilon", 

137 ] 

138 | str 

139 ), 

140) -> None: 

141 """ 

142 Set the *Colour* safe division function mode. 

143 

144 Parameters 

145 ---------- 

146 mode 

147 *Colour* safe division mode. See :func:`colour.algebra.sdiv` 

148 definition for an explanation of the possible modes. 

149 

150 Examples 

151 -------- 

152 >>> with sdiv_mode(get_sdiv_mode()): 

153 ... print(get_sdiv_mode()) 

154 ... set_sdiv_mode("Raise") 

155 ... print(get_sdiv_mode()) 

156 ignore zero conversion 

157 raise 

158 """ 

159 

160 global _SDIV_MODE # noqa: PLW0603 

161 

162 _SDIV_MODE = cast( 

163 "Literal['Numpy', 'Ignore', 'Warning', 'Raise', " # pyright: ignore 

164 "'Ignore Zero Conversion', 'Warning Zero Conversion', " 

165 "'Ignore Limit Conversion', 'Warning Limit Conversion', " 

166 "'Replace With Epsilon', 'Warning Replace With Epsilon']", 

167 validate_method( 

168 mode, 

169 ( 

170 "Numpy", 

171 "Ignore", 

172 "Warning", 

173 "Raise", 

174 "Ignore Zero Conversion", 

175 "Warning Zero Conversion", 

176 "Ignore Limit Conversion", 

177 "Warning Limit Conversion", 

178 "Replace With Epsilon", 

179 "Warning Replace With Epsilon", 

180 ), 

181 ), 

182 ) 

183 

184 

185class sdiv_mode: 

186 """ 

187 Context manager and decorator for temporarily modifying *Colour* safe 

188 division function mode. 

189 

190 This utility enables temporary modification of the safe division behavior 

191 in *Colour* computations, allowing control over how division operations 

192 handle edge cases such as division by zero or near-zero values. The 

193 context manager ensures automatic restoration of the original mode upon 

194 exit. 

195 

196 Parameters 

197 ---------- 

198 mode 

199 *Colour* safe division function mode, see :func:`colour.algebra.sdiv` 

200 definition for an explanation about the possible modes. 

201 """ 

202 

203 def __init__( 

204 self, 

205 mode: ( 

206 Literal[ 

207 "Numpy", 

208 "Ignore", 

209 "Warning", 

210 "Raise", 

211 "Ignore Zero Conversion", 

212 "Warning Zero Conversion", 

213 "Ignore Limit Conversion", 

214 "Warning Limit Conversion", 

215 "Replace With Epsilon", 

216 "Warning Replace With Epsilon", 

217 ] 

218 | None 

219 ) = None, 

220 ) -> None: 

221 self._mode = optional(mode, get_sdiv_mode()) 

222 self._previous_mode = get_sdiv_mode() 

223 

224 def __enter__(self) -> Self: 

225 """ 

226 Set the *Colour* safe/symmetrical power function state to the 

227 specified value upon entering the context manager. 

228 """ 

229 

230 set_sdiv_mode(self._mode) 

231 

232 return self 

233 

234 def __exit__(self, *args: Any) -> None: 

235 """ 

236 Restore the *Colour* safe / symmetrical power function enabled state 

237 upon exiting the context manager. 

238 """ 

239 

240 set_sdiv_mode(self._previous_mode) 

241 

242 def __call__(self, function: Callable) -> Callable: 

243 """ 

244 Call the wrapped definition. 

245 

246 The decorator applies the specified spectral power distribution 

247 state to the wrapped function during its execution. 

248 """ 

249 

250 @functools.wraps(function) 

251 def wrapper(*args: Any, **kwargs: Any) -> Any: 

252 with self: 

253 return function(*args, **kwargs) 

254 

255 return wrapper 

256 

257 

258def sdiv(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: 

259 """ 

260 Perform safe division of array :math:`a` by array :math:`b` while 

261 handling zero-division cases. 

262 

263 Avoid NaN and +/- inf generation when array :math:`b` contains zero 

264 values. The zero-division handling behaviour is controlled by the 

265 :func:`colour.algebra.set_sdiv_mode` definition or the 

266 :func:`sdiv_mode` context manager. The following modes are available: 

267 

268 - ``Numpy``: The current *Numpy* zero-division handling occurs. 

269 - ``Ignore``: Zero-division occurs silently. 

270 - ``Warning``: Zero-division occurs with a warning. 

271 - ``Ignore Zero Conversion``: Zero-division occurs silently and NaNs 

272 or +/- infs values are converted to zeros. See 

273 :func:`numpy.nan_to_num` definition for more details. 

274 - ``Warning Zero Conversion``: Zero-division occurs with a warning 

275 and NaNs or +/- infs values are converted to zeros. See 

276 :func:`numpy.nan_to_num` definition for more details. 

277 - ``Ignore Limit Conversion``: Zero-division occurs silently and 

278 NaNs or +/- infs values are converted to zeros or the largest +/- 

279 finite floating point values representable by the division result 

280 :class:`numpy.dtype`. See :func:`numpy.nan_to_num` definition for 

281 more details. 

282 - ``Warning Limit Conversion``: Zero-division occurs with a warning 

283 and NaNs or +/- infs values are converted to zeros or the largest 

284 +/- finite floating point values representable by the division 

285 result :class:`numpy.dtype`. 

286 - ``Replace With Epsilon``: Zero-division is avoided by replacing 

287 zero denominators with the machine epsilon value from 

288 :attr:`colour.constants.EPSILON`. 

289 - ``Warning Replace With Epsilon``: Zero-division is avoided by 

290 replacing zero denominators with the machine epsilon value from 

291 :attr:`colour.constants.EPSILON` with a warning. 

292 

293 Parameters 

294 ---------- 

295 a 

296 Numerator array :math:`a`. 

297 b 

298 Denominator array :math:`b`. 

299 

300 Returns 

301 ------- 

302 :class:`np.float` or :class:`numpy.ndarray` 

303 Array :math:`a` safely divided by :math:`b`. 

304 

305 Examples 

306 -------- 

307 >>> a = np.array([0, 1, 2]) 

308 >>> b = np.array([2, 1, 0]) 

309 >>> sdiv(a, b) 

310 array([ 0., 1., 0.]) 

311 >>> try: 

312 ... with sdiv_mode("Raise"): 

313 ... sdiv(a, b) 

314 ... except Exception as error: 

315 ... error # doctest: +ELLIPSIS 

316 FloatingPointError('divide by zero encountered in...divide') 

317 >>> with sdiv_mode("Ignore Zero Conversion"): 

318 ... sdiv(a, b) 

319 array([ 0., 1., 0.]) 

320 >>> with sdiv_mode("Warning Zero Conversion"): 

321 ... sdiv(a, b) 

322 array([ 0., 1., 0.]) 

323 >>> with sdiv_mode("Ignore Limit Conversion"): 

324 ... sdiv(a, b) # doctest: +SKIP 

325 array([ 0.00000000e+000, 1.00000000e+000, 1.79769313e+308]) 

326 >>> with sdiv_mode("Warning Limit Conversion"): 

327 ... sdiv(a, b) # doctest: +SKIP 

328 array([ 0.00000000e+000, 1.00000000e+000, 1.79769313e+308]) 

329 >>> with sdiv_mode("Replace With Epsilon"): 

330 ... sdiv(a, b) # doctest: +ELLIPSIS 

331 array([ 0.00000000e+00, 1.00000000e+00, ...]) 

332 >>> with sdiv_mode("Warning Replace With Epsilon"): 

333 ... sdiv(a, b) # doctest: +ELLIPSIS 

334 array([ 0.00000000e+00, 1.00000000e+00, ...]) 

335 """ 

336 

337 a = as_float_array(a) 

338 b = as_float_array(b) 

339 

340 mode = validate_method( 

341 _SDIV_MODE, 

342 ( 

343 "Numpy", 

344 "Ignore", 

345 "Warning", 

346 "Raise", 

347 "Ignore Zero Conversion", 

348 "Warning Zero Conversion", 

349 "Ignore Limit Conversion", 

350 "Warning Limit Conversion", 

351 "Replace With Epsilon", 

352 "Warning Replace With Epsilon", 

353 ), 

354 ) 

355 

356 if mode == "numpy": 

357 c = a / b 

358 elif mode == "ignore": 

359 with np.errstate(divide="ignore", invalid="ignore"): 

360 c = a / b 

361 elif mode == "warning": 

362 with np.errstate(divide="warn", invalid="warn"): 

363 c = a / b 

364 elif mode == "raise": 

365 with np.errstate(divide="raise", invalid="raise"): 

366 c = a / b 

367 elif mode == "ignore zero conversion": 

368 with np.errstate(divide="ignore", invalid="ignore"): 

369 c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0) 

370 elif mode == "warning zero conversion": 

371 with np.errstate(divide="warn", invalid="warn"): 

372 c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0) 

373 elif mode == "ignore limit conversion": 

374 with np.errstate(divide="ignore", invalid="ignore"): 

375 c = np.nan_to_num(a / b) 

376 elif mode == "warning limit conversion": 

377 with np.errstate(divide="warn", invalid="warn"): 

378 c = np.nan_to_num(a / b) 

379 elif mode == "replace with epsilon": 

380 b = np.where(b == 0, EPSILON, b) 

381 c = a / b 

382 elif mode == "warning replace with epsilon": 

383 if np.any(b == 0): 

384 runtime_warning("Zero(s) detected in denominator, replacing with EPSILON.") 

385 b = np.where(b == 0, EPSILON, b) 

386 c = a / b 

387 

388 return c 

389 

390 

391_SPOW_ENABLED: bool = True 

392""" 

393Global variable storing the current *Colour* safe / symmetrical power function 

394enabled state. 

395""" 

396 

397 

398def is_spow_enabled() -> bool: 

399 """ 

400 Return whether *Colour* safe / symmetrical power function is enabled. 

401 

402 Returns 

403 ------- 

404 :class:`bool` 

405 Whether *Colour* safe / symmetrical power function is enabled. 

406 

407 Examples 

408 -------- 

409 >>> with spow_enable(False): 

410 ... is_spow_enabled() 

411 False 

412 >>> with spow_enable(True): 

413 ... is_spow_enabled() 

414 True 

415 """ 

416 

417 return _SPOW_ENABLED 

418 

419 

420def set_spow_enable(enable: bool) -> None: 

421 """ 

422 Set the *Colour* safe/symmetrical power function enabled state. 

423 

424 Parameters 

425 ---------- 

426 enable 

427 Whether to enable the *Colour* safe/symmetrical power function. 

428 

429 Examples 

430 -------- 

431 >>> with spow_enable(is_spow_enabled()): 

432 ... print(is_spow_enabled()) 

433 ... set_spow_enable(False) 

434 ... print(is_spow_enabled()) 

435 True 

436 False 

437 """ 

438 

439 global _SPOW_ENABLED # noqa: PLW0603 

440 

441 _SPOW_ENABLED = enable 

442 

443 

444class spow_enable: 

445 """ 

446 Context manager and decorator for temporarily setting the state of *Colour* 

447 safe/symmetrical power function. 

448 

449 This utility provides both context manager and decorator functionality to 

450 temporarily enable or disable the safe/symmetrical power function used 

451 throughout the *Colour* library. When enabled, power operations use a 

452 symmetrical implementation that handles negative values appropriately for 

453 colour science computations. 

454 

455 Parameters 

456 ---------- 

457 enable 

458 Whether to enable or disable the *Colour* safe/symmetrical power 

459 function for the duration of the context or decorated function. 

460 """ 

461 

462 def __init__(self, enable: bool) -> None: 

463 self._enable = enable 

464 self._previous_state = is_spow_enabled() 

465 

466 def __enter__(self) -> Self: 

467 """ 

468 Set the *Colour* safe / symmetrical power function enabled state 

469 upon entering the context manager. 

470 """ 

471 

472 set_spow_enable(self._enable) 

473 

474 return self 

475 

476 def __exit__(self, *args: Any) -> None: 

477 """ 

478 Set the *Colour* safe / symmetrical power function enabled state 

479 upon exiting the context manager. 

480 """ 

481 

482 set_spow_enable(self._previous_state) 

483 

484 def __call__(self, function: Callable) -> Callable: 

485 """Call the wrapped definition.""" 

486 

487 @functools.wraps(function) 

488 def wrapper(*args: Any, **kwargs: Any) -> Any: 

489 with self: 

490 return function(*args, **kwargs) 

491 

492 return wrapper 

493 

494 

495@typing.overload 

496def spow(a: float | DTypeFloat, p: float | DTypeFloat) -> DTypeFloat: ... 

497@typing.overload 

498def spow(a: NDArray, p: ArrayLike) -> NDArrayFloat: ... 

499@typing.overload 

500def spow(a: ArrayLike, p: NDArray) -> NDArrayFloat: ... 

501@typing.overload 

502def spow(a: ArrayLike, p: ArrayLike) -> DTypeFloat | NDArrayFloat: ... 

503def spow(a: ArrayLike, p: ArrayLike) -> DTypeFloat | NDArrayFloat: 

504 """ 

505 Raise specified array :math:`a` to the power :math:`p` as follows: 

506 :math:`\\text{sign}(a) \\cdot |a|^p`. 

507 

508 This definition avoids NaN generation when array :math:`a` is negative 

509 and power :math:`p` is fractional. This behaviour can be enabled or 

510 disabled with the :func:`colour.algebra.set_spow_enable` definition or 

511 with the :func:`spow_enable` context manager. 

512 

513 Parameters 

514 ---------- 

515 a 

516 Array :math:`a`. 

517 p 

518 Power :math:`p`. 

519 

520 Returns 

521 ------- 

522 :class:`np.float` or :class:`numpy.ndarray` 

523 Array :math:`a` safely raised to the power :math:`p`. 

524 

525 Examples 

526 -------- 

527 >>> np.power(-2, 0.15) 

528 nan 

529 >>> spow(-2, 0.15) # doctest: +ELLIPSIS 

530 -1.1095694... 

531 >>> spow(0, 0) 

532 0.0 

533 """ 

534 

535 if not _SPOW_ENABLED: 

536 return np.power(a, p) 

537 

538 a = as_float_array(a) 

539 p = as_float_array(p) 

540 

541 a_p = np.sign(a) * np.abs(a) ** p 

542 

543 return as_float(0 if a_p.ndim == 0 and np.isnan(a_p) else a_p) 

544 

545 

546def normalise_vector(a: ArrayLike) -> NDArrayFloat: 

547 """ 

548 Normalise the specified vector :math:`a`. 

549 

550 The normalisation process scales the vector to have unit length, ensuring 

551 that the magnitude of the resulting vector equals 1. 

552 

553 Parameters 

554 ---------- 

555 a 

556 Vector :math:`a` to normalise. 

557 

558 Returns 

559 ------- 

560 :class:`numpy.ndarray` 

561 Normalised vector :math:`a` with unit length. 

562 

563 Examples 

564 -------- 

565 >>> a = np.array([0.20654008, 0.12197225, 0.05136952]) 

566 >>> normalise_vector(a) # doctest: +ELLIPSIS 

567 array([ 0.8419703..., 0.4972256..., 0.2094102...]) 

568 """ 

569 

570 a = as_float_array(a) 

571 

572 with sdiv_mode(): 

573 return sdiv(a, np.linalg.norm(a)) 

574 

575 

576def normalise_maximum( 

577 a: ArrayLike, 

578 axis: int | None = None, 

579 factor: float = 1, 

580 clip: bool = True, 

581) -> NDArrayFloat: 

582 """ 

583 Normalise specified array :math:`a` values by :math:`a` maximum value 

584 and optionally clip them between [0, factor]. 

585 

586 Parameters 

587 ---------- 

588 a 

589 Array :math:`a` to normalise. 

590 axis 

591 Normalization axis. 

592 factor 

593 Normalization factor. 

594 clip 

595 Clip values to domain [0, 'factor']. 

596 

597 Returns 

598 ------- 

599 :class:`numpy.ndarray` 

600 Maximum normalised array :math:`a`. 

601 

602 Examples 

603 -------- 

604 >>> a = np.array([0.48222001, 0.31654775, 0.22070353]) 

605 >>> normalise_maximum(a) # doctest: +ELLIPSIS 

606 array([ 1. , 0.6564384..., 0.4576822...]) 

607 """ 

608 

609 a = as_float_array(a) 

610 

611 maximum = np.max(a, axis=axis) 

612 

613 with sdiv_mode(): 

614 a = a * sdiv(1, maximum[..., None]) * factor 

615 

616 return np.clip(a, 0, factor) if clip else a 

617 

618 

619def vecmul(m: ArrayLike, v: ArrayLike) -> NDArrayFloat: 

620 """ 

621 Perform batched multiplication between the matrix array :math:`m` and 

622 vector array :math:`v`. 

623 

624 This function is equivalent to :func:`numpy.matmul` but specifically 

625 designed for vector multiplication by a matrix. Vector dimensionality is 

626 automatically increased to enable broadcasting. The operation can be 

627 expressed using :func:`numpy.einsum` with subscripts 

628 *'...ij,...j->...i'*. 

629 

630 Parameters 

631 ---------- 

632 m 

633 Matrix array :math:`m`. 

634 v 

635 Vector array :math:`v`. 

636 

637 Returns 

638 ------- 

639 :class:`numpy.ndarray` 

640 Multiplied vector array :math:`v`. 

641 

642 Examples 

643 -------- 

644 >>> m = np.array( 

645 ... [ 

646 ... [0.7328, 0.4296, -0.1624], 

647 ... [-0.7036, 1.6975, 0.0061], 

648 ... [0.0030, 0.0136, 0.9834], 

649 ... ] 

650 ... ) 

651 >>> m = np.reshape(np.tile(m, (6, 1)), (6, 3, 3)) 

652 >>> v = np.array([0.20654008, 0.12197225, 0.05136952]) 

653 >>> v = np.tile(v, (6, 1)) 

654 >>> vecmul(m, v) # doctest: +ELLIPSIS 

655 array([[ 0.1954094..., 0.0620396..., 0.0527952...], 

656 [ 0.1954094..., 0.0620396..., 0.0527952...], 

657 [ 0.1954094..., 0.0620396..., 0.0527952...], 

658 [ 0.1954094..., 0.0620396..., 0.0527952...], 

659 [ 0.1954094..., 0.0620396..., 0.0527952...], 

660 [ 0.1954094..., 0.0620396..., 0.0527952...]]) 

661 """ 

662 

663 return np.matmul(as_float_array(m), as_float_array(v)[..., None]).squeeze(-1) 

664 

665 

666def euclidean_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: 

667 """ 

668 Calculate the *Euclidean* distance between the specified point arrays 

669 :math:`a` and :math:`b`. 

670 

671 For a two-dimensional space, the metric is as follows: 

672 

673 :math:`E_D = [(x_a - x_b)^2 + (y_a - y_b)^2]^{1/2}` 

674 

675 Parameters 

676 ---------- 

677 a 

678 Point array :math:`a`. 

679 b 

680 Point array :math:`b`. 

681 

682 Returns 

683 ------- 

684 :class:`numpy.float64` or :class:`numpy.ndarray` 

685 *Euclidean* distance between the two point arrays. 

686 

687 Examples 

688 -------- 

689 >>> a = np.array([100.00000000, 21.57210357, 272.22819350]) 

690 >>> b = np.array([100.00000000, 426.67945353, 72.39590835]) 

691 >>> euclidean_distance(a, b) # doctest: +ELLIPSIS 

692 451.7133019... 

693 """ 

694 

695 return as_float(np.linalg.norm(as_float_array(a) - as_float_array(b), axis=-1)) 

696 

697 

698def manhattan_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat: 

699 """ 

700 Compute the *Manhattan* (or *City-Block*) distance between point array 

701 :math:`a` and point array :math:`b`. 

702 

703 For a two-dimensional space, the metric is defined as: 

704 

705 :math:`M_D = |x_a - x_b| + |y_a - y_b|` 

706 

707 Parameters 

708 ---------- 

709 a 

710 Point array :math:`a`. 

711 b 

712 Point array :math:`b`. 

713 

714 Returns 

715 ------- 

716 :class:`np.float` or :class:`numpy.ndarray` 

717 *Manhattan* distance. 

718 

719 Examples 

720 -------- 

721 >>> a = np.array([100.00000000, 21.57210357, 272.22819350]) 

722 >>> b = np.array([100.00000000, 426.67945353, 72.39590835]) 

723 >>> manhattan_distance(a, b) # doctest: +ELLIPSIS 

724 604.9396351... 

725 """ 

726 

727 return as_float(np.sum(np.abs(as_float_array(a) - as_float_array(b)), axis=-1)) 

728 

729 

730def linear_conversion( 

731 a: ArrayLike, old_range: ArrayLike, new_range: ArrayLike 

732) -> NDArrayFloat: 

733 """ 

734 Perform simple linear conversion of the specified array :math:`a` between the 

735 old and new ranges. 

736 

737 Parameters 

738 ---------- 

739 a 

740 Array :math:`a` to perform the linear conversion onto. 

741 old_range 

742 Old range. 

743 new_range 

744 New range. 

745 

746 Returns 

747 ------- 

748 :class:`numpy.ndarray` 

749 Linear conversion result. 

750 

751 Examples 

752 -------- 

753 >>> a = np.linspace(0, 1, 10) 

754 >>> linear_conversion(a, np.array([0, 1]), np.array([1, 10])) 

755 array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.]) 

756 """ 

757 

758 a = as_float_array(a) 

759 

760 in_min, in_max = tsplit(old_range) 

761 out_min, out_max = tsplit(new_range) 

762 

763 return ((a - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min 

764 

765 

766def linstep_function( 

767 x: ArrayLike, 

768 a: ArrayLike = 0, 

769 b: ArrayLike = 1, 

770 clip: bool = False, 

771) -> NDArrayFloat: 

772 """ 

773 Perform linear interpolation between specified arrays :math:`a` and 

774 :math:`b` using array :math:`x`. 

775 

776 Parameters 

777 ---------- 

778 x 

779 Array :math:`x` containing values to use for interpolation between 

780 array :math:`a` and array :math:`b`. 

781 a 

782 Array :math:`a`, the start of the interpolation range. 

783 b 

784 Array :math:`b`, the end of the interpolation range. 

785 clip 

786 Whether to clip the output values to range [:math:`a`, :math:`b`]. 

787 

788 Returns 

789 ------- 

790 :class:`numpy.ndarray` 

791 Linear interpolation result. 

792 

793 Examples 

794 -------- 

795 >>> a = 0 

796 >>> b = 2 

797 >>> linstep_function(0.5, a, b) 

798 1.0 

799 """ 

800 

801 x = as_float_array(x) 

802 a = as_float_array(a) 

803 b = as_float_array(b) 

804 

805 y = (1.0 - x) * a + x * b 

806 

807 return np.clip(y, a, b) if clip else y 

808 

809 

810lerp = linstep_function 

811 

812 

813def smoothstep_function( 

814 x: ArrayLike, 

815 a: ArrayLike = 0, 

816 b: ArrayLike = 1, 

817 clip: bool = False, 

818) -> NDArrayFloat: 

819 """ 

820 Apply the *smoothstep* cubic Hermite interpolation function to 

821 array :math:`x`. 

822 

823 The *smoothstep* function creates a smooth S-shaped curve between 

824 specified edge values, commonly used for smooth transitions in 

825 colour interpolation and rendering operations. 

826 

827 Parameters 

828 ---------- 

829 x 

830 Input array :math:`x` containing values to be transformed. 

831 a 

832 Lower edge value for the interpolation domain. 

833 b 

834 Upper edge value for the interpolation domain. 

835 clip 

836 Whether to normalize and constrain input values to the domain 

837 [:math:`a`, :math:`b`] before applying the *smoothstep* function. 

838 

839 Returns 

840 ------- 

841 :class:`numpy.ndarray` 

842 Transformed array with values smoothly interpolated using the 

843 cubic Hermite polynomial :math:`3x^2 - 2x^3`. 

844 

845 Examples 

846 -------- 

847 >>> x = np.linspace(-2, 2, 5) 

848 >>> smoothstep_function(x, -2, 2, clip=True) 

849 array([ 0. , 0.15625, 0.5 , 0.84375, 1. ]) 

850 """ 

851 

852 x = as_float_array(x) 

853 a = as_float_array(a) 

854 b = as_float_array(b) 

855 

856 i = np.clip((x - a) / (b - a), 0, 1) if clip else x 

857 

858 return (i**2) * (3.0 - 2.0 * i) 

859 

860 

861smooth = smoothstep_function 

862 

863 

864def is_identity(a: ArrayLike) -> bool: 

865 """ 

866 Determine whether the specified array :math:`a` is an identity matrix. 

867 

868 An identity matrix is a square matrix with ones on the main diagonal 

869 and zeros elsewhere, satisfying :math:`I \\cdot A = A \\cdot I = A` 

870 for any compatible matrix :math:`A`. 

871 

872 Parameters 

873 ---------- 

874 a 

875 Array :math:`a` to test for identity matrix properties. 

876 

877 Returns 

878 ------- 

879 :class:`bool` 

880 Whether the specified array :math:`a` is an identity matrix. 

881 

882 Examples 

883 -------- 

884 >>> is_identity(np.reshape(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]), (3, 3))) 

885 True 

886 >>> is_identity(np.reshape(np.array([1, 2, 0, 0, 1, 0, 0, 0, 1]), (3, 3))) 

887 False 

888 """ 

889 

890 return np.array_equal(np.identity(len(np.diag(a))), a) 

891 

892 

893def eigen_decomposition( 

894 a: ArrayLike, 

895 eigen_w_v_count: int | None = None, 

896 descending_order: bool = True, 

897 covariance_matrix: bool = False, 

898) -> Tuple[NDArrayFloat, NDArrayFloat]: 

899 """ 

900 Compute the eigenvalues :math:`w` and eigenvectors :math:`v` of the 

901 specified array :math:`a` in the specified order. 

902 

903 Parameters 

904 ---------- 

905 a 

906 Array to compute the eigenvalues :math:`w` and eigenvectors :math:`v` 

907 for. 

908 eigen_w_v_count 

909 Number of eigenvalues :math:`w` and eigenvectors :math:`v` to return. 

910 descending_order 

911 Whether to return the eigenvalues :math:`w` and eigenvectors :math:`v` 

912 in descending order. 

913 covariance_matrix 

914 Whether to compute the eigenvalues :math:`w` and eigenvectors 

915 :math:`v` of the array :math:`a` covariance matrix 

916 :math:`A = a^T \\cdot a`. 

917 

918 Returns 

919 ------- 

920 :class:`tuple` 

921 Tuple of eigenvalues :math:`w` and eigenvectors :math:`v`. The 

922 eigenvalues are in the specified order, each repeated according to 

923 its multiplicity. The column ``v[:, i]`` is the normalized eigenvector 

924 corresponding to the eigenvalue ``w[i]``. 

925 

926 Examples 

927 -------- 

928 >>> a = np.diag([1, 2, 3]) 

929 >>> w, v = eigen_decomposition(a) 

930 >>> w 

931 array([ 3., 2., 1.]) 

932 >>> v 

933 array([[ 0., 0., 1.], 

934 [ 0., 1., 0.], 

935 [ 1., 0., 0.]]) 

936 >>> w, v = eigen_decomposition(a, 1) 

937 >>> w 

938 array([ 3.]) 

939 >>> v 

940 array([[ 0.], 

941 [ 0.], 

942 [ 1.]]) 

943 >>> w, v = eigen_decomposition(a, descending_order=False) 

944 >>> w 

945 array([ 1., 2., 3.]) 

946 >>> v 

947 array([[ 1., 0., 0.], 

948 [ 0., 1., 0.], 

949 [ 0., 0., 1.]]) 

950 >>> w, v = eigen_decomposition(a, covariance_matrix=True) 

951 >>> w 

952 array([ 9., 4., 1.]) 

953 >>> v 

954 array([[ 0., 0., 1.], 

955 [ 0., 1., 0.], 

956 [ 1., 0., 0.]]) 

957 """ 

958 

959 A = as_float_array(a) 

960 

961 if covariance_matrix: 

962 A = np.dot(np.transpose(A), A) 

963 

964 w, v = np.linalg.eigh(A) 

965 

966 if eigen_w_v_count is not None: 

967 w = w[-eigen_w_v_count:] 

968 v = v[..., -eigen_w_v_count:] 

969 

970 if descending_order: 

971 w = np.flipud(w) 

972 v = np.fliplr(v) 

973 

974 return w, v