Coverage for io/tm2714.py: 62%

296 statements  

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

1""" 

2IES TM-27-14 Data Input / Output 

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

4 

5Define classes and utilities for handling *IES TM-27-14* spectral data *XML* 

6files. 

7 

8This module provides the :class:`colour.SpectralDistribution_IESTM2714` class 

9for reading and writing spectral distribution data according to the *IES 

10TM-27-14* standard format for electronic transfer of spectral data. 

11 

12References 

13---------- 

14- :cite:`IESComputerCommittee2014a` : IES Computer Committee, & TM-27-14 

15 Working Group. (2014). IES Standard Format for the Electronic Transfer of 

16 Spectral Data Electronic Transfer of Spectral Data. Illuminating 

17 Engineering Society. ISBN:978-0-87995-295-2 

18""" 

19 

20from __future__ import annotations 

21 

22import os 

23import re 

24import typing 

25from dataclasses import dataclass, field 

26from pathlib import Path 

27from xml.dom import minidom 

28from xml.etree import ElementTree as ET 

29 

30from colour.colorimetry import SpectralDistribution 

31 

32if typing.TYPE_CHECKING: 

33 from colour.hints import Any, Callable, Literal, PathLike 

34 

35from colour.utilities import ( 

36 Structure, 

37 as_float_array, 

38 as_float_scalar, 

39 attest, 

40 is_numeric, 

41 multiline_repr, 

42 multiline_str, 

43 optional, 

44 tstack, 

45) 

46 

47__author__ = "Colour Developers" 

48__copyright__ = "Copyright 2013 Colour Developers" 

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

50__maintainer__ = "Colour Developers" 

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

52__status__ = "Production" 

53 

54__all__ = [ 

55 "VERSION_IESTM2714", 

56 "NAMESPACE_IESTM2714", 

57 "Element_Specification_IESTM2714", 

58 "Header_IESTM2714", 

59 "SpectralDistribution_IESTM2714", 

60] 

61 

62VERSION_IESTM2714: str = "1.0" 

63 

64NAMESPACE_IESTM2714: str = "http://www.ies.org/iestm2714" 

65 

66 

67@dataclass 

68class Element_Specification_IESTM2714: 

69 """ 

70 Define *IES TM-27-14* spectral data *XML* file element specification. 

71 

72 Parameters 

73 ---------- 

74 element 

75 Element name. 

76 attribute 

77 Associated attribute name. 

78 type_ 

79 Element type. 

80 required 

81 Is element required. 

82 read_conversion 

83 Method to convert from *XML* to type on reading. 

84 write_conversion 

85 Method to convert from type to *XML* on writing. 

86 """ 

87 

88 element: str 

89 attribute: str 

90 type_: Any = field(default_factory=str) 

91 required: bool = field(default_factory=lambda: False) 

92 read_conversion: Callable = field( 

93 default_factory=lambda: lambda x: None if x == "None" else str(x) 

94 ) 

95 write_conversion: Callable = field(default_factory=lambda: str) 

96 

97 

98class Header_IESTM2714: 

99 """ 

100 Define the header object for an *IES TM-27-14* spectral distribution. 

101 

102 Parameters 

103 ---------- 

104 manufacturer 

105 Manufacturer of the device under test. 

106 catalog_number 

107 Manufacturer's product catalog number. 

108 description 

109 Description of the spectral data in the spectral data *XML* file. 

110 document_creator 

111 Creator of the spectral data *XML* file, which may be a test lab, 

112 a research group, a standard body, a company or an individual. 

113 unique_identifier 

114 Unique identifier to the product under test or the spectral data 

115 in the document. 

116 measurement_equipment 

117 Description of the equipment used to measure the spectral data. 

118 laboratory 

119 Testing laboratory name that performed the spectral data 

120 measurements. 

121 report_number 

122 Testing laboratory report number. 

123 report_date 

124 Testing laboratory report date using the *XML DateTime Data Type*, 

125 *YYYY-MM-DDThh:mm:ss*. 

126 document_creation_date 

127 Spectral data *XML* file creation date using the 

128 *XML DateTime Data Type*, *YYYY-MM-DDThh:mm:ss*. 

129 comments 

130 Additional information relating to the tested and reported data. 

131 

132 Attributes 

133 ---------- 

134 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.mapping` 

135 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.manufacturer` 

136 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.catalog_number` 

137 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.description` 

138 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.document_creator` 

139 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.unique_identifier` 

140 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.measurement_equipment` 

141 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.laboratory` 

142 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.report_number` 

143 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.report_date` 

144 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.document_creation_date` 

145 - :attr:`~colour.io.ies_tm2714.Header_IESTM2714.comments` 

146 

147 Methods 

148 ------- 

149 - :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__init__` 

150 - :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__str__` 

151 - :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__repr__` 

152 - :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__hash__` 

153 - :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__eq__` 

154 - :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__ne__` 

155 

156 Examples 

157 -------- 

158 >>> Header_IESTM2714("colour-science") # doctest: +ELLIPSIS 

159 Header_IESTM2714('colour-science', 

160 None, 

161 None, 

162 None, 

163 None, 

164 None, 

165 None, 

166 None, 

167 None, 

168 None, 

169 None) 

170 >>> Header_IESTM2714("colour-science").manufacturer # doctest: +SKIP 

171 'colour-science' 

172 """ 

173 

174 def __init__( 

175 self, 

176 manufacturer: str | None = None, 

177 catalog_number: str | None = None, 

178 description: str | None = None, 

179 document_creator: str | None = None, 

180 unique_identifier: str | None = None, 

181 measurement_equipment: str | None = None, 

182 laboratory: str | None = None, 

183 report_number: str | None = None, 

184 report_date: str | None = None, 

185 document_creation_date: str | None = None, 

186 comments: str | None = None, 

187 ) -> None: 

188 self._mapping: Structure = Structure( 

189 element="Header", 

190 elements=( 

191 Element_Specification_IESTM2714("Manufacturer", "manufacturer"), 

192 Element_Specification_IESTM2714("CatalogNumber", "catalog_number"), 

193 Element_Specification_IESTM2714( 

194 "Description", "description", required=True 

195 ), 

196 Element_Specification_IESTM2714( 

197 "DocumentCreator", "document_creator", required=True 

198 ), 

199 Element_Specification_IESTM2714( 

200 "UniqueIdentifier", "unique_identifier" 

201 ), 

202 Element_Specification_IESTM2714( 

203 "MeasurementEquipment", "measurement_equipment" 

204 ), 

205 Element_Specification_IESTM2714("Laboratory", "laboratory"), 

206 Element_Specification_IESTM2714("ReportNumber", "report_number"), 

207 Element_Specification_IESTM2714("ReportDate", "report_date"), 

208 Element_Specification_IESTM2714( 

209 "DocumentCreationDate", 

210 "document_creation_date", 

211 required=True, 

212 ), 

213 Element_Specification_IESTM2714("Comments", "comments", False), 

214 ), 

215 ) 

216 

217 self._manufacturer: str | None = None 

218 self.manufacturer = manufacturer 

219 self._catalog_number: str | None = None 

220 self.catalog_number = catalog_number 

221 self._description: str | None = None 

222 self.description = description 

223 self._document_creator: str | None = None 

224 self.document_creator = document_creator 

225 self._unique_identifier: str | None = None 

226 self.unique_identifier = unique_identifier 

227 self._measurement_equipment: str | None = None 

228 self.measurement_equipment = measurement_equipment 

229 self._laboratory: str | None = None 

230 self.laboratory = laboratory 

231 self._report_number: str | None = None 

232 self.report_number = report_number 

233 self._report_date: str | None = None 

