Coverage for colour/phenomena/interference.py: 100%
54 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Thin Film Interference
3======================
5Provides support for thin film interference calculations and visualization.
7- :func:`colour.phenomena.light_water_molar_refraction_Schiebener1990`
8- :func:`colour.phenomena.light_water_refractive_index_Schiebener1990`
9- :func:`colour.phenomena.thin_film_tmm`
10- :func:`colour.phenomena.multilayer_tmm`
12References
13----------
14- :cite:`Byrnes2016` : Byrnes, S. J. (2016). Multilayer optical
15 calculations. arXiv:1603.02720 [Physics].
16 http://arxiv.org/abs/1603.02720
17"""
19from __future__ import annotations
21from typing import TYPE_CHECKING
23import numpy as np
25from colour.phenomena.tmm import matrix_transfer_tmm
26from colour.utilities import as_float_array, tstack
28if TYPE_CHECKING:
29 from colour.hints import ArrayLike, NDArrayFloat
31__author__ = "Colour Developers"
32__copyright__ = "Copyright 2013 Colour Developers"
33__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
34__maintainer__ = "Colour Developers"
35__email__ = "colour-developers@colour-science.org"
36__status__ = "Production"
38__all__ = [
39 "light_water_molar_refraction_Schiebener1990",
40 "light_water_refractive_index_Schiebener1990",
41 "thin_film_tmm",
42 "multilayer_tmm",
43]
46def light_water_molar_refraction_Schiebener1990(
47 wavelength: ArrayLike,
48 temperature: ArrayLike = 294,
49 density: ArrayLike = 1000,
50) -> NDArrayFloat:
51 """
52 Calculate water molar refraction using Schiebener et al. (1990) model.
54 Parameters
55 ----------
56 wavelength : array_like
57 Wavelength values :math:`\\lambda` in nanometers.
58 temperature : float, optional
59 Water temperature :math:`T` in Kelvin. Default is 294 K (21°C).
60 density : float, optional
61 Water density :math:`\\rho` in kg/m³. Default is 1000 kg/m³.
63 Returns
64 -------
65 :class:`numpy.ndarray`
66 Molar refraction values.
68 Examples
69 --------
70 >>> light_water_molar_refraction_Schiebener1990(589) # doctest: +ELLIPSIS
71 0.2062114...
72 >>> light_water_molar_refraction_Schiebener1990([400, 500, 600])
73 ... # doctest: +ELLIPSIS
74 array([ 0.2119202..., 0.2081386..., 0.2060235...])
76 References
77 ----------
78 :cite:`Schiebener1990`
79 """
81 wl = as_float_array(wavelength) / 589
82 T = as_float_array(temperature) / 273.15
83 p = as_float_array(density) / 1000
85 a_0 = 0.243905091
86 a_1 = 9.53518094 * 10**-3
87 a_2 = -3.64358110 * 10**-3
88 a_3 = 2.65666426 * 10**-4
89 a_4 = 1.59189325 * 10**-3
90 a_5 = 2.45733798 * 10**-3
91 a_6 = 0.897478251
92 a_7 = -1.63066183 * 10**-2
93 wl_UV = 0.2292020
94 wl_IR = 5.432937
96 wl_2 = wl**2
98 return (
99 a_0
100 + a_1 * p
101 + a_2 * T
102 + a_3 * wl_2 * T
103 + a_4 / wl_2
104 + (a_5 / (wl_2 - wl_UV**2))
105 + (a_6 / (wl_2 - wl_IR**2))
106 + a_7 * p**2
107 )
110def light_water_refractive_index_Schiebener1990(
111 wavelength: ArrayLike,
112 temperature: ArrayLike = 294,
113 density: ArrayLike = 1000,
114) -> NDArrayFloat:
115 """
116 Calculate water refractive index using Schiebener et al. (1990) model.
118 Parameters
119 ----------
120 wavelength : array_like
121 Wavelength values :math:`\\lambda` in nanometers.
122 temperature : float, optional
123 Water temperature :math:`T` in Kelvin. Default is 294 K (21°C).
124 density : float, optional
125 Water density :math:`\\rho` in kg/m³. Default is 1000 kg/m³.
127 Returns
128 -------
129 :class:`numpy.ndarray`
130 Refractive index values for water.
132 Examples
133 --------
134 >>> light_water_refractive_index_Schiebener1990(
135 ... [400, 500, 600]
136 ... ) # doctest: +ELLIPSIS
137 array([ 1.3441433..., 1.3373637..., 1.3335851...])
139 References
140 ----------
141 :cite:`Schiebener1990`
142 """
144 p_s = as_float_array(density) / 1000
146 LL = light_water_molar_refraction_Schiebener1990(wavelength, temperature, density)
148 return np.sqrt((2 * LL + 1 / p_s) / (1 / p_s - LL))
151def thin_film_tmm(
152 n: ArrayLike,
153 t: ArrayLike,
154 wavelength: ArrayLike,
155 theta: ArrayLike = 0,
156) -> tuple[NDArrayFloat, NDArrayFloat]:
157 """
158 Calculate thin film reflectance and transmittance using *Transfer Matrix Method*.
160 Unified function that returns both R and T in a single call, matching the
161 approach used by Byrnes' tmm and TMM-Fast packages. Supports **outer product
162 broadcasting** and wavelength-dependent refractive index (dispersion).
164 Parameters
165 ----------
166 n : array_like
167 Complete refractive index stack :math:`n_j` for single-layer film. Shape:
168 (3,) or (3, wavelengths_count). The array should contain
169 [n_incident, n_film, n_substrate].
171 For example: constant n ``[1.0, 1.5, 1.0]`` (air | film | air), or
172 dispersive n ``[[1.0, 1.0, 1.0], [1.52, 1.51, 1.50], [1.0, 1.0, 1.0]]``
173 for wavelength-dependent film refractive index.
174 t : array_like
175 Film thickness :math:`d` in nanometers. Can be:
177 - **Scalar**: Single thickness value (e.g., ``250``) → shape
178 ``(W, A, 1, 2)``
179 - **1D array**: Multiple thickness values (e.g., ``[200, 250, 300]``)
180 for **thickness sweeps** via outer product broadcasting → shape
181 ``(W, A, T, 2)``
183 When an array is provided, the function computes reflectance and
184 transmittance for ALL combinations of thickness x wavelength x angle
185 values.
186 wavelength : array_like
187 Wavelength values :math:`\\lambda` in nanometers. Can be scalar or array.
188 theta : array_like, optional
189 Incident angle :math:`\\theta` in degrees. Scalar or array of shape
190 (angles_count,) for angle broadcasting. Default is 0 (normal incidence).
192 Returns
193 -------
194 tuple
195 (R, T) where:
197 - **R**: Reflectance, :class:`numpy.ndarray`, shape **(W, A, T, 2)**
198 for [R_s, R_p]
199 - **T**: Transmittance, :class:`numpy.ndarray`, shape **(W, A, T, 2)**
200 for [T_s, T_p]
202 where **W** = number of wavelengths, **A** = number of angles,
203 **T** = number of thicknesses (the **Spectroscopy Convention**)
205 Examples
206 --------
207 Basic usage:
209 >>> R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, 555)
210 >>> R.shape, T.shape
211 ((1, 1, 1, 2), (1, 1, 1, 2))
212 >>> R[0, 0, 0] # [R_s, R_p] at 555nm, shape (W, A, T, 2) = (1, 1, 1, 2)
213 ... # doctest: +ELLIPSIS
214 array([ 0.1215919..., 0.1215919...])
215 >>> np.allclose(R + T, 1.0) # Energy conservation
216 True
218 Multiple wavelengths:
220 >>> R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, [400, 500, 600])
221 >>> R.shape # (W, A, T, 2) = (3 wavelengths, 1 angle, 1 thickness, 2 pols)
222 (3, 1, 1, 2)
224 **Thickness sweep** (outer product broadcasting):
226 >>> R, T = thin_film_tmm([1.0, 1.5, 1.0], [200, 250, 300], [400, 500, 600])
227 >>> R.shape # (W, A, T, 2) = (3 wavelengths, 1 angle, 3 thicknesses, 2 pols)
228 (3, 1, 3, 2)
229 >>> # Access via R[wl_idx, ang_idx, thick_idx, pol_idx]
230 >>> # R[0, 0, 0] = reflectance at λ=400nm, thickness=200nm
231 >>> # R[1, 0, 1] = reflectance at λ=500nm, thickness=250nm
233 **Angle broadcasting**:
235 >>> R, T = thin_film_tmm([1.0, 1.5, 1.0], 250, [400, 500, 600], [0, 30, 45, 60])
236 >>> R.shape # (W, A, T, 2) = (3 wavelengths, 4 angles, 1 thickness, 2 pols)
237 (3, 4, 1, 2)
238 >>> # R[0, 0, 0] = reflectance at λ=400nm, θ=0°
239 >>> # R[1, 2, 0] = reflectance at λ=500nm, θ=45°
241 **Dispersion**: wavelength-dependent refractive index:
243 >>> wavelengths = [400, 500, 600]
244 >>> n_dispersive = [[1.0, 1.0, 1.0], [1.52, 1.51, 1.50], [1.0, 1.0, 1.0]]
245 >>> R, T = thin_film_tmm(n_dispersive, 250, wavelengths)
246 >>> R.shape # (W, A, T, 2) = (3 wavelengths, 1 angle, 1 thickness, 2 pols)
247 (3, 1, 1, 2)
249 Notes
250 -----
251 - **Thickness broadcasting** (outer product): When ``t`` is an array with
252 multiple values, ALL combinations of thickness x wavelength x angle are
253 computed. For example: 3 thicknesses x 5 wavelengths x 2 angles = 30
254 total calculations, returned in shape ``(W, A, T, 2)`` = ``(5, 2, 3, 2)``.
256 This differs from multilayer specification where thickness specifies
257 one value per layer (e.g., ``[250, 150]`` for a 2-layer stack).
259 - **Spectroscopy Convention**: Output arrays use wavelength-first ordering
260 ``(W, A, T, 2)`` which is natural for spectroscopy applications where
261 you typically iterate over wavelengths in the outer loop.
263 - **Dispersion support**: If ``n`` is 2D, the second dimension must match the
264 ``wavelength`` array length. Each wavelength uses its corresponding n value.
266 - **Energy conservation**: For non-absorbing media, R + T = 1.
268 - Supports complex refractive indices for absorbing materials (e.g., metals).
270 - For absorbing media: R + T < 1 (absorption A = 1 - R - T).
272 References
273 ----------
274 :cite:`Byrnes2016`
275 """
277 t = np.atleast_1d(as_float_array(t))
279 # Handle thickness broadcasting: reshape from (T,) to (T, 1) for single-layer
280 t = t[:, np.newaxis] if len(t) > 1 else t
282 return multilayer_tmm(n=n, t=t, wavelength=wavelength, theta=theta)
285def multilayer_tmm(
286 n: ArrayLike,
287 t: ArrayLike,
288 wavelength: ArrayLike,
289 theta: ArrayLike = 0,
290) -> tuple[NDArrayFloat, NDArrayFloat]:
291 """
292 Calculate multilayer reflectance and transmittance using *Transfer Matrix Method*.
294 Unified function that returns both R and T in a single call, eliminating duplication
295 and matching industry-standard TMM implementations. Computes both values from the
296 same transfer matrix for efficiency.
298 Parameters
299 ----------
300 n : array_like
301 Complete refractive index stack :math:`n_j` including incident medium,
302 layers, and substrate. Shape: (media_count,) or
303 (media_count, wavelengths_count). Can be complex for absorbing
304 materials. The array should contain [n_incident, n_layer_1, ...,
305 n_layer_n, n_substrate].
307 For example: single layer ``[1.0, 1.5, 1.0]`` (air | film | air),
308 two layers ``[1.0, 1.5, 2.0, 1.0]`` (air | film1 | film2 | air), or
309 with dispersion ``[[1.0, 1.0, 1.0], [1.52, 1.51, 1.50], [1.0, 1.0, 1.0]]``
310 for wavelength-dependent n.
311 t : array_like
312 Thicknesses of each layer :math:`t_j` in nanometers (excluding incident
313 and substrate). Shape: (layers_count,).
315 **Important**: This parameter specifies ONE thickness value per layer
316 in the multilayer stack. It does NOT perform thickness sweeps.
318 For example: single layer ``[250]``, two-layer stack ``[250, 150]``,
319 three-layer stack ``[100, 200, 100]``.
321 **For thickness sweeps**, use :func:`thin_film_tmm` with an array of
322 thickness values (e.g., ``[200, 250, 300]``), which computes all
323 combinations via outer product broadcasting.
324 wavelength : array_like
325 Wavelength values :math:`\\lambda` in nanometers.
326 theta : array_like, optional
327 Incident angle :math:`\\theta` in degrees. Scalar or array of shape
328 (angles_count,) for angle broadcasting. Default is 0 (normal incidence).
330 Returns
331 -------
332 tuple
333 (R, T) where:
335 - **R**: Reflectance, :class:`numpy.ndarray`, shape **(W, A, 1, 2)**
336 for [R_s, R_p]
337 - **T**: Transmittance, :class:`numpy.ndarray`, shape **(W, A, 1, 2)**
338 for [T_s, T_p]
340 where **W** = number of wavelengths, **A** = number of angles
341 (the **Spectroscopy Convention**). The thickness dimension is always 1
342 for multilayer stacks.
344 Examples
345 --------
346 Single layer:
348 >>> R, T = multilayer_tmm([1.0, 1.5, 1.0], [250], 555)
349 >>> R.shape, T.shape
350 ((1, 1, 1, 2), (1, 1, 1, 2))
351 >>> np.allclose(R + T, 1.0) # Energy conservation
352 True
354 Two-layer stack:
356 >>> R, T = multilayer_tmm([1.0, 1.5, 2.0, 1.0], [250, 150], 555)
357 >>> R.shape # (W, A, T, 2) = (1 wavelength, 1 angle, 1 thickness, 2 pols)
358 (1, 1, 1, 2)
360 Multiple wavelengths:
362 >>> R, T = multilayer_tmm([1.0, 1.5, 1.0], [250], [400, 500, 600])
363 >>> R.shape # (W, A, T, 2) = (3 wavelengths, 1 angle, 1 thickness, 2 pols)
364 (3, 1, 1, 2)
366 Multiple angles (angle broadcasting):
368 >>> R, T = multilayer_tmm([1.0, 1.5, 1.0], [250], [400, 500, 600], [0, 30, 45, 60])
369 >>> R.shape # (W, A, T, 2) = (3 wavelengths, 4 angles, 1 thickness, 2 pols)
370 (3, 4, 1, 2)
372 Notes
373 -----
374 - The reflectance is calculated from (*Equation 15* from :cite:`Byrnes2016`):
376 .. math::
378 r = \\frac{\\tilde{M}_{10}}{\\tilde{M}_{00}}, \\quad R = |r|^2
380 - The transmittance is calculated from (*Equations 14 and 21-22* from
381 :cite:`Byrnes2016`):
383 .. math::
385 t = \\frac{1}{\\tilde{M}_{00}}
387 .. math::
389 T = |t|^2 \\frac{\\text{Re}[n_{\\text{substrate}} \
390\\cos \\theta_{\\text{final}}]}{\\text{Re}[n_{\\text{incident}} \\cos \\theta_i]}
392 Where:
394 - :math:`\\tilde{M}`: Overall transfer matrix for the multilayer stack
395 - :math:`r, t`: Complex reflection and transmission amplitude coefficients
396 - :math:`R, T`: Reflectance and transmittance (fraction of incident power)
398 - **Energy conservation**: For non-absorbing media, R + T = 1.
399 - Supports complex refractive indices for absorbing materials (e.g., metals).
400 - For absorbing media: R + T < 1 (absorption A = 1 - R - T).
402 References
403 ----------
404 :cite:`Byrnes2016`
405 """
407 theta = np.atleast_1d(as_float_array(theta))
409 result = matrix_transfer_tmm(
410 n=n,
411 t=np.atleast_1d(as_float_array(t)),
412 theta=theta,
413 wavelength=wavelength,
414 )
416 # Extract n_incident and n_substrate from result.n
417 # result.n has shape (media_count, wavelengths_count)
418 n_incident = result.n[0, 0]
419 n_substrate = result.n[-1, 0]
421 # T = thickness_count, A = angles_count, W = wavelengths_count
422 # Reflectance (Byrnes Eq. 15, 23)
423 r_s = np.abs(result.M_s[:, :, :, 1, 0] / result.M_s[:, :, :, 0, 0]) ** 2
424 r_p = np.abs(result.M_p[:, :, :, 1, 0] / result.M_p[:, :, :, 0, 0]) ** 2
426 # Transmittance (Byrnes Eq. 14, 21-22)
427 t_s = 1 / result.M_s[:, :, :, 0, 0]
428 t_p = 1 / result.M_p[:, :, :, 0, 0]
430 # Transmittance correction factor: Re[n_f cos(θ_f) / n_i cos(θ_i)]
431 # result.theta has shape (A, M) where M = media_count
432 cos_theta_i = np.cos(np.radians(theta))[:, None] # (A, 1)
433 cos_theta_f = np.cos(np.radians(result.theta[:, -1]))[:, None] # (A, 1)
434 transmittance_correction = np.real(
435 (n_substrate * cos_theta_f) / (n_incident * cos_theta_i)
436 ) # (A, 1)
438 # Broadcast to thickness dimension: (1, A, 1)
439 transmittance_correction = transmittance_correction[None, :, :]
441 t_s = np.abs(t_s) ** 2 * transmittance_correction # (T, A, W)
442 t_p = np.abs(t_p) ** 2 * transmittance_correction # (T, A, W)
444 # Stack results: (T, A, W, 2)
445 R = tstack([r_s, r_p])
446 T = tstack([t_s, t_p])
448 return R, T