Coverage for phenomena/interference.py: 70%

1"""

2 Thin Film Interference

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

4

5 Provides 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

12 References

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

19 from __future__ import annotations

20

21 from typing import TYPE_CHECKING

22

23 import numpy as np

24

25 from colour.phenomena.tmm import matrix_transfer_tmm

26 from colour.utilities import as_float_array, tstack

27

28 if 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

46 def 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

110 def 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

151 def 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

285 def 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