234 self.report_date = report_date 

235 self._document_creation_date: str | None = None 

236 self.document_creation_date = document_creation_date 

237 self._comments: str | None = None 

238 self.comments = comments 

239 

240 @property 

241 def mapping(self) -> Structure: 

242 """ 

243 Getter for the mapping structure. 

244 

245 Returns 

246 ------- 

247 :class:`colour.utilities.Structure` 

248 Mapping structure. 

249 """ 

250 

251 return self._mapping 

252 

253 @property 

254 def manufacturer(self) -> str | None: 

255 """ 

256 Getter and setter for the manufacturer name. 

257 

258 Parameters 

259 ---------- 

260 value 

261 Value to set the manufacturer with. 

262 

263 Returns 

264 ------- 

265 :class:`str` or :py:data:`None` 

266 Manufacturer name. 

267 """ 

268 

269 return self._manufacturer 

270 

271 @manufacturer.setter 

272 def manufacturer(self, value: str | None) -> None: 

273 """Setter for the **self.manufacturer** property.""" 

274 

275 if value is not None: 

276 attest( 

277 isinstance(value, str), 

278 f'"manufacturer" property: "{value}" type is not "str"!', 

279 ) 

280 

281 self._manufacturer = value 

282 

283 @property 

284 def catalog_number(self) -> str | None: 

285 """ 

286 Getter and setter for the catalog number. 

287 

288 Parameters 

289 ---------- 

290 value 

291 Value to set the catalog number with. 

292 

293 Returns 

294 ------- 

295 :class:`str` or :py:data:`None` 

296 Catalog number. 

297 """ 

298 

299 return self._catalog_number 

300 

301 @catalog_number.setter 

302 def catalog_number(self, value: str | None) -> None: 

303 """Setter for the **self.catalog_number** property.""" 

304 

305 if value is not None: 

306 attest( 

307 isinstance(value, str), 

308 f'"catalog_number" property: "{value}" type is not "str"!', 

309 ) 

310 

311 self._catalog_number = value 

312 

313 @property 

314 def description(self) -> str | None: 

315 """ 

316 Getter and setter for the description. 

317 

318 Parameters 

319 ---------- 

320 value 

321 Value to set the description with. 

322 

323 Returns 

324 ------- 

325 :class:`str` or :py:data:`None` 

326 Description. 

327 """ 

328 

329 return self._description 

330 

331 @description.setter 

332 def description(self, value: str | None) -> None: 

333 """Setter for the **self.description** property.""" 

334 

335 if value is not None: 

336 attest( 

337 isinstance(value, str), 

338 f'"description" property: "{value}" type is not "str"!', 

339 ) 

340 

341 self._description = value 

342 

343 @property 

344 def document_creator(self) -> str | None: 

345 """ 

346 Getter and setter for the document creator. 

347 

348 Parameters 

349 ---------- 

350 value 

351 Value to set the document creator with. 

352 

353 Returns 

354 ------- 

355 :class:`str` or :py:data:`None` 

356 Document creator. 

357 """ 

358 

359 return self._document_creator 

360 

361 @document_creator.setter 

362 def document_creator(self, value: str | None) -> None: 

363 """Setter for the **self.document_creator** property.""" 

364 

365 if value is not None: 

366 attest( 

367 isinstance(value, str), 

368 f'"document_creator" property: "{value}" type is not "str"!', 

369 ) 

370 

371 self._document_creator = value 

372 

373 @property 

374 def unique_identifier(self) -> str | None: 

375 """ 

376 Getter and setter for the unique identifier. 

377 

378 Parameters 

379 ---------- 

380 value 

381 Value to set the unique identifier with. 

382 

383 Returns 

384 ------- 

385 :class:`str` or :py:data:`None` 

386 Unique identifier. 

387 """ 

388 

389 return self._unique_identifier 

390 

391 @unique_identifier.setter 

392 def unique_identifier(self, value: str | None) -> None: 

393 """Setter for the **self.unique_identifier** property.""" 

394 

395 if value is not None: 

396 attest( 

397 isinstance(value, str), 

398 f'"unique_identifier" property: "{value}" type is not "str"!', 

399 ) 

400 

401 self._unique_identifier = value 

402 

403 @property 

404 def measurement_equipment(self) -> str | None: 

405 """ 

406 Getter and setter for the measurement equipment. 

407 

408 Parameters 

409 ---------- 

410 value 

411 Value to set the measurement equipment with. 

412 

413 Returns 

414 ------- 

415 :class:`str` or :py:data:`None` 

416 Measurement equipment. 

417 """ 

418 

419 return self._measurement_equipment 

420 

421 @measurement_equipment.setter 

422 def measurement_equipment(self, value: str | None) -> None: 

423 """Setter for the **self.measurement_equipment** property.""" 

424 

425 if value is not None: 

426 attest( 

427 isinstance(value, str), 

428 f'"measurement_equipment" property: "{value}" type is not "str"!', 

429 ) 

430 

431 self._measurement_equipment = value 

432 

433 @property 

434 def laboratory(self) -> str | None: 

435 """ 

436 Getter and setter for the laboratory information. 

437 

438 Parameters 

439 ---------- 

440 value 

441 Value to set the laboratory with. 

442 

443 Returns 

444 ------- 

445 :class:`str` or :py:data:`None` 

446 Laboratory. 

447 """ 

448 

449 return self._laboratory 

450 

451 @laboratory.setter 

452 def laboratory(self, value: str | None) -> None: 

453 """Setter for the **self.laboratory** property.""" 

454 

455 if value is not None: 

456 attest( 

457 isinstance(value, str), 

458 f'"laboratory" property: "{value}" type is not "str"!', 

459 ) 

460 

461 self._laboratory = value 

462 

463 @property 

464 def report_number(self) -> str | None: 

465 """ 

466 Getter and setter for the report number. 

467 

468 Parameters 

469 ---------- 

470 value 

471 Value to set the report number with. 

472 

473 Returns 

474 ------- 

475 :class:`str` or :py:data:`None` 

476 Report number. 

477 """ 

478 

479 return self._report_number 

480 

481 @report_number.setter 

482 def report_number(self, value: str | None) -> None: 

483 """Setter for the **self.report_number** property.""" 

484 

485 if value is not None: 

486 attest( 

487 isinstance(value, str), 

488 f'"report_number" property: "{value}" type is not "str"!', 

489 ) 

490 

491 self._report_number = value 

492 

493 @property 

494 def report_date(self) -> str | None: 

495 """ 

496 Getter and setter for the report date. 

497 

498 Parameters 

499 ---------- 

500 value 

501 Value to set the report date with. 

502 

503 Returns 

504 ------- 

505 :class:`str` or :py:data:`None` 

506 Report date. 

507 """ 

508 

509 return self._report_date 

510 

511 @report_date.setter 

512 def report_date(self, value: str | None) -> None: 

513 """Setter for the **self.report_date** property.""" 

514 

515 if value is not None: 

516 attest( 

517 isinstance(value, str), 

518 f'"report_date" property: "{value}" type is not "str"!', 

519 ) 

520 

521 self._report_date = value 

522 

523 @property 

524 def document_creation_date(self) -> str | None: 

525 """ 

526 Getter and setter for the document creation date. 

527 

528 Parameters 

529 ---------- 

530 value 

531 Value to set the document creation date with. 

532 

533 Returns 

534 ------- 

535 :class:`str` or :py:data:`None` 

536 Document creation date. 

537 """ 

538 

539 return self._document_creation_date 

540 

541 @document_creation_date.setter 

542 def document_creation_date(self, value: str | None) -> None: 

543 """Setter for the **self.document_creation_date** property.""" 

