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

160 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.interference` module.""" 

2 

3from __future__ import annotations 

4 

5import numpy as np 

6 

7from colour.constants import TOLERANCE_ABSOLUTE_TESTS 

8from colour.phenomena.interference import ( 

9 light_water_molar_refraction_Schiebener1990, 

10 light_water_refractive_index_Schiebener1990, 

11 multilayer_tmm, 

12 thin_film_tmm, 

13) 

14from colour.utilities import ignore_numpy_errors 

15 

16__author__ = "Colour Developers" 

17__copyright__ = "Copyright 2013 Colour Developers" 

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

19__maintainer__ = "Colour Developers" 

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

21__status__ = "Production" 

22 

23__all__ = [ 

24 "TestLightWaterMolarRefractionSchiebener1990", 

25 "TestLightWaterRefractiveIndexSchiebener1990", 

26 "TestThinFilmTmm", 

27 "TestMultilayerTmm", 

28] 

29 

30 

31class TestLightWaterMolarRefractionSchiebener1990: 

32 """ 

33 Define :func:`colour.phenomena.interference.\ 

34light_water_molar_refraction_Schiebener1990` definition unit tests methods. 

35 """ 

36 

37 def test_light_water_molar_refraction_Schiebener1990(self) -> None: 

38 """ 

39 Test :func:`colour.phenomena.interference.\ 

40light_water_molar_refraction_Schiebener1990` definition. 

41 """ 

42 

43 np.testing.assert_allclose( 

44 light_water_molar_refraction_Schiebener1990(589), 

45 0.206211470522, 

46 atol=TOLERANCE_ABSOLUTE_TESTS, 

47 ) 

48 

49 np.testing.assert_allclose( 

50 light_water_molar_refraction_Schiebener1990(400, 300, 1000), 

51 0.211842881763, 

52 atol=TOLERANCE_ABSOLUTE_TESTS, 

53 ) 

54 

55 np.testing.assert_allclose( 

56 light_water_molar_refraction_Schiebener1990(700, 280, 998), 

57 0.204829756928, 

58 atol=TOLERANCE_ABSOLUTE_TESTS, 

59 ) 

60 

61 def test_n_dimensional_light_water_molar_refraction_Schiebener1990( 

62 self, 

63 ) -> None: 

64 """ 

65 Test :func:`colour.phenomena.interference.\ 

66light_water_molar_refraction_Schiebener1990` definition n-dimensional arrays support. 

67 """ 

68 

69 wl = 589 

70 LL = light_water_molar_refraction_Schiebener1990(wl) 

71 

72 wl = np.tile(wl, 6) 

73 LL = np.tile(LL, 6) 

74 np.testing.assert_allclose( 

75 light_water_molar_refraction_Schiebener1990(wl), 

76 LL, 

77 atol=TOLERANCE_ABSOLUTE_TESTS, 

78 ) 

79 

80 wl = np.reshape(wl, (2, 3)) 

81 LL = np.reshape(LL, (2, 3)) 

82 np.testing.assert_allclose( 

83 light_water_molar_refraction_Schiebener1990(wl), 

84 LL, 

85 atol=TOLERANCE_ABSOLUTE_TESTS, 

86 ) 

87 

88 wl = np.reshape(wl, (2, 3, 1)) 

89 LL = np.reshape(LL, (2, 3, 1)) 

90 np.testing.assert_allclose( 

91 light_water_molar_refraction_Schiebener1990(wl), 

92 LL, 

93 atol=TOLERANCE_ABSOLUTE_TESTS, 

94 ) 

95 

96 @ignore_numpy_errors 

97 def test_nan_light_water_molar_refraction_Schiebener1990(self) -> None: 

98 """ 

99 Test :func:`colour.phenomena.interference.\ 

100light_water_molar_refraction_Schiebener1990` definition nan support. 

101 """ 

102 

103 light_water_molar_refraction_Schiebener1990( 

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

105 ) 

106 

107 

108class TestLightWaterRefractiveIndexSchiebener1990: 

109 """ 

110 Define :func:`colour.phenomena.interference.\ 

111light_water_refractive_index_Schiebener1990` definition unit tests methods. 

112 """ 

113 

114 def test_light_water_refractive_index_Schiebener1990(self) -> None: 

115 """ 

116 Test :func:`colour.phenomena.interference.\ 

