Coverage for phenomena/tests/test_tmm.py: 100%

216 statements  

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

1"""Define the unit tests for the :mod:`colour.phenomena.tmm` module.""" 

2 

3from __future__ import annotations 

4 

5import numpy as np 

6 

7from colour.constants import TOLERANCE_ABSOLUTE_TESTS 

8from colour.phenomena.interference import matrix_transfer_tmm 

9from colour.phenomena.tmm import ( 

10 polarised_light_magnitude_elements, 

11 polarised_light_reflection_amplitude, 

12 polarised_light_reflection_coefficient, 

13 polarised_light_transmission_amplitude, 

14 polarised_light_transmission_coefficient, 

15 snell_law, 

16) 

17from colour.utilities import ignore_numpy_errors 

18 

19__author__ = "Colour Developers" 

20__copyright__ = "Copyright 2013 Colour Developers" 

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

22__maintainer__ = "Colour Developers" 

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

24__status__ = "Production" 

25 

26__all__ = [ 

27 "TestSnellLaw", 

28 "TestPolarisedLightMagnitudeElements", 

29 "TestPolarisedLightReflectionAmplitude", 

30 "TestPolarisedLightReflectionCoefficient", 

31 "TestPolarisedLightTransmissionAmplitude", 

32 "TestPolarisedLightTransmissionCoefficient", 

33 "TestMatrixTransferTmm", 

34] 

35 

36 

37class TestSnellLaw: 

38 """ 

39 Define :func:`colour.phenomena.tmm.snell_law` definition unit tests 

40 methods. 

41 """ 

42 

43 def test_snell_law(self) -> None: 

44 """Test :func:`colour.phenomena.tmm.snell_law` definition.""" 

45 

46 np.testing.assert_allclose( 

47 snell_law(1.0, 1.5, 30.0), 

48 19.4712206345, 

49 atol=TOLERANCE_ABSOLUTE_TESTS, 

50 ) 

51 

52 np.testing.assert_allclose( 

53 snell_law(1.0, 1.33, 45.0), 

54 32.117631278, 

55 atol=TOLERANCE_ABSOLUTE_TESTS, 

56 ) 

57 

58 np.testing.assert_allclose( 

59 snell_law(1.5, 1.0, 19.47), 

60 30.0, 

61 atol=0.01, 

62 ) 

63 

64 # Test normal incidence (0 degrees) 

65 np.testing.assert_allclose( 

66 snell_law(1.0, 1.5, 0.0), 

67 0.0, 

68 atol=TOLERANCE_ABSOLUTE_TESTS, 

69 ) 

70 

71 def test_n_dimensional_snell_law(self) -> None: 

72 """ 

73 Test :func:`colour.phenomena.tmm.snell_law` definition n-dimensional 

74 arrays support. 

75 """ 

76 

77 n_1 = 1.0 

78 n_2 = 1.5 

79 theta_i = 30.0 

80 theta_t = snell_law(n_1, n_2, theta_i) 

81 

82 theta_i = np.tile(theta_i, 6) 

83 theta_t = np.tile(theta_t, 6) 

84 np.testing.assert_allclose( 

85 snell_law(n_1, n_2, theta_i), 

86 theta_t, 

87 atol=TOLERANCE_ABSOLUTE_TESTS, 

88 ) 

89 

90 theta_i = np.reshape(theta_i, (2, 3)) 

91 theta_t = np.reshape(theta_t, (2, 3)) 

92 np.testing.assert_allclose( 

93 snell_law(n_1, n_2, theta_i), 

94 theta_t, 

95 atol=TOLERANCE_ABSOLUTE_TESTS, 

96 ) 

97 

98 @ignore_numpy_errors 

99 def test_nan_snell_law(self) -> None: 

100 """Test :func:`colour.phenomena.tmm.snell_law` definition nan support.""" 

101 

102 snell_law( 

103 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), 

104 1.5, 

105 30.0, 

106 ) 

107 

108 

109class TestPolarisedLightMagnitudeElements: 

