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

1""" 

2Thin Film Interference 

3====================== 

4 

5Provides support for thin film interference calculations and visualization. 

6 

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` 

11 

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""" 

18 

19from __future__ import annotations 

20 

21from typing import TYPE_CHECKING 

22 

23import numpy as np 

24 

25from colour.phenomena.tmm import matrix_transfer_tmm 

26from colour.utilities import as_float_array, tstack 

27 

28if TYPE_CHECKING: 

29 from colour.hints import ArrayLike, NDArrayFloat 

30 

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" 

37 

38__all__ = [ 

39 "light_water_molar_refraction_Schiebener1990", 

40 "light_water_refractive_index_Schiebener1990", 

41 "thin_film_tmm", 

42 "multilayer_tmm", 

43] 

44 

45 

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. 

53 

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³. 

62 

63 Returns 

64 ------- 

65 :class:`numpy.ndarray` 

66 Molar refraction values. 

67 

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...]) 

75 

76 References 

77 ---------- 

78 :cite:`Schiebener1990` 

79 """ 

80 

81 wl = as_float_array(wavelength) / 589 

82 T = as_float_array(temperature) / 273.15 

83 p = as_float_array(density) / 1000 

84 

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 

95 

96 wl_2 = wl**2 

97 

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 ) 

108 

109 

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. 

117 

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³. 

126 

127 Returns 

128 ------- 

129 :class:`numpy.ndarray` 

130 Refractive index values for water. 

131 

132 Examples 

133 -------- 

134 >>> light_water_refractive_index_Schiebener1990( 

135 ... [400, 500, 600] 

136 ... ) # doctest: +ELLIPSIS 

137 array([ 1.3441433..., 1.3373637..., 1.3335851...]) 

138 

139 References 

140 ---------- 

141 :cite:`Schiebener1990` 

142 """ 

143 

144 p_s = as_float_array(density) / 1000 

145 

146 LL = light_water_molar_refraction_Schiebener1990(wavelength, temperature, density) 

147 

148 return np.sqrt((2 * LL + 1 / p_s) / (1 / p_s - LL)) 

149 

150 

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*. 

159 

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). 

163 

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]. 

170 

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: 

176 

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)`` 

182 

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). 

191 

192 Returns 

193 ------- 

194 tuple 

195 (R, T) where: 

196 

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] 

201 

202 where **W** = number of wavelengths, **A** = number of angles, 

203 **T** = number of thicknesses (the **Spectroscopy Convention**) 

204 

205 Examples 

206 -------- 

207 Basic usage: 

208 

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 

217 

218 Multiple wavelengths: 

219 

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) 

223 

224 **Thickness sweep** (outer product broadcasting): 

225 

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 

232 

233 **Angle broadcasting**: 

234 

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° 

240 

241 **Dispersion**: wavelength-dependent refractive index: 

242 

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) 

248 

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)``. 

255 

256 This differs from multilayer specification where thickness specifies 

257 one value per layer (e.g., ``[250, 150]`` for a 2-layer stack). 

258 

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. 

262 

263 - **Dispersion support**: If ``n`` is 2D, the second dimension must match the 

264 ``wavelength`` array length. Each wavelength uses its corresponding n value. 

265 

266 - **Energy conservation**: For non-absorbing media, R + T = 1. 

267 

268 - Supports complex refractive indices for absorbing materials (e.g., metals). 

269 

270 - For absorbing media: R + T < 1 (absorption A = 1 - R - T). 

271 

272 References 

273 ---------- 

274 :cite:`Byrnes2016` 

275 """ 

276 

277 t = np.atleast_1d(as_float_array(t)) 

278 

279 # Handle thickness broadcasting: reshape from (T,) to (T, 1) for single-layer 

280 t = t[:, np.newaxis] if len(t) > 1 else t 

281 

282 return multilayer_tmm(n=n, t=t, wavelength=wavelength, theta=theta) 

283 

284 

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*. 

293 

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. 

297 

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]. 

306 

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,). 

314 

315 **Important**: This parameter specifies ONE thickness value per layer 

316 in the multilayer stack. It does NOT perform thickness sweeps. 

317 

318 For example: single layer ``[250]``, two-layer stack ``[250, 150]``, 

319 three-layer stack ``[100, 200, 100]``. 

320 

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). 

329 

330 Returns 

331 ------- 

332 tuple 

333 (R, T) where: 

334 

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] 

339 

340 where **W** = number of wavelengths, **A** = number of angles 

341 (the **Spectroscopy Convention**). The thickness dimension is always 1 

342 for multilayer stacks. 

343 

344 Examples 

345 -------- 

346 Single layer: 

347 

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 

353 

354 Two-layer stack: 

355 

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) 

359 

360 Multiple wavelengths: 

361 

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) 

365 

366 Multiple angles (angle broadcasting): 

367 

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) 

371 

372 Notes 

373 ----- 

374 - The reflectance is calculated from (*Equation 15* from :cite:`Byrnes2016`): 

375 

376 .. math:: 

377 

378 r = \\frac{\\tilde{M}_{10}}{\\tilde{M}_{00}}, \\quad R = |r|^2 

379 

380 - The transmittance is calculated from (*Equations 14 and 21-22* from 

381 :cite:`Byrnes2016`): 

382 

383 .. math:: 

384 

385 t = \\frac{1}{\\tilde{M}_{00}} 

386 

387 .. math:: 

388 

389 T = |t|^2 \\frac{\\text{Re}[n_{\\text{substrate}} \ 

390\\cos \\theta_{\\text{final}}]}{\\text{Re}[n_{\\text{incident}} \\cos \\theta_i]} 

391 

392 Where: 

393 

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) 

397 

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). 

401 

402 References 

403 ---------- 

404 :cite:`Byrnes2016` 

405 """ 

406 

407 theta = np.atleast_1d(as_float_array(theta)) 

408 

409 result = matrix_transfer_tmm( 

410 n=n, 

411 t=np.atleast_1d(as_float_array(t)), 

412 theta=theta, 

413 wavelength=wavelength, 

414 ) 

415 

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] 

420 

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 

425 

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] 

429 

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) 

437 

438 # Broadcast to thickness dimension: (1, A, 1) 

439 transmittance_correction = transmittance_correction[None, :, :] 

440 

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) 

443 

444 # Stack results: (T, A, W, 2) 

445 R = tstack([r_s, r_p]) 

446 T = tstack([t_s, t_p]) 

447 

448 return R, T