Coverage for utilities/array.py: 72%

404 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Array Utilities 

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

4 

5Provide utilities for array manipulation and computational operations. 

6 

7References 

8---------- 

9- :cite:`Castro2014a` : Castro, S. (2014). Numpy: Fastest way of computing 

10 diagonal for each row of a 2d array. Retrieved August 22, 2014, from 

11 http://stackoverflow.com/questions/26511401/\ 

12numpy-fastest-way-of-computing-diagonal-for-each-row-of-a-2d-array/\ 

1326517247#26517247 

14- :cite:`Yorke2014a` : Yorke, R. (2014). Python: Change format of np.array or 

15 allow tolerance in in1d function. Retrieved March 27, 2015, from 

16 http://stackoverflow.com/a/23521245/931625 

17""" 

18 

19from __future__ import annotations 

20 

21import functools 

22import re 

23import sys 

24import typing 

25from collections.abc import KeysView, ValuesView 

26from contextlib import contextmanager 

27from dataclasses import fields, is_dataclass, replace 

28from operator import add, mul, pow, sub, truediv # noqa: A004 

29from typing import Union, get_args, get_origin, get_type_hints 

30 

31import numpy as np 

32 

33from colour.constants import ( 

34 DTYPE_COMPLEX_DEFAULT, 

35 DTYPE_FLOAT_DEFAULT, 

36 DTYPE_INT_DEFAULT, 

37 EPSILON, 

38) 

39 

40if typing.TYPE_CHECKING: 

41 from colour.hints import ( 

42 Any, 

43 Callable, 

44 DType, 

45 DTypeBoolean, 

46 DTypeComplex, 

47 DTypeReal, 

48 Dataclass, 

49 Generator, 

50 Literal, 

51 NDArray, 

52 NDArrayComplex, 

53 NDArrayFloat, 

54 NDArrayInt, 

55 Real, 

56 Self, 

57 Sequence, 

58 Type, 

59 ) 

60 

61from colour.hints import ArrayLike, DTypeComplex, DTypeFloat, DTypeInt, cast 

62from colour.utilities import ( 

63 CACHE_REGISTRY, 

64 attest, 

65 int_digest, 

66 is_caching_enabled, 

67 optional, 

68 suppress_warnings, 

69 validate_method, 

70) 

71 

72__author__ = "Colour Developers" 

73__copyright__ = "Copyright 2013 Colour Developers" 

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

75__maintainer__ = "Colour Developers" 

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

77__status__ = "Production" 

78 

79__all__ = [ 

80 "MixinDataclassFields", 

81 "MixinDataclassIterable", 

82 "MixinDataclassArray", 

83 "MixinDataclassArithmetic", 

84 "as_array", 

85 "as_int", 

86 "as_float", 

87 "as_int_array", 

88 "as_float_array", 

89 "as_int_scalar", 

90 "as_float_scalar", 

91 "as_complex_array", 

92 "set_default_int_dtype", 

93 "set_default_float_dtype", 

94 "get_domain_range_scale", 

95 "set_domain_range_scale", 

96 "domain_range_scale", 

97 "get_domain_range_scale_metadata", 

98 "to_domain_1", 

99 "to_domain_10", 

100 "to_domain_100", 

101 "to_domain_degrees", 

102 "to_domain_int", 

103 "from_range_1", 

104 "from_range_10", 

105 "from_range_100", 

106 "from_range_degrees", 

107 "from_range_int", 

108 "is_ndarray_copy_enabled", 

109 "set_ndarray_copy_enable", 

110 "ndarray_copy_enable", 

111 "ndarray_copy", 

112 "closest_indexes", 

113 "closest", 

114 "interval", 

115 "is_uniform", 

116 "in_array", 

117 "tstack", 

118 "tsplit", 

119 "row_as_diagonal", 

120 "orient", 

121 "centroid", 

122 "fill_nan", 

123 "has_only_nan", 

124 "ndarray_write", 

125 "zeros", 

126 "ones", 

127 "full", 

128 "index_along_last_axis", 

129 "format_array_as_row", 

130] 

131 

132 

133class MixinDataclassFields: 

134 """ 

135 Provide fields introspection for :class:`dataclass`-like classes. 

136 

137 This mixin extends dataclass functionality to enable introspection 

138 capabilities, allowing programmatic access to field metadata and 

139 properties. 

140 

141 Attributes 

142 ---------- 

143 - :attr:`~colour.utilities.MixinDataclassFields.fields` 

144 """ 

145 

146 @property 

147 def fields(self) -> tuple: 

148 """ 

149 Getter for the fields of the :class:`dataclass`-like class. 

150 

151 Returns 

152 ------- 

153 :class:`tuple` 

154 :class:`dataclass`-like class fields. 

155 """ 

156 

157 return fields(self) # pyright: ignore 

158 

159 

160class MixinDataclassIterable(MixinDataclassFields): 

161 """ 

162 Provide iteration capabilities over :class:`dataclass`-like classes. 

163 

164 This mixin extends dataclass functionality to enable dictionary-like 

165 iteration over fields, allowing access to field names, values, and 

166 name-value pairs through standard iteration protocols. 

167 

168 Attributes 

169 ---------- 

170 - :attr:`~colour.utilities.MixinDataclassIterable.keys` 

171 - :attr:`~colour.utilities.MixinDataclassIterable.values` 

172 - :attr:`~colour.utilities.MixinDataclassIterable.items` 

173 

174 Methods 

175 ------- 

176 - :meth:`~colour.utilities.MixinDataclassIterable.__iter__` 

177 

178 Notes 

179 ----- 

180 - The :class:`colour.utilities.MixinDataclassIterable` class inherits 

181 the methods from the following class: 

182 

183 - :class:`colour.utilities.MixinDataclassFields` 

184 """ 

185 

186 @property 

187 def keys(self) -> tuple: 

188 """ 

189 Getter for the :class:`dataclass`-like class keys, i.e., the field 

190 names. 

191 

192 Returns 

193 ------- 

194 :class:`tuple` 

195 :class:`dataclass`-like class keys. 

196 """ 

197 

198 return tuple(field for field, _value in self) 

199 

200 @property 

201 def values(self) -> tuple: 

202 """ 

203 Getter for the :class:`dataclass`-like class field values. 

204 

205 Returns 

206 ------- 

207 :class:`tuple` 

208 :class:`dataclass`-like class field values. 

209 """ 

210 

211 return tuple(value for _field, value in self) 

212 

213 @property 

214 def items(self) -> tuple: 

215 """ 

216 Getter for the :class:`dataclass`-like class items, i.e., the field 

217 names and values. 

218 

219 Returns 

220 ------- 

221 :class:`tuple` 

222 :class:`dataclass`-like class items. 

223 """ 

224 

225 return tuple((field, value) for field, value in self) 

226 

227 def __iter__(self) -> Generator: 

228 """ 

229 Yield the :class:`dataclass`-like class fields. 

230 

231 Yields 

232 ------ 

233 Generator 

234 :class:`dataclass`-like class field generator. 

235 """ 

236 

237 yield from { 

238 field.name: getattr(self, field.name) for field in self.fields 

239 }.items() 

240 

241 

242class MixinDataclassArray(MixinDataclassIterable): 

243 """ 

244 Provide conversion methods for :class:`dataclass`-like classes to 

245 :class:`numpy.ndarray` objects. 

246 

247 This mixin extends dataclass functionality to enable seamless conversion 

248 to NumPy arrays, facilitating numerical operations on structured data. 

249 

250 Methods 

251 ------- 

252 - :meth:`~colour.utilities.MixinDataclassArray.__array__` 

253 

254 Notes 

255 ----- 

256 - The :class:`colour.utilities.MixinDataclassArray` class 

257 inherits the methods from the following classes: 

258 

259 - :class:`colour.utilities.MixinDataclassIterable` 

260 - :class:`colour.utilities.MixinDataclassFields` 

261 """ 

262 

263 def __array__( 

264 self, dtype: Type[DTypeReal] | None = None, copy: bool = True 

265 ) -> NDArray: 

266 """ 

267 Implement support for :class:`dataclass`-like class conversion to 

268 :class:`numpy.ndarray` class. 

269 

270 A field set to *None* will be filled with `np.nan` according to the 

271 shape of the first field not set with *None*. 

272 

273 Parameters 

274 ---------- 

275 dtype 

276 :class:`numpy.dtype` to use for conversion to `np.ndarray`, 

277 default to the :class:`numpy.dtype` defined by 

278 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

279 copy 

280 Whether to return a copy of the underlying data, will always be 

281 `True`, irrespective of the parameter value. 

282 

283 Returns 

284 ------- 

285 :class:`numpy.ndarray` 

286 :class:`dataclass`-like class converted to 

287 :class:`numpy.ndarray`. 

288 """ 

289 

290 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

291 

292 default = None 

293 for _field, value in self: 

294 if value is not None: 

295 default = full(as_float_array(value).shape, np.nan) 

296 break 

297 

298 return tstack( 

299 cast( 

300 "ArrayLike", 

301 [value if value is not None else default for value in self.values], 

302 ), 

303 dtype=dtype, 

304 ) 

305 

306 

307class MixinDataclassArithmetic(MixinDataclassArray): 

308 """ 

309 Provide mathematical operations for :class:`dataclass`-like classes. 

310 

311 This mixin extends dataclass functionality to enable arithmetic 

312 operations, facilitating mathematical computations on dataclass instances 

313 containing array-like data. 

314 

315 Methods 

316 ------- 

317 - :meth:`~colour.utilities.MixinDataclassArray.__iadd__` 

318 - :meth:`~colour.utilities.MixinDataclassArray.__add__` 

319 - :meth:`~colour.utilities.MixinDataclassArray.__isub__` 

320 - :meth:`~colour.utilities.MixinDataclassArray.__sub__` 

321 - :meth:`~colour.utilities.MixinDataclassArray.__imul__` 

322 - :meth:`~colour.utilities.MixinDataclassArray.__mul__` 

323 - :meth:`~colour.utilities.MixinDataclassArray.__idiv__` 

324 - :meth:`~colour.utilities.MixinDataclassArray.__div__` 

325 - :meth:`~colour.utilities.MixinDataclassArray.__ipow__` 

326 - :meth:`~colour.utilities.MixinDataclassArray.__pow__` 

327 - :meth:`~colour.utilities.MixinDataclassArray.arithmetical_operation` 

328 

329 Notes 

330 ----- 

331 - The :class:`colour.utilities.MixinDataclassArithmetic` class inherits 

332 the methods from the following classes: 

333 

334 - :class:`colour.utilities.MixinDataclassArray` 

335 - :class:`colour.utilities.MixinDataclassIterable` 

336 - :class:`colour.utilities.MixinDataclassFields` 

337 """ 

338 

339 def __add__(self, a: Any) -> Self: 

340 """ 

341 Implement support for addition. 

342 

343 Parameters 

344 ---------- 

345 a 

346 Variable :math:`a` to add. 

347 

348 Returns 

349 ------- 

350 :class:`dataclass` 

351 Variable added :class:`dataclass`-like class. 

352 """ 

353 

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

355 

356 def __iadd__(self, a: Any) -> Self: 

357 """ 

358 Implement support for in-place addition. 

359 

360 Parameters 

361 ---------- 

362 a 

363 Variable :math:`a` to add in-place. 

364 

365 Returns 

366 ------- 

367 :class:`dataclass` 

368 In-place variable added :class:`dataclass`-like class. 

369 """ 

370 

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

372 

373 def __sub__(self, a: Any) -> Self: 

374 """ 

375 Implement support for subtraction. 

376 

377 Parameters 

378 ---------- 

379 a 

380 Variable :math:`a` to subtract. 

381 

382 Returns 

383 ------- 

384 :class:`dataclass` 

385 Variable subtracted :class:`dataclass`-like class. 

386 """ 

387 

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

389 

390 def __isub__(self, a: Any) -> Self: 

391 """ 

392 Implement support for in-place subtraction. 

393 

394 Parameters 

395 ---------- 

396 a 

397 Variable :math:`a` to subtract in-place. 

398 

399 Returns 

400 ------- 

401 :class:`dataclass` 

402 In-place variable subtracted :class:`dataclass`-like class. 

403 """ 

404 

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

406 

407 def __mul__(self, a: Any) -> Self: 

408 """ 

409 Implement support for multiplication. 

410 

411 Parameters 

412 ---------- 

413 a 

414 Variable :math:`a` to multiply by. 

415 

416 Returns 

417 ------- 

418 :class:`dataclass` 

419 Variable multiplied :class:`dataclass`-like class. 