544 

545 if value is not None: 

546 attest( 

547 isinstance(value, str), 

548 f'"document_creation_date" property: "{value}" type is not "str"!', 

549 ) 

550 

551 self._document_creation_date = value 

552 

553 @property 

554 def comments(self) -> str | None: 

555 """ 

556 Getter and setter for the comments associated with the object. 

557 

558 Parameters 

559 ---------- 

560 value 

561 Value to set the comments with. 

562 

563 Returns 

564 ------- 

565 :class:`str` or :py:data:`None` 

566 Comments. 

567 """ 

568 

569 return self._comments 

570 

571 @comments.setter 

572 def comments(self, value: str | None) -> None: 

573 """Setter for the **self.comments** property.""" 

574 

575 if value is not None: 

576 attest( 

577 isinstance(value, str), 

578 f'"comments" property: "{value}" type is not "str"!', 

579 ) 

580 

581 self._comments = value 

582 

583 def __str__(self) -> str: 

584 """ 

585 Return a formatted string representation of the *IES TM-27-14* header. 

586 

587 Returns 

588 ------- 

589 :class:`str` 

590 Formatted string representation displaying the header attributes. 

591 

592 Examples 

593 -------- 

594 >>> print(Header_IESTM2714("colour-science")) 

595 Manufacturer : colour-science 

596 Catalog Number : None 

597 Description : None 

598 Document Creator : None 

599 Unique Identifier : None 

600 Measurement Equipment : None 

601 Laboratory : None 

602 Report Number : None 

603 Report Date : None 

604 Document Creation Date : None 

605 Comments : None 

606 """ 

607 

608 return multiline_str( 

609 self, 

610 [ 

611 {"name": "_manufacturer", "label": "Manufacturer"}, 

612 {"name": "_catalog_number", "label": "Catalog Number"}, 

613 {"name": "_description", "label": "Description"}, 

614 {"name": "_document_creator", "label": "Document Creator"}, 

615 {"name": "_unique_identifier", "label": "Unique Identifier"}, 

616 { 

617 "name": "_measurement_equipment", 

618 "label": "Measurement Equipment", 

619 }, 

620 {"name": "_laboratory", "label": "Laboratory"}, 

621 {"name": "_report_number", "label": "Report Number"}, 

622 {"name": "_report_date", "label": "Report Date"}, 

623 { 

624 "name": "_document_creation_date", 

625 "label": "Document Creation Date", 

626 }, 

627 {"name": "_comments", "label": "Comments"}, 

628 ], 

629 ) 

630 

631 def __repr__(self) -> str: 

632 """ 

633 Return an evaluable string representation of the header. 

634 

635 Returns 

636 ------- 

637 :class:`str` 

638 Evaluable string representation. 

639 

640 Examples 

641 -------- 

642 >>> Header_IESTM2714("colour-science") 

643 Header_IESTM2714('colour-science', 

644 None, 

645 None, 

646 None, 

647 None, 

648 None, 

649 None, 

650 None, 

651 None, 

652 None, 

653 None) 

654 """ 

655 

656 return multiline_repr( 

657 self, 

658 [ 

659 {"name": "_manufacturer"}, 

660 {"name": "_catalog_number"}, 

661 {"name": "_description"}, 

662 {"name": "_document_creator"}, 

663 {"name": "_unique_identifier"}, 

664 {"name": "_measurement_equipment"}, 

665 {"name": "_laboratory"}, 

666 {"name": "_report_number"}, 

667 {"name": "_report_date"}, 

668 {"name": "_document_creation_date"}, 

669 {"name": "_comments"}, 

670 ], 

671 ) 

672 

673 def __hash__(self) -> int: 

674 """ 

675 Return the header hash. 

676 

677 Returns 

678 ------- 

679 :class:`int` 

680 Object hash. 

681 """ 

682 

683 return hash( 

684 ( 

685 self._manufacturer, 

686 self._catalog_number, 

687 self._description, 

688 self._document_creator, 

689 self._unique_identifier, 

690 self._measurement_equipment, 

691 self._laboratory, 

692 self._report_number, 

693 self._report_date, 

694 self._document_creation_date, 

695 self._comments, 

696 ) 

697 ) 

698 

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

700 """ 

701 Determine whether the header is equal to the specified other object. 

702 

703 Compare all header attributes to determine equality between this header 

704 and the specified object. Two headers are considered equal when all 

705 their attributes match exactly. 

706 

707 Parameters 

708 ---------- 

709 other 

710 Object to test whether it is equal to the header. 

711 

712 Returns 

713 ------- 

714 :class:`bool` 

715 Whether the specified object is equal to the header. 

716 

717 Examples 

718 -------- 

719 >>> Header_IESTM2714("Foo") == Header_IESTM2714("Foo") 

720 True 

721 >>> Header_IESTM2714("Foo") == Header_IESTM2714("Bar") 

722 False 

723 """ 

724 

725 if isinstance(other, Header_IESTM2714): 

726 return all( 

727 [ 

728 self._manufacturer == other.manufacturer, 

729 self._catalog_number == other.catalog_number, 

730 self._description == other.description, 

731 self._document_creator == other.document_creator, 

732 self._unique_identifier == other.unique_identifier, 

733 self._measurement_equipment == other.measurement_equipment, 

734 self._laboratory == other.laboratory, 

735 self._report_number == other.report_number, 

736 self._report_date == other.report_date, 

737 self._document_creation_date == other.document_creation_date, 

738 self._comments == other.comments, 

739 ] 

740 ) 

741 

742 return False 

743 

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

745 """ 

746 Determine whether the header is not equal to the specified other object. 

747 

748 Parameters 

749 ---------- 

750 other 

751 Object to test whether it is not equal to the header. 

752 

753 Returns 

754 ------- 

755 :class:`bool` 

756 Whether specified object is not equal to the header. 

757 

758 Examples 

759 -------- 

760 >>> Header_IESTM2714("Foo") != Header_IESTM2714("Foo") 

761 False 

762 >>> Header_IESTM2714("Foo") != Header_IESTM2714("Bar") 

763 True 

764 """ 

765 

766 return not (self == other) 

767 

768 

769class SpectralDistribution_IESTM2714(SpectralDistribution): 