110 """ 

111 Define :func:`colour.phenomena.tmm.polarised_light_magnitude_elements` 

112 definition unit tests methods. 

113 """ 

114 

115 def test_polarised_light_magnitude_elements(self) -> None: 

116 """ 

117 Test :func:`colour.phenomena.tmm.polarised_light_magnitude_elements` 

118 definition. 

119 """ 

120 

121 result = polarised_light_magnitude_elements(1.0, 1.5, 0.0, 0.0) 

122 np.testing.assert_allclose(result[0], 1.0 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) 

123 np.testing.assert_allclose(result[1], 1.0 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) 

124 np.testing.assert_allclose(result[2], 1.5 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) 

125 np.testing.assert_allclose(result[3], 1.5 + 0j, atol=TOLERANCE_ABSOLUTE_TESTS) 

126 

127 # Test at 45 degrees 

128 result_45 = polarised_light_magnitude_elements(1.0, 1.5, 45.0, 30.0) 

129 assert len(result_45) == 4 

130 

131 def test_n_dimensional_polarised_light_magnitude_elements(self) -> None: 

132 """ 

133 Test :func:`colour.phenomena.tmm.polarised_light_magnitude_elements` 

134 definition n-dimensional arrays support. 

135 """ 

136 

137 n_1 = 1.0 

138 n_2 = 1.5 

139 theta_i = 0.0 

140 theta_t = 0.0 

141 m0, m1, m2, m3 = polarised_light_magnitude_elements(n_1, n_2, theta_i, theta_t) 

142 

143 theta_i_array = np.tile(theta_i, 6) 

144 theta_t_array = np.tile(theta_t, 6) 

145 m0_array, m1_array, m2_array, m3_array = polarised_light_magnitude_elements( 

146 n_1, n_2, theta_i_array, theta_t_array 

147 ) 

148 np.testing.assert_allclose( 

149 m0_array, np.tile(m0, 6), atol=TOLERANCE_ABSOLUTE_TESTS 

150 ) 

151 np.testing.assert_allclose( 

152 m1_array, np.tile(m1, 6), atol=TOLERANCE_ABSOLUTE_TESTS 

153 ) 

154 np.testing.assert_allclose( 

155 m2_array, np.tile(m2, 6), atol=TOLERANCE_ABSOLUTE_TESTS 

156 ) 

157 np.testing.assert_allclose( 

158 m3_array, np.tile(m3, 6), atol=TOLERANCE_ABSOLUTE_TESTS 

159 ) 

160 

161 theta_i_array = np.reshape(theta_i_array, (2, 3)) 

162 theta_t_array = np.reshape(theta_t_array, (2, 3)) 

163 m0_array, m1_array, m2_array, m3_array = polarised_light_magnitude_elements( 

164 n_1, n_2, theta_i_array, theta_t_array 

165 ) 

