Skip to content

API Reference

This page contains autodocumented classes and functions from the PyEtalon package.

Etalon class

Etalon

Etalon(materials: list[Material], material_names: list[str] | None, d_stack: list[float] | ndarray, idx_stack: list[int], wavelength_min: float | None = None, wavelength_max: float | None = None, wavelength: list[float] | ndarray = None, d_spacer: float = 0.01, aoi: float = 0.0, identifier: str = 'Etalon')

Class for modeling an etalon with a multilayer thin film stack.

Attributes:

  • materials

    List of materials used in the stack

  • names

    List of names of the materials

  • d_stack_design

    List of layer thicknesses in [nm]

  • d_corrections

    List of layer thickness corrections in [nm] - default is an array of zeros (same length as d_stack)

  • num_layers

    Number of layers in the stack

  • identifier

    Identifier for the etalon

  • wavelength_min

    Minimum wavelength in [nm] (if not given, it is calculated from the wavelength vector)

  • wavelength_max

    Maximum wavelength in [nm] (if not given, it is calculated from the wavelength vector)

  • wavelength

    Wavelength vector in [nm]

  • normalized_wavelength

    Normalized wavelength vector

  • aoi

    Angle of incidence in [rad]

  • _d_spacer

    Spacer thickness in [m] (incl. corrections if given)

  • d_spacer_correction

    Spacer thickness correction in [nm] - default is 0.0

  • idx_stack

    List of indices of the materials in the stack

  • _m

    Peak order number

Parameters:

  • materials

    (list[Material]) –

    List of materials used in the stack

  • material_names

    (list[str] | None) –

    List of names of the materials

  • d_stack

    (list[float] | ndarray) –

    List of layer thicknesses in [nm]

  • idx_stack

    (list[int]) –

    List of indices of the materials in the stack

  • wavelength_min

    (float | None, default: None ) –

    Minimum wavelength in [nm] (if not given, it is calculated from the wavelength vector)

  • wavelength_max

    (float | None, default: None ) –

    Maximum wavelength in [nm] (if not given, it is calculated from the wavelength vector)

  • wavelength

    (list[float] | ndarray, default: None ) –

    Wavelength vector in [nm]

  • d_spacer

    (float, default: 0.01 ) –

    Spacer thickness in [m] (incl. corrections if given)

  • aoi

    (float, default: 0.0 ) –

    Angle of incidence in [deg]

  • identifier

    (str, default: 'Etalon' ) –

    Identifier for the etalon

Methods:

Source code in PyEtalon/etalon.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def __init__(
    self,
    materials: list[Material],
    material_names: list[str] | None,
    d_stack: list[float] | np.ndarray,
    idx_stack: list[int],
    wavelength_min: float | None = None,
    wavelength_max: float | None = None,
    wavelength: list[float] | np.ndarray = None,
    d_spacer: float = 10e-3,
    aoi: float = 0.0,
    identifier: str = "Etalon",
):
    """Constructor for the Etalon class

    Args:
        materials: List of materials used in the stack
        material_names: List of names of the materials
        d_stack: List of layer thicknesses in [nm]
        idx_stack: List of indices of the materials in the stack
        wavelength_min: Minimum wavelength in [nm] (if not given, it is calculated from the wavelength vector)
        wavelength_max: Maximum wavelength in [nm] (if not given, it is calculated from the wavelength vector)
        wavelength: Wavelength vector in [nm]
        d_spacer: Spacer thickness in [m] (incl. corrections if given)
        aoi: Angle of incidence in [deg]
        identifier: Identifier for the etalon
    """
    if material_names is None:
        material_names = [m.name for m in materials]
    assert len(materials) == len(material_names), (
        "Materials and Material_names need to be the same length."
    )
    assert len(d_stack) == len(idx_stack), (
        f"Length of d_stack ({len(d_stack)}) and idx_stack ({len(idx_stack)}) is not equal."
    )

    assert wavelength is not None or (
        wavelength_min is not None and wavelength_max is not None
    ), (
        "Either specify the wavelength vector for calculations explicitly or specify minimum and maximum wavelength"
    )

    self.materials = materials
    self.names = material_names
    self.d_stack_design = np.array(d_stack)
    self.d_corrections = np.zeros_like(
        d_stack[1:-1]
    )  # exclude the spacer and ambient material
    self.num_layers = len(d_stack) - 2  # exclude the spacer and ambient material
    self.identifier = identifier
    if wavelength is not None:
        self.wavelength = wavelength
        self.wavelength_min = np.min(wavelength)
        self.wavelength_max = np.max(wavelength)
        self.normalized_wavelength = np.linspace(0, 1, len(wavelength))

    else:
        self.wavelength_min = wavelength_min
        self.wavelength_max = wavelength_max
        self.wavelength = np.linspace(wavelength_min, wavelength_max, 5000)
        self.normalized_wavelength = np.linspace(0, 1, len(self.wavelength))

    self.aoi = aoi
    self._d_spacer = d_spacer
    self.d_spacer_correction = 0.0

    self.idx_stack = np.array(idx_stack, dtype=int)

    self.material_coefficients = []
    for i, m in enumerate(materials[1:-1]):
        # check if material has attribute 'coefficients' - could be TabulatedIndexData
        if hasattr(m.n, "coefficients"):
            self.material_coefficients.append(m.n.coefficients)
        if m.k is not None:
            if hasattr(m.k, "coefficients"):
                self.material_coefficients.append(m.k.coefficients)

    # Convert given wavelength to peak order number
    # self._m = guess_m(self.wavelength, self.d_spacer, self.aoi, self.n(0), self.phase_spline)
    self._m = np.arange(
        int(
            (
                2.0
                * self.d_spacer
                * self.get_refractive_index(0, self.wavelength_max)
            )
            / (self.wavelength_max * 1e-9)
        ),
        int(
            (
                2.0
                * self.d_spacer
                * self.get_refractive_index(0, self.wavelength_min)
            )
            / (self.wavelength_min * 1e-9)
        ),
    )[::-1]

coefficient_of_finesse property

coefficient_of_finesse

Returns the coefficient of finesse of the mirror coating stack

The coefficient of finesse is given by the formula:

F = 4 * R / (1 - R)^2

where: R: mirror reflectivity

d_spacer property

d_spacer: float

Distance of spacer in [m] (incl. corrections if given)

d_stack property

d_stack

Returns the stack thickness including any corrections given (if any)

d_stack_normalized property

d_stack_normalized

Returns the stack thickness normalized to the maximum thickness

fsr property

fsr

Return the real free spectral range in [GHz]

The free spectral range is given by the formula:

FSR [GHz] = c / (2 * n * d * cos(theta) - lambda^2 * 10^-9 / pi * dphi/dlambda)

see https://doi.org/10.1103/PhysRevA.37.1802

where: c: speed of light [m/s] n: refractive index of cavity material d: spacer thickness [m] theta: angle of incidence [rad] lambda: peak wavelength [nm] dphi/dlambda: derivative of phase with respect to wavelength [rad/nm]

The factor 10^-9 is used to convert the wavelength from nm to m. There is a factor of 10^-18 from lambda^2, but also a factor of 10^9 from the derivative of the phase. The factor of 10^-9 cancels out.

fsr_wavelength property

fsr_wavelength

Return the free spectral range in [nm]

ChatGPT gives me this as approximation (phase shift is a small correction to optical path)

FSR_lambda = lambda^2 / (2nL cos(theta)) (1+ (lambda^2/(2pi n L cos(theta)) dPhi/dlambda))

gd property

gd

Returns the group delay upon reflection in [fs]

gd_spline property

gd_spline

Returns the group delay upon reflection in [fs] as a spline

gdd property

gdd

Returns the group delay dispersion upon reflection in [fs^2]

gdd_spline property

gdd_spline

Returns the group delay dispersion upon reflection in [fs^2] as a spline