420 """ 

421 

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

423 

424 def __imul__(self, a: Any) -> Self: 

425 """ 

426 Implement support for in-place multiplication. 

427 

428 Parameters 

429 ---------- 

430 a 

431 Variable :math:`a` to multiply by in-place. 

432 

433 Returns 

434 ------- 

435 :class:`dataclass` 

436 In-place variable multiplied :class:`dataclass`-like class. 

437 """ 

438 

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

440 

441 def __div__(self, a: Any) -> Self: 

442 """ 

443 Implement support for division. 

444 

445 Parameters 

446 ---------- 

447 a 

448 Variable :math:`a` to divide by. 

449 

450 Returns 

451 ------- 

452 :class:`dataclass` 

453 Variable divided :class:`dataclass`-like class. 

454 """ 

455 

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

457 

458 def __idiv__(self, a: Any) -> Self: 

459 """ 

460 Implement support for in-place division. 

461 

462 Parameters 

463 ---------- 

464 a 

465 Variable :math:`a` to divide by in-place. 

466 

467 Returns 

468 ------- 

469 :class:`dataclass` 

470 In-place variable divided :class:`dataclass`-like class. 

471 """ 

472 

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

474 

475 __itruediv__ = __idiv__ 

476 __truediv__ = __div__ 

477 

478 def __pow__(self, a: Any) -> Self: 

479 """ 

480 Implement support for exponentiation. 

481 

482 Parameters 

483 ---------- 

484 a 

485 Variable :math:`a` to exponentiate by. 

486 

487 Returns 

488 ------- 

489 :class:`dataclass` 

490 Variable exponentiated :class:`dataclass`-like class. 

491 """ 

492 

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

494 

495 def __ipow__(self, a: Any) -> Self: 

496 """ 

497 Implement support for in-place exponentiation. 

498 

499 Parameters 

500 ---------- 

501 a 

502 Variable :math:`a` to exponentiate by in-place. 

503 

504 Returns 

505 ------- 

506 :class:`dataclass` 

507 In-place variable exponentiated :class:`dataclass`-like 

508 class. 

509 """ 

510 

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

512 

513 def arithmetical_operation( 

514 self, a: Any, operation: str, in_place: bool = False 

515 ) -> Dataclass: 

516 """ 

517 Perform the specified arithmetical operation with the :math:`a` 

518 operand on the :class:`dataclass`-like class. 

519 

520 Parameters 

521 ---------- 

522 a 

523 Operand. 

524 operation 

525 Operation to perform. 

526 in_place 

527 Operation happens in place. 

528 

529 Returns 

530 ------- 

531 :class:`dataclass` 

532 :class:`dataclass`-like class with the arithmetical operation 

533 performed. 

534 """ 

535 

536 callable_operation = { 

537 "+": add, 

538 "-": sub, 

539 "*": mul, 

540 "/": truediv, 

541 "**": pow, 

542 }[operation] 

543 

544 if is_dataclass(a): 

545 a = as_float_array(a) # pyright: ignore 

546 

547 values = tsplit(callable_operation(as_float_array(self), a)) 

548 field_values = {field: values[i] for i, field in enumerate(self.keys)} 

549 field_values.update({field: None for field, value in self if value is None}) 

550 

551 dataclass = replace(self, **field_values) # pyright: ignore 

552 

553 if in_place: 

554 for field in self.keys: 

555 setattr(self, field, getattr(dataclass, field)) 

556 

557 return self 

558 

559 return dataclass 

560 

561 

562# NOTE : The following messages are pre-generated for performance reasons. 

563_ASSERTION_MESSAGE_DTYPE_INT = ( 

564 f'"dtype" must be one of the following types: "{DTypeInt.__args__}"' 

565) 

566 

567_ASSERTION_MESSAGE_DTYPE_FLOAT = ( 

568 f'"dtype" must be one of the following types: "{DTypeFloat.__args__}"' 

569) 

570 

571_ASSERTION_MESSAGE_DTYPE_COMPLEX = ( 

572 f'"dtype" must be one of the following types: "{DTypeComplex.__args__}"' 

573) 

574 

575 

576def as_array( 

577 a: ArrayLike | KeysView | ValuesView, 

578 dtype: Type[DType] | None = None, 

579) -> NDArray: 

580 """ 

581 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using 

582 the specified :class:`numpy.dtype`. 

583 

584 Parameters 

585 ---------- 

586 a 

587 Variable :math:`a` to convert. 

588 dtype 

589 :class:`numpy.dtype` to use for conversion, default to the 

590 :class:`numpy.dtype` defined by the 

591 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

592 

593 Returns 

594 ------- 

595 :class:`numpy.ndarray` 

596 Variable :math:`a` converted to :class:`numpy.ndarray`. 

597 

598 Examples 

599 -------- 

600 >>> as_array([1, 2, 3]) # doctest: +ELLIPSIS 

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

602 >>> as_array([1, 2, 3], dtype=DTYPE_FLOAT_DEFAULT) 

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

604 """ 

605 

606 # TODO: Remove when https://github.com/numpy/numpy/issues/5718 is 

607 # addressed. 

608 if isinstance(a, (KeysView, ValuesView)): 

609 a = list(a) 

610 

611 return np.asarray(a, dtype) 

612 

613 

614@typing.overload 

615def as_int(a: float | DTypeFloat, dtype: Type[DTypeInt] | None = None) -> DTypeInt: ... 

616@typing.overload 

617def as_int( 

618 a: NDArray | Sequence[int], dtype: Type[DTypeInt] | None = None 

619) -> NDArrayInt: ... 

620@typing.overload 

621def as_int( 

622 a: ArrayLike, dtype: Type[DTypeInt] | None = None 

623) -> DTypeInt | NDArrayInt: ... 

624def as_int(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> DTypeInt | NDArrayInt: 

625 """ 

626 Convert the specified variable :math:`a` to :class:`numpy.integer` using 

627 the specified :class:`numpy.dtype`. 

628 

629 The function converts variable :math:`a` to an integer type. If variable 

630 :math:`a` is not a scalar or 0-dimensional array, it is converted to 

631 :class:`numpy.ndarray`. 

632 

633 Parameters 

634 ---------- 

635 a 

636 Variable :math:`a` to convert. 

637 dtype 

638 :class:`numpy.dtype` to use for conversion, default to the 

639 :class:`numpy.dtype` defined by the 

640 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute. 

641 

642 Returns 

643 ------- 

644 :class:`numpy.ndarray` 

645 Variable :math:`a` converted to :class:`numpy.integer`. 

646 

647 Examples 

648 -------- 

649 >>> as_int(np.array(1)) 

650 1 

651 >>> as_int(np.array([1])) # doctest: +SKIP 

652 array([1]) 

653 >>> as_int(np.arange(10)) # doctest: +SKIP 

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

655 """ 

656 

657 dtype = optional(dtype, DTYPE_INT_DEFAULT) 

658 

659 attest(dtype in DTypeInt.__args__, _ASSERTION_MESSAGE_DTYPE_INT) 

660 

661 return dtype(a) # pyright: ignore 

662 

663 

664@typing.overload 

665def as_float( 

666 a: float | DTypeFloat, dtype: Type[DTypeFloat] | None = None 

667) -> DTypeFloat: ... 

668@typing.overload 

669def as_float( 

670 a: NDArray | Sequence[float], dtype: Type[DTypeFloat] | None = None 

671) -> NDArrayFloat: ... 

672@typing.overload 

673def as_float( 

674 a: ArrayLike, dtype: Type[DTypeFloat] | None = None 

675) -> DTypeFloat | NDArrayFloat: ... 

676def as_float( 

677 a: ArrayLike, dtype: Type[DTypeFloat] | None = None 

678) -> DTypeFloat | NDArrayFloat: 

679 """ 

680 Convert the specified variable :math:`a` to :class:`numpy.floating` using 

681 the specified :class:`numpy.dtype`. 

682 

683 If variable :math:`a` is not a scalar or 0-dimensional, it is converted 

684 to :class:`numpy.ndarray`. 

685 

686 Parameters 

687 ---------- 

688 a 

689 Variable :math:`a` to convert. 

690 dtype 

691 :class:`numpy.dtype` to use for conversion, default to the 

692 :class:`numpy.dtype` defined by the 

693 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

694 

695 Returns 

696 ------- 

697 :class:`numpy.ndarray` 

698 Variable :math:`a` converted to :class:`numpy.floating`. 

699 

700 Examples 

701 -------- 

702 >>> as_float(np.array(1)) 

703 1.0 

704 >>> as_float(np.array([1])) 

705 array([ 1.]) 

706 >>> as_float(np.arange(10)) 

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

708 """ 

709 

710 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

711 

712 attest(dtype in DTypeFloat.__args__, _ASSERTION_MESSAGE_DTYPE_FLOAT) 

713 

714 # NOTE: "np.float64" reduces dimensionality: 

715 # >>> np.int64(np.array([[1]])) 

716 # array([[1]]) 

717 # >>> np.float64(np.array([[1]])) 

718 # 1.0 

719 # See for more information https://github.com/numpy/numpy/issues/24283 

720 if isinstance(a, np.ndarray) and a.size == 1 and a.ndim != 0: 

721 return as_float_array(a, dtype) 

722 

723 return dtype(a) # pyright: ignore 

724 

725 

726def as_int_array(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> NDArrayInt: 

727 """ 

728 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using 

729 the specified integer :class:`numpy.dtype`. 

730 

731 Parameters 

732 ---------- 

733 a 

734 Variable :math:`a` to convert. 

735 dtype 

736 :class:`numpy.dtype` to use for conversion, default to the 

737 :class:`numpy.dtype` defined by the 

738 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute. 

739 

740 Returns 

741 ------- 

742 :class:`numpy.ndarray` 

743 Variable :math:`a` converted to integer :class:`numpy.ndarray`. 

744 

745 Examples 

746 -------- 

747 >>> as_int_array([1.0, 2.0, 3.0]) # doctest: +ELLIPSIS 

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

749 """ 

750 

751 dtype = optional(dtype, DTYPE_INT_DEFAULT) 

752 

753 attest(dtype in DTypeInt.__args__, _ASSERTION_MESSAGE_DTYPE_INT) 

754 

755 return as_array(a, dtype) 

756 

757 

758def as_float_array(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat: 

759 """ 

760 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using 

761 the specified floating-point :class:`numpy.dtype`. 

762 

763 Parameters 

764 ---------- 

765 a 

766 Variable :math:`a` to convert. 

767 dtype 

768 Floating-point :class:`numpy.dtype` to use for conversion, default 

769 to the :class:`numpy.dtype` defined by the 

770 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

771 

772 Returns 

773 ------- 

774 :class:`numpy.ndarray` 

775 Variable :math:`a` converted to floating-point 

776 :class:`numpy.ndarray`. 

777 

778 Examples 

779 -------- 

780 >>> as_float_array([1, 2, 3]) 

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

782 """ 

783 

784 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

785 

786 attest(dtype in DTypeFloat.__args__, _ASSERTION_MESSAGE_DTYPE_FLOAT) 

787 

788 return as_array(a, dtype) 

789 

790 

791def as_int_scalar(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> int: 

792 """ 

793 Convert the specified variable :math:`a` to :class:`numpy.integer` using 

794 the specified :class:`numpy.dtype`. 

795 

796 Parameters 

797 ---------- 

798 a 

799 Variable :math:`a` to convert. 

800 dtype 

801 :class:`numpy.dtype` to use for conversion, default to the 

802 :class:`numpy.dtype` defined by the 

803 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute. 

804 

805 Returns 

806 ------- 

807 :class:`int` 

808 Variable :math:`a` converted to :class:`numpy.integer`. 

809 

810 Warnings 

811 -------- 

812 - The return type is effectively annotated as :class:`int` and not 

813 :class:`numpy.integer`. 

814 

815 Examples 

816 -------- 

817 >>> as_int_scalar(np.array(1)) 

818 1 

819 """ 

820 

821 a = np.squeeze(as_int_array(a, dtype)) 

822 

823 attest(a.ndim == 0, f'"{a}" cannot be converted to "int" scalar!') 

824 

825 # TODO: Revisit when Numpy types are well established. 

826 return cast("int", as_int(a, dtype)) 

827 

828 

829def as_float_scalar(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> float: 

830 """ 

831 Convert the specified variable :math:`a` to :class:`numpy.floating` using 

832 the specified :class:`numpy.dtype`. 

833 

834 Parameters 

835 ---------- 

836 a 

837 Variable :math:`a` to convert. 

838 dtype 

839 :class:`numpy.dtype` to use for conversion, default to the 

840 :class:`numpy.dtype` defined by the 