166 np.testing.assert_allclose( 

167 m0_array, np.tile(m0, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS 

168 ) 

169 np.testing.assert_allclose( 

170 m1_array, np.tile(m1, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS 

171 ) 

172 np.testing.assert_allclose( 

173 m2_array, np.tile(m2, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS 

174 ) 

175 np.testing.assert_allclose( 

176 m3_array, np.tile(m3, 6).reshape(2, 3), atol=TOLERANCE_ABSOLUTE_TESTS 

177 ) 

178 

179 @ignore_numpy_errors 

180 def test_nan_polarised_light_magnitude_elements(self) -> None: 

181 """ 

182 Test :func:`colour.phenomena.tmm.polarised_light_magnitude_elements` 

183 definition nan support. 

184 """ 

185 

186 polarised_light_magnitude_elements( 

187 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), 

188 1.5, 

189 0.0, 

190 0.0, 

191 ) 

192 

193 

194class TestPolarisedLightReflectionAmplitude: 

195 """ 

196 Define :func:`colour.phenomena.tmm.polarised_light_reflection_amplitude` 

197 definition unit tests methods. 

198 """ 

199 

200 def test_polarised_light_reflection_amplitude(self) -> None: 

201 """ 

202 Test :func:`colour.phenomena.tmm.polarised_light_reflection_amplitude` 

203 definition. 

204 """ 

205 

206 np.testing.assert_allclose( 

207 polarised_light_reflection_amplitude(1.0, 1.5, 0.0, 0.0), 

208 np.array([-0.2 + 0j, -0.2 + 0j]), 

209 atol=TOLERANCE_ABSOLUTE_TESTS, 

210 ) 

211 

212 np.testing.assert_allclose( 

213 polarised_light_reflection_amplitude(1.0, 1.5, 30.0, 19.47), 

214 np.array([-0.24041175 + 0j, -0.15889613 + 0j]), 

215 atol=TOLERANCE_ABSOLUTE_TESTS, 

216 ) 

217 

218 def test_n_dimensional_polarised_light_reflection_amplitude(self) -> None: 

219 """ 

220 Test :func:`colour.phenomena.tmm.polarised_light_reflection_amplitude` 

221 definition n-dimensional arrays support. 

222 """ 

223 

224 n_1 = 1.0 

225 n_2 = 1.5 

226 theta_i = 0.0 

227 theta_t = 0.0 

228 r = polarised_light_reflection_amplitude(n_1, n_2, theta_i, theta_t) 

229 

230 theta_i_array = np.tile(theta_i, 6) 

231 theta_t_array = np.tile(theta_t, 6) 

232 r_array = polarised_light_reflection_amplitude( 

233 n_1, n_2, theta_i_array, theta_t_array 

234 ) 

235 np.testing.assert_allclose( 

236 r_array, 

237 np.tile(r, (6, 1)), 

238 atol=TOLERANCE_ABSOLUTE_TESTS, 

239 ) 

240 

241 theta_i_array = np.reshape(theta_i_array, (2, 3)) 

242 theta_t_array = np.reshape(theta_t_array, (2, 3)) 

243 r_array = polarised_light_reflection_amplitude( 

244 n_1, n_2, theta_i_array, theta_t_array 

245 ) 

246 np.testing.assert_allclose( 

247 r_array, 

248 np.tile(r, (6, 1)).reshape(2, 3, 2), 

249 atol=TOLERANCE_ABSOLUTE_TESTS, 

250 ) 

251 

252 @ignore_numpy_errors 

253 def test_nan_polarised_light_reflection_amplitude(self) -> None: 

254 """ 

255 Test :func:`colour.phenomena.tmm.polarised_light_reflection_amplitude` 

256 definition nan support. 

257 """ 

258 

259 polarised_light_reflection_amplitude( 

260 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), 

261 1.5, 

262 0.0, 

263 0.0, 

264 ) 

265 

266 

267class TestPolarisedLightReflectionCoefficient: 

268 """ 

269 Define :func:`colour.phenomena.tmm.polarised_light_reflection_coefficient` 

270 definition unit tests methods. 

271 """ 

272 

273 def test_polarised_light_reflection_coefficient(self) -> None: 

274 """ 

275 Test :func:`colour.phenomena.tmm.polarised_light_reflection_coefficient` 

276 definition. 

277 """ 

278 

279 np.testing.assert_allclose( 

280 polarised_light_reflection_coefficient(1.0, 1.5, 0.0, 0.0), 

281 np.array([0.04 + 0j, 0.04 + 0j]), 

282 atol=TOLERANCE_ABSOLUTE_TESTS, 

283 ) 

284 

285 # Test that reflectance is always between 0 and 1 

286 R = polarised_light_reflection_coefficient(1.0, 1.5, 30.0, 19.47) 

287 assert np.all(np.real(R) >= 0) 

288 assert np.all(np.real(R) <= 1) 

289 

290 def test_n_dimensional_polarised_light_reflection_coefficient(self) -> None: 

291 """ 

292 Test :func:`colour.phenomena.tmm.polarised_light_reflection_coefficient` 

293 definition n-dimensional arrays support. 

294 """ 

295 

296 n_1 = 1.0 

297 n_2 = 1.5 

298 theta_i = 0.0 

299 theta_t = 0.0 

300 R = polarised_light_reflection_coefficient(n_1, n_2, theta_i, theta_t) 

301 

302 theta_i_array = np.tile(theta_i, 6) 

303 theta_t_array = np.tile(theta_t, 6) 

304 R_array = polarised_light_reflection_coefficient( 

305 n_1, n_2, theta_i_array, theta_t_array 

306 ) 

307 np.testing.assert_allclose( 

308 R_array, 

309 np.tile(R, (6, 1)), 

310 atol=TOLERANCE_ABSOLUTE_TESTS, 

311 ) 

312 

313 theta_i_array = np.reshape(theta_i_array, (2, 3)) 

314 theta_t_array = np.reshape(theta_t_array, (2, 3)) 

315 R_array = polarised_light_reflection_coefficient( 

316 n_1, n_2, theta_i_array, theta_t_array 

317 ) 

318 np.testing.assert_allclose( 

319 R_array, 

320 np.tile(R, (6, 1)).reshape(2, 3, 2), 

321 atol=TOLERANCE_ABSOLUTE_TESTS, 

322 ) 

323 

324 @ignore_numpy_errors 

325 def test_nan_polarised_light_reflection_coefficient(self) -> None: 

326 """ 

327 Test :func:`colour.phenomena.tmm.polarised_light_reflection_coefficient` 

328 definition nan support. 

329 """ 

330 

331 polarised_light_reflection_coefficient( 

332 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), 

333 1.5, 

334 0.0, 

335 0.0, 

336 ) 

