"""
*Transfer Matrix Method* (TMM) for Multilayer Optical Calculations
==================================================================
Implements the *Transfer Matrix Method* for computing optical properties of
multilayer planar stacks.
The *TMM* calculations handle two orthogonal polarisation states:
- **s-polarisation (Transverse Electric)**: The electric field vector
:math:`\\vec{E}_s` oscillates perpendicular to the plane of incidence.
This is sometimes called TE-mode because the electric field is
transverse to the plane of incidence.
- **p-polarisation (TM - Transverse Magnetic)**: The electric field vector
:math:`\\vec{E}_p` oscillates parallel to the plane of incidence.
This is sometimes called TM polarisation because the magnetic field is
transverse to the plane of incidence.
The plane of incidence is defined by the incident light ray and the surface
normal vector. For unpolarised light, the reflectance and transmittance are
the average of the s and p components: :math:`R = (R_s + R_p)/2`.
- :func:`colour.phenomena.snell_law`
- :func:`colour.phenomena.polarised_light_magnitude_elements`
- :func:`colour.phenomena.polarised_light_reflection_amplitude`
- :func:`colour.phenomena.polarised_light_reflection_coefficient`
- :func:`colour.phenomena.polarised_light_transmission_amplitude`
- :func:`colour.phenomena.polarised_light_transmission_coefficient`
- :class:`colour.phenomena.TransferMatrixResult_TMM`
- :func:`colour.phenomena.matrix_transfer_tmm`
References
----------
- :cite:`Byrnes2016` : Byrnes, S. J. (2016). Multilayer optical
calculations. arXiv:1603.02720 [Physics].
http://arxiv.org/abs/1603.02720
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
import numpy as np
from colour.constants import DTYPE_COMPLEX_DEFAULT
from colour.utilities import (
MixinDataclassArithmetic,
as_complex_array,
as_float_array,
tsplit,
tstack,
)
if TYPE_CHECKING:
from colour.hints import ArrayLike, NDArrayComplex, NDArrayFloat, Tuple
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"snell_law",
"polarised_light_magnitude_elements",
"polarised_light_reflection_amplitude",
"polarised_light_reflection_coefficient",
"polarised_light_transmission_amplitude",
"polarised_light_transmission_coefficient",
"TransferMatrixResult",
"matrix_transfer_tmm",
]
def _tsplit_complex(a: ArrayLike) -> NDArrayComplex:
"""
Split the specified stacked array along the last axis (tail)
to produce an array of complex arrays.
Convenience wrapper around :func:`colour.utilities.tsplit` that
automatically uses ``DTYPE_COMPLEX_DEFAULT`` for complex number
operations in *Transfer Matrix Method* calculations.
Parameters
----------
a
Stacked array to split.
Returns
-------
:class:`numpy.ndarray`
Array of complex arrays.
"""
return tsplit(a, dtype=DTYPE_COMPLEX_DEFAULT) # type: ignore[arg-type]
def _tstack_complex(a: ArrayLike) -> NDArrayComplex:
"""
Stack the specified array of arrays along the last axis (tail)
to produce a stacked complex array.
Convenience wrapper around :func:`colour.utilities.tstack` that
automatically uses ``DTYPE_COMPLEX_DEFAULT`` for complex number
operations in *Transfer Matrix Method* calculations.
Parameters
----------
a
Array of arrays to stack along the last axis.
Returns
-------
:class:`numpy.ndarray`
Stacked complex array.
References
----------
:cite:`Byrnes2016`
"""
return tstack(a, dtype=DTYPE_COMPLEX_DEFAULT) # type: ignore[arg-type]
[docs]
def snell_law(
n_1: ArrayLike,
n_2: ArrayLike,
theta_i: ArrayLike,
) -> NDArrayFloat:
"""
Compute the refraction angle using *Snell's Law*.
Parameters
----------
n_1
Refractive index of the incident medium :math:`n_1`.
n_2
Refractive index of the refracting medium :math:`n_2`.
theta_i
Incident angle :math:`\\theta_i` in degrees.
Returns
-------
:class:`numpy.ndarray`
Refracted angle in degrees.
Notes
-----
- *Snell's Law* relates the angles of incidence and refraction when light
passes through a boundary between two different media (*Equation 3*
from :cite:`Byrnes2016`):
.. math::
n_i \\sin \\theta_i = n_j \\sin \\theta_j
Where:
- :math:`n_i, n_j`: Refractive indices of the incident and refracting media
- :math:`\\theta_i, \\theta_j`: Angles of incidence and refraction
References
----------
:cite:`Byrnes2016`
Examples
--------
>>> snell_law(1.0, 1.5, 30.0) # doctest: +ELLIPSIS
np.float64(19.4712206...)
"""
n_1 = np.real(as_complex_array(n_1))
n_2 = np.real(as_complex_array(n_2))
theta_i = np.radians(as_float_array(theta_i))
# Apply Snell's law: n_i * sin(theta_i) = n_j * sin(theta_j) (Byrnes Eq. 3)
return np.degrees(np.arcsin(n_1 * np.sin(theta_i) / n_2))
[docs]
def polarised_light_magnitude_elements(
n_1: ArrayLike,
n_2: ArrayLike,
theta_i: ArrayLike,
theta_t: ArrayLike,
) -> Tuple[NDArrayComplex, NDArrayComplex, NDArrayComplex, NDArrayComplex]:
"""
Compute common magnitude elements for *Fresnel* equations.
This function computes the common terms used in the *Fresnel* equations
for both s-polarisation (perpendicular) and p-polarisation (parallel)
components of light at a dielectric interface.
Parameters
----------
n_1
Refractive index of the incident medium :math:`n_1`.
n_2
Refractive index of the transmitted medium :math:`n_2`.
theta_i
Incident angle :math:`\\theta_i` in degrees.
theta_t
Transmitted angle :math:`\\theta_t` in degrees.
Returns
-------
:class:`tuple`
Tuple of precomputed magnitude elements:
:math:`(n_1 \\cos \\theta_i, n_1 \\cos \\theta_t, n_2 \\cos \\theta_i,
n_2 \\cos \\theta_t)`
Notes
-----
These magnitude elements are fundamental components in the *Fresnel* equations:
- :math:`n_1 \\cos \\theta_i`: Incident medium magnitude
- :math:`n_1 \\cos \\theta_t`: Incident medium magnitude (transmitted angle)
- :math:`n_2 \\cos \\theta_i`: Transmitted medium magnitude (incident angle)
- :math:`n_2 \\cos \\theta_t`: Transmitted medium magnitude
These terms appear in all *Fresnel* amplitude and power coefficients for
both reflection and transmission at dielectric interfaces.
Examples
--------
>>> polarised_light_magnitude_elements(1.0, 1.5, 0.0, 0.0) # doctest: +ELLIPSIS
(np.complex128(1+0j), np.complex128(1+0j), np.complex128(1.5+0j), \
np.complex128(1.5+0j))
"""
n_1 = as_complex_array(n_1)
n_2 = as_complex_array(n_2)
cos_theta_i = np.cos(np.radians(as_float_array(theta_i)))
cos_theta_t = np.cos(np.radians(as_float_array(theta_t)))
n_1_cos_theta_i = n_1 * cos_theta_i
n_1_cos_theta_t = n_1 * cos_theta_t
n_2_cos_theta_i = n_2 * cos_theta_i
n_2_cos_theta_t = n_2 * cos_theta_t
return n_1_cos_theta_i, n_1_cos_theta_t, n_2_cos_theta_i, n_2_cos_theta_t
[docs]
def polarised_light_reflection_amplitude(
n_1: ArrayLike,
n_2: ArrayLike,
theta_i: ArrayLike,
theta_t: ArrayLike,
) -> NDArrayComplex:
"""
Compute *Fresnel* reflection amplitude coefficients.
This function computes the complex reflection amplitude coefficients for
both s-polarisation (perpendicular) and p-polarisation (parallel) components
of electromagnetic waves at a dielectric interface.
Parameters
----------
n_1
Refractive index of the incident medium :math:`n_1`.
n_2
Refractive index of the transmitted medium :math:`n_2`.
theta_i
Incident angle :math:`\\theta_i` in degrees.
theta_t
Transmitted angle :math:`\\theta_t` in degrees.
Returns
-------
:class:`numpy.ndarray`
*Fresnel* reflection amplitude coefficients for s and p polarisations
stacked along the last axis. The array contains :math:`[r_s, r_p]` where
:math:`r_s` and :math:`r_p` are the complex reflection coefficients.
Notes
-----
The *Fresnel* reflection amplitude coefficients are given by (*Equation 6*
from :cite:`Byrnes2016`):
.. math::
r_s &= \\frac{n_1 \\cos \\theta_1 - n_2 \\cos \\theta_2}{n_1 \\cos \
\\theta_1 + n_2 \\cos \\theta_2} \\\\
r_p &= \\frac{n_2 \\cos \\theta_1 - n_1 \\cos \\theta_2}{n_2 \\cos \
\\theta_1 + n_1 \\cos \\theta_2}
Where:
- :math:`r_s`: s-polarisation reflection amplitude (electric field perpendicular
to the plane of incidence)
- :math:`r_p`: p-polarisation reflection amplitude (electric field parallel
to the plane of incidence)
- :math:`n_1, n_2`: Refractive indices of incident and transmitted media
- :math:`\\theta_1, \\theta_2`: Incident and transmitted angles
Examples
--------
>>> polarised_light_reflection_amplitude(1.0, 1.5, 0.0, 0.0)
array([-0.2+0.j, -0.2+0.j])
"""
n_1_cos_theta_i, n_1_cos_theta_t, n_2_cos_theta_i, n_2_cos_theta_t = (
polarised_light_magnitude_elements(n_1, n_2, theta_i, theta_t)
)
# Fresnel reflection amplitudes (Byrnes Eq. 6)
r_s = (n_1_cos_theta_i - n_2_cos_theta_t) / (n_1_cos_theta_i + n_2_cos_theta_t)
r_p = (n_1_cos_theta_t - n_2_cos_theta_i) / (n_1_cos_theta_t + n_2_cos_theta_i)
return _tstack_complex([r_s, r_p])
[docs]
def polarised_light_reflection_coefficient(
n_1: ArrayLike,
n_2: ArrayLike,
theta_i: ArrayLike,
theta_t: ArrayLike,
) -> NDArrayComplex:
"""
Compute *Fresnel* reflection power coefficients (reflectance).
This function computes the reflection power coefficients, which represent
the fraction of incident power that is reflected at a dielectric interface
for both s-polarisation (perpendicular) and p-polarisation (parallel) components.
Parameters
----------
n_1
Refractive index of the incident medium :math:`n_1`.
n_2
Refractive index of the transmitted medium :math:`n_2`.
theta_i
Incident angle :math:`\\theta_i` in degrees.
theta_t
Transmitted angle :math:`\\theta_t` in degrees.
Returns
-------
:class:`numpy.ndarray`
*Fresnel* reflection power coefficients (reflectance) for s and p
polarisations stacked along the last axis. The array contains
:math:`[R_s, R_p]`.
Notes
-----
The *Fresnel* reflection power coefficients (reflectance) are given by:
.. math::
R_s &= |r_s|^2 = \\left|\\frac{n_1 \\cos \\theta_i - n_2 \\cos \
\\theta_t}{n_1 \\cos \\theta_i + n_2 \\cos \\theta_t}\\right|^2 \\\\
R_p &= |r_p|^2 = \\left|\\frac{n_1 \\cos \\theta_t - n_2 \\cos \
\\theta_i}{n_1 \\cos \\theta_t + n_2 \\cos \\theta_i}\\right|^2
Where:
- :math:`R_s`: s-polarisation reflectance (fraction of incident power reflected)
- :math:`R_p`: p-polarisation reflectance (fraction of incident power reflected)
- :math:`r_s, r_p`: complex reflection amplitude coefficients
- The s-polarisation electric field is perpendicular to the plane of incidence
- The p-polarisation electric field is parallel to the plane of incidence
The reflectance values satisfy: :math:`0 \\leq R_s, R_p \\leq 1`.
References
----------
:cite:`Byrnes2016`
Examples
--------
>>> result = polarised_light_reflection_coefficient(1.0, 1.5, 0.0, 0.0)
>>> result.real
array([0.04, 0.04])
"""
# Reflectance: R = |r|^2 (Byrnes Eq. 23)
R = np.abs(polarised_light_reflection_amplitude(n_1, n_2, theta_i, theta_t)) ** 2
return as_complex_array(R)
[docs]
def polarised_light_transmission_amplitude(
n_1: ArrayLike,
n_2: ArrayLike,
theta_i: ArrayLike,
theta_t: ArrayLike,
) -> NDArrayComplex:
"""
Compute *Fresnel* transmission amplitude coefficients.
This function computes the complex transmission amplitude coefficients for
both s-polarisation (perpendicular) and p-polarisation (parallel) components
of electromagnetic waves at a dielectric interface.
Parameters
----------
n_1
Refractive index of the incident medium :math:`n_1`.
n_2
Refractive index of the transmitted medium :math:`n_2`.
theta_i
Incident angle :math:`\\theta_i` in degrees.
theta_t
Transmitted angle :math:`\\theta_t` in degrees.
Returns
-------
:class:`numpy.ndarray`
*Fresnel* transmission amplitude coefficients for s and p polarisations
stacked along the last axis. The array contains :math:`[t_s, t_p]` where
:math:`t_s` and :math:`t_p` are the complex transmission coefficients.
Notes
-----
The *Fresnel* transmission amplitude coefficients are given by (*Equation 6*
from :cite:`Byrnes2016`):
.. math::
t_s &= \\frac{2n_1 \\cos \\theta_1}{n_1 \\cos \\theta_1 + n_2 \\cos \
\\theta_2} \\\\
t_p &= \\frac{2n_1 \\cos \\theta_1}{n_2 \\cos \\theta_1 + n_1 \\cos \
\\theta_2}
Where:
- :math:`t_s`: s-polarisation transmission amplitude (electric field perpendicular
to the plane of incidence)
- :math:`t_p`: p-polarisation transmission amplitude (electric field parallel
to the plane of incidence)
- :math:`n_1, n_2`: Refractive indices of incident and transmitted media
- :math:`\\theta_1, \\theta_2`: Incident and transmitted angles
Examples
--------
>>> polarised_light_transmission_amplitude(1.0, 1.5, 0.0, 0.0)
array([0.8+0.j, 0.8+0.j])
"""
n_1_cos_theta_i, n_1_cos_theta_t, n_2_cos_theta_i, n_2_cos_theta_t = (
polarised_light_magnitude_elements(n_1, n_2, theta_i, theta_t)
)
two_n_1_cos_theta_i = 2 * n_1_cos_theta_i
# Fresnel transmission amplitudes (Byrnes Eq. 6)
t_s = two_n_1_cos_theta_i / (n_1_cos_theta_i + n_2_cos_theta_t)
t_p = two_n_1_cos_theta_i / (n_2_cos_theta_i + n_1_cos_theta_t)
return _tstack_complex([t_s, t_p])
[docs]
def polarised_light_transmission_coefficient(
n_1: ArrayLike,
n_2: ArrayLike,
theta_i: ArrayLike,
theta_t: ArrayLike,
) -> NDArrayComplex:
"""
Compute *Fresnel* transmission power coefficients (transmittance).
This function computes the transmission power coefficients, which represent
the fraction of incident power that is transmitted through a dielectric interface
for both s-polarisation (perpendicular) and p-polarisation (parallel) components.
Parameters
----------
n_1
Refractive index of the incident medium :math:`n_1`.
n_2
Refractive index of the transmitted medium :math:`n_2`.
theta_i
Incident angle :math:`\\theta_i` in degrees.
theta_t
Transmitted angle :math:`\\theta_t` in degrees.
Returns
-------
:class:`numpy.ndarray`
*Fresnel* transmission power coefficients (transmittance) for s and p
polarisations stacked along the last axis. The array contains
:math:`[T_s, T_p]`.
Notes
-----
The *Fresnel* transmission power coefficients (transmittance) are given by:
.. math::
T_s &= \\frac{n_2 \\cos \\theta_t}{n_1 \\cos \\theta_i} |t_s|^2 = \
\\frac{n_2 \\cos \\theta_t}{n_1 \\cos \\theta_i} \\left|\\frac{2n_1 \\cos \
\\theta_i}{n_1 \\cos \\theta_i + n_2 \\cos \\theta_t}\\right|^2 \\\\
T_p &= \\frac{n_2 \\cos \\theta_t}{n_1 \\cos \\theta_i} |t_p|^2 = \
\\frac{n_2 \\cos \\theta_t}{n_1 \\cos \\theta_i} \\left|\\frac{2n_1 \\cos \
\\theta_i}{n_2 \\cos \\theta_i + n_1 \\cos \\theta_t}\\right|^2
Where:
- :math:`T_s`: s-polarisation transmittance (fraction of incident power
transmitted)
- :math:`T_p`: p-polarisation transmittance (fraction of incident power
transmitted)
- :math:`t_s, t_p`: complex transmission amplitude coefficients
- The s-polarisation electric field is perpendicular to the plane of incidence
- The p-polarisation electric field is parallel to the plane of incidence
The refractive index factor
:math:`\\frac{n_2 \\cos \\theta_t}{n_1 \\cos \\theta_i}` accounts for the
change in beam cross-section and energy density in the transmission medium.
**Energy Conservation**: For non-absorbing media:
:math:`R_s + T_s = 1` and :math:`R_p + T_p = 1`, where :math:`R_s, R_p` are the
corresponding reflectance coefficients.
The transmittance values satisfy: :math:`0 \\leq T_s, T_p \\leq 1`.
References
----------
:cite:`Byrnes2016`
Examples
--------
>>> polarised_light_transmission_coefficient(1.0, 1.5, 0.0, 0.0)
array([0.96+0.j, 0.96+0.j])
"""
n_1 = as_complex_array(n_1)
n_2 = as_complex_array(n_2)
n_1_cos_theta_i, _n_1_cos_theta_t, _n_2_cos_theta_i, n_2_cos_theta_t = (
polarised_light_magnitude_elements(n_1, n_2, theta_i, theta_t)
)
# Transmittance with beam cross-section correction (Byrnes Eq. 21-22)
T = (n_2_cos_theta_t / n_1_cos_theta_i)[..., None] * np.abs(
polarised_light_transmission_amplitude(n_1, n_2, theta_i, theta_t)
) ** 2
return as_complex_array(T)
[docs]
@dataclass
class TransferMatrixResult(MixinDataclassArithmetic):
"""
Define the *Transfer Matrix Method* calculation results.
Parameters
----------
M_s
Transfer matrix for s-polarisation :math:`M_s`, shape
(..., wavelengths_count, 2, 2).
M_p
Transfer matrix for p-polarisation :math:`M_p`, shape
(..., wavelengths_count, 2, 2).
theta
Propagation angles in each layer :math:`\\theta_j` (degrees), shape
(..., n_layers+2). Includes [incident, layer_1, ..., layer_n, substrate].
n
Complete multilayer stack :math:`n_j`, shape
(..., n_layers+2, wavelengths_count). Includes
[n_incident, n_layer_1, ..., n_layer_n, n_substrate].
References
----------
:cite:`Byrnes2016`
"""
M_s: NDArrayComplex
M_p: NDArrayComplex
theta: NDArrayFloat
n: NDArrayComplex
[docs]
def matrix_transfer_tmm(
n: ArrayLike,
t: ArrayLike,
theta: ArrayLike,
wavelength: ArrayLike,
) -> TransferMatrixResult:
"""
Calculate transfer matrices for multilayer thin film structures using the
*Transfer Matrix Method*.
This function constructs the transfer matrices for s-polarised and
p-polarised light propagating through a multilayer structure. The transfer
matrices encode the optical properties of the structure and are used to
calculate reflectance and transmittance.
Parameters
----------
n
Complete refractive index stack :math:`n_j` including incident medium,
layers, and substrate. Shape: (media_count,) or
(media_count, wavelengths_count). Can be complex for absorbing
materials. The array should contain [n_incident, n_layer_1, ...,
n_layer_n, n_substrate].
t
Thicknesses of each layer :math:`t_j` in nanometers (excluding incident
and substrate). Shape: (layers_count,) or (thickness_count, layers_count).
- **1D array** ``[t1, t2, ...]``: One thickness per layer for a single
multilayer configuration. Shape: ``(layers_count,)``
- **2D array** ``[[t1, t2, ...], [t1', t2', ...]]``: Multiple thickness
configurations for outer product broadcasting. Shape:
``(thickness_count, layers_count)``
Most users should use :func:`thin_film_tmm` or :func:`multilayer_tmm`
instead, which provide simpler interfaces.
theta
Incident angle :math:`\\theta` in degrees. Scalar or array of shape
(angles_count,) for angle broadcasting.
wavelength
Vacuum wavelength values :math:`\\lambda` in nanometers.
Returns
-------
:class:`colour.TransferMatrixResult`
Transfer matrix calculation results containing M_s, M_p, theta,
and n arrays.
Examples
--------
Single layer at one wavelength:
>>> result = matrix_transfer_tmm(
... n=[1.0, 1.5, 1.0],
... t=[250],
... theta=0,
... wavelength=550,
... )
>>> result.M_s.shape
(1, 1, 1, 2, 2)
>>> result.theta.shape
(1, 3)
Multiple wavelengths:
>>> result = matrix_transfer_tmm(
... n=[1.0, 1.5, 1.0],
... t=[250],
... theta=0,
... wavelength=[400, 500, 600],
... )
>>> result.M_s.shape
(3, 1, 1, 2, 2)
Multiple angles (angle broadcasting):
>>> result = matrix_transfer_tmm(
... n=[1.0, 1.5, 1.0],
... t=[250],
... theta=[0, 30, 45, 60],
... wavelength=[400, 500, 600],
... )
>>> result.M_s.shape
(3, 4, 1, 2, 2)
>>> result.theta.shape
(4, 3)
Notes
-----
- The *Transfer Matrix Method* relates the field amplitudes across the entire
multilayer structure (*Equations 10-15* from :cite:`Byrnes2016`):
.. math::
\\begin{pmatrix} v_n \\\\ w_n \\end{pmatrix} = M_n \\begin{pmatrix} \
v_{n+1} \\\\ w_{n+1} \\end{pmatrix}
Where :math:`M_n` combines the layer propagation and interface matrices:
.. math::
M_n = L_n \\cdot I_{n,n+1} =
\\begin{pmatrix}
e^{-i\\delta_n} & 0 \\\\ 0 & e^{i\\delta_n}
\\end{pmatrix}
\\frac{1}{t_{n,n+1}}
\\begin{pmatrix}
1 & r_{n,n+1} \\\\ r_{n,n+1} & 1
\\end{pmatrix}
The overall transfer matrix :math:`\\tilde{M}` for the complete structure is:
.. math::
\\tilde{M} = \\frac{1}{t_{0,1}}
\\begin{pmatrix}
1 & r_{0,1} \\\\ r_{0,1} & 1
\\end{pmatrix}
M_1 M_2 \\cdots M_{N-2}
From which the overall reflection and transmission coefficients are extracted:
.. math::
\\begin{pmatrix} 1 \\\\ r \\end{pmatrix} = \\tilde{M} \
\\begin{pmatrix} t \\\\ 0 \\end{pmatrix}
.. math::
t = \\frac{1}{\\tilde{M}_{00}}, \\quad r = \
\\frac{\\tilde{M}_{10}}{\\tilde{M}_{00}}
Where:
- :math:`v_n, w_n`: Forward and backward field amplitudes in layer :math:`n`
- :math:`M_n`: Transfer matrix for layer :math:`n`
- :math:`L_n`: Layer propagation matrix
- :math:`I_{n,n+1}`: Interface matrix between layers :math:`n` and :math:`n+1`
- :math:`\\tilde{M}`: Overall transfer matrix
- :math:`r, t`: Overall reflection and transmission amplitude coefficients
- Supports complex refractive indices for absorbing materials.
- **Angle broadcasting**: All computations are vectorized across angles.
The output always includes the angle dimension.
- The transfer matrices always have shape (angles_count, wavelengths_count, 2, 2),
even for scalar theta (angles_count=1).
References
----------
:cite:`Byrnes2016`
"""
n = as_complex_array(n)
t = as_float_array(t)
theta = np.atleast_1d(as_float_array(theta))
wavelength = np.atleast_1d(as_float_array(wavelength))
wavelengths_count = wavelength.shape[0]
# Convert 1D n to column vector and tile across wavelengths
# (M,) -> (M, 1) -> (M, W)
if n.ndim == 1:
n = np.transpose(np.atleast_2d(n))
n = np.tile(n, (1, wavelengths_count))
# (1, layers_count)
if t.ndim == 1:
t = t[np.newaxis, :]
media_count = n.shape[0]
layers_count = media_count - 2
n_0 = n[0, 0] if n.ndim == 2 else n[0]
# Snell's law: n_i * sin(theta_i) = n_j * sin(theta_j) (Byrnes Eq. 3)
# Broadcasting: theta (A,) → theta_media (A, M)
theta_media = snell_law(
n_0, (n[:, 0] if n.ndim == 2 else n)[:, None], theta[None, :]
).T
# Fresnel coefficients (Byrnes Eq. 6)
# Broadcasting: n (M, W), theta_media (A, M) → coefficients (A, M-1, W)
n_1 = n[:-1, :] # (M-1, W)
n_2 = n[1:, :] # (M-1, W)
theta_1 = theta_media[:, :-1] # (A, M-1)
theta_2 = theta_media[:, 1:] # (A, M-1)
r_media_s, r_media_p = _tsplit_complex(
polarised_light_reflection_amplitude(
n_1[None, :, :], # (1, M-1, W)
n_2[None, :, :], # (1, M-1, W)
theta_1[:, :, None], # (A, M-1, 1)
theta_2[:, :, None], # (A, M-1, 1)
)
) # Output: (A, M-1, W)
t_media_s, t_media_p = _tsplit_complex(
polarised_light_transmission_amplitude(
n_1[None, :, :], # (1, M-1, W)
n_2[None, :, :], # (1, M-1, W)
theta_1[:, :, None], # (A, M-1, 1)
theta_2[:, :, None], # (A, M-1, 1)
)
) # Output: (A, M-1, W)
# Phase accumulation: delta = d * k_z (Byrnes Eq. 8)
# Broadcasting directly in (W, A, T, L) order
n_previous = n[0:layers_count, :] # (L, W) - Media before each layer
n_layer = n[1 : layers_count + 1, :] # (L, W) - Each layer's refractive index
theta_layer = theta_media[:, 0:layers_count] # (A, L)
theta_radians = np.radians(theta_layer)[:, :, None] # (A, L, 1)
k_z_layers = np.sqrt(
n_layer[None, :, :] ** 2
- n_previous[None, :, :] ** 2 * np.sin(theta_radians) ** 2
) # (A, L, W)
# Compute phase: delta = (2π/λ) * d * k_z
phase_factor = 2 * np.pi / wavelength[:, None, None, None] # (W, 1, 1, 1)
# Reshape k_z from (A, L, W) to (W, A, 1, L) for broadcasting with thickness
k_z = np.transpose(k_z_layers, (2, 0, 1))[:, :, None, :] # (W, A, 1, L)
delta = phase_factor * t[None, None, :, :] * k_z # (W, A, T, L)
A = np.exp(1j * delta) # (W, A, T, L)
# Layer matrices: M_n = L_n * I_{n,n+1} (Byrnes Eq. 10-11)
# (W, A, T, L, 2, 2, 2) for [wavelengths, angles, thickness, layers, 2x2, pol]
r_s = r_media_s[:, 1 : layers_count + 1, :] # (A, L, W)
r_p = r_media_p[:, 1 : layers_count + 1, :] # (A, L, W)
t_s = t_media_s[:, 1 : layers_count + 1, :] # (A, L, W)
t_p = t_media_p[:, 1 : layers_count + 1, :] # (A, L, W)
# Broadcast Fresnel coefficients from (A, L, W) to (W, A, 1, L)
# (A,L,W) -> (W,A,L) -> (W,A,1,L)
r_s_b = np.transpose(r_s, (2, 0, 1))[:, :, None, :]
r_p_b = np.transpose(r_p, (2, 0, 1))[:, :, None, :]
t_s_b = np.transpose(t_s, (2, 0, 1))[:, :, None, :]
t_p_b = np.transpose(t_p, (2, 0, 1))[:, :, None, :]
# Build 2x2 matrices for s and p polarizations using np.stack
M_s_layer = np.stack(
[
np.stack([1 / (A * t_s_b), r_s_b / (A * t_s_b)], axis=-1),
np.stack([A * r_s_b / t_s_b, A / t_s_b], axis=-1),
],
axis=-2,
)
M_p_layer = np.stack(
[
np.stack([1 / (A * t_p_b), r_p_b / (A * t_p_b)], axis=-1),
np.stack([A * r_p_b / t_p_b, A / t_p_b], axis=-1),
],
axis=-2,
)
M = np.stack([M_s_layer, M_p_layer], axis=-1)
# Initial interface matrix (Byrnes Eq. 11)
# Shape: (W, A, T, 2, 2)
# Fresnel coefficients at incident → first layer interface
t_s_01 = t_media_s[:, 0, :] # (A, W)
r_s_01 = r_media_s[:, 0, :] # (A, W)
inv_t_s = (1 / t_s_01).T[:, :, None] # (W, A, 1)
r_over_t_s = (r_s_01 / t_s_01).T[:, :, None]
M_s = np.stack(
[
np.stack([inv_t_s, r_over_t_s], axis=-1),
np.stack([r_over_t_s, inv_t_s], axis=-1),
],
axis=-2,
)
t_p_01 = t_media_p[:, 0, :] # (A, W)
r_p_01 = r_media_p[:, 0, :] # (A, W)
inv_t_p = (1 / t_p_01).T[:, :, None]
r_over_t_p = (r_p_01 / t_p_01).T[:, :, None]
M_p = np.stack(
[
np.stack([inv_t_p, r_over_t_p], axis=-1),
np.stack([r_over_t_p, inv_t_p], axis=-1),
],
axis=-2,
)
# Overall transfer matrix: M_tilde = I_01 @ M_1 @ M_2 @ ... (Byrnes Eq. 12)
for i in range(layers_count):
M_s = np.matmul(M_s, M[:, :, :, i, :, :, 0])
M_p = np.matmul(M_p, M[:, :, :, i, :, :, 1])
return TransferMatrixResult(
M_s=M_s,
M_p=M_p,
theta=theta_media,
n=n,
)