770 """ 

771 Define an *IES TM-27-14* spectral distribution for electronic transfer of 

772 spectral data. 

773 

774 This class provides functionality to read and write spectral distribution 

775 data using the *IES TM-27-14* standard format. The standard defines 

776 an *XML* schema for exchanging spectral measurement data between systems, 

777 ensuring consistent representation of wavelength-dependent measurements 

778 across different applications and instruments. 

779 

780 Parameters 

781 ---------- 

782 path 

783 Spectral data *XML* file path. 

784 header 

785 *IES TM-27-14* spectral distribution header containing metadata. 

786 spectral_quantity 

787 Quantity of measurement for each element of the spectral data (e.g., 

788 "reflectance", "transmittance", "radiance"). 

789 reflection_geometry 

790 Spectral reflectance factors geometric conditions. 

791 transmission_geometry 

792 Spectral transmittance factors geometric conditions. 

793 bandwidth_FWHM 

794 Spectroradiometer full-width half-maximum bandwidth in nanometers. 

795 bandwidth_corrected 

796 Specifies if bandwidth correction has been applied to the measured 

797 data. 

798 

799 Other Parameters 

800 ---------------- 

801 data 

802 Data to be stored in the spectral distribution. 

803 domain 

804 Values to initialise the 

805 :attr:`colour.SpectralDistribution.wavelength` property with. 

806 If both ``data`` and ``domain`` arguments are defined, the latter will 

807 be used to initialise the 

808 :attr:`colour.SpectralDistribution.wavelength` property. 

809 extrapolator 

810 Extrapolator class type to use as extrapolating function. 

811 extrapolator_kwargs 

812 Arguments to use when instantiating the extrapolating function. 

813 interpolator 

814 Interpolator class type to use as interpolating function. 

815 interpolator_kwargs 

816 Arguments to use when instantiating the interpolating function. 

817 name 

818 Spectral distribution name. 

819 display_name 

820 Spectral distribution name for figures, default to 

821 :attr:`colour.SpectralDistribution.name` property value. 

822 

823 Notes 

824 ----- 

825 *Reflection Geometry* 

826 

827 - di:8: Diffuse / eight-degree, specular component included. 

828 - de:8: Diffuse / eight-degree, specular component excluded. 

829 - 8:di: Eight-degree / diffuse, specular component included. 

830 - 8:de: Eight-degree / diffuse, specular component excluded. 

831 - d:d: Diffuse / diffuse. 

832 - d:0: Alternative diffuse. 

833 - 45a:0: Forty-five degree annular / normal. 

834 - 45c:0: Forty-five degree circumferential / normal. 

835 - 0:45a: Normal / forty-five degree annular. 

836 - 45x:0: Forty-five degree directional / normal. 

837 - 0:45x: Normal / forty-five degree directional. 

838 - other: User-specified in comments. 

839 

840 *Transmission Geometry* 

841 

842 - 0:0: Normal / normal. 

843 - di:0: Diffuse / normal, regular component included. 

844 - de:0: Diffuse / normal, regular component excluded. 

845 - 0:di: Normal / diffuse, regular component included. 

846 - 0:de: Normal / diffuse, regular component excluded. 

847 - d:d: Diffuse / diffuse. 

848 - other: User-specified in comments. 

849 

850 Attributes 

851 ---------- 

852 - :attr:`~colour.SpectralDistribution_IESTM2714.mapping` 

853 - :attr:`~colour.SpectralDistribution_IESTM2714.path` 

854 - :attr:`~colour.SpectralDistribution_IESTM2714.header` 

855 - :attr:`~colour.SpectralDistribution_IESTM2714.spectral_quantity` 

856 - :attr:`~colour.SpectralDistribution_IESTM2714.reflection_geometry` 

857 - :attr:`~colour.SpectralDistribution_IESTM2714.transmission_geometry` 

858 - :attr:`~colour.SpectralDistribution_IESTM2714.bandwidth_FWHM` 

859 - :attr:`~colour.SpectralDistribution_IESTM2714.bandwidth_corrected` 

860 

861 Methods 

862 ------- 

863 - :meth:`~colour.SpectralDistribution_IESTM2714.__init__` 

864 - :meth:`~colour.SpectralDistribution_IESTM2714.__str__` 

865 - :meth:`~colour.SpectralDistribution_IESTM2714.__repr__` 

866 - :meth:`~colour.SpectralDistribution_IESTM2714.read` 

867 - :meth:`~colour.SpectralDistribution_IESTM2714.write` 

868 

869 References 

870 ---------- 

871 :cite:`IESComputerCommittee2014a` 

872 

873 Examples 

874 -------- 

875 >>> from os.path import dirname, join 

876 >>> directory = join(dirname(__file__), "tests", "resources") 

877 >>> sd = SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx")) 

878 >>> sd.name # doctest: +SKIP 

879 'Unknown - N/A - Rare earth fluorescent lamp' 

880 >>> sd.header.comments 

881 'Ambient temperature 25 degrees C.' 

882 >>> sd[501.7] # doctest: +ELLIPSIS 

883 0.0950000... 

884 """ 

885 

886 def __init__( 

887 self, 

888 path: str | PathLike | None = None, 

889 header: Header_IESTM2714 | None = None, 

890 spectral_quantity: ( 

891 Literal[ 

892 "absorptance", 

893 "exitance", 

894 "flux", 

895 "intensity", 

896 "irradiance", 

897 "radiance", 

898 "reflectance", 

899 "relative", 

900 "transmittance", 

901 "R-Factor", 

902 "T-Factor", 

903 "other", 

904 ] 

905 | None 

906 ) = None, 

907 reflection_geometry: ( 

908 Literal[ 

909 "di:8", 

910 "de:8", 

911 "8:di", 

912 "8:de", 

913 "d:d", 

914 "d:0", 

915 "45a:0", 

916 "45c:0", 

917 "0:45a", 

918 "45x:0", 

919 "0:45x", 

920 "other", 

921 ] 

922 | None 

923 ) = None, 

924 transmission_geometry: ( 

925 Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None 

926 ) = None, 

927 bandwidth_FWHM: float | None = None, 

928 bandwidth_corrected: bool | None = None, 

929 **kwargs: Any, 

930 ) -> None: 

931 super().__init__(**kwargs) 

932 

933 self._mapping: Structure = Structure( 

934 element="SpectralDistribution", 

935 elements=( 

936 Element_Specification_IESTM2714( 

937 "SpectralQuantity", "spectral_quantity", required=True 

938 ), 

939 Element_Specification_IESTM2714( 

940 "ReflectionGeometry", "reflection_geometry" 

941 ), 

942 Element_Specification_IESTM2714( 

943 "TransmissionGeometry", "transmission_geometry" 

944 ), 

945 Element_Specification_IESTM2714( 

946 "BandwidthFWHM", 

947 "bandwidth_FWHM", 

948 read_conversion=( 

949 lambda x: (None if x == "None" else as_float_scalar(x)) 

950 ), 

951 ), 

952 Element_Specification_IESTM2714( 

953 "BandwidthCorrected", 

954 "bandwidth_corrected", 

955 read_conversion=(lambda x: bool(x == "true")), 

956 write_conversion=(lambda x: "true" if x is True else "false"), 

957 ), 

958 ), 

959 data=Element_Specification_IESTM2714( 

960 "SpectralData", "wavelength", required=True 

961 ), 

962 ) 

963 

964 self._path: str | None = None 

965 self.path = path 

966 self._header: Header_IESTM2714 = Header_IESTM2714() 

967 self.header = optional(header, self._header) 

968 self._spectral_quantity: ( 

969 Literal[ 

970 "absorptance", 

971 "exitance", 

972 "flux", 

973 "intensity", 

974 "irradiance", 

975 "radiance", 

976 "reflectance", 

977 "relative", 

978 "transmittance", 

979 "R-Factor", 

980 "T-Factor", 

981 "other", 

982 ] 

983 | None 

984 ) = None 

985 self.spectral_quantity = spectral_quantity 

986 self._reflection_geometry: ( 

987 Literal[ 

988 "di:8", 

989 "de:8", 

990 "8:di", 

991 "8:de", 

992 "d:d", 

993 "d:0", 

994 "45a:0", 

995 "45c:0", 

996 "0:45a", 

997 "45x:0", 

998 "0:45x", 

999 "other", 

1000 ] 

1001 | None 

1002 ) = None 

1003 self.reflection_geometry = reflection_geometry 

1004 self._transmission_geometry: ( 

1005 Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None 

1006 ) = None 

1007 self.transmission_geometry = transmission_geometry 

1008 self._bandwidth_FWHM: float | None = None 

1009 self.bandwidth_FWHM = bandwidth_FWHM 

1010 self._bandwidth_corrected: bool | None = None 

1011 self.bandwidth_corrected = bandwidth_corrected 

1012 

1013 if self.path is not None and os.path.exists( 

1014 self.path # pyright: ignore 

1015 ): 

1016 self.read() 

1017 

1018 @property 

1019 def mapping(self) -> Structure: 