337 

338 

339class TestPolarisedLightTransmissionAmplitude: 

340 """ 

341 Define :func:`colour.phenomena.tmm.polarised_light_transmission_amplitude` 

342 definition unit tests methods. 

343 """ 

344 

345 def test_polarised_light_transmission_amplitude(self) -> None: 

346 """ 

347 Test :func:`colour.phenomena.tmm.polarised_light_transmission_amplitude` 

348 definition. 

349 """ 

350 

351 np.testing.assert_allclose( 

352 polarised_light_transmission_amplitude(1.0, 1.5, 0.0, 0.0), 

353 np.array([0.8 + 0j, 0.8 + 0j]), 

354 atol=TOLERANCE_ABSOLUTE_TESTS, 

355 ) 

356 

357 def test_n_dimensional_polarised_light_transmission_amplitude(self) -> None: 

358 """ 

359 Test :func:`colour.phenomena.tmm.polarised_light_transmission_amplitude` 

360 definition n-dimensional arrays support. 

361 """ 

362 

363 n_1 = 1.0 

364 n_2 = 1.5 

365 theta_i = 0.0 

366 theta_t = 0.0 

367 t = polarised_light_transmission_amplitude(n_1, n_2, theta_i, theta_t) 

368 

369 theta_i_array = np.tile(theta_i, 6) 

370 theta_t_array = np.tile(theta_t, 6) 

371 t_array = polarised_light_transmission_amplitude( 

372 n_1, n_2, theta_i_array, theta_t_array 

373 ) 

374 np.testing.assert_allclose( 

375 t_array, 

376 np.tile(t, (6, 1)), 

377 atol=TOLERANCE_ABSOLUTE_TESTS, 

378 ) 

379 

380 theta_i_array = np.reshape(theta_i_array, (2, 3)) 

381 theta_t_array = np.reshape(theta_t_array, (2, 3)) 

382 t_array = polarised_light_transmission_amplitude( 

383 n_1, n_2, theta_i_array, theta_t_array 

384 ) 

385 np.testing.assert_allclose( 

386 t_array, 

387 np.tile(t, (6, 1)).reshape(2, 3, 2), 

388 atol=TOLERANCE_ABSOLUTE_TESTS, 

389 ) 

390 

391 @ignore_numpy_errors 