ideal_fsr property

ideal_fsr: float

Return the ideal free spectral range in [GHz]

The ideal free spectral range is given by the formula:

FSR [GHz] = c / (2 * n * d * cos(theta))

where:
c: speed of light [m/s]
n: refractive index of cavity material
d: spacer thickness [m]
theta: angle of incidence [rad]

It neglects the phase shift upon reflection.

mirror_reflectivity property

mirror_reflectivity

Returns the reflectivity of the mirror coating stack

mirror_transmission property

mirror_transmission

Returns the transmission of the mirror coating stack

n_parameter cached property

n_parameter

Total number of parameters

Number of layers + 1 for the spacer + number of coefficients for each coating material

num_material_coefficients cached property

num_material_coefficients

Returns the total number of coefficients for all coating materials

The substrate and ambient material are not included.

phase property

phase

Returns the unwrapped phase shift upon reflection in [rad] as a function of wavelength

phase_spline property

phase_spline

Returns the phase as a spline

reflectivity_finesse property

reflectivity_finesse

Returns the reflectivity finesse of the mirror coating stack

The reflectivity finesse is given by the formula:

F = pi / (2 * arcsin(1 / sqrt(F)))

where: F: coefficient of finesse

rt property

rt

Returns the reflection and transmission coefficients of the mirror coating stack

calculate_reflectivity_transmissivity

calculate_reflectivity_transmissivity() -> tuple[ndarray, ndarray]

Calculate reflectivity and transmissivity from reflection and transmission coefficients.

Returns:

  • tuple[ndarray, ndarray]

    tuple[np.ndarray, np.ndarray]:ay of float64 values. - Transmissivity (T): 1D array of float64 values.

Source code in PyEtalon/etalon.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
def calculate_reflectivity_transmissivity(self) -> tuple[np.ndarray, np.ndarray]:
    """Calculate reflectivity and transmissivity from reflection and transmission coefficients.

    Returns:
        tuple[np.ndarray, np.ndarray]:ay of float64 values.
            - Transmissivity (T): 1D array of float64 values.
    """
    # Convert angle of incidence to radians
    aoi_rad = np.radians(self.aoi)

    # Extract refractive indices at the boundaries
    n_i = self.nk_stack[:, 0]  # Incident medium
    n_t = self.nk_stack[:, -1]  # Transmitted medium

    # Calculate transmitted angle using Snell's law: n_i * sin(theta_i) = n_t * sin(theta_t)
    sin_theta_t = np.real(n_i) * np.sin(aoi_rad) / np.real(n_t)
    cos_theta_t = np.sqrt(
        1 - sin_theta_t**2
    )  # cos(theta_t), assuming real refractive indices
    cos_theta_i = np.cos(aoi_rad)  # cos(theta_i)
    r, t = self.rt
    # Reflectivity: R = |r|^2
    R = np.abs(r) ** 2

    # Transmissivity: T = |t|^2 * Re(n_t cos(theta_t)) / Re(n_i cos(theta_i))
    T = (
        (np.abs(t) ** 2)
        * (np.real(n_t) * cos_theta_t)
        / (np.real(n_i) * cos_theta_i)
    )

    return R, T

get_refractive_index_cavity

get_refractive_index_cavity(wl)

Returns the refractive index of the cavity material

Source code in PyEtalon/etalon.py
277
278
279
def get_refractive_index_cavity(self, wl):
    """Returns the refractive index of the cavity material"""
    return self.get_refractive_index(0, wl)

print_parameters

print_parameters()

print parameters names and parameters and index in a table

Source code in PyEtalon/etalon.py
366
367
368
369
370
371
372
373
def print_parameters(self):
    """print parameters names and parameters and index in a table"""
    print(f"{'Index':<5} {'Parameter':<40} {'Value':<20}")
    print("-" * 65)
    for i, (name, par) in enumerate(
        zip(self.all_parameter_names, self.all_parameters)
    ):
        print(f"{i:<5} {name:<40} {par:<20}")