841 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

842 

843 Returns 

844 ------- 

845 :class:`float` 

846 Variable :math:`a` converted to :class:`numpy.floating`. 

847 

848 Warnings 

849 -------- 

850 - The return type is effectively annotated as :class:`float` and not 

851 :class:`numpy.floating`. 

852 

853 Examples 

854 -------- 

855 >>> as_float_scalar(np.array(1)) 

856 1.0 

857 """ 

858 

859 a = np.squeeze(as_float_array(a, dtype)) 

860 

861 attest(a.ndim == 0, f'"{a}" cannot be converted to "float" scalar!') 

862 

863 # TODO: Revisit when Numpy types are well established. 

864 return cast("float", as_float(a, dtype)) 

865 

866 

867def as_complex_array( 

868 a: ArrayLike, 

869 dtype: Type[DTypeComplex] | None = None, 

870) -> NDArrayComplex: 

871 """ 

872 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using 

873 the specified complex :class:`numpy.dtype`. 

874 

875 Parameters 

876 ---------- 

877 a 

878 Variable :math:`a` to convert. 

879 dtype 

880 Complex :class:`numpy.dtype` to use for conversion, default 

881 to the :class:`numpy.dtype` defined by the 

882 :attr:`colour.constant.DTYPE_COMPLEX_DEFAULT` attribute. 

883 

884 Returns 

885 ------- 

886 :class:`numpy.ndarray` 

887 Variable :math:`a` converted to complex 

888 :class:`numpy.ndarray`. 

889 

890 Examples 

891 -------- 

892 >>> as_complex_array([1, 2, 3]) 

893 array([ 1.+0.j, 2.+0.j, 3.+0.j]) 

894 >>> as_complex_array([1 + 2j, 3 + 4j]) 

895 array([ 1.+2.j, 3.+4.j]) 

896 """ 

897 

898 dtype = optional(dtype, DTYPE_COMPLEX_DEFAULT) 

899 

900 attest(dtype in DTypeComplex.__args__, _ASSERTION_MESSAGE_DTYPE_COMPLEX) 

901 

902 return as_array(a, dtype) 

903 

904 

905def set_default_int_dtype( 

906 dtype: Type[DTypeInt] = DTYPE_INT_DEFAULT, 

907) -> None: 

908 """ 

909 Set the *Colour* default :class:`numpy.integer` precision by setting 

910 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute with the specified 

911 :class:`numpy.dtype` wherever the attribute is imported. 

912 

913 Parameters 

914 ---------- 

915 dtype 

916 :class:`numpy.dtype` to set 

917 :attr:`colour.constant.DTYPE_INT_DEFAULT` with. 

918 

919 Notes 

920 ----- 

921 - It is possible to define the integer precision at import time by 

922 setting the *COLOUR_SCIENCE__DEFAULT_INT_DTYPE* environment 

923 variable, for example `set COLOUR_SCIENCE__DEFAULT_INT_DTYPE=int32`. 

924 

925 Warnings 

926 -------- 

927 This definition is mostly given for consistency purposes with 

928 :func:`colour.utilities.set_default_float_dtype` definition but contrary 

929 to the latter, changing *integer* precision will almost certainly 

930 completely break *Colour*. With great power comes great responsibility. 

931 

932 Examples 

933 -------- 

934 >>> as_int_array(np.ones(3)).dtype # doctest: +SKIP 

935 dtype('int64') 

936 >>> set_default_int_dtype(np.int32) # doctest: +SKIP 

937 >>> as_int_array(np.ones(3)).dtype # doctest: +SKIP 

938 dtype('int32') 

939 >>> set_default_int_dtype(np.int64) 

940 >>> as_int_array(np.ones(3)).dtype # doctest: +SKIP 

941 dtype('int64') 

942 """ 

943 

944 # TODO: Investigate behaviour on Windows. 

945 with suppress_warnings(colour_usage_warnings=True): 

946 for module in sys.modules.values(): 

947 if not hasattr(module, "DTYPE_INT_DEFAULT"): 

948 continue 

949 

950 module.DTYPE_INT_DEFAULT = dtype # pyright: ignore 

951 

952 CACHE_REGISTRY.clear_all_caches() 

953 

954 

955def set_default_float_dtype( 

956 dtype: Type[DTypeFloat] = DTYPE_FLOAT_DEFAULT, 

957) -> None: 

958 """ 

959 Set the *Colour* default :class:`numpy.floating` precision by setting 

960 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute with the 

961 specified :class:`numpy.dtype` wherever the attribute is imported. 

962 

963 Parameters 

964 ---------- 

965 dtype 

966 :class:`numpy.dtype` to set 

967 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` with. 

968 

969 Notes 

970 ----- 

971 - It is possible to define the *float* precision at import time by 

972 setting the *COLOUR_SCIENCE__DEFAULT_FLOAT_DTYPE* environment 

973 variable, for example 

974 `set COLOUR_SCIENCE__DEFAULT_FLOAT_DTYPE=float32`. 

975 - Some definition returning a single-scalar ndarray might not 

976 honour the specified *float* precision: 

977 https://github.com/numpy/numpy/issues/16353 

978 

979 Warnings 

980 -------- 

981 Changing *float* precision might result in various *Colour* 

982 functionality breaking entirely: 

983 https://github.com/numpy/numpy/issues/6860. With great power comes 

984 great responsibility. 

985 

986 Examples 

987 -------- 

988 >>> as_float_array(np.ones(3)).dtype 

989 dtype('float64') 

990 >>> set_default_float_dtype(np.float16) # doctest: +SKIP 

991 >>> as_float_array(np.ones(3)).dtype # doctest: +SKIP 

992 dtype('float16') 

993 >>> set_default_float_dtype(np.float64) 

994 >>> as_float_array(np.ones(3)).dtype 

995 dtype('float64') 

996 """ 

997 

998 with suppress_warnings(colour_usage_warnings=True): 

999 for module in sys.modules.values(): 

1000 if not hasattr(module, "DTYPE_FLOAT_DEFAULT"): 

1001 continue 

1002 

1003 module.DTYPE_FLOAT_DEFAULT = dtype # pyright: ignore 

1004 

1005 CACHE_REGISTRY.clear_all_caches() 

1006 

1007 

1008# TODO: Annotate with "Union[Literal['ignore', 'reference', '1', '100'], str]" 

1009# when Python 3.7 is dropped. 

1010_DOMAIN_RANGE_SCALE = "reference" 

1011""" 

1012Global variable storing the current *Colour* domain-range scale. 

1013 

1014_DOMAIN_RANGE_SCALE 

1015""" 

1016 

1017 

1018def get_domain_range_scale() -> Literal["ignore", "reference", "1", "100"] | str: 

1019 """ 

1020 Return the current *Colour* domain-range scale. 

1021 

1022 The following scales are available: 

1023 

1024 - **'Reference'**, the default *Colour* domain-range scale which 

1025 varies depending on the referenced algorithm, e.g., [0, 1], 

1026 [0, 10], [0, 100], [0, 255], etc... 

1027 - **'1'**, a domain-range scale normalised to [0, 1], it is 

1028 important to acknowledge that this is a soft normalisation 

1029 and it is possible to use negative out of gamut values or 

1030 high dynamic range data exceeding 1. 

1031 

1032 Returns 

1033 ------- 

1034 :class:`str` 

1035 *Colour* domain-range scale. 

1036 

1037 Warnings 

1038 -------- 

1039 - The **'Ignore'** and **'100'** domain-range scales are for 

1040 internal usage only! 

1041 """ 

1042 

1043 return _DOMAIN_RANGE_SCALE 

1044 

1045 

1046def set_domain_range_scale( 

1047 scale: ( 

1048 Literal["ignore", "reference", "Ignore", "Reference", "1", "100"] | str 

1049 ) = "reference", 

1050) -> None: 

1051 """ 

1052 Set the current *Colour* domain-range scale. 

1053 

1054 The following scales are available: 

1055 

1056 - **'Reference'**, the default *Colour* domain-range scale which 

1057 varies depending on the referenced algorithm, e.g., [0, 1], 

1058 [0, 10], [0, 100], [0, 255], etc... 

1059 - **'1'**, a domain-range scale normalised to [0, 1], it is 

1060 important to acknowledge that this is a soft normalisation and it 

1061 is possible to use negative out of gamut values or high dynamic 

1062 range data exceeding 1. 

1063 

1064 Parameters 

1065 ---------- 

1066 scale 

1067 *Colour* domain-range scale to set. 

1068 

1069 Warnings 

1070 -------- 

1071 - The **'Ignore'** and **'100'** domain-range scales are for 

1072 internal usage only! 

1073 """ 

1074 

1075 global _DOMAIN_RANGE_SCALE # noqa: PLW0603 

1076 

1077 _DOMAIN_RANGE_SCALE = validate_method( 

1078 str(scale), 

1079 ("ignore", "reference", "1", "100"), 

1080 '"{0}" scale is invalid, it must be one of {1}!', 

1081 ) 

1082 

1083 

1084class domain_range_scale: 

1085 """ 

1086 Define a context manager and decorator to temporarily set the *Colour* 

1087 domain-range scale. 

1088 

1089 The following scales are available: 

1090 

1091 - **'Reference'**, the default *Colour* domain-range scale which 

1092 varies depending on the referenced algorithm, e.g., [0, 1], 

1093 [0, 10], [0, 100], [0, 255], etc... 

1094 - **'1'**, a domain-range scale normalised to [0, 1], it is 

1095 important to acknowledge that this is a soft normalisation and it 

1096 is possible to use negative out of gamut values or high dynamic 

1097 range data exceeding 1. 

1098 

1099 Parameters 

1100 ---------- 

1101 scale 

1102 *Colour* domain-range scale to set. 

1103 

1104 Warnings 

1105 -------- 

1106 - The **'Ignore'** and **'100'** domain-range scales are for 

1107 internal usage only! 

1108 

1109 Examples 

1110 -------- 

1111 With *Colour* domain-range scale set to **'Reference'**: 

1112 

1113 >>> with domain_range_scale("1"): 

1114 ... to_domain_1(1) 

1115 array(1.0) 

1116 >>> with domain_range_scale("Reference"): 

1117 ... from_range_1(1) 

1118 array(1.0) 

1119 

1120 With *Colour* domain-range scale set to **'1'**: 

1121 

1122 >>> with domain_range_scale("1"): 

1123 ... to_domain_1(1) 

1124 array(1.0) 

1125 >>> with domain_range_scale("1"): 

1126 ... from_range_1(1) 

1127 array(1.0) 

1128 

1129 With *Colour* domain-range scale set to **'100'** (unsupported): 

1130 

1131 >>> with domain_range_scale("100"): 

1132 ... to_domain_1(1) 

1133 array(0.01) 

1134 >>> with domain_range_scale("100"): 

1135 ... from_range_1(1) 

1136 array(100.0) 

1137 """ 

1138 

1139 def __init__( 

1140 self, 

1141 scale: ( 

1142 Literal["ignore", "reference", "Ignore", "Reference", "1", "100"] | str 

1143 ), 

1144 ) -> None: 

1145 self._scale = scale 

1146 self._previous_scale = get_domain_range_scale() 

1147 

1148 def __enter__(self) -> Self: 

1149 """Set the new domain-range scale upon entering the context manager.""" 

1150 

1151 set_domain_range_scale(self._scale) 

1152 

1153 return self 

1154 

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

1156 """ 

1157 Restore the previous domain-range scale upon exiting the context 

1158 manager. 

1159 """ 

1160 

1161 set_domain_range_scale(self._previous_scale) 

1162 

1163 def __call__(self, function: Callable) -> Any: 

1164 """ 

1165 Call the wrapped definition with domain-range scale management. 

1166 """ 

1167 

1168 @functools.wraps(function) 

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

1170 with self: 

1171 return function(*args, **kwargs) 

1172 

1173 return wrapper 

1174 

1175 

1176_CACHE_DOMAIN_RANGE_SCALE_METADATA: dict = CACHE_REGISTRY.register_cache( 

1177 f"{__name__}._CACHE_DOMAIN_RANGE_SCALE_METADATA" 

1178) 

1179 

1180 

1181def get_domain_range_scale_metadata(function: Callable) -> dict[str, Any]: 

1182 """ 

1183 Extract domain-range scale metadata from function type hints. 

1184 

1185 Extracts scale factors from PEP 593 ``Annotated`` type hints on function 

1186 parameters and return values. This metadata indicates which scale factors 

1187 to use when converting between 'Reference' and '1' modes. 

1188 

1189 Parameters 

1190 ---------- 

1191 function 

1192 Function to extract metadata from. 

1193 

1194 Returns 

1195 ------- 

1196 :class:`dict` 

1197 Dictionary with keys: 

1198 

1199 - ``domain``: Dict mapping parameter names to their scale factors 

1200 - ``range``: Scale factor for return value (int, tuple, or None) 

1201 

1202 Examples 

1203 -------- 

1204 >>> from colour.hints import Annotated, ArrayLike, NDArrayFloat 

1205 >>> def example_function( 

1206 ... XYZ: Domain1, 

1207 ... illuminant: ArrayLike = None, 

1208 ... ) -> Range100: 

1209 ... pass 

1210 >>> metadata = get_domain_range_scale_metadata(example_function) 

1211 >>> metadata["domain"] 

1212 {'XYZ': 1} 

1213 >>> metadata["range"] 

1214 100 

1215 """ 

1216 

1217 # Unwrap functools.partial to get the underlying function 

1218 if hasattr(function, "func"): 

1219 function = function.func # pyright: ignore 

1220 

1221 cache_key = id(function) 

1222 

1223 if is_caching_enabled() and cache_key in _CACHE_DOMAIN_RANGE_SCALE_METADATA: 

1224 return _CACHE_DOMAIN_RANGE_SCALE_METADATA[cache_key] 

1225 

1226 metadata: dict[str, Any] = {"domain": {}, "range": None} 

1227 

1228 def extract_scale_from_hint(hint: Any) -> Any | None: 

1229 """ 

1230 Extract scale metadata from a type hint, handling Union types. 

1231 

1232 Parameters 

1233 ---------- 

1234 hint 

1235 Type hint to extract scale from. 

1236 

1237 Returns 

1238 ------- 

1239 :class:`int` | :class:`tuple` | :class:`None` 

1240 Scale metadata if found, None otherwise. 

1241 """ 

1242 

1243 # Direct Annotated type with __metadata__ 

1244 if hasattr(hint, "__metadata__") and hint.__metadata__: 

1245 return next(iter(hint.__metadata__)) 

1246 

1247 # Union type: check if any arg is Annotated 

1248 origin = get_origin(hint) 

1249 if origin is Union: 

1250 for arg in get_args(hint): 

1251 if hasattr(arg, "__metadata__") and arg.__metadata__: 

1252 return next(iter(arg.__metadata__)) 

1253 

1254 return None 

1255 

1256 try: 

1257 hints = get_type_hints(function, include_extras=True) 

1258 # Process hints from get_type_hints (actual types with __metadata__) 

1259 for parameter_name, hint in hints.items(): 

1260 scale = extract_scale_from_hint(hint) 

1261 if scale is not None: 

1262 if parameter_name == "return": 

1263 metadata["range"] = scale 

1264 else: 

1265 metadata["domain"][parameter_name] = scale 

1266 except (AttributeError, TypeError, NameError): 

1267 # Fallback: parse string annotations (when `from __future__ import annotations`) 

1268 # Mapping of type alias names to their scale values 

1269 type_alias_scales = { 

1270 "Domain1": 1, 

1271 "Domain10": 10, 

1272 "Domain100": 100, 

1273 "Domain360": 360, 

1274 "Domain100_100_360": (100, 100, 360), 

1275 "Range1": 1, 

1276 "Range10": 10, 

1277 "Range100": 100, 

1278 "Range360": 360, 

1279 "Range100_100_360": (100, 100, 360), 

1280 } 

1281 

1282 hints = getattr(function, "__annotations__", {}) 

1283 for parameter_name, hint in hints.items(): 

1284 scale = None 

1285 

1286 # Check if hint is a type alias name 

1287 if isinstance(hint, str) and hint in type_alias_scales: 

1288 scale = type_alias_scales[hint] 

1289 # Extract scale from string: "Annotated[Type, scale]" -> scale 

1290 elif ( 

1291 isinstance(hint, str) 

1292 and "Annotated[" in hint 

1293 and (match := re.search(r"Annotated\[[^,]+,\s*([^\]]+)\]", hint)) 

1294 ): 

1295 scale_string = match.group(1).strip() 

1296 # Evaluate scale (could be int, tuple, etc.) 

1297 try: 

1298 scale = eval(scale_string) # noqa: S307 

1299 except (SyntaxError, NameError, ValueError): 

1300 scale = scale_string 

1301 

1302 if scale is not None: 

1303 if parameter_name == "return": 

1304 metadata["range"] = scale 

1305 else: 

1306 metadata["domain"][parameter_name] = scale 

1307 

1308 if is_caching_enabled(): 

1309 _CACHE_DOMAIN_RANGE_SCALE_METADATA[cache_key] = metadata 

1310 

1311 return metadata 

1312 

1313 

1314def to_domain_1( 

1315 a: ArrayLike, 

1316 scale_factor: ArrayLike = 100, 

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

1318) -> NDArray: 

1319 """ 

1320 Scale the specified array :math:`a` to domain **'1'**. 

1321 

1322 The behaviour is as follows: 

1323 

1324 - If *Colour* domain-range scale is **'Reference'** or **'1'**, the 

1325 definition is almost entirely by-passed and will conveniently 

1326 convert array :math:`a` to :class:`np.ndarray`. 

1327 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1328 private value only used for unit tests), array :math:`a` is divided 

1329 by ``scale_factor``, typically 100. 

1330 

1331 Parameters 

1332 ---------- 

1333 a 

1334 Array :math:`a` to scale to domain **'1'**. 

1335 scale_factor 

1336 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray` 

