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
« 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."""
3from __future__ import annotations
5import numpy as np
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
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"
23__all__ = [
24 "TestLightWaterMolarRefractionSchiebener1990",
25 "TestLightWaterRefractiveIndexSchiebener1990",
26 "TestThinFilmTmm",
27 "TestMultilayerTmm",
28]
31class TestLightWaterMolarRefractionSchiebener1990:
32 """
33 Define :func:`colour.phenomena.interference.\
34light_water_molar_refraction_Schiebener1990` definition unit tests methods.
35 """
37 def test_light_water_molar_refraction_Schiebener1990(self) -> None:
38 """
39 Test :func:`colour.phenomena.interference.\
40light_water_molar_refraction_Schiebener1990` definition.
41 """
43 np.testing.assert_allclose(
44 light_water_molar_refraction_Schiebener1990(589),
45 0.206211470522,
46 atol=TOLERANCE_ABSOLUTE_TESTS,
47 )
49 np.testing.assert_allclose(
50 light_water_molar_refraction_Schiebener1990(400, 300, 1000),
51 0.211842881763,
52 atol=TOLERANCE_ABSOLUTE_TESTS,
53 )
55 np.testing.assert_allclose(
56 light_water_molar_refraction_Schiebener1990(700, 280, 998),
57 0.204829756928,
58 atol=TOLERANCE_ABSOLUTE_TESTS,
59 )
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 """
69 wl = 589
70 LL = light_water_molar_refraction_Schiebener1990(wl)
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 )
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 )
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 )
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 """
103 light_water_molar_refraction_Schiebener1990(
104 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan])
105 )
108class TestLightWaterRefractiveIndexSchiebener1990:
109 """
110 Define :func:`colour.phenomena.interference.\
111light_water_refractive_index_Schiebener1990` definition unit tests methods.
112 """
114 def test_light_water_refractive_index_Schiebener1990(self) -> None:
115 """
116 Test :func:`colour.phenomena.interference.\
117light_water_refractive_index_Schiebener1990` definition.
118 """
120 np.testing.assert_allclose(
121 light_water_refractive_index_Schiebener1990(400),
122 1.344143366618,
123 atol=TOLERANCE_ABSOLUTE_TESTS,
124 )
126 np.testing.assert_allclose(
127 light_water_refractive_index_Schiebener1990(500),
128 1.337363795367,
129 atol=TOLERANCE_ABSOLUTE_TESTS,
130 )
132 np.testing.assert_allclose(
133 light_water_refractive_index_Schiebener1990(600),
134 1.333585122179,
135 atol=TOLERANCE_ABSOLUTE_TESTS,
136 )
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 """
146 wl = 400
147 n = light_water_refractive_index_Schiebener1990(wl)
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 )
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 )
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 )
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 """
180 light_water_refractive_index_Schiebener1990(
181 np.array([-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan])
182 )
185class TestThinFilmTmm:
186 """
187 Define :func:`colour.phenomena.interference.thin_film_tmm`
188 definition unit tests methods.
189 """
191 def test_thin_film_tmm(self) -> None:
192 """
193 Test :func:`colour.phenomena.interference.thin_film_tmm`
194 definition.
195 """
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))
204 # Test energy conservation
205 np.testing.assert_allclose(R + T, 1.0, atol=1e-6)
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))
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 )
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 """
226 wl = 555
227 R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, wl)
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)
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 """
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 )
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 """
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)
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))
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}"
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)
274 # Silver should have high reflectance
275 assert np.mean(R_silver) > 0.5
278class TestMultilayerTmm:
279 """
280 Define :func:`colour.phenomena.interference.multilayer_tmm`
281 definition unit tests methods.
282 """
284 def test_multilayer_tmm(self) -> None:
285 """
286 Test :func:`colour.phenomena.interference.multilayer_tmm`
287 definition.
288 """
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)
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))
303 # Test energy conservation
304 np.testing.assert_allclose(R + T, 1.0, atol=1e-6)
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)
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 """
317 wl = 555
318 R, T = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], wl)
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)
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 """
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 )
340 def test_multilayer_tmm_complex_n(self) -> None:
341 """
342 Test :func:`colour.phenomena.interference.multilayer_tmm`
343 with complex refractive indices.
344 """
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])
351 R, T = multilayer_tmm(n_layers, thicknesses, wavelengths)
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))
359 # For absorbing media: R + T < 1
360 assert np.all(R + T < 1.0 + 1e-6)
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)
367 # High reflectance expected for metal
368 assert np.mean(R_metal) > 0.5
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 """
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
382 n_layers = [1.0, n_ar, n_absorber, n_substrate]
383 thicknesses = [100, 300]
384 wavelength = 550
386 R, T = multilayer_tmm(n_layers, thicknesses, wavelength)
388 # Basic validity
389 assert np.all((R >= 0) & (R <= 1))
390 assert np.all((T >= 0) & (T <= 1))
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}"
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])
409 R_mixed, T_mixed = multilayer_tmm(
410 n_layers_mixed, thicknesses_mixed, wavelengths
411 )
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))
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)
430 # For lossless media: R + T = 1
431 np.testing.assert_allclose(R_real + T_real, 1.0, atol=1e-6)