print_parameters_horizontally

print_parameters_horizontally(values_only=False)

print index, parameter names and values in a table. First row contains index, second row contains name, third row contains all values. Fix the column width to 20 characters

Source code in PyEtalon/etalon.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
def print_parameters_horizontally(self, values_only=False):
    """print index, parameter names and values in a table.
    First row contains index, second row contains name, third row contains all values.
    Fix the column width to 20 characters"""
    if not values_only:
        print(f"{'Index':<10}", end="")
        for i in range(self.n_parameter):
            print(f"{i:<6}", end="")
        print()
        print("-" * 65)
        print(f"{'Parameter':<10}", end="")
        for name in self.all_parameter_names:
            print(f"{name:<7}", end="")
        print()
        print("-" * 65)
    print(f"{'Value':<10}", end="")
    for par in self.all_parameters:
        print(f"{par:7.3f}", end="")
    print()

transmission_spectrum

transmission_spectrum(wavelength)

Returns the transmission spectrum of the entire etalon for the given wavelength.

The transmission spectrum is calculated as:

T = 1 / (1 + F * sin^2(pi * d / lambda + phi))

where: T: transmission spectrum F: coefficient of finesse d: spacer thickness lambda: wavelength phi: phase shift upon reflection

Parameters:

  • wavelength

    Wavelength in [nm]

Source code in PyEtalon/etalon.py
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
def transmission_spectrum(self, wavelength):
    """Returns the transmission spectrum of the entire etalon for the given wavelength.

    The transmission spectrum is calculated as:

    T = 1 / (1 + F * sin^2(pi * d / lambda + phi))

    where:
    T: transmission spectrum
    F: coefficient of finesse
    d: spacer thickness
    lambda: wavelength
    phi: phase shift upon reflection

    Args:
        wavelength: Wavelength in [nm]

    """
    F_spline = UnivariateSpline(
        self.wavelength, self.coefficient_of_finesse, k=3, s=0
    )

    return 1.0 / (
        1.0
        + F_spline(wavelength)
        * np.sin(np.pi * self.d_spacer * 1e9 / wavelength) ** 2
    )

Module-level utilities

guess_m

guess_m(wavelength: float | ndarray, d: float = 0.00999, aoi: float = 0.0, n: float | Callable = 1.0, phase: Callable | None = None) -> tuple[int | ndarray, ndarray]

Guesses the peak number m for a given peak wavelength.

Parameters:

  • wavelength

    (float | ndarray) –

    peak wavelength in [nm]

  • d

    (float, default: 0.00999 ) –

    spacer thickness in [m]

  • aoi

    (float, default: 0.0 ) –

    angle of incidence in [rad]

  • n

    (float | Callable, default: 1.0 ) –

    refractive index of cavity material

  • phase

    (Callable | None, default: None ) –

    phase shift upon reflection in [rad]

Returns:

  • tuple[int | ndarray, ndarray]

    guessed peak numbers, difference between integer and float peak numbers

Source code in PyEtalon/etalon.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def guess_m(
    wavelength: float | np.ndarray,
    d: float = 9.99e-3,
    aoi: float = 0.0,
    n: float | Callable = 1.0,
    phase: Callable | None = None,
) -> tuple[int | np.ndarray, np.ndarray]:
    """
    Guesses the peak number m for a given peak wavelength.

    Args:
        wavelength: peak wavelength in [nm]
        d: spacer thickness in [m]
        aoi: angle of incidence in [rad]
        n: refractive index of cavity material
        phase: phase shift upon reflection in [rad]

    Returns:
        guessed peak numbers, difference between integer and float peak numbers
    """
    if phase is None:
        if isinstance(n, float):
            float_m = 2.0 * n * d * np.cos(aoi) / (wavelength * 1e-9)
        elif callable(n):
            float_m = 2.0 * n(wavelength) * d * np.cos(aoi) / (wavelength * 1e-9)
        else:
            raise ValueError("n needs to be a float or a callable function")
        int_m = np.array(np.rint(float_m), dtype=int)
    else:
        if isinstance(n, float):
            float_m = (
                2.0 * d * n * np.cos(aoi)
                + phase(wavelength) * (wavelength * 1e-9) / np.pi
            ) / (wavelength * 1e-9)
        elif callable(n):
            float_m = (
                2.0 * d * n(wavelength.copy()) * np.cos(aoi)
                + phase(wavelength.copy()) * (wavelength * 1e-9) / np.pi
            ) / (wavelength * 1e-9)
        elif isinstance(n, np.ndarray):
            float_m = (
                2.0 * d * n * np.cos(aoi)
                + phase(wavelength.copy()) * (wavelength * 1e-9) / np.pi
            ) / (wavelength * 1e-9)
        else:
            raise ValueError(
                "n needs to be a float, an array of floats or a callable function."
            )
        int_m = np.array(np.rint(float_m), dtype=int)
    return int_m, float_m - int_m

