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

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

2

3 from __future__ import annotations

4

5 import numpy as np

6

7 from colour.constants import TOLERANCE_ABSOLUTE_TESTS

8 from colour.phenomena.interference import (

9 light_water_molar_refraction_Schiebener1990,

10 light_water_refractive_index_Schiebener1990,

11 multilayer_tmm,

12 thin_film_tmm,

13)

14 from 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

31 class TestLightWaterMolarRefractionSchiebener1990:

32 """

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

34 light_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.\

40 light_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.\

66 light_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.\

100 light_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

108 class TestLightWaterRefractiveIndexSchiebener1990:

109 """

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

111 light_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.\

117 light_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.\

143 light_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.\

177 light_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

185 class 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

278 class 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)