1020 """ 

1021 Getter for the structure containing file mappings. 

1022 

1023 Returns 

1024 ------- 

1025 :class:`colour.utilities.Structure` 

1026 Mapping structure. 

1027 """ 

1028 

1029 return self._mapping 

1030 

1031 @property 

1032 def path(self) -> str | None: 

1033 """ 

1034 Getter and setter for the resource path. 

1035 

1036 Parameters 

1037 ---------- 

1038 value 

1039 Value to set the path with. 

1040 

1041 Returns 

1042 ------- 

1043 :class:`str` or :py:data:`None` 

1044 Path to the resource. 

1045 """ 

1046 

1047 return self._path 

1048 

1049 @path.setter 

1050 def path(self, value: str | PathLike | None) -> None: 

1051 """Setter for the **self.path** property.""" 

1052 

1053 if value is not None: 

1054 attest( 

1055 isinstance(value, (str, Path)), 

1056 f'"path" property: "{value}" type is not "str" or "Path"!', 

1057 ) 

1058 

1059 value = str(value) 

1060 

1061 self._path = value 

1062 

1063 @property 

1064 def header(self) -> Header_IESTM2714: 

1065 """ 

1066 Getter and setter for the *IES TM-27-14* spectral distribution header. 

1067 

1068 Parameters 

1069 ---------- 

1070 value 

1071 Value to set the header with. 

1072 

1073 Returns 

1074 ------- 

1075 :class:`colour.io.tm2714.Header_IESTM2714` 

1076 Header object containing spectral distribution metadata. 

1077 """ 

1078 

1079 return self._header 

1080 

1081 @header.setter 

1082 def header(self, value: Header_IESTM2714) -> None: 

1083 """Setter for the **self.header** property.""" 

1084 

1085 attest( 

1086 isinstance(value, Header_IESTM2714), 

1087 f'"header" property: "{value}" type is not "Header_IESTM2714"!', 

1088 ) 

1089 

1090 self._header = value 

1091 

1092 @property 

1093 def spectral_quantity( 

1094 self, 

1095 ) -> ( 

1096 Literal[ 

1097 "absorptance", 

1098 "exitance", 

1099 "flux", 

1100 "intensity", 

1101 "irradiance", 

1102 "radiance", 

1103 "reflectance", 

1104 "relative", 

1105 "transmittance", 

1106 "R-Factor", 

1107 "T-Factor", 

1108 "other", 

1109 ] 

1110 | None 

1111 ): 

1112 """ 

1113 Getter and setter for the spectral quantity. 

1114 

1115 Parameters 

1116 ---------- 

1117 value 

1118 Value to set the spectral quantity with. 

1119 

1120 Returns 

1121 ------- 

1122 :class:`str` or :py:data:`None` 

1123 Spectral quantity. 

1124 """ 

1125 

1126 return self._spectral_quantity 

1127 

1128 @spectral_quantity.setter 

1129 def spectral_quantity( 

1130 self, 

1131 value: ( 

1132 Literal[ 

1133 "absorptance", 

1134 "exitance", 

1135 "flux", 

1136 "intensity", 

1137 "irradiance", 

1138 "radiance", 

1139 "reflectance", 

1140 "relative", 

1141 "transmittance", 

1142 "R-Factor", 

1143 "T-Factor", 

1144 "other", 

1145 ] 

1146 | None 

1147 ), 

1148 ) -> None: 

1149 """Setter for the **self.spectral_quantity** property.""" 

1150 

1151 if value is not None: 

1152 attest( 

1153 isinstance(value, str), 

1154 f'"spectral_quantity" property: "{value}" type is not "str"!', 

1155 ) 

1156 

1157 self._spectral_quantity = value 

1158 

1159 @property 

1160 def reflection_geometry( 

1161 self, 

1162 ) -> ( 

1163 Literal[ 

1164 "di:8", 

1165 "de:8", 

1166 "8:di", 

1167 "8:de", 

1168 "d:d", 

1169 "d:0", 

1170 "45a:0", 

1171 "45c:0", 

1172 "0:45a", 

1173 "45x:0", 

1174 "0:45x", 

1175 "other", 

1176 ] 

1177 | None 

1178 ): 

1179 """ 

1180 Getter and setter for the reflection geometry. 

1181 

1182 Parameters 

1183 ---------- 

1184 value 

1185 Value to set the reflection geometry with. 

1186 

1187 Returns 

1188 ------- 

1189 :class:`str` or :py:data:`None` 

1190 Reflection geometry. 

1191 """ 

1192 

1193 return self._reflection_geometry 

1194 

1195 @reflection_geometry.setter 

1196 def reflection_geometry( 

1197 self, 

1198 value: ( 

1199 Literal[ 

1200 "di:8", 

1201 "de:8", 

1202 "8:di", 

1203 "8:de", 

1204 "d:d", 

1205 "d:0", 

1206 "45a:0", 

1207 "45c:0", 

1208 "0:45a", 

1209 "45x:0", 

1210 "0:45x", 

1211 "other", 

1212 ] 

1213 | None 

1214 ), 

1215 ) -> None: 

1216 """Setter for the **self.reflection_geometry** property.""" 

1217 

1218 if value is not None: 

1219 attest( 

1220 isinstance(value, str), 

1221 f'"reflection_geometry" property: "{value}" type is not "str"!', 

1222 ) 

1223 

1224 self._reflection_geometry = value 

1225 

1226 @property 

1227 def transmission_geometry( 

1228 self, 

1229 ) -> Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None: 

1230 """ 

1231 Getter and setter for the transmission geometry. 

1232 

1233 Parameters 

1234 ---------- 

1235 value 

1236 Value to set the transmission geometry with. 

1237 

1238 Returns 

1239 ------- 

1240 :class:`str` or :py:data:`None` 

1241 Transmission geometry. 

1242 """ 

1243 

1244 return self._transmission_geometry 

1245 

1246 @transmission_geometry.setter 

1247 def transmission_geometry( 

1248 self, 

1249 value: (Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None), 

1250 ) -> None: 

1251 """Setter for the **self.transmission_geometry** property.""" 

1252 

1253 if value is not None: 

1254 attest( 

1255 isinstance(value, str), 

1256 f'"transmission_geometry" property: "{value}" type is not "str"!', 

1257 ) 

1258 

1259 self._transmission_geometry = value 

1260 

1261 @property 

1262 def bandwidth_FWHM(self) -> float | None: 

1263 """ 

1264 Getter and setter for the full-width half-maximum (FWHM) bandwidth of 

1265 the spectral measurement. 

1266 

1267 Parameters 

1268 ---------- 

1269 value 

1270 Value to set the full-width half-maximum bandwidth with. 

1271 

1272 Returns 

1273 ------- 

1274 :class:`float` or :py:data:`None` 

1275 Full-width half-maximum bandwidth. 

1276 """ 

1277 

1278 return self._bandwidth_FWHM 

1279 

1280 @bandwidth_FWHM.setter 

1281 def bandwidth_FWHM(self, value: float | None) -> None: 

1282 """Setter for the **self.bandwidth_FWHM** property.""" 

1283 

1284 if value is not None: 

1285 attest( 

1286 is_numeric(value), 

1287 f'"bandwidth_FWHM" property: "{value}" is not a "number"!', 

1288 ) 

1289 

1290 value = as_float_scalar(value) 

1291 

1292 self._bandwidth_FWHM = value 

1293 

1294 @property 

1295 def bandwidth_corrected(self) -> bool | None: 