lambda_peaks

lambda_peaks(m: int | ndarray, d: float = 0.00999, n: float | Callable = 1.0, aoi: float = 0.0, phase: None | Callable = None) -> float | ndarray

Returns peak wavelength for given etalon parameters

It uses the dispersion formula as given in https://doi.org/10.1364/AO.30.004126 Args: m: Diffraction order (int) or 1D array with 'int' type. d: physical mirror distance [m] n: Refractive index of medium in between mirrors aoi: Angle of incidence [rad] phase: Interpolation function f = phase_shift(wavelength[nm])

Returns:

  • float | ndarray

    peak wavelength in [nm]

Source code in PyEtalon/etalon.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def lambda_peaks(
    m: int | np.ndarray,
    d: float = 9.99e-3,
    n: float | Callable = 1.0,
    aoi: float = 0.0,
    phase: None | Callable = None,
) -> float | np.ndarray:
    """
    Returns peak wavelength for given etalon parameters

    It uses the dispersion formula as given in https://doi.org/10.1364/AO.30.004126
    Args:
        m: Diffraction order (int) or 1D array with 'int' type.
        d: physical mirror distance [m]
        n: Refractive index of medium in between mirrors
        aoi: Angle of incidence [rad]
        phase: Interpolation function f = phase_shift(wavelength[nm])

    Returns:
        peak wavelength in [nm]

    """
    repeat = 3
    # approximate wavelength without phase shift:
    wl = 2.0 * d * 1.0 * np.cos(aoi) / m * 1e9
    if isinstance(n, float):
        wl = 2.0 * d * n * np.cos(aoi) / m * 1e9
    if callable(n):
        for _ in range(repeat):
            wl = 2.0 * d * n(wl) * np.cos(aoi) / m * 1e9
    if phase is not None:
        if isinstance(n, float):
            for _ in range(repeat):
                wl = (2.0 * d * 1e9 * np.cos(aoi) * n + phase(wl) / np.pi * wl) / m
        if callable(n):
            for _ in range(repeat):
                wl = (2.0 * d * 1e9 * np.cos(aoi) * n(wl) + phase(wl) / np.pi * wl) / m
    return wl

Add additional modules or symbols as needed, for example:

rt

rt(n: ndarray, d: ndarray, wvl: ndarray, aoi: float = 0.0, pol: int = 0) -> tuple[ndarray, ndarray]

Calculate the reflection and transmission coefficients for a multilayer.

Parameters:

  • n

    (ndarray) –

    2D array-like, shape (num_wavelengths, num_layers) Refractive indices for each layer and wavelength.

  • d

    (ndarray) –

    1D array-like, shape (num_layers) Thickness of each layer (units must match wavelength).

  • wvl

    (ndarray) –

    1D array-like, shape (num_wavelengths) Wavelengths of light (units must match thickness).

  • aoi

    (float, default: 0.0 ) –

    float, optional Angle of incidence in degrees. Default is 0.0.

  • pol

    (int, default: 0 ) –

    int, optional Polarization state, 0 for TE (s) or 1 for TM (p). Default is 0.