392 def test_nan_polarised_light_transmission_amplitude(self) -> None: 

393 """ 

394 Test :func:`colour.phenomena.tmm.polarised_light_transmission_amplitude` 

395 definition nan support. 

396 """ 

397 

398 polarised_light_transmission_amplitude( 

399 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), 

400 1.5, 

401 0.0, 

402 0.0, 

403 ) 

404 

405 

406class TestPolarisedLightTransmissionCoefficient: 

407 """ 

408 Define :func:`colour.phenomena.tmm.polarised_light_transmission_coefficient` 

409 definition unit tests methods. 

410 """ 

411 

412 def test_polarised_light_transmission_coefficient(self) -> None: 

413 """ 

414 Test :func:`colour.phenomena.tmm.polarised_light_transmission_coefficient` 

415 definition. 

416 """ 

417 

418 np.testing.assert_allclose( 

419 polarised_light_transmission_coefficient(1.0, 1.5, 0.0, 0.0), 

420 np.array([0.96 + 0j, 0.96 + 0j]), 

421 atol=TOLERANCE_ABSOLUTE_TESTS, 

422 ) 

423 

424 # Test energy conservation: R + T = 1 

425 R = polarised_light_reflection_coefficient(1.0, 1.5, 0.0, 0.0) 

426 T = polarised_light_transmission_coefficient(1.0, 1.5, 0.0, 0.0) 

427 np.testing.assert_allclose( 

428 np.real(R + T), np.array([1.0, 1.0]), atol=TOLERANCE_ABSOLUTE_TESTS 

429 ) 

430 

431 def test_n_dimensional_polarised_light_transmission_coefficient( 

432 self, 

433 ) -> None: 

434 """ 

435 Test :func:`colour.phenomena.tmm.polarised_light_transmission_coefficient` 

436 definition n-dimensional arrays support. 

437 """ 

438 

439 n_1 = 1.0 

440 n_2 = 1.5 

441 theta_i = 0.0 

442 theta_t = 0.0 

443 T = polarised_light_transmission_coefficient(n_1, n_2, theta_i, theta_t) 

444 

445 theta_i_array = np.tile(theta_i, 6) 

446 theta_t_array = np.tile(theta_t, 6) 

447 T_array = polarised_light_transmission_coefficient( 

448 n_1, n_2, theta_i_array, theta_t_array 

449 ) 

450 np.testing.assert_allclose( 

451 T_array, 

452 np.tile(T, (6, 1)), 

453 atol=TOLERANCE_ABSOLUTE_TESTS, 

454 ) 

455 

456 theta_i_array = np.reshape(theta_i_array, (2, 3)) 

457 theta_t_array = np.reshape(theta_t_array, (2, 3)) 

458 T_array = polarised_light_transmission_coefficient( 

459 n_1, n_2, theta_i_array, theta_t_array 

460 ) 

461 np.testing.assert_allclose( 

462 T_array, 

463 np.tile(T, (6, 1)).reshape(2, 3, 2), 

464 atol=TOLERANCE_ABSOLUTE_TESTS, 

465 ) 

466 

467 @ignore_numpy_errors 

468 def test_nan_polarised_light_transmission_coefficient(self) -> None: 

469 """ 

470 Test :func:`colour.phenomena.tmm.polarised_light_transmission_coefficient` 

471 definition nan support. 

472 """ 

473 

474 polarised_light_transmission_coefficient( 

475 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]), 

476 1.5, 

477 0.0, 

478 0.0, 

479 ) 

480 

481 

482class TestMatrixTransferTmm: 

483 """ 

484 Define :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

485 definition unit tests methods. 

486 """ 

487 

488 def test_matrix_transfer_tmm(self) -> None: 

489 """ 

490 Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

491 definition. 

492 """ 

493 

494 # Single layer structure 

495 result = matrix_transfer_tmm( 

496 n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=550 

497 ) 

498 

499 # Check shapes - (W, A, T, 2, 2) 