1296 """ 

1297 Getter and setter for whether bandwidth correction has been applied to 

1298 the measured data. 

1299 

1300 Parameters 

1301 ---------- 

1302 value 

1303 Whether bandwidth correction has been applied to the measured 

1304 data. 

1305 

1306 Returns 

1307 ------- 

1308 :class:`bool` or :py:data:`None` 

1309 Whether bandwidth correction has been applied to the measured 

1310 data. 

1311 """ 

1312 

1313 return self._bandwidth_corrected 

1314 

1315 @bandwidth_corrected.setter 

1316 def bandwidth_corrected(self, value: bool | None) -> None: 

1317 """Setter for the **self.bandwidth_corrected** property.""" 

1318 

1319 if value is not None: 

1320 attest( 

1321 isinstance(value, bool), 

1322 f'"bandwidth_corrected" property: "{value}" type is not "bool"!', 

1323 ) 

1324 

1325 self._bandwidth_corrected = value 

1326 

1327 def __str__(self) -> str: 

1328 """ 

1329 Generate a formatted string representation of the *IES TM-27-14* 

1330 spectral distribution. 

1331 

1332 Returns 

1333 ------- 

1334 :class:`str` 

1335 Formatted string representation containing path, spectral 

1336 metadata, header details, and spectral data values. 

1337 

1338 Examples 

1339 -------- 

1340 >>> from os.path import dirname, join 

1341 >>> directory = join(dirname(__file__), "tests", "resources") 

1342 >>> print(SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx"))) 

1343 ... # doctest: +ELLIPSIS 

1344 IES TM-27-14 Spectral Distribution 

1345 ================================== 

1346 <BLANKLINE> 

1347 Path : ... 

1348 Spectral Quantity : relative 

1349 Reflection Geometry : other 

1350 Transmission Geometry : other 

1351 Bandwidth (FWHM) : 2.0 

1352 Bandwidth Corrected : True 

1353 <BLANKLINE> 

1354 Header 

1355 ------ 

1356 <BLANKLINE> 

1357 Manufacturer : Unknown 

1358 Catalog Number : N/A 

1359 Description : Rare earth fluorescent lamp 

1360 Document Creator : byHeart Consultants 

1361 Unique Identifier : C3567553-C75B-4354-961E-35CEB9FEB42C 

1362 Measurement Equipment : None 

1363 Laboratory : N/A 

1364 Report Number : N/A 

1365 Report Date : N/A 

1366 Document Creation Date : 2014-06-23 

1367 Comments : Ambient temperature 25 degrees C. 

1368 <BLANKLINE> 

1369 Spectral Data 

1370 ------------- 

1371 <BLANKLINE> 

1372 [[ 4.00000000e+02 3.40000000e-02] 

1373 [ 4.03100000e+02 3.70000000e-02] 

1374 [ 4.05500000e+02 6.90000000e-02] 

1375 [ 4.07500000e+02 3.70000000e-02] 

1376 [ 4.20600000e+02 4.20000000e-02] 

1377 [ 4.31000000e+02 4.90000000e-02] 

1378 [ 4.33700000e+02 6.00000000e-02] 

1379 [ 4.37000000e+02 3.57000000e-01] 

1380 [ 4.38900000e+02 6.00000000e-02] 

1381 [ 4.60000000e+02 6.80000000e-02] 

1382 [ 4.77000000e+02 7.50000000e-02] 

1383 [ 4.81000000e+02 8.50000000e-02] 

1384 [ 4.88200000e+02 2.04000000e-01] 

1385 [ 4.92600000e+02 1.66000000e-01] 

1386 [ 5.01700000e+02 9.50000000e-02] 

1387 [ 5.07600000e+02 7.80000000e-02] 

1388 [ 5.17600000e+02 7.10000000e-02] 

1389 [ 5.29900000e+02 7.60000000e-02] 

1390 [ 5.35400000e+02 9.90000000e-02] 

1391 [ 5.39900000e+02 4.23000000e-01] 

1392 [ 5.43200000e+02 8.02000000e-01] 

1393 [ 5.44400000e+02 7.13000000e-01] 

1394 [ 5.47200000e+02 9.99000000e-01] 

1395 [ 5.48700000e+02 5.73000000e-01] 

1396 [ 5.50200000e+02 3.40000000e-01] 

1397 [ 5.53800000e+02 2.08000000e-01] 

1398 [ 5.57300000e+02 1.39000000e-01] 

1399 [ 5.63700000e+02 1.29000000e-01] 

1400 [ 5.74800000e+02 1.31000000e-01] 

1401 [ 5.78000000e+02 1.98000000e-01] 

1402 [ 5.79200000e+02 1.90000000e-01] 

1403 [ 5.80400000e+02 2.05000000e-01] 

1404 [ 5.84800000e+02 2.44000000e-01] 

1405 [ 5.85900000e+02 2.36000000e-01] 

1406 [ 5.87500000e+02 2.56000000e-01] 

1407 [ 5.90300000e+02 1.80000000e-01] 

1408 [ 5.93500000e+02 2.18000000e-01] 

1409 [ 5.95500000e+02 1.59000000e-01] 

1410 [ 5.97000000e+02 1.47000000e-01] 

1411 [ 5.99400000e+02 1.70000000e-01] 

1412 [ 6.02200000e+02 1.34000000e-01] 

1413 [ 6.04600000e+02 1.21000000e-01] 

1414 [ 6.07400000e+02 1.40000000e-01] 

1415 [ 6.09400000e+02 2.29000000e-01] 

1416 [ 6.10200000e+02 4.65000000e-01] 

1417 [ 6.12000000e+02 9.52000000e-01] 

1418 [ 6.14600000e+02 4.77000000e-01] 

1419 [ 6.16900000e+02 2.08000000e-01] 

1420 [ 6.18500000e+02 1.35000000e-01] 

1421 [ 6.22100000e+02 1.50000000e-01] 

1422 [ 6.25600000e+02 1.55000000e-01] 

1423 [ 6.28400000e+02 1.34000000e-01] 

1424 [ 6.31200000e+02 1.68000000e-01] 

1425 [ 6.33200000e+02 8.70000000e-02] 

1426 [ 6.35600000e+02 6.80000000e-02] 

1427 [ 6.42700000e+02 5.80000000e-02] 

1428 [ 6.48700000e+02 5.80000000e-02] 

1429 [ 6.50700000e+02 7.40000000e-02] 

1430 [ 6.52600000e+02 6.30000000e-02] 

1431 [ 6.56200000e+02 5.30000000e-02] 

1432 [ 6.57000000e+02 5.60000000e-02] 

1433 [ 6.60600000e+02 4.90000000e-02] 

1434 [ 6.62600000e+02 5.90000000e-02] 

1435 [ 6.64200000e+02 4.80000000e-02] 

1436 [ 6.86000000e+02 4.10000000e-02] 

1437 [ 6.87600000e+02 4.80000000e-02] 

1438 [ 6.89200000e+02 3.90000000e-02] 

1439 [ 6.92400000e+02 3.80000000e-02] 

1440 [ 6.93500000e+02 4.40000000e-02] 

1441 [ 6.95500000e+02 3.40000000e-02] 

1442 [ 7.02300000e+02 3.60000000e-02] 

1443 [ 7.06700000e+02 4.20000000e-02] 

1444 [ 7.07100000e+02 6.10000000e-02] 

1445 [ 7.10200000e+02 6.10000000e-02] 

1446 [ 7.11000000e+02 4.10000000e-02] 

1447 [ 7.12200000e+02 5.20000000e-02] 

1448 [ 7.14200000e+02 3.30000000e-02] 

1449 [ 7.48400000e+02 3.40000000e-02] 

1450 [ 7.57900000e+02 3.10000000e-02] 

1451 [ 7.60700000e+02 3.90000000e-02] 

1452 [ 7.63900000e+02 2.90000000e-02] 

1453 [ 8.08800000e+02 2.90000000e-02] 

1454 [ 8.10700000e+02 3.90000000e-02] 

1455 [ 8.12700000e+02 3.00000000e-02] 

1456 [ 8.50100000e+02 3.00000000e-02]] 

1457 """ 