Returns:

  • r ( ndarray ) –

    1D array, shape (num_wavelengths) Reflection coefficients for each wavelength.

  • t ( ndarray ) –

    1D array, shape (num_wavelengths) Transmission coefficients for each wavelength.

Source code in PyEtalon/tmm.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@njit(cache=True)
def rt(
    n: np.ndarray, d: np.ndarray, wvl: np.ndarray, aoi: float = 0.0, pol: int = 0
) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculate the reflection and transmission coefficients for a multilayer.

    Parameters:
        n: 2D array-like, shape (num_wavelengths, num_layers)
            Refractive indices for each layer and wavelength.
        d: 1D array-like, shape (num_layers)
            Thickness of each layer (units must match wavelength).
        wvl: 1D array-like, shape (num_wavelengths)
            Wavelengths of light (units must match thickness).
        aoi: float, optional
            Angle of incidence in degrees. Default is 0.0.
        pol: int, optional
            Polarization state, 0 for TE (s) or 1 for TM (p). Default is 0.

    Returns:
        r: 1D array, shape (num_wavelengths)
            Reflection coefficients for each wavelength.
        t: 1D array, shape (num_wavelengths)
            Transmission coefficients for each wavelength.
    """

    # Calculate squared sine of angle of incidence
    sin_squared_aoi = np.sin(np.radians(aoi)) ** 2
    n_squared = n**2

    # Precompute interface matrices
    n_current_layer, n_next_layer = n[:, :-1], n[:, 1:]
    s2_per_layer = (n_squared[:, 0:1] * sin_squared_aoi) / n_squared
    cos_current_layer, cos_next_layer = (
        np.sqrt(1 - s2_per_layer[:, :-1]),
        np.sqrt(1 - s2_per_layer[:, 1:]),
    )

    num1 = n_current_layer * cos_current_layer + n_next_layer * cos_next_layer
    num2 = n_current_layer * cos_next_layer + n_next_layer * cos_current_layer

    rjk_TE = (
        n_current_layer * cos_current_layer - n_next_layer * cos_next_layer
    ) / num1
    tjk_TE = 2.0 * n_current_layer * cos_current_layer / num1
    rjk_TM = (
        n_current_layer * cos_next_layer - n_next_layer * cos_current_layer
    ) / num2
    tjk_TM = 2.0 * n_current_layer * cos_next_layer / num2

    rjk = rjk_TE if pol == 0 else rjk_TM
    tjk = tjk_TE if pol == 0 else tjk_TM

    # Precompute layer matrices
    wave_vector_z = (
        np.sqrt(n_squared[:, 1:-1] - n_squared[:, 0:1] * sin_squared_aoi) * d[1:-1]
    )
    Bj = 2j * np.pi * wave_vector_z / wvl[:, None]
    exp_neg_Bj, exp_pos_Bj = np.exp(-Bj), np.exp(Bj)

    # Total transfer matrix initialization
    S11, S12 = 1.0 / tjk[:, 0], rjk[:, 0] / tjk[:, 0]
    S21, S22 = S12, S11

    for j in range(d.shape[0] - 2):  # Loop over middle layers
        B11, B12 = (
            exp_neg_Bj[:, j] / tjk[:, j + 1],
            exp_neg_Bj[:, j] * rjk[:, j + 1] / tjk[:, j + 1],
        )
        B21, B22 = (
            exp_pos_Bj[:, j] * rjk[:, j + 1] / tjk[:, j + 1],
            exp_pos_Bj[:, j] / tjk[:, j + 1],
        )

        # Update total matrix S = S * B
        C11 = S11 * B11 + S12 * B21
        C12 = S11 * B12 + S12 * B22
        C21 = S21 * B11 + S22 * B21
        C22 = S21 * B12 + S22 * B22

        S11, S12, S21, S22 = C11, C12, C21, C22

    # Final calculation for reflection and transmission
    r = S21 / S11
    t = 1.0 / S11

    return r, t