117light_water_refractive_index_Schiebener1990` definition. 

118 """ 

119 

120 np.testing.assert_allclose( 

121 light_water_refractive_index_Schiebener1990(400), 

122 1.344143366618, 

123 atol=TOLERANCE_ABSOLUTE_TESTS, 

124 ) 

125 

126 np.testing.assert_allclose( 

127 light_water_refractive_index_Schiebener1990(500), 

128 1.337363795367, 

129 atol=TOLERANCE_ABSOLUTE_TESTS, 

130 ) 

131 

132 np.testing.assert_allclose( 

133 light_water_refractive_index_Schiebener1990(600), 

134 1.333585122179, 

135 atol=TOLERANCE_ABSOLUTE_TESTS, 

136 ) 

137 

138 def test_n_dimensional_light_water_refractive_index_Schiebener1990( 

139 self, 

140 ) -> None: 

141 """ 

142 Test :func:`colour.phenomena.interference.\ 

143light_water_refractive_index_Schiebener1990` definition n-dimensional arrays support. 

144 """ 

145 

146 wl = 400 

147 n = light_water_refractive_index_Schiebener1990(wl) 

148 

149 wl = np.tile(wl, 6) 

150 n = np.tile(n, 6) 

151 np.testing.assert_allclose( 

152 light_water_refractive_index_Schiebener1990(wl), 

153 n, 

154 atol=TOLERANCE_ABSOLUTE_TESTS, 

155 ) 

156 

157 wl = np.reshape(wl, (2, 3)) 

158 n = np.reshape(n, (2, 3)) 

159 np.testing.assert_allclose( 

160 light_water_refractive_index_Schiebener1990(wl), 

161 n, 

162 atol=TOLERANCE_ABSOLUTE_TESTS, 

163 ) 

164 

165 wl = np.reshape(wl, (2, 3, 1)) 

166 n = np.reshape(n, (2, 3, 1)) 

167 np.testing.assert_allclose( 

168 light_water_refractive_index_Schiebener1990(wl), 

169 n, 

170 atol=TOLERANCE_ABSOLUTE_TESTS, 

171 ) 

172 

173 @ignore_numpy_errors 

174 def test_nan_light_water_refractive_index_Schiebener1990(self) -> None: 

175 """ 

176 Test :func:`colour.phenomena.interference.\ 