1337 if some axes need different scaling to be brought to domain **'1'**. 

1338 dtype 

1339 Data type used for the conversion to :class:`np.ndarray`. 

1340 

1341 Returns 

1342 ------- 

1343 :class:`numpy.ndarray` 

1344 Array :math:`a` scaled to domain **'1'**. 

1345 

1346 Examples 

1347 -------- 

1348 With *Colour* domain-range scale set to **'Reference'**: 

1349 

1350 >>> with domain_range_scale("Reference"): 

1351 ... to_domain_1(1) 

1352 array(1.0) 

1353 

1354 With *Colour* domain-range scale set to **'1'**: 

1355 

1356 >>> with domain_range_scale("1"): 

1357 ... to_domain_1(1) 

1358 array(1.0) 

1359 

1360 With *Colour* domain-range scale set to **'100'** (unsupported): 

1361 

1362 >>> with domain_range_scale("100"): 

1363 ... to_domain_1(1) 

1364 array(0.01) 

1365 """ 

1366 

1367 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1368 

1369 a = as_float_array(a, dtype).copy() 

1370 

1371 if _DOMAIN_RANGE_SCALE == "100": 

1372 a /= as_float_array(scale_factor) 

1373 

1374 return a 

1375 

1376 

1377def to_domain_10( 

1378 a: ArrayLike, 

1379 scale_factor: ArrayLike = 10, 

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

1381) -> NDArray: 

1382 """ 

1383 Scale the specified array :math:`a` to domain **'10'**, used by the 

1384 *Munsell Renotation System*. 

1385 

1386 The behaviour is as follows: 

1387 

1388 - If *Colour* domain-range scale is **'Reference'**, the definition 

1389 is almost entirely by-passed and will conveniently convert array 

1390 :math:`a` to :class:`np.ndarray`. 

1391 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1392 multiplied by ``scale_factor``, typically 10. 

1393 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1394 private value only used for unit tests), array :math:`a` is 

1395 divided by ``scale_factor``, typically 10. 

1396 

1397 Parameters 

1398 ---------- 

1399 a 

1400 Array :math:`a` to scale to domain **'10'**. 

1401 scale_factor 

1402 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray` 

1403 if some axes need different scaling to be brought to domain 

1404 **'10'**. 

1405 dtype 

1406 Data type used for the conversion to :class:`np.ndarray`. 

1407 

1408 Returns 

1409 ------- 

1410 :class:`numpy.ndarray` 

1411 Array :math:`a` scaled to domain **'10'**. 

1412 

1413 Examples 

1414 -------- 

1415 With *Colour* domain-range scale set to **'Reference'**: 

1416 

1417 >>> with domain_range_scale("Reference"): 

1418 ... to_domain_10(1) 

1419 array(1.0) 

1420 

1421 With *Colour* domain-range scale set to **'1'**: 

1422 

1423 >>> with domain_range_scale("1"): 

1424 ... to_domain_10(1) 

1425 array(10.0) 

1426 

1427 With *Colour* domain-range scale set to **'100'** (unsupported): 

1428 

1429 >>> with domain_range_scale("100"): 

1430 ... to_domain_10(1) 

1431 array(0.1) 

1432 """ 

1433 

1434 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1435 

1436 a = as_float_array(a, dtype).copy() 

1437 

1438 if _DOMAIN_RANGE_SCALE == "1": 

1439 a *= as_float_array(scale_factor) 

1440 

1441 if _DOMAIN_RANGE_SCALE == "100": 

1442 a /= as_float_array(scale_factor) 

1443 

1444 return a 

1445 

1446 

1447def to_domain_100( 

1448 a: ArrayLike, 

1449 scale_factor: ArrayLike = 100, 

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

1451) -> NDArray: 

1452 """ 

1453 Scale the specified array :math:`a` to domain **'100'**. 

1454 

1455 The behaviour is as follows: 

1456 

1457 - If *Colour* domain-range scale is **'Reference'** or **'100'** 

1458 (currently unsupported private value only used for unit tests), the 

1459 definition is almost entirely by-passed and will conveniently 

1460 convert array :math:`a` to :class:`np.ndarray`. 

1461 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1462 multiplied by ``scale_factor``, typically 100. 

1463 

1464 Parameters 

1465 ---------- 

1466 a 

1467 Array :math:`a` to scale to domain **'100'**. 

1468 scale_factor 

1469 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray` 

1470 if some axes need different scaling to be brought to domain 

1471 **'100'**. 

1472 dtype 

1473 Data type used for the conversion to :class:`np.ndarray`. 

1474 

1475 Returns 

1476 ------- 

1477 :class:`numpy.ndarray` 

1478 Array :math:`a` scaled to domain **'100'**. 

1479 

1480 Examples 

1481 -------- 

1482 With *Colour* domain-range scale set to **'Reference'**: 

1483 

1484 >>> with domain_range_scale("Reference"): 

1485 ... to_domain_100(1) 

1486 array(1.0) 

1487 

1488 With *Colour* domain-range scale set to **'1'**: 

1489 

1490 >>> with domain_range_scale("1"): 

1491 ... to_domain_100(1) 

1492 array(100.0) 

1493 

1494 With *Colour* domain-range scale set to **'100'** (unsupported): 

1495 

1496 >>> with domain_range_scale("100"): 

1497 ... to_domain_100(1) 

1498 array(1.0) 

1499 """ 

1500 

1501 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1502 

1503 a = as_float_array(a, dtype).copy() 

1504 

1505 if _DOMAIN_RANGE_SCALE == "1": 

1506 a *= as_float_array(scale_factor) 

1507 

1508 return a 

1509 

1510 

1511def to_domain_degrees( 

1512 a: ArrayLike, 

1513 scale_factor: ArrayLike = 360, 

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

1515) -> NDArray: 

1516 """ 

1517 Scale the specified array :math:`a` to degrees domain. 

1518 

1519 The behaviour is as follows: 

1520 

1521 - If *Colour* domain-range scale is **'Reference'**, the definition 

1522 is almost entirely by-passed and will conveniently convert array 

1523 :math:`a` to :class:`np.ndarray`. 

1524 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1525 multiplied by ``scale_factor``, typically 360. 

1526 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1527 private value only used for unit tests), array :math:`a` is 

1528 multiplied by ``scale_factor`` / 100, typically 360 / 100. 

1529 

1530 Parameters 

1531 ---------- 

1532 a 

1533 Array :math:`a` to scale to degrees domain. 

1534 scale_factor 