500 assert result.M_s.shape == ( 

501 1, 

502 1, 

503 1, 

504 2, 

505 2, 

506 ) # (wavelengths=1, angles=1, thickness=1, 2, 2) 

507 assert result.M_p.shape == (1, 1, 1, 2, 2) 

508 # theta has shape (angles, media) 

509 assert result.theta.shape == (1, 3) # (angles=1, media=3) 

510 assert len(result.n) == 3 # incident, layer, substrate 

511 

512 # Check refractive indices 

513 # n has shape (media_count, wavelengths_count) 

514 assert result.n.shape == (3, 1) 

515 np.testing.assert_allclose( 

516 result.n[:, 0], [1.0, 1.5, 1.0], atol=TOLERANCE_ABSOLUTE_TESTS 

517 ) 

518 

519 # Check angles (normal incidence) 

520 assert result.theta[0, 0] == 0.0 # incident 

521 assert result.theta[0, -1] == 0.0 # substrate (by Snell's law) 

522 

523 # Check transfer matrix properties (should be 2x2 complex) 

524 assert result.M_s.dtype in [np.complex64, np.complex128] 

525 assert result.M_p.dtype in [np.complex64, np.complex128] 

526 

527 def test_matrix_transfer_tmm_multilayer(self) -> None: 

528 """ 

529 Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

530 with multiple layers. 

531 """ 

532 

533 # Two-layer structure 

534 result = matrix_transfer_tmm( 

535 n=[1.0, 1.5, 2.0, 1.5], 

536 t=[250, 150], 

537 theta=0, 

538 wavelength=550, 

539 ) 

540 

541 # Check shapes - (W, A, T, 2, 2) 

542 assert result.M_s.shape == ( 

543 1, 

544 1, 

545 1, 

546 2, 

547 2, 

548 ) # (wavelengths=1, angles=1, thickness=1, 2, 2) 

549 assert result.M_p.shape == (1, 1, 1, 2, 2) 

550 # theta has shape (angles, media) 

551 assert result.theta.shape == (1, 4) # (angles=1, media=4) 

552 assert len(result.n) == 4 

553 

554 # Check refractive indices 

555 # n has shape (media_count, wavelengths_count) 

556 assert result.n.shape == (4, 1) 

557 np.testing.assert_allclose( 

558 result.n[:, 0], [1.0, 1.5, 2.0, 1.5], atol=TOLERANCE_ABSOLUTE_TESTS 

559 ) 

560 

561 def test_matrix_transfer_tmm_multiple_wavelengths(self) -> None: 

562 """ 

563 Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

564 with multiple wavelengths. 

565 """ 

566 

567 wavelengths = [400, 500, 600] 

568 result = matrix_transfer_tmm( 

569 n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=wavelengths 

570 ) 

571 

572 # Check shapes - (W, A, T, 2, 2) 

573 assert result.M_s.shape == ( 

574 3, 

575 1, 

576 1, 

577 2, 

578 2, 

579 ) # (wavelengths=3, angles=1, thickness=1, 2, 2) 

580 assert result.M_p.shape == (3, 1, 1, 2, 2) 

581 

582 # theta has shape (angles, media) 

583 assert result.theta.shape == (1, 3) # (angles=1, media=3) 

584 # n has shape (media_count, wavelengths_count) 

585 assert result.n.shape == (3, 3) 

586 

587 def test_matrix_transfer_tmm_complex_n(self) -> None: 

588 """ 

589 Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

590 with complex refractive indices. 

591 """ 

592 

593 # Absorbing layer 

594 n_absorbing = 2.0 + 0.5j 

595 result = matrix_transfer_tmm( 

596 n=[1.0, n_absorbing, 1.0], t=[250], theta=0, wavelength=550 

597 ) 

598 

599 # Check that complex n is preserved 

600 # n has shape (media_count, wavelengths_count) 

601 assert np.iscomplex(result.n[1, 0]) 

602 np.testing.assert_allclose( 

603 result.n[1, 0], n_absorbing, atol=TOLERANCE_ABSOLUTE_TESTS 

604 ) 