177light_water_refractive_index_Schiebener1990` definition nan support. 

178 """ 

179 

180 light_water_refractive_index_Schiebener1990( 

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

182 ) 

183 

184 

185class TestThinFilmTmm: 

186 """ 

187 Define :func:`colour.phenomena.interference.thin_film_tmm` 

188 definition unit tests methods. 

189 """ 

190 

191 def test_thin_film_tmm(self) -> None: 

192 """ 

193 Test :func:`colour.phenomena.interference.thin_film_tmm` 

194 definition. 

195 """ 

196 

197 # Test single wavelength - returns (R, T) tuple 

198 R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, 500) 

199 assert R.shape == (1, 1, 1, 2) # (W, A, T, 2) - [R_s, R_p] 

200 assert T.shape == (1, 1, 1, 2) # (W, A, T, 2) - [T_s, T_p] 

201 assert np.all((R >= 0) & (R <= 1)) 

202 assert np.all((T >= 0) & (T <= 1)) 

203 

204 # Test energy conservation 

205 np.testing.assert_allclose(R + T, 1.0, atol=1e-6) 

206 

207 # Test multiple wavelengths 

208 R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, [400, 500, 600]) 

209 assert R.shape == (3, 1, 1, 2) # (W=3, A=1, T=1, 2) - Spectroscopy Convention 

210 assert T.shape == (3, 1, 1, 2) 

211 assert np.all((R >= 0) & (R <= 1)) 

212 assert np.all((T >= 0) & (T <= 1)) 

213 

214 # Test that s and p polarisations are similar at normal incidence 

215 R_normal, _ = thin_film_tmm([1.0, 1.5, 1.0], 250, 500, theta=0) 

216 np.testing.assert_allclose( 

217 R_normal[0, 0, 0, 0], R_normal[0, 0, 0, 1], atol=1e-10 

218 ) 

219 

220 def test_n_dimensional_thin_film_tmm(self) -> None: 

221 """ 

222 Test :func:`colour.phenomena.interference.thin_film_tmm` 

223 definition n-dimensional arrays support. 

224 """ 

225 

226 wl = 555 

227 R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, wl) 

228 

229 wl = np.tile(wl, 6) 

230 R = np.tile(R, (6, 1, 1, 1)) 

231 T = np.tile(T, (6, 1, 1, 1)) 

232 R_array, T_array = thin_film_tmm([1.0, 1.5, 1.0], 250, wl) 

233 np.testing.assert_allclose(R_array, R, atol=TOLERANCE_ABSOLUTE_TESTS) 

234 np.testing.assert_allclose(T_array, T, atol=TOLERANCE_ABSOLUTE_TESTS) 

235 

236 @ignore_numpy_errors 

237 def test_nan_thin_film_tmm(self) -> None: 

238 """ 

239 Test :func:`colour.phenomena.interference.thin_film_tmm` 

240 definition nan support. 

241 """ 

242 

243 thin_film_tmm( 

244 [1.0, 1.5, 1.0], 

245 250, 

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

247 ) 

248 

249 def test_thin_film_tmm_complex_n(self) -> None: 

250 """ 

251 Test :func:`colour.phenomena.interference.thin_film_tmm` 

252 with complex refractive indices (absorbing layers). 

253 """ 

254 

255 # Absorbing layer: n = 2.0 + 0.5j 

256 n_absorbing = 2.0 + 0.5j 

257 R, T = thin_film_tmm([1.0, n_absorbing, 1.0], 250, 500) 

258 

259 assert R.shape == (1, 1, 1, 2) 

260 assert T.shape == (1, 1, 1, 2) 

261 assert np.all((R >= 0) & (R <= 1)) 

262 assert np.all((T >= 0) & (T <= 1)) 

263 

264 # For absorbing media: R + T < 1 (absorption A = 1 - R - T > 0) 

265 R_avg = np.mean(R) 

266 T_avg = np.mean(T) 

267 A = 1 - R_avg - T_avg 

268 assert A > 0, f"Expected absorption > 0, got A = {A}" 

269 

270 # Silver mirror: n ≈ 0.18 + 3.15j at 500nm 

271 n_silver = 0.18 + 3.15j 

272 R_silver, _ = thin_film_tmm([1.0, n_silver, 1.0], 50, 500) 

273 

274 # Silver should have high reflectance 

275 assert np.mean(R_silver) > 0.5 

276 

277 

278class TestMultilayerTmm: 

279 """ 

280 Define :func:`colour.phenomena.interference.multilayer_tmm` 

281 definition unit tests methods. 

282 """ 

283 

284 def test_multilayer_tmm(self) -> None: 

285 """ 

286 Test :func:`colour.phenomena.interference.multilayer_tmm` 

287 definition. 

288 """ 

289 

290 # Test single layer (should match thin_film_tmm) 

291 R_multi, T_multi = multilayer_tmm([1.0, 1.5, 1.0], [250], 500) 

292 R_single, T_single = thin_film_tmm([1.0, 1.5, 1.0], 250, 500) 

293 np.testing.assert_allclose(R_multi, R_single, atol=TOLERANCE_ABSOLUTE_TESTS) 

294 np.testing.assert_allclose(T_multi, T_single, atol=TOLERANCE_ABSOLUTE_TESTS) 

295 

296 # Test multiple layers 

297 R, T = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], [400, 500, 600]) 

298 assert R.shape == (3, 1, 1, 2) # (W=3, A=1, T=1, 2) - Spectroscopy Convention 

299 assert T.shape == (3, 1, 1, 2) 

300 assert np.all((R >= 0) & (R <= 1)) 

301 assert np.all((T >= 0) & (T <= 1)) 

302 

303 # Test energy conservation 

304 np.testing.assert_allclose(R + T, 1.0, atol=1e-6) 

305 

306 # Test with different substrate 

307 R_sub, T_sub = multilayer_tmm([1.0, 1.5, 1.5], [250], 500) 

308 assert R_sub.shape == (1, 1, 1, 2) 

309 assert T_sub.shape == (1, 1, 1, 2) 

310 

311 def test_n_dimensional_multilayer_tmm(self) -> None: 

312 """ 

313 Test :func:`colour.phenomena.interference.multilayer_tmm` 

314 definition n-dimensional arrays support. 

315 """ 

316 

317 wl = 555 

318 R, T = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], wl) 

319 

320 wl = np.tile(wl, 6) 

321 R = np.tile(R, (6, 1, 1, 1)) 

322 T = np.tile(T, (6, 1, 1, 1)) 

323 R_array, T_array = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], wl) 

324 np.testing.assert_allclose(R_array, R, atol=TOLERANCE_ABSOLUTE_TESTS) 

325 np.testing.assert_allclose(T_array, T, atol=TOLERANCE_ABSOLUTE_TESTS) 

326 

327 @ignore_numpy_errors 

328 def test_nan_multilayer_tmm(self) -> None: 

329 """ 

330 Test :func:`colour.phenomena.interference.multilayer_tmm` 

331 definition nan support. 

332 """ 

333 

334 multilayer_tmm( 

335 [1.0, 1.5, 1.0], 

336 [250], 

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

338 ) 

339 

340 def test_multilayer_tmm_complex_n(self) -> None: 

341 """ 

342 Test :func:`colour.phenomena.interference.multilayer_tmm` 

343 with complex refractive indices. 

344 """ 

345 

346 # Stack of two absorbing layers: air | layer1 | layer2 | air 

347 n_layers = [1.0, 2.0 + 0.5j, 1.8 + 0.3j, 1.0] 

348 thicknesses = [200, 300] 

349 wavelengths = np.array([400, 500, 600]) 

350 

351 R, T = multilayer_tmm(n_layers, thicknesses, wavelengths) 

352 

353 # Check shapes and validity 

354 assert R.shape == (3, 1, 1, 2) # (W=3, A=1, T=1, 2) - Spectroscopy Convention 

355 assert T.shape == (3, 1, 1, 2) 

356 assert np.all((R >= 0) & (R <= 1)) 

357 assert np.all((T >= 0) & (T <= 1)) 

358 

359 # For absorbing media: R + T < 1 

360 assert np.all(R + T < 1.0 + 1e-6) 

361 

362 # Glass + Silver + Glass structure: glass | glass | silver | glass | glass 

363 n_layers_metal = [1.5, 1.5, 0.18 + 3.15j, 1.5, 1.5] 

364 thicknesses_metal = [100, 50, 100] 

365 R_metal, _ = multilayer_tmm(n_layers_metal, thicknesses_metal, 500) 

366 

367 # High reflectance expected for metal 

368 assert np.mean(R_metal) > 0.5 

369 

370 def test_multilayer_tmm_mixed_structures(self) -> None: 

371 """ 

372 Test :func:`colour.phenomena.interference.multilayer_tmm` 

373 with mixed transparent and absorbing layers. 

374 """ 

375 

376 # Anti-reflection coating + absorbing layer + glass substrate 

377 # air | AR coating | absorber | glass 

378 n_ar = 1.38 # MgF2 (transparent) 

379 n_absorber = 2.0 + 0.5j # Absorbing layer 

380 n_substrate = 1.5 # Glass 

381 

382 n_layers = [1.0, n_ar, n_absorber, n_substrate] 

383 thicknesses = [100, 300] 

384 wavelength = 550 

385 

386 R, T = multilayer_tmm(n_layers, thicknesses, wavelength) 

387 

388 # Basic validity 

389 assert np.all((R >= 0) & (R <= 1)) 

390 assert np.all((T >= 0) & (T <= 1)) 

391 

392 # For absorbing media: R + T < 1 

393 R_avg = np.mean(R) 

394 T_avg = np.mean(T) 

395 A = 1 - R_avg - T_avg 

396 assert A > 0, f"Expected absorption > 0, got A = {A}" 

397 

398 # Three-layer stack: air | transparent | absorbing | transparent | air 

399 n_layers_mixed = [ 

400 1.0, 

401 1.5, 

402 2.0 + 0.3j, 

403 1.7, 

404 1.0, 

405 ] # air, Real, Complex, Real, air 

406 thicknesses_mixed = [150, 200, 250] 

407 wavelengths = np.array([450, 550, 650]) 

408 

409 R_mixed, T_mixed = multilayer_tmm( 

410 n_layers_mixed, thicknesses_mixed, wavelengths 

411 ) 

412 

413 # Check shapes and validity 

414 assert R_mixed.shape == ( 

415 3, 

416 1, 

417 1, 

418 2, 

419 ) # (W=3, A=1, T=1, 2) - Spectroscopy Convention 

420 assert T_mixed.shape == (3, 1, 1, 2) 

421 assert np.all((R_mixed >= 0) & (R_mixed <= 1)) 

422 assert np.all((T_mixed >= 0) & (T_mixed <= 1)) 

423 

424 # Test with real refractive indices (lossless): 

425 # air | layer1 | layer2 | layer3 | air 

426 n_layers_real = [1.0, 1.38, 2.0, 1.7, 1.0] 

427 thicknesses_real = [100, 200, 150] 

428 R_real, T_real = multilayer_tmm(n_layers_real, thicknesses_real, 550) 

429 

430 # For lossless media: R + T = 1 

431 np.testing.assert_allclose(R_real + T_real, 1.0, atol=1e-6)