1535 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray` 

1536 if some axes need different scaling to be brought to degrees domain. 

1537 dtype 

1538 Data type used for the conversion to :class:`np.ndarray`. 

1539 

1540 Returns 

1541 ------- 

1542 :class:`numpy.ndarray` 

1543 Array :math:`a` scaled to degrees domain. 

1544 

1545 Examples 

1546 -------- 

1547 With *Colour* domain-range scale set to **'Reference'**: 

1548 

1549 >>> with domain_range_scale("Reference"): 

1550 ... to_domain_degrees(1) 

1551 array(1.0) 

1552 

1553 With *Colour* domain-range scale set to **'1'**: 

1554 

1555 >>> with domain_range_scale("1"): 

1556 ... to_domain_degrees(1) 

1557 array(360.0) 

1558 

1559 With *Colour* domain-range scale set to **'100'** (unsupported): 

1560 

1561 >>> with domain_range_scale("100"): 

1562 ... to_domain_degrees(1) 

1563 array(3.6) 

1564 """ 

1565 

1566 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1567 

1568 a = as_float_array(a, dtype).copy() 

1569 

1570 if _DOMAIN_RANGE_SCALE == "1": 

1571 a *= as_float_array(scale_factor) 

1572 

1573 if _DOMAIN_RANGE_SCALE == "100": 

1574 a *= as_float_array(scale_factor) / 100 

1575 

1576 return a 

1577 

1578 

1579def to_domain_int( 

1580 a: ArrayLike, 

1581 bit_depth: ArrayLike = 8, 

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

1583) -> NDArray: 

1584 """ 

1585 Scale the specified array :math:`a` to integer domain. 

1586 

1587 The behaviour is as follows: 

1588 

1589 - If *Colour* domain-range scale is **'Reference'**, the definition 

1590 is almost entirely by-passed and will conveniently convert array 

1591 :math:`a` to :class:`np.ndarray`. 

1592 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1593 multiplied by :math:`2^{bit\\_depth} - 1`. 

1594 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1595 private value only used for unit tests), array :math:`a` is 

1596 multiplied by :math:`2^{bit\\_depth} - 1`. 

1597 

1598 Parameters 

1599 ---------- 

1600 a 

1601 Array :math:`a` to scale to integer domain. 

1602 bit_depth 

1603 Bit-depth, usually *int* but can be a :class:`numpy.ndarray` if 

1604 some axis need different scaling to be brought to integer domain. 

1605 dtype 

1606 Data type used for the conversion to :class:`np.ndarray`. 

1607 

1608 Returns 

1609 ------- 

1610 :class:`numpy.ndarray` 

1611 Array :math:`a` scaled to integer domain. 

1612 

1613 Notes 

1614 ----- 

1615 - To avoid precision issues and rounding, the scaling is performed 

1616 on *float* numbers. 

1617 

1618 Examples 

1619 -------- 

1620 With *Colour* domain-range scale set to **'Reference'**: 

1621 

1622 >>> with domain_range_scale("Reference"): 

1623 ... to_domain_int(1) 

1624 array(1.0) 

1625 

1626 With *Colour* domain-range scale set to **'1'**: 

1627 

1628 >>> with domain_range_scale("1"): 

1629 ... to_domain_int(1) 

1630 array(255.0) 

1631 

1632 With *Colour* domain-range scale set to **'100'** (unsupported): 

1633 

1634 >>> with domain_range_scale("100"): 

1635 ... to_domain_int(1) 

1636 array(2.55) 

1637 """ 

1638 

1639 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1640 

1641 a = as_float_array(a, dtype).copy() 

1642 

1643 maximum_code_value: NDArray[DTypeInt] = np.power(2, bit_depth) - 1 

1644 if _DOMAIN_RANGE_SCALE == "1": 

1645 a *= maximum_code_value 

1646 

1647 if _DOMAIN_RANGE_SCALE == "100": 

1648 a *= maximum_code_value / 100 

1649 

1650 return a 

1651 

1652 

1653def from_range_1( 

1654 a: ArrayLike, 

1655 scale_factor: ArrayLike = 100, 

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

1657) -> NDArray: 

1658 """ 

1659 Scale the specified array :math:`a` from range **'1'**. 

1660 

1661 The behaviour is as follows: 

1662 

1663 - If *Colour* domain-range scale is **'Reference'** or **'1'**, the 

1664 definition is entirely by-passed. 

1665 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1666 private value only used for unit tests), array :math:`a` is 

1667 multiplied by ``scale_factor``, typically 100. 

1668 

1669 Parameters 

1670 ---------- 

1671 a 

1672 Array :math:`a` to scale from range **'1'**. 

1673 scale_factor 

1674 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray` 

1675 if some axis need different scaling to be brought from range 

1676 **'1'**. 

1677 dtype 

1678 Data type used for the conversion to :class:`np.ndarray`. 

1679 

1680 Returns 

1681 ------- 

1682 :class:`numpy.ndarray` 

1683 Array :math:`a` scaled from range **'1'**. 

1684 

1685 Warnings 

1686 -------- 

1687 The scale conversion of variable :math:`a` happens in-place, i.e., 

1688 :math:`a` will be mutated! 

1689 

1690 Examples 

1691 -------- 

1692 With *Colour* domain-range scale set to **'Reference'**: 

1693 

1694 >>> with domain_range_scale("Reference"): 

1695 ... from_range_1(1) 

1696 array(1.0) 

1697 

1698 With *Colour* domain-range scale set to **'1'**: 

1699 

1700 >>> with domain_range_scale("1"): 

1701 ... from_range_1(1) 

1702 array(1.0) 

1703 

1704 With *Colour* domain-range scale set to **'100'** (unsupported): 

1705 

1706 >>> with domain_range_scale("100"): 

1707 ... from_range_1(1) 

1708 array(100.0) 

1709 """ 

1710 

1711 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1712 

1713 a = as_float_array(a, dtype) 

1714 

1715 if _DOMAIN_RANGE_SCALE == "100": 

1716 a *= as_float_array(scale_factor) 

1717 

1718 return a 

1719 

1720 

1721def from_range_10( 

1722 a: ArrayLike, 

1723 scale_factor: ArrayLike = 10, 

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

1725) -> NDArray: 

1726 """ 

1727 Scale the specified array :math:`a` from range **'10'**, used by the 

1728 *Munsell Renotation System*. 

1729 

1730 The behaviour is as follows: 

1731 

1732 - If *Colour* domain-range scale is **'Reference'**, the definition 

1733 is entirely by-passed. 

1734 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1735 divided by ``scale_factor``, typically 10. 

1736 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1737 private value only used for unit tests), array :math:`a` is 

1738 multiplied by ``scale_factor``, typically 10. 

1739 

1740 Parameters 

1741 ---------- 

1742 a 

1743 Array :math:`a` to scale from range **'10'**. 

1744 scale_factor 

1745 Scale factor, usually *numeric* but can be a 

1746 :class:`numpy.ndarray` if some axis need different scaling to be 

1747 brought from range **'10'**. 

1748 dtype 

1749 Data type used for the conversion to :class:`np.ndarray`. 

1750 

1751 Returns 

1752 ------- 

1753 :class:`numpy.ndarray` 

1754 Array :math:`a` scaled from range **'10'**. 

1755 

1756 Warnings 

1757 -------- 

1758 The scale conversion of variable :math:`a` happens in-place, i.e., 

1759 :math:`a` will be mutated! 

1760 

1761 Examples 

1762 -------- 

1763 With *Colour* domain-range scale set to **'Reference'**: 

1764 

1765 >>> with domain_range_scale("Reference"): 

1766 ... from_range_10(1) 

1767 array(1.0) 

1768 

1769 With *Colour* domain-range scale set to **'1'**: 

1770 

1771 >>> with domain_range_scale("1"): 

1772 ... from_range_10(1) 

1773 array(0.1) 

1774 

1775 With *Colour* domain-range scale set to **'100'** (unsupported): 

1776 

1777 >>> with domain_range_scale("100"): 

1778 ... from_range_10(1) 

1779 array(10.0) 

1780 """ 

1781 

1782 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1783 

1784 a = as_float_array(a, dtype) 

1785 

1786 if _DOMAIN_RANGE_SCALE == "1": 

1787 a /= as_float_array(scale_factor) 

1788 

1789 if _DOMAIN_RANGE_SCALE == "100": 

1790 a *= as_float_array(scale_factor) 

1791 

1792 return a 

1793 

1794 

1795def from_range_100( 

1796 a: ArrayLike, 

1797 scale_factor: ArrayLike = 100, 

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

1799) -> NDArray: 

1800 """ 

1801 Scale the specified array :math:`a` from range **'100'**. 

1802 

1803 The behaviour is as follows: 

1804 

1805 - If *Colour* domain-range scale is **'Reference'** or **'100'** 

1806 (currently unsupported private value only used for unit tests), the 

1807 definition is entirely by-passed. 

1808 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1809 divided by ``scale_factor``, typically 100. 

1810 

1811 Parameters 

1812 ---------- 

1813 a 

1814 Array :math:`a` to scale from range **'100'**. 

1815 scale_factor 