1458 

1459 try: 

1460 str_parent = super().__str__() 

1461 

1462 return multiline_str( 

1463 self, 

1464 [ 

1465 { 

1466 "label": "IES TM-27-14 Spectral Distribution", 

1467 "header": True, 

1468 }, 

1469 {"line_break": True}, 

1470 {"name": "path", "label": "Path"}, 

1471 { 

1472 "name": "spectral_quantity", 

1473 "label": "Spectral Quantity", 

1474 }, 

1475 { 

1476 "name": "reflection_geometry", 

1477 "label": "Reflection Geometry", 

1478 }, 

1479 { 

1480 "name": "transmission_geometry", 

1481 "label": "Transmission Geometry", 

1482 }, 

1483 {"name": "bandwidth_FWHM", "label": "Bandwidth (FWHM)"}, 

1484 { 

1485 "name": "bandwidth_corrected", 

1486 "label": "Bandwidth Corrected", 

1487 }, 

1488 {"line_break": True}, 

1489 {"label": "Header", "section": True}, 

1490 {"line_break": True}, 

1491 {"formatter": lambda x: str(self.header)}, # noqa: ARG005 

1492 {"line_break": True}, 

1493 {"label": "Spectral Data", "section": True}, 

1494 {"line_break": True}, 

1495 {"formatter": lambda x: str_parent}, # noqa: ARG005 

1496 ], 

1497 ) 

1498 except TypeError: # pragma: no cover 

1499 return super().__str__() 

1500 

1501 def __repr__(self) -> str: 

1502 """ 

1503 Return an evaluable string representation of the *IES TM-27-14* 

1504 spectral distribution. 

1505 

1506 Returns 

1507 ------- 

1508 :class:`str` 

1509 Evaluable string representation. 

1510 

1511 Examples 

1512 -------- 

1513 >>> from os.path import dirname, join 

1514 >>> directory = join(dirname(__file__), "tests", "resources") 

1515 >>> SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx")) 

1516 ... # doctest: +ELLIPSIS 

1517 SpectralDistribution_IESTM2714('...', 

1518 Header_IESTM2714('Unknown', 

1519 'N/A', 

1520 'Rare earth ...', 

1521 'byHeart Consultants', 

1522 'C3567553-C75B-...', 

1523 None, 

1524 'N/A', 

1525 'N/A', 

1526 'N/A', 

1527 '2014-06-23', 

1528 'Ambient ...'), 

1529 'relative', 

1530 'other', 

1531 'other', 

1532 2.0, 

1533 True, 

1534 [[ 4.00000000e+02, 3.40000000e-02], 

1535 [ 4.03100000e+02, 3.70000000e-02], 

1536 [ 4.05500000e+02, 6.90000000e-02], 

1537 [ 4.07500000e+02, 3.70000000e-02], 

1538 [ 4.20600000e+02, 4.20000000e-02], 

1539 [ 4.31000000e+02, 4.90000000e-02], 

1540 [ 4.33700000e+02, 6.00000000e-02], 

1541 [ 4.37000000e+02, 3.57000000e-01], 

1542 [ 4.38900000e+02, 6.00000000e-02], 

1543 [ 4.60000000e+02, 6.80000000e-02], 

1544 [ 4.77000000e+02, 7.50000000e-02], 

1545 [ 4.81000000e+02, 8.50000000e-02], 

1546 [ 4.88200000e+02, 2.04000000e-01], 

1547 [ 4.92600000e+02, 1.66000000e-01], 

1548 [ 5.01700000e+02, 9.50000000e-02], 

1549 [ 5.07600000e+02, 7.80000000e-02], 

1550 [ 5.17600000e+02, 7.10000000e-02], 

1551 [ 5.29900000e+02, 7.60000000e-02], 

1552 [ 5.35400000e+02, 9.90000000e-02], 

1553 [ 5.39900000e+02, 4.23000000e-01], 

1554 [ 5.43200000e+02, 8.02000000e-01], 

1555 [ 5.44400000e+02, 7.13000000e-01], 

1556 [ 5.47200000e+02, 9.99000000e-01], 

1557 [ 5.48700000e+02, 5.73000000e-01], 

1558 [ 5.50200000e+02, 3.40000000e-01], 

1559 [ 5.53800000e+02, 2.08000000e-01], 

1560 [ 5.57300000e+02, 1.39000000e-01], 

1561 [ 5.63700000e+02, 1.29000000e-01], 

1562 [ 5.74800000e+02, 1.31000000e-01], 

1563 [ 5.78000000e+02, 1.98000000e-01], 

1564 [ 5.79200000e+02, 1.90000000e-01], 

1565 [ 5.80400000e+02, 2.05000000e-01], 

1566 [ 5.84800000e+02, 2.44000000e-01], 

1567 [ 5.85900000e+02, 2.36000000e-01], 

1568 [ 5.87500000e+02, 2.56000000e-01], 

1569 [ 5.90300000e+02, 1.80000000e-01], 

1570 [ 5.93500000e+02, 2.18000000e-01], 

1571 [ 5.95500000e+02, 1.59000000e-01], 

1572 [ 5.97000000e+02, 1.47000000e-01], 

1573 [ 5.99400000e+02, 1.70000000e-01], 

1574 [ 6.02200000e+02, 1.34000000e-01], 

1575 [ 6.04600000e+02, 1.21000000e-01], 

1576 [ 6.07400000e+02, 1.40000000e-01], 

1577 [ 6.09400000e+02, 2.29000000e-01], 

1578 [ 6.10200000e+02, 4.65000000e-01], 

1579 [ 6.12000000e+02, 9.52000000e-01], 

1580 [ 6.14600000e+02, 4.77000000e-01], 

1581 [ 6.16900000e+02, 2.08000000e-01], 

1582 [ 6.18500000e+02, 1.35000000e-01], 

1583 [ 6.22100000e+02, 1.50000000e-01], 

1584 [ 6.25600000e+02, 1.55000000e-01], 

1585 [ 6.28400000e+02, 1.34000000e-01], 

1586 [ 6.31200000e+02, 1.68000000e-01], 

1587 [ 6.33200000e+02, 8.70000000e-02], 

1588 [ 6.35600000e+02, 6.80000000e-02], 

1589 [ 6.42700000e+02, 5.80000000e-02], 

1590 [ 6.48700000e+02, 5.80000000e-02], 

1591 [ 6.50700000e+02, 7.40000000e-02], 

1592 [ 6.52600000e+02, 6.30000000e-02], 

1593 [ 6.56200000e+02, 5.30000000e-02], 

1594 [ 6.57000000e+02, 5.60000000e-02], 

1595 [ 6.60600000e+02, 4.90000000e-02], 

1596 [ 6.62600000e+02, 5.90000000e-02], 

1597 [ 6.64200000e+02, 4.80000000e-02], 

1598 [ 6.86000000e+02, 4.10000000e-02], 

1599 [ 6.87600000e+02, 4.80000000e-02], 

1600 [ 6.89200000e+02, 3.90000000e-02], 

1601 [ 6.92400000e+02, 3.80000000e-02], 

1602 [ 6.93500000e+02, 4.40000000e-02], 

1603 [ 6.95500000e+02, 3.40000000e-02], 

1604 [ 7.02300000e+02, 3.60000000e-02], 

1605 [ 7.06700000e+02, 4.20000000e-02], 

1606 [ 7.07100000e+02, 6.10000000e-02], 

1607 [ 7.10200000e+02, 6.10000000e-02], 

1608 [ 7.11000000e+02, 4.10000000e-02], 

1609 [ 7.12200000e+02, 5.20000000e-02], 

1610 [ 7.14200000e+02, 3.30000000e-02], 

1611 [ 7.48400000e+02, 3.40000000e-02], 

1612 [ 7.57900000e+02, 3.10000000e-02], 

1613 [ 7.60700000e+02, 3.90000000e-02], 

1614 [ 7.63900000e+02, 2.90000000e-02], 

1615 [ 8.08800000e+02, 2.90000000e-02], 

1616 [ 8.10700000e+02, 3.90000000e-02], 

1617 [ 8.12700000e+02, 3.00000000e-02], 

1618 [ 8.50100000e+02, 3.00000000e-02]], 

1619 CubicSplineInterpolator, 

1620 {}, 

1621 Extrapolator, 

1622 {...}) 

1623 """ 