605 

606 # Transfer matrices should be complex 

607 assert np.iscomplexobj(result.M_s) 

608 assert np.iscomplexobj(result.M_p) 

609 

610 def test_matrix_transfer_tmm_oblique_incidence(self) -> None: 

611 """ 

612 Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

613 with oblique incidence. 

614 """ 

615 

616 theta_i = 30.0 # 30 degrees 

617 result = matrix_transfer_tmm( 

618 n=[1.0, 1.5, 1.0], t=[250], theta=theta_i, wavelength=550 

619 ) 

620 

621 # Check incident angle - theta has shape (angles, media) 

622 np.testing.assert_allclose( 

623 result.theta[0, 0], theta_i, atol=TOLERANCE_ABSOLUTE_TESTS 

624 ) 

625 

626 # Check that angle changes in layer (Snell's law) 

627 assert result.theta[0, 1] != theta_i # Should be refracted 

628 

629 # s and p matrices should differ at oblique incidence 

630 assert not np.allclose(result.M_s, result.M_p, atol=TOLERANCE_ABSOLUTE_TESTS) 

631 

632 def test_matrix_transfer_tmm_energy_consistency(self) -> None: 

633 """ 

634 Test that transfer matrices from transfer_matrix_tmm give 

635 consistent R and T values. 

636 """ 

637 

638 # Build transfer matrix 

639 result = matrix_transfer_tmm( 

640 n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=550 

641 ) 

642 

643 # Extract R and T manually - M_s has shape (W, A, T, 2, 2) 

644 r_s = result.M_s[0, 0, 0, 1, 0] / result.M_s[0, 0, 0, 0, 0] 

645 R_s = np.abs(r_s) ** 2 

646 

647 t_s = 1.0 / result.M_s[0, 0, 0, 0, 0] 

648 theta_i_rad = np.radians(0.0) 

649 theta_f_rad = np.radians(result.theta[0, -1]) 

650 

651 # Extract incident and substrate from result.n 

652 n_incident = result.n[0, 0] 

653 n_substrate = result.n[-1, 0] 

654 

655 angle_factor = np.real(n_substrate * np.cos(theta_f_rad)) / np.real( 

656 n_incident * np.cos(theta_i_rad) 

657 ) 

658 T_s = np.abs(t_s) ** 2 * angle_factor 

659 

660 # Energy conservation for lossless media: R + T = 1 

661 np.testing.assert_allclose(R_s + T_s, 1.0, atol=TOLERANCE_ABSOLUTE_TESTS) 

662 

663 def test_n_dimensional_matrix_transfer_tmm(self) -> None: 

664 """ 

665 Test :func:`colour.phenomena.tmm.matrix_transfer_tmm` 

666 definition n-dimensional arrays support. 

667 """ 

668 

669 wl = 555 

670 result = matrix_transfer_tmm(n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=wl) 

671 

672 wl_array = np.tile(wl, 6) 

673 result_array = matrix_transfer_tmm( 

674 n=[1.0, 1.5, 1.0], t=[250], theta=0, wavelength=wl_array 

675 ) 

676 

677 # Check shape - (W, A, T, 2, 2) 

678 assert result_array.M_s.shape == ( 

679 6, 

680 1, 

681 1, 

682 2, 

683 2, 

684 ) # (wavelengths=6, angles=1, thickness=1, 2, 2) 

685 assert result_array.M_p.shape == (6, 1, 1, 2, 2) 

686 

687 # theta shapes: result has (1, 3), result_array has (1, 3) 

688 assert result_array.theta.shape == result.theta.shape 

689 # n shapes: result has (3, 1), result_array has (3, 6) 

690 # For constant n, all wavelength columns should match 

691 assert result_array.n.shape == (3, 6) 

692 assert result.n.shape == (3, 1) 

693 np.testing.assert_allclose( 

694 result_array.n[:, 0], 

695 result.n[:, 0], 

696 atol=TOLERANCE_ABSOLUTE_TESTS, 

697 )