1816 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray` 

1817 if some axes require different scaling to be brought from range 

1818 **'100'**. 

1819 dtype 

1820 Data type used for the conversion to :class:`numpy.ndarray`. 

1821 

1822 Returns 

1823 ------- 

1824 :class:`numpy.ndarray` 

1825 Array :math:`a` scaled from range **'100'**. 

1826 

1827 Warnings 

1828 -------- 

1829 The scale conversion of variable :math:`a` happens in-place, i.e., 

1830 :math:`a` will be mutated! 

1831 

1832 Examples 

1833 -------- 

1834 With *Colour* domain-range scale set to **'Reference'**: 

1835 

1836 >>> with domain_range_scale("Reference"): 

1837 ... from_range_100(1) 

1838 array(1.0) 

1839 

1840 With *Colour* domain-range scale set to **'1'**: 

1841 

1842 >>> with domain_range_scale("1"): 

1843 ... from_range_100(1) 

1844 array(0.01) 

1845 

1846 With *Colour* domain-range scale set to **'100'** (unsupported): 

1847 

1848 >>> with domain_range_scale("100"): 

1849 ... from_range_100(1) 

1850 array(1.0) 

1851 """ 

1852 

1853 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1854 

1855 a = as_float_array(a, dtype) 

1856 

1857 if _DOMAIN_RANGE_SCALE == "1": 

1858 a /= as_float_array(scale_factor) 

1859 

1860 return a 

1861 

1862 

1863def from_range_degrees( 

1864 a: ArrayLike, 

1865 scale_factor: ArrayLike = 360, 

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

1867) -> NDArray: 

1868 """ 

1869 Scale the specified array :math:`a` from degrees range. 

1870 

1871 The behaviour is as follows: 

1872 

1873 - If *Colour* domain-range scale is **'Reference'**, the definition 

1874 is entirely by-passed. 

1875 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1876 divided by ``scale_factor``, typically 360. 

1877 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1878 private value only used for unit tests), array :math:`a` is 

1879 divided by ``scale_factor`` / 100, typically 360 / 100. 

1880 

1881 Parameters 

1882 ---------- 

1883 a 

1884 Array :math:`a` to scale from degrees range. 

1885 scale_factor 

1886 Scale factor, usually *numeric* but can be a 

1887 :class:`numpy.ndarray` if some axes need different scaling to be 

1888 brought from degrees range. 

1889 dtype 

1890 Data type used for the conversion to :class:`numpy.ndarray`. 

1891 

1892 Returns 

1893 ------- 

1894 :class:`numpy.ndarray` 

1895 Array :math:`a` scaled from degrees range. 

1896 

1897 Warnings 

1898 -------- 

1899 The scale conversion of variable :math:`a` happens in-place, i.e., 

1900 :math:`a` will be mutated! 

1901 

1902 Examples 

1903 -------- 

1904 With *Colour* domain-range scale set to **'Reference'**: 

1905 

1906 >>> with domain_range_scale("Reference"): 

1907 ... from_range_degrees(1) 

1908 array(1.0) 

1909 

1910 With *Colour* domain-range scale set to **'1'**: 

1911 

1912 >>> with domain_range_scale("1"): 

1913 ... from_range_degrees(1) # doctest: +ELLIPSIS 

1914 array(0.0027777...) 

1915 

1916 With *Colour* domain-range scale set to **'100'** (unsupported): 

1917 

1918 >>> with domain_range_scale("100"): 

1919 ... from_range_degrees(1) # doctest: +ELLIPSIS 

1920 array(0.2777777...) 

1921 """ 

1922 

1923 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

1924 

1925 a = as_float_array(a, dtype) 

1926 

1927 if _DOMAIN_RANGE_SCALE == "1": 

1928 a /= as_float_array(scale_factor) 

1929 

1930 if _DOMAIN_RANGE_SCALE == "100": 

1931 a /= as_float_array(scale_factor) / 100 

1932 

1933 return a 

1934 

1935 

1936def from_range_int( 

1937 a: ArrayLike, 

1938 bit_depth: ArrayLike = 8, 

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

1940) -> NDArray: 

1941 """ 

1942 Scale the specified array :math:`a` from integer range. 

1943 

1944 The behaviour is as follows: 

1945 

1946 - If *Colour* domain-range scale is **'Reference'**, the definition 

1947 is entirely by-passed. 

1948 - If *Colour* domain-range scale is **'1'**, array :math:`a` is 

1949 converted to :class:`np.ndarray` and divided by 

1950 :math:`2^{bit\\_depth} - 1`. 

1951 - If *Colour* domain-range scale is **'100'** (currently unsupported 

1952 private value only used for unit tests), array :math:`a` is 

1953 converted to :class:`np.ndarray` and divided by 

1954 :math:`2^{bit\\_depth} - 1`. 

1955 

1956 Parameters 

1957 ---------- 

1958 a 

1959 Array :math:`a` to scale from integer range. 

1960 bit_depth 

1961 Bit-depth, usually *int* but can be a :class:`numpy.ndarray` if 

1962 some axes need different scaling to be brought from integer range. 

1963 dtype 

1964 Data type used for the conversion to :class:`np.ndarray`. 

1965 

1966 Returns 

1967 ------- 

1968 :class:`numpy.ndarray` 

1969 Array :math:`a` scaled from integer range. 

1970 

1971 Warnings 

1972 -------- 

1973 The scale conversion of variable :math:`a` happens in-place, i.e., 

1974 :math:`a` will be mutated! 

1975 

1976 Notes 

1977 ----- 

1978 - To avoid precision issues and rounding, the scaling is performed on 

1979 *float* numbers. 

1980 

1981 Examples 

1982 -------- 

1983 With *Colour* domain-range scale set to **'Reference'**: 

1984 

1985 >>> with domain_range_scale("Reference"): 

1986 ... from_range_int(1) 

1987 array(1.0) 

1988 

1989 With *Colour* domain-range scale set to **'1'**: 

1990 

1991 >>> with domain_range_scale("1"): 

1992 ... from_range_int(1) # doctest: +ELLIPSIS 

1993 array(0.0039215...) 

1994 

1995 With *Colour* domain-range scale set to **'100'** (unsupported): 

1996 

1997 >>> with domain_range_scale("100"): 

1998 ... from_range_int(1) # doctest: +ELLIPSIS 

1999 array(0.3921568...) 

2000 """ 

2001 

2002 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

2003 

2004 a = as_float_array(a, dtype) 

2005 

2006 maximum_code_value: NDArray[DTypeInt] = np.power(2, bit_depth) - 1 

2007 if _DOMAIN_RANGE_SCALE == "1": 

2008 a /= maximum_code_value 

2009 

2010 if _DOMAIN_RANGE_SCALE == "100": 

2011 a /= maximum_code_value / 100 

2012 

2013 return a 

2014 

2015 

2016_NDARRAY_COPY_ENABLED: bool = True 

2017""" 

2018Global variable storing the current *Colour* state for 

2019:class:`numpy.ndarray` copy. 

2020""" 

2021 

2022 

2023def is_ndarray_copy_enabled() -> bool: 

2024 """ 

2025 Determine whether *Colour* :class:`numpy.ndarray` copy is enabled. 

2026 

2027 Various API objects return a copy of their internal 

2028 :class:`numpy.ndarray` for safety purposes, but this can be a slow 

2029 operation impacting performance. 

2030 

2031 Returns 

2032 ------- 

2033 :class:`bool` 

2034 Whether *Colour* :class:`numpy.ndarray` copy is enabled. 

2035 

2036 Examples 

2037 -------- 

2038 >>> with ndarray_copy_enable(False): 

2039 ... is_ndarray_copy_enabled() 

2040 False 

2041 >>> with ndarray_copy_enable(True): 

2042 ... is_ndarray_copy_enabled() 

2043 True 

2044 """ 

2045 

2046 return _NDARRAY_COPY_ENABLED 

2047 

2048 

2049def set_ndarray_copy_enable(enable: bool) -> None: 

2050 """ 

2051 Set the *Colour* :class:`numpy.ndarray` copy enabled state. 

2052 

2053 Parameters 

2054 ---------- 

2055 enable 

2056 Whether to enable *Colour* :class:`numpy.ndarray` copy. 

2057 

2058 Examples 

2059 -------- 

2060 >>> with ndarray_copy_enable(is_ndarray_copy_enabled()): 

2061 ... print(is_ndarray_copy_enabled()) 

2062 ... set_ndarray_copy_enable(False) 

2063 ... print(is_ndarray_copy_enabled()) 

2064 True 

2065 False 

2066 """ 

2067 

2068 global _NDARRAY_COPY_ENABLED # noqa: PLW0603 

2069 

2070 _NDARRAY_COPY_ENABLED = enable 

2071 

2072 

2073class ndarray_copy_enable: 

2074 """ 

2075 Define a context manager and decorator to temporarily set the *Colour* 

2076 :class:`numpy.ndarray` copy enabled state. 

2077 

2078 Parameters 

2079 ---------- 

2080 enable 

2081 Whether to enable or disable *Colour* :class:`numpy.ndarray` copy. 

2082 """ 

2083 

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

2085 self._enable = enable 

2086 self._previous_state = is_ndarray_copy_enabled() 

2087 

2088 def __enter__(self) -> Self: 

2089 """ 

2090 Set the *Colour* :class:`numpy.ndarray` copy enabled state upon 

2091 entering the context manager. 

2092 """ 

2093 

2094 set_ndarray_copy_enable(self._enable) 

2095 

2096 return self 

2097 

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

2099 """ 

2100 Restore the *Colour* :class:`numpy.ndarray` copy enabled state upon 

2101 exiting the context manager. 

2102 """ 

2103 

2104 set_ndarray_copy_enable(self._previous_state) 

2105 

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

2107 """ 

2108 Decorate and call the specified function with array copy control. 

2109 

2110 Parameters 

2111 ---------- 

2112 function 

2113 Function to be decorated with array copy state management. 

2114 

2115 Returns 

2116 ------- 

2117 :class:`Callable` 

2118 Decorated function that executes within the configured array copy 

2119 state context. 

2120 """ 

2121 

2122 @functools.wraps(function) 

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

2124 with self: 

2125 return function(*args, **kwargs) 

2126 

2127 return wrapper 

2128 

2129 

2130def ndarray_copy(a: NDArray) -> NDArray: 

2131 """ 

2132 Return a :class:`numpy.ndarray` copy if the relevant *Colour* state is 

2133 enabled. 

2134 

2135 Various API objects return a copy of their internal 

2136 :class:`numpy.ndarray` for safety purposes, but this can be a slow 

2137 operation impacting performance. 

2138 

2139 Parameters 

2140 ---------- 

2141 a 

2142 Array :math:`a` to return a copy of. 

2143 

2144 Returns 

2145 ------- 

2146 :class:`numpy.ndarray` 

2147 Array :math:`a` copy according to *Colour* state. 

2148 

2149 Examples 

2150 -------- 

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

2152 >>> id(a) == id(ndarray_copy(a)) 

2153 False 

2154 >>> with ndarray_copy_enable(False): 

2155 ... id(a) == id(ndarray_copy(a)) 

2156 True 

2157 """ 

2158 

2159 if _NDARRAY_COPY_ENABLED: 

2160 return np.copy(a) 

2161 return a 

2162 

2163 

2164def closest_indexes(a: ArrayLike, b: ArrayLike) -> NDArray: 

2165 """ 

2166 Return the closest element indexes from array :math:`a` to reference array 

2167 :math:`b` elements. 

2168 

2169 Parameters 

2170 ---------- 

2171 a 

2172 Array :math:`a` to search for the closest elements. 

2173 b 

2174 Reference array :math:`b`. 

2175 

2176 Returns 

2177 ------- 

2178 :class:`numpy.ndarray` 

2179 Closest array :math:`a` element indexes. 

2180 

2181 Examples 

2182 -------- 

2183 >>> a = np.array( 

2184 ... [ 

2185 ... 24.31357115, 

2186 ... 63.62396289, 

2187 ... 55.71528816, 

2188 ... 62.70988028, 

2189 ... 46.84480573, 

2190 ... 25.40026416, 

2191 ... ] 

2192 ... ) 

2193 >>> print(closest_indexes(a, 63)) 

2194 [3] 

2195 >>> print(closest_indexes(a, [63, 25])) 

2196 [3 5] 

2197 """ 

2198 

2199 a = np.ravel(a)[:, None] 

2200 b = np.ravel(b)[None, :] 

2201 

2202 return np.abs(a - b).argmin(axis=0) 

2203 

2204 

2205def closest(a: ArrayLike, b: ArrayLike) -> NDArray: 

2206 """ 

2207 Return the closest array :math:`a` elements to reference array 

2208 :math:`b` elements. 

2209 

2210 Parameters 

2211 ---------- 

2212 a 

2213 Array :math:`a` to search for the closest elements. 

2214 b 

2215 Reference array :math:`b`. 

2216 

2217 Returns 

2218 ------- 

2219 :class:`numpy.ndarray` 

2220 Closest array :math:`a` elements. 

2221 

2222 Examples 

2223 -------- 

2224 >>> a = np.array( 

2225 ... [ 

2226 ... 24.31357115, 

2227 ... 63.62396289, 

2228 ... 55.71528816, 

2229 ... 62.70988028, 

2230 ... 46.84480573, 

2231 ... 25.40026416, 

2232 ... ] 

2233 ... ) 

2234 >>> closest(a, 63) 

2235 array([ 62.70988028]) 

2236 >>> closest(a, [63, 25]) 

2237 array([ 62.70988028, 25.40026416]) 

2238 """ 

2239 

2240 a = np.array(a) 

2241 

2242 return a[closest_indexes(a, b)] 

2243 

2244 

2245_CACHE_DISTRIBUTION_INTERVAL: dict = CACHE_REGISTRY.register_cache( 

2246 f"{__name__}._CACHE_DISTRIBUTION_INTERVAL" 

2247) 

2248 

2249 

2250def interval(distribution: ArrayLike, unique: bool = True) -> NDArray: 

2251 """ 

2252 Return the interval size of the specified distribution. 

2253 

2254 Parameters 

2255 ---------- 

2256 distribution 

2257 Distribution to retrieve the interval from. 

2258 unique 

2259 Whether to return unique intervals if the distribution is 

2260 non-uniformly spaced or the complete intervals. 

2261 

2262 Returns 

2263 ------- 

2264 :class:`numpy.ndarray` 

2265 Distribution interval. 

2266 

2267 Examples 

2268 -------- 

2269 Uniformly spaced variable: 

2270 

2271 >>> y = np.array([1, 2, 3, 4, 5]) 

2272 >>> interval(y) 

2273 array([ 1.]) 

2274 >>> interval(y, False) 

2275 array([ 1., 1., 1., 1.]) 

2276 

2277 Non-uniformly spaced variable: 

2278 

2279 >>> y = np.array([1, 2, 3, 4, 8]) 

2280 >>> interval(y) 

2281 array([ 1., 4.]) 

2282 >>> interval(y, False) 

2283 array([ 1., 1., 1., 4.]) 

2284 """ 

2285 

2286 distribution = as_float_array(distribution) 

2287 hash_key = hash( 

2288 ( 

2289 int_digest(distribution.tobytes()), 

2290 distribution.shape, 

2291 unique, 

2292 ) 

2293 ) 

2294 

2295 if is_caching_enabled() and hash_key in _CACHE_DISTRIBUTION_INTERVAL: 

2296 return np.copy(_CACHE_DISTRIBUTION_INTERVAL[hash_key]) 

2297 

2298 differences = np.abs(distribution[1:] - distribution[:-1]) 

2299 

2300 if unique and np.all(differences == differences[0]): 

2301 interval_ = np.array([differences[0]]) 

2302 elif unique: 

2303 interval_ = np.unique(differences) 

2304 else: 

2305 interval_ = differences 

2306 

2307 _CACHE_DISTRIBUTION_INTERVAL[hash_key] = np.copy(interval_) 

2308 

2309 return interval_ 

2310 

2311 

2312def is_uniform(distribution: ArrayLike) -> bool: 

2313 """ 

2314 Determine whether the specified distribution is uniform. 

2315 

2316 Parameters 

2317 ---------- 

2318 distribution 

2319 Distribution to check for uniformity. 

2320 

2321 Returns 

2322 ------- 

2323 :class:`bool` 

2324 Whether the distribution is uniform. 

2325 

2326 Examples 

2327 -------- 

2328 Uniformly spaced variable: 

2329 

2330 >>> a = np.array([1, 2, 3, 4, 5]) 

2331 >>> is_uniform(a) 

2332 True 

2333 

2334 Non-uniformly spaced variable: 

2335 

2336 >>> a = np.array([1, 2, 3.1415, 4, 5]) 

2337 >>> is_uniform(a) 

2338 False 

2339 """ 

2340 

2341 return bool(interval(distribution).size == 1) 

2342 

2343 

2344def in_array(a: ArrayLike, b: ArrayLike, tolerance: Real = EPSILON) -> NDArray: 

2345 """ 

2346 Determine whether each element of array :math:`a` is present in array 

2347 :math:`b` within the specified tolerance. 

2348 

2349 Parameters 

2350 ---------- 

2351 a 

2352 Array :math:`a` to test the elements from. 

2353 b 

2354 Array :math:`b` against which to test the elements of array 

2355 :math:`a`. 

2356 tolerance 

2357 Tolerance value. 

2358 

2359 Returns 

2360 ------- 

2361 :class:`numpy.ndarray` 

2362 Boolean array with array :math:`a` shape indicating whether each 

2363 element of array :math:`a` is present in array :math:`b` within the 

2364 specified tolerance. 

2365 

2366 References 

2367 ---------- 

2368 :cite:`Yorke2014a` 

2369 

2370 Examples 

2371 -------- 

2372 >>> a = np.array([0.50, 0.60]) 

2373 >>> b = np.linspace(0, 10, 101) 

2374 >>> np.isin(a, b) 

2375 array([ True, False], dtype=bool) 

2376 >>> in_array(a, b) 

2377 array([ True, True], dtype=bool) 

2378 """ 

2379 

2380 a = as_float_array(a) 

2381 b = as_float_array(b) 

2382 

2383 d = np.abs(np.ravel(a) - b[..., None]) 

2384 

2385 return np.reshape(np.any(d <= tolerance, axis=0), a.shape) 

2386 

2387 

2388def tstack( 

2389 a: ArrayLike, 

2390 dtype: Type[DTypeBoolean] | Type[DTypeReal] | None = None, 

2391) -> NDArray: 

2392 """ 

2393 Stack the specified array of arrays :math:`a` along the last axis (tail) 

2394 to produce a stacked array. 

2395 

2396 Used to stack an array of arrays produced by the 

2397 :func:`colour.utilities.tsplit` definition. 

2398 

2399 Parameters 

2400 ---------- 

2401 a 

2402 Array of arrays :math:`a` to stack along the last axis. 

2403 dtype 

2404 :class:`numpy.dtype` to use for initial conversion to 

2405 :class:`numpy.ndarray`, default to the :class:`numpy.dtype` defined 

2406 by :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2407 

2408 Returns 

2409 ------- 

2410 :class:`numpy.ndarray` 

2411 Stacked array. 

2412 

2413 Examples 

2414 -------- 

2415 >>> a = 0 

2416 >>> tstack([a, a, a]) 

2417 array([ 0., 0., 0.]) 

2418 >>> a = np.arange(0, 6) 

2419 >>> tstack([a, a, a]) 

2420 array([[ 0., 0., 0.], 

2421 [ 1., 1., 1.], 

2422 [ 2., 2., 2.], 

2423 [ 3., 3., 3.], 

2424 [ 4., 4., 4.], 

2425 [ 5., 5., 5.]]) 

2426 >>> a = np.reshape(a, (1, 6)) 

2427 >>> tstack([a, a, a]) 

2428 array([[[ 0., 0., 0.], 

2429 [ 1., 1., 1.], 

2430 [ 2., 2., 2.], 

2431 [ 3., 3., 3.], 

2432 [ 4., 4., 4.], 

2433 [ 5., 5., 5.]]]) 

2434 >>> a = np.reshape(a, (1, 1, 6)) 

2435 >>> tstack([a, a, a]) 

2436 array([[[[ 0., 0., 0.], 

2437 [ 1., 1., 1.], 

2438 [ 2., 2., 2.], 

2439 [ 3., 3., 3.], 

2440 [ 4., 4., 4.], 

2441 [ 5., 5., 5.]]]]) 

2442 """ 

2443 

2444 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

2445 

2446 a = as_array(a, dtype) 

2447 

2448 return np.concatenate([x[..., np.newaxis] for x in a], axis=-1) 

2449 

2450 

2451def tsplit( 

2452 a: ArrayLike, 

2453 dtype: Type[DTypeBoolean] | Type[DTypeReal] | None = None, 

2454) -> NDArray: 

2455 """ 

2456 Split the specified stacked array :math:`a` along the last axis (tail) 

2457 to produce an array of arrays. 

2458 

2459 Used to split a stacked array produced by the :func:`colour.utilities.tstack` 

2460 definition. 

2461 

2462 Parameters 

2463 ---------- 

2464 a 

2465 Stacked array :math:`a` to split. 

2466 dtype 

2467 :class:`numpy.dtype` to use for initial conversion to 

2468 :class:`numpy.ndarray`, default to the :class:`numpy.dtype` defined 

2469 by :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2470 

2471 Returns 

2472 ------- 

2473 :class:`numpy.ndarray` 

2474 Array of arrays. 

2475 

2476 Examples 

2477 -------- 

2478 >>> a = np.array([0, 0, 0]) 

2479 >>> tsplit(a) 

2480 array([ 0., 0., 0.]) 

2481 >>> a = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4], [5, 5, 5]]) 

2482 >>> tsplit(a) 

2483 array([[ 0., 1., 2., 3., 4., 5.], 

2484 [ 0., 1., 2., 3., 4., 5.], 

2485 [ 0., 1., 2., 3., 4., 5.]]) 

2486 >>> a = np.array( 

2487 ... [ 

2488 ... [ 

2489 ... [0, 0, 0], 

2490 ... [1, 1, 1], 

2491 ... [2, 2, 2], 

2492 ... [3, 3, 3], 

2493 ... [4, 4, 4], 

2494 ... [5, 5, 5], 

2495 ... ] 

2496 ... ] 

2497 ... ) 

2498 >>> tsplit(a) 

2499 array([[[ 0., 1., 2., 3., 4., 5.]], 

2500 <BLANKLINE> 

2501 [[ 0., 1., 2., 3., 4., 5.]], 

2502 <BLANKLINE> 

2503 [[ 0., 1., 2., 3., 4., 5.]]]) 

2504 """ 

2505 

2506 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

2507 

2508 a = as_array(a, dtype) 

2509 

2510 return np.array([a[..., x] for x in range(a.shape[-1])]) 

2511 

2512 

2513def row_as_diagonal(a: ArrayLike) -> NDArray: 

2514 """ 

2515 Return the rows of the specified array :math:`a` as diagonal matrices. 

2516 

2517 Parameters 

2518 ---------- 

2519 a 

2520 Array :math:`a` to return the rows of as diagonal matrices. 

2521 

2522 Returns 

2523 ------- 

2524 :class:`numpy.ndarray` 

2525 Array :math:`a` rows as diagonal matrices. 

2526 

2527 References 

2528 ---------- 

2529 :cite:`Castro2014a` 

2530 

2531 Examples 

2532 -------- 

2533 >>> a = np.array( 

2534 ... [ 

2535 ... [0.25891593, 0.07299478, 0.36586996], 

2536 ... [0.30851087, 0.37131459, 0.16274825], 

2537 ... [0.71061831, 0.67718718, 0.09562581], 

2538 ... [0.71588836, 0.76772047, 0.15476079], 

2539 ... [0.92985142, 0.22263399, 0.88027331], 

2540 ... ] 

2541 ... ) 

2542 >>> row_as_diagonal(a) 

2543 array([[[ 0.25891593, 0. , 0. ], 

2544 [ 0. , 0.07299478, 0. ], 

2545 [ 0. , 0. , 0.36586996]], 

2546 <BLANKLINE> 

2547 [[ 0.30851087, 0. , 0. ], 

2548 [ 0. , 0.37131459, 0. ], 

2549 [ 0. , 0. , 0.16274825]], 

2550 <BLANKLINE> 

2551 [[ 0.71061831, 0. , 0. ], 

2552 [ 0. , 0.67718718, 0. ], 

2553 [ 0. , 0. , 0.09562581]], 

2554 <BLANKLINE> 

2555 [[ 0.71588836, 0. , 0. ], 

2556 [ 0. , 0.76772047, 0. ], 

2557 [ 0. , 0. , 0.15476079]], 

2558 <BLANKLINE> 

2559 [[ 0.92985142, 0. , 0. ], 

2560 [ 0. , 0.22263399, 0. ], 

2561 [ 0. , 0. , 0.88027331]]]) 

2562 """ 

2563 

2564 d = as_array(a) 

2565 

2566 d = np.expand_dims(d, -2) 

2567 

2568 return np.eye(d.shape[-1]) * d 

2569 

2570 

2571def orient( 

2572 a: ArrayLike, 

2573 orientation: ( 

2574 Literal["Ignore", "Flip", "Flop", "90 CW", "90 CCW", "180"] | str 

2575 ) = "Ignore", 

2576) -> NDArray: 

2577 """ 

2578 Orient the specified array :math:`a` using the specified orientation. 

2579 

2580 Parameters 

2581 ---------- 

2582 a 

2583 Array :math:`a` to orient. 

2584 orientation 

2585 Orientation to perform. 

2586 

2587 Returns 

2588 ------- 

2589 :class:`numpy.ndarray` 

2590 Oriented array. 

2591 

2592 Examples 

2593 -------- 

2594 >>> a = np.tile(np.arange(5), (5, 1)) 

2595 >>> a 

2596 array([[0, 1, 2, 3, 4], 

2597 [0, 1, 2, 3, 4], 

2598 [0, 1, 2, 3, 4], 

2599 [0, 1, 2, 3, 4], 

2600 [0, 1, 2, 3, 4]]) 

2601 >>> orient(a, "90 CW") 

2602 array([[ 0., 0., 0., 0., 0.], 

2603 [ 1., 1., 1., 1., 1.], 

2604 [ 2., 2., 2., 2., 2.], 

2605 [ 3., 3., 3., 3., 3.], 

2606 [ 4., 4., 4., 4., 4.]]) 

2607 >>> orient(a, "Flip") 

2608 array([[ 4., 3., 2., 1., 0.], 

2609 [ 4., 3., 2., 1., 0.], 

2610 [ 4., 3., 2., 1., 0.], 

2611 [ 4., 3., 2., 1., 0.], 

2612 [ 4., 3., 2., 1., 0.]]) 

2613 """ 

2614 

2615 a = as_float_array(a) 

2616 

2617 orientation = validate_method( 

2618 orientation, ("Ignore", "Flip", "Flop", "90 CW", "90 CCW", "180") 

2619 ) 

2620 

2621 if orientation == "ignore": 

2622 oriented = a 

2623 elif orientation == "flip": 

2624 oriented = np.fliplr(a) 

2625 elif orientation == "flop": 

2626 oriented = np.flipud(a) 

2627 elif orientation == "90 cw": 

2628 oriented = np.rot90(a, 3) 

2629 elif orientation == "90 ccw": 

2630 oriented = np.rot90(a) 

2631 elif orientation == "180": 

2632 oriented = np.rot90(a, 2) 

2633 

2634 return oriented 

2635 

2636 

2637def centroid(a: ArrayLike) -> NDArrayInt: 

2638 """ 

2639 Return the centroid indexes of the specified array :math:`a`. 

2640 

2641 Parameters 

2642 ---------- 

2643 a 

2644 Array :math:`a` to return the centroid indexes of. 

2645 

2646 Returns 

2647 ------- 

2648 :class:`numpy.ndarray` 

2649 Centroid indexes of array :math:`a`. 

2650 

2651 Examples 

2652 -------- 

2653 >>> a = np.tile(np.arange(0, 5), (5, 1)) 

2654 >>> centroid(a) # doctest: +ELLIPSIS 

2655 array([2, 3]...) 

2656 """ 

2657 

2658 a = as_float_array(a) 

2659 

2660 a_s = np.sum(a) 

2661 

2662 ranges = [np.arange(0, a.shape[i]) for i in range(a.ndim)] 

2663 coordinates = np.meshgrid(*ranges) 

2664 

2665 a_ci = [] 

2666 for axis in coordinates: 

2667 axis = np.transpose(axis) # noqa: PLW2901 

2668 # Aligning axis for N-D arrays where N is normalised to 

2669 # range [3, :math:`\\\infty`] 

2670 for i in range(axis.ndim - 2, 0, -1): 

2671 axis = np.rollaxis(axis, i - 1, axis.ndim) # noqa: PLW2901 

2672 

2673 a_ci.append(np.sum(axis * a) // a_s) 

2674 

2675 # NOTE: Cannot use `as_int_array` as presence of NaN will raise a ValueError 

2676 # exception. 

2677 return np.array(a_ci).astype(DTYPE_INT_DEFAULT) 

2678 

2679 

2680def fill_nan( 

2681 a: ArrayLike, 

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

2683 default: Real = 0, 

2684) -> NDArray: 

2685 """ 

2686 Fill the NaN values in the specified array :math:`a` using the specified 

2687 method. 

2688 

2689 Parameters 

2690 ---------- 

2691 a 

2692 Array :math:`a` to fill the NaNs of. 

2693 method 

2694 *Interpolation* method linearly interpolates through the NaN values, 

2695 *Constant* method replaces NaN values with ``default``. 

2696 default 

2697 Value to use with the *Constant* method. 

2698 

2699 Returns 

2700 ------- 

2701 :class:`numpy.ndarray` 

2702 NaN-filled array :math:`a`. 

2703 

2704 Examples 

2705 -------- 

2706 >>> a = np.array([0.1, 0.2, np.nan, 0.4, 0.5]) 

2707 >>> fill_nan(a) 

2708 array([ 0.1, 0.2, 0.3, 0.4, 0.5]) 

2709 >>> fill_nan(a, method="Constant") 

2710 array([ 0.1, 0.2, 0. , 0.4, 0.5]) 

2711 """ 

2712 

2713 a = np.array(a, copy=True) 

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

2715 

2716 mask = np.isnan(a) 

2717 

2718 if method == "interpolation": 

2719 a[mask] = np.interp(np.flatnonzero(mask), np.flatnonzero(~mask), a[~mask]) 

2720 elif method == "constant": 

2721 a[mask] = default 

2722 

2723 return a 

2724 

2725 

2726def has_only_nan(a: ArrayLike) -> bool: 

2727 """ 

2728 Return whether the specified array :math:`a` contains only *NaN* values. 

2729 

2730 Parameters 

2731 ---------- 

2732 a 

2733 Array :math:`a` to check whether it contains only *NaN* values. 

2734 

2735 Returns 

2736 ------- 

2737 :class:`bool` 

2738 Whether array :math:`a` contains only *NaN* values. 

2739 

2740 Examples 

2741 -------- 

2742 >>> has_only_nan(None) 

2743 True 

2744 >>> has_only_nan([None, None]) 

2745 True 

2746 >>> has_only_nan([True, None]) 

2747 False 

2748 >>> has_only_nan([0.1, np.nan, 0.3]) 

2749 False 

2750 """ 

2751 

2752 a = as_float_array(a) 

2753 

2754 return bool(np.all(np.isnan(a))) 

2755 

2756 

2757@contextmanager 

2758def ndarray_write(a: ArrayLike) -> Generator: 

2759 """ 

2760 Define a context manager that temporarily sets the specified array 

2761 :math:`a` to writeable for operations, then restores it to read-only. 

2762 

2763 Parameters 

2764 ---------- 

2765 a 

2766 Array :math:`a` to operate on. 

2767 

2768 Yields 

2769 ------ 

2770 Generator 

2771 Array :math:`a` made temporarily writeable. 

2772 

2773 Examples 

2774 -------- 

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

2776 >>> a.setflags(write=False) 

2777 >>> try: 

2778 ... a += 1 

2779 ... except ValueError: 

2780 ... pass 

2781 >>> with ndarray_write(a): 

2782 ... a += 1 

2783 """ 

2784 

2785 a = as_float_array(a) 

2786 

2787 a.setflags(write=True) 

2788 

2789 try: 

2790 yield a 

2791 finally: 

2792 a.setflags(write=False) 

2793 

2794 

2795def zeros( 

2796 shape: int | Sequence[int], 

2797 dtype: Type[DTypeReal] | None = None, 

2798 order: Literal["C", "F"] = "C", 

2799) -> NDArray: 

2800 """ 

2801 Create an array of zeros with the active dtype. 

2802 

2803 Wrap :func:`np.zeros` definition to create an array with the active 

2804 :class:`numpy.dtype` defined by the 

2805 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2806 

2807 Parameters 

2808 ---------- 

2809 shape 

2810 Shape of the new array, e.g., ``(2, 3)`` or ``2``. 

2811 dtype 

2812 :class:`numpy.dtype` to use for conversion, default to the 

2813 :class:`numpy.dtype` defined by the 

2814 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2815 order 

2816 Whether to store multi-dimensional data in row-major 

2817 (C-style) or column-major (Fortran-style) order in memory. 

2818 

2819 Returns 

2820 ------- 

2821 :class:`numpy.ndarray` 

2822 Array of the specified shape and :class:`numpy.dtype`, filled 

2823 with zeros. 

2824 

2825 Examples 

2826 -------- 

2827 >>> zeros(3) 

2828 array([ 0., 0., 0.]) 

2829 """ 

2830 

2831 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

2832 

2833 return np.zeros(shape, dtype, order) 

2834 

2835 

2836def ones( 

2837 shape: int | Sequence[int], 

2838 dtype: Type[DTypeReal] | None = None, 

2839 order: Literal["C", "F"] = "C", 

2840) -> NDArray: 

2841 """ 

2842 Create an array of ones with the active dtype. 

2843 

2844 Wrap :func:`np.ones` definition to create an array with the active 

2845 :class:`numpy.dtype` defined by the 

2846 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2847 

2848 Parameters 

2849 ---------- 

2850 shape 

2851 Shape of the new array, e.g., ``(2, 3)`` or ``2``. 

2852 dtype 

2853 :class:`numpy.dtype` to use for conversion, default to the 

2854 :class:`numpy.dtype` defined by the 

2855 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2856 order 

2857 Whether to store multi-dimensional data in row-major (C-style) or 

2858 column-major (Fortran-style) order in memory. 

2859 

2860 Returns 

2861 ------- 

2862 :class:`numpy.ndarray` 

2863 Array of the specified shape and :class:`numpy.dtype`, filled with ones. 

2864 

2865 Examples 

2866 -------- 

2867 >>> ones(3) 

2868 array([ 1., 1., 1.]) 

2869 """ 

2870 

2871 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

2872 

2873 return np.ones(shape, dtype, order) 

2874 

2875 

2876def full( 

2877 shape: int | Sequence[int], 

2878 fill_value: Real, 

2879 dtype: Type[DTypeReal] | None = None, 

2880 order: Literal["C", "F"] = "C", 

2881) -> NDArray: 

2882 """ 

2883 Create an array of the specified value with the active dtype. 

2884 

2885 Wrap :func:`np.full` definition to create an array with the active 

2886 :class:`numpy.dtype` defined by the 

2887 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2888 

2889 Parameters 

2890 ---------- 

2891 shape 

2892 Shape of the new array, e.g., ``(2, 3)`` or ``2``. 

2893 fill_value 

2894 Fill value. 

2895 dtype 

2896 :class:`numpy.dtype` to use for conversion, default to the 

2897 :class:`numpy.dtype` defined by the 

2898 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute. 

2899 order 

2900 Whether to store multi-dimensional data in row-major (C-style) or 

2901 column-major (Fortran-style) order in memory. 

2902 

2903 Returns 

2904 ------- 

2905 :class:`numpy.ndarray` 

2906 Array of the specified shape and :class:`numpy.dtype`, filled with 

2907 the specified value. 

2908 

2909 Examples 

2910 -------- 

2911 >>> full(3, 2.5) 

2912 array([ 2.5, 2.5, 2.5]) 

2913 """ 

2914 

2915 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) 

2916 

2917 return np.full(shape, fill_value, dtype, order) 

2918 

2919 

2920def index_along_last_axis(a: ArrayLike, indexes: ArrayLike) -> NDArray: 

2921 """ 

2922 Reduce the dimension of array :math:`a` by one, using an array of 

2923 indexes to select elements from the last axis. 

2924 

2925 Parameters 

2926 ---------- 

2927 a 

2928 Array :math:`a` to be indexed. 

2929 indexes 

2930 *Integer* array with the same shape as :math:`a` but with one 

2931 dimension fewer, containing indices to the last dimension of 

2932 :math:`a`. All elements must be numbers between 0 and 

2933 :math:`m - 1`. 

2934 

2935 Returns 

2936 ------- 

2937 :class:`numpy.ndarray` 

2938 Indexed array :math:`a`. 

2939 

2940 Raises 

2941 ------ 

2942 :class:`ValueError` 

2943 If the array :math:`a` and ``indexes`` have incompatible shapes. 

2944 :class:`IndexError` 

2945 If ``indexes`` has elements outside of the allowed range of 0 to 

2946 :math:`m - 1` or if it is not an *integer* array. 

2947 

2948 Examples 

2949 -------- 

2950 >>> a = np.array( 

2951 ... [ 

2952 ... [ 

2953 ... [0.3, 0.5, 6.9], 

2954 ... [3.3, 4.4, 1.6], 

2955 ... [4.4, 7.5, 2.3], 

2956 ... [2.3, 1.6, 7.4], 

2957 ... ], 

2958 ... [ 

2959 ... [2.0, 5.9, 2.8], 

2960 ... [6.2, 4.9, 8.6], 

2961 ... [3.7, 9.7, 7.3], 

2962 ... [6.3, 4.3, 3.2], 

2963 ... ], 

2964 ... [ 

2965 ... [0.8, 1.9, 0.7], 

2966 ... [5.6, 4.0, 1.7], 

2967 ... [6.7, 8.2, 1.7], 

2968 ... [1.2, 7.1, 1.4], 

2969 ... ], 

2970 ... [ 

2971 ... [4.0, 4.8, 8.9], 

2972 ... [4.0, 0.3, 6.9], 

2973 ... [3.5, 7.1, 4.5], 

2974 ... [1.4, 1.9, 1.6], 

2975 ... ], 

2976 ... ] 

2977 ... ) 

2978 >>> indexes = np.array([[2, 0, 1, 1], [2, 1, 1, 0], [0, 0, 1, 2], [0, 0, 1, 2]]) 

2979 >>> index_along_last_axis(a, indexes) 

2980 array([[ 6.9, 3.3, 7.5, 1.6], 

2981 [ 2.8, 4.9, 9.7, 6.3], 

2982 [ 0.8, 5.6, 8.2, 1.4], 

2983 [ 4. , 4. , 7.1, 1.6]]) 

2984 

2985 This function can be used to compute the result of :func:`np.min` along 

2986 the last axis given the corresponding :func:`np.argmin` indexes. 

2987 

2988 >>> indexes = np.argmin(a, axis=-1) 

2989 >>> np.array_equal(index_along_last_axis(a, indexes), np.min(a, axis=-1)) 

2990 True 

2991 

2992 In particular, this can be used to manipulate the indexes specified by 

2993 functions like :func:`np.min` before indexing the array. For example, to 

2994 get elements directly following the smallest elements: 

2995 

2996 >>> index_along_last_axis(a, (indexes + 1) % 3) 

2997 array([[ 0.5, 3.3, 4.4, 7.4], 

2998 [ 5.9, 8.6, 9.7, 6.3], 

2999 [ 0.8, 5.6, 6.7, 7.1], 

3000 [ 4.8, 6.9, 7.1, 1.9]]) 

3001 """ 

3002 

3003 a = np.array(a) 

3004 indexes = np.array(indexes) 

3005 

3006 if a.shape[:-1] != indexes.shape: 

3007 error = ( 

3008 f"Array and indexes have incompatible shapes: {a.shape} and {indexes.shape}" 

3009 ) 

3010 

3011 raise ValueError(error) 

3012 

3013 return np.take_along_axis(a, indexes[..., None], axis=-1).squeeze(axis=-1) 

3014 

3015 

3016def format_array_as_row(a: ArrayLike, decimals: int = 7, separator: str = " ") -> str: 

3017 """ 

3018 Format the specified array :math:`a` as a row. 

3019 

3020 Parameters 

3021 ---------- 

3022 a 

3023 Array to format. 

3024 decimals 

3025 Decimal count to use when formatting as a row. 

3026 separator 

3027 Separator used to join the array :math:`a` items. 

3028 

3029 Returns 

3030 ------- 

3031 :class:`str` 

3032 Array formatted as a row. 

3033 

3034 Examples 

3035 -------- 

3036 >>> format_array_as_row([1.25, 2.5, 3.75]) 

3037 '1.2500000 2.5000000 3.7500000' 

3038 >>> format_array_as_row([1.25, 2.5, 3.75], 3) 

3039 '1.250 2.500 3.750' 

3040 >>> format_array_as_row([1.25, 2.5, 3.75], 3, ", ") 

3041 '1.250, 2.500, 3.750' 

3042 """ 

3043 

3044 a = np.ravel(a) 

3045 

3046 return separator.join( 

3047 "{1:0.{0}f}".format(decimals, x) 

3048 for x in a # noqa: PLE1300, RUF100 

3049 )