1624 

1625 try: 

1626 return multiline_repr( 

1627 self, 

1628 [ 

1629 {"name": "path"}, 

1630 {"name": "header"}, 

1631 {"name": "spectral_quantity"}, 

1632 {"name": "reflection_geometry"}, 

1633 {"name": "transmission_geometry"}, 

1634 {"name": "bandwidth_FWHM"}, 

1635 {"name": "bandwidth_corrected"}, 

1636 { 

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

1638 tstack([self.domain, self.range]) 

1639 ), 

1640 }, 

1641 { 

1642 "name": "interpolator", 

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

1644 self.interpolator.__name__ 

1645 ), 

1646 }, 

1647 {"name": "interpolator_kwargs"}, 

1648 { 

1649 "name": "extrapolator", 

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

1651 self.extrapolator.__name__ 

1652 ), 

1653 }, 

1654 {"name": "extrapolator_kwargs"}, 

1655 ], 

1656 ) 

1657 except TypeError: # pragma: no cover 

1658 return super().__repr__() 

1659 

1660 def read(self) -> SpectralDistribution_IESTM2714: 

1661 """ 

1662 Read and parse the spectral data from the specified *XML* file path. 

1663 

1664 Returns 

1665 ------- 

1666 :class:`colour.SpectralDistribution_IESTM2714` 

1667 *IES TM-27-14* spectral distribution. 

1668 

1669 Raises 

1670 ------ 

1671 ValueError 

1672 If the *IES TM-27-14* spectral distribution path is undefined. 

1673 

1674 Examples 

1675 -------- 

1676 >>> from os.path import dirname, join 

1677 >>> directory = join(dirname(__file__), "tests", "resources") 

1678 >>> sd = SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx")) 

1679 >>> sd.name # doctest: +SKIP 

1680 'Unknown - N/A - Rare earth fluorescent lamp' 

1681 >>> sd.header.comments 

1682 'Ambient temperature 25 degrees C.' 

1683 >>> sd[400] # doctest: +ELLIPSIS 

1684 0.0340000... 

1685 """ 

1686 

1687 if self._path is not None: 

1688 formatter = "./{{{0}}}{1}/{{{0}}}{2}" 

1689 

1690 tree = ET.parse(self._path) # noqa: S314 

1691 root = tree.getroot() 

1692 

1693 match = re.match("{(.*)}", root.tag) 

1694 if match: 

1695 namespace = match.group(1) 

1696 else: 

1697 error = ( 

1698 'The "IES TM-27-14" spectral distribution namespace was not found!' 

1699 ) 

1700 

1701 raise ValueError(error) 

1702 

1703 self.name = os.path.splitext(os.path.basename(self._path))[0] 

1704 

1705 iterator = root.iter 

1706 

1707 for header_element in (self.header, self): 

1708 mapping = header_element.mapping 

1709 for specification in mapping.elements: 

1710 element = root.find( 

1711 formatter.format( 

1712 namespace, mapping.element, specification.element 

1713 ) 

1714 ) 

1715 if element is not None: 

1716 setattr( 

1717 header_element, 

1718 specification.attribute, 

1719 specification.read_conversion(element.text), 

1720 ) 

1721 

1722 # Reading spectral data. 

1723 wavelengths = [] 

1724 values = [] 

1725 for spectral_data in iterator( 

1726 f"{{{namespace}}}{self.mapping.data.element}" 

1727 ): 

1728 wavelengths.append(spectral_data.attrib[self.mapping.data.attribute]) 

1729 values.append(spectral_data.text) 

1730 

1731 components = [ 

1732 component 

1733 for component in ( 

1734 self.header.manufacturer, 

1735 self.header.catalog_number, 

1736 self.header.description, 

1737 ) 

1738 if component is not None 

1739 ] 

1740 self.name = "Undefined" if len(components) == 0 else " - ".join(components) 

1741 

1742 self.wavelengths = as_float_array(wavelengths) 

1743 self.values = as_float_array(values) 

1744 

1745 return self 

1746 

1747 error = 'The "IES TM-27-14" spectral distribution path is undefined!' 

1748 

1749 raise ValueError(error) 

1750 

1751 def write(self) -> bool: 

1752 """ 

1753 Write the spectral distribution spectral data to the specified *XML* 

1754 file path. 

1755 

1756 Returns 

1757 ------- 

1758 :class:`bool` 

1759 Definition success. 

1760 

1761 Raises 

1762 ------ 

1763 ValueError 

1764 If the *IES TM-27-14* spectral distribution path is undefined. 

1765 

1766 Examples 

1767 -------- 

1768 >>> from os.path import dirname, join 

1769 >>> from shutil import rmtree 

1770 >>> from tempfile import mkdtemp 

1771 >>> directory = join(dirname(__file__), "tests", "resources") 

1772 >>> sd = SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx")) 

1773 >>> temporary_directory = mkdtemp() 

1774 >>> sd.path = join(temporary_directory, "Fluorescent.spdx") 

1775 >>> sd.write() 

1776 True 

1777 >>> rmtree(temporary_directory) 

1778 """ 

1779 

1780 if self._path is not None: 

1781 root = ET.Element("IESTM2714") 

1782 root.attrib = { 

1783 "xmlns": NAMESPACE_IESTM2714, 

1784 "version": VERSION_IESTM2714, 

1785 } 

1786 

1787 spectral_distribution = ET.Element("") 

1788 for header_element in (self.header, self): 

1789 mapping = header_element.mapping 

1790 element = ET.SubElement(root, mapping.element) 

1791 for specification in mapping.elements: 

1792 element_child = ET.SubElement(element, specification.element) 

1793 value = getattr(header_element, specification.attribute) 

1794 element_child.text = specification.write_conversion(value) 

1795 

1796 if header_element is self: 

1797 spectral_distribution = element 

1798 

1799 # Writing spectral data. 

1800 for wavelength, value in tstack([self.wavelengths, self.values]): 

1801 element_child = ET.SubElement( 

1802 spectral_distribution, mapping.data.element 

1803 ) 

1804 element_child.text = mapping.data.write_conversion(value) 

1805 element_child.attrib = { 

1806 mapping.data.attribute: mapping.data.write_conversion(wavelength) 

1807 } 

1808 

1809 xml = minidom.parseString( # noqa: S318 

1810 ET.tostring(root) 

1811 ).toprettyxml() 

1812 

1813 with open(self._path, "w") as file: 

1814 file.write(xml) 

1815 

1816 return True 

1817 

1818 error = 'The "IES TM-27-14" spectral distribution path is undefined!' 

1819 

1820 raise ValueError(error)