attoworld.spectrum

This module will contain functions related to the processing of spectra.

 1"""
 2This module will contain functions related to the processing of spectra.
 3"""
 4
 5from .spectrum import (
 6    wavelength_to_frequency,
 7    frequency_to_wavelength,
 8    transform_limited_pulse_from_spectrometer,
 9)
10from ..personal.marco import (
11    load_calibration_data,
12    read_spectrometer_excel,
13    read_spectrum_ocean_optics,
14)
15from ..personal.marco import calibrate as calibrate_reso
16
17__all__ = [
18    "wavelength_to_frequency",
19    "frequency_to_wavelength",
20    "transform_limited_pulse_from_spectrometer",
21    "load_calibration_data",
22    "read_spectrometer_excel",
23    "read_spectrum_ocean_optics",
24    "calibrate_reso",
25]
def wavelength_to_frequency( wavelengths_nm: numpy.ndarray, spectrum: numpy.ndarray, frequencies: Optional[numpy.ndarray] = None):
32def wavelength_to_frequency(
33    wavelengths_nm: np.ndarray,
34    spectrum: np.ndarray,
35    frequencies: Optional[np.ndarray] = None,
36):
37    """Convert a wavelength spectrum in W/nm into a frequency spectrum in W/THz
38
39    Args:
40        wavelength_nm (np.ndarray): the wavelengths included in the data, in nanometers
41        spectrum (np.ndarray): the spectrum corresponding to the input wavelength scale
42        frequencies: (optional) frequency vector for the output. If not specified, a vector will be calculated such that resolution and range are preserved.
43    Returns:
44        f: frequencies (Hz)
45        scaled_spectrum: the frequency-domain spectrum
46    """
47    # Contributed by Nick Karpowicz
48    input_frequencies = 1e9 * constants.speed_of_light / wavelengths_nm
49
50    if frequencies is None:
51        frequency_step = np.min(np.abs(np.diff(input_frequencies)))
52        min_frequency = np.min(input_frequencies)
53        max_frequency = np.max(input_frequencies)
54        frequency_count = int(np.ceil(max_frequency - min_frequency) / frequency_step)
55        frequencies = min_frequency + frequency_step * np.array(
56            range(0, frequency_count), dtype=float
57        )
58
59    # apply Jakobian scaling and use W/THz
60    scaled_spectrum = (
61        1e12 * constants.speed_of_light * spectrum / (input_frequencies**2)
62    )
63
64    scaled_spectrum = interpolate(
65        frequencies, input_frequencies, scaled_spectrum, inputs_are_sorted=False
66    )
67
68    return frequencies, scaled_spectrum

Convert a wavelength spectrum in W/nm into a frequency spectrum in W/THz

Arguments:
  • wavelength_nm (np.ndarray): the wavelengths included in the data, in nanometers
  • spectrum (np.ndarray): the spectrum corresponding to the input wavelength scale
  • frequencies: (optional) frequency vector for the output. If not specified, a vector will be calculated such that resolution and range are preserved.
Returns:

f: frequencies (Hz) scaled_spectrum: the frequency-domain spectrum

def frequency_to_wavelength( frequencies: numpy.ndarray, spectrum: numpy.ndarray, wavelengths: Optional[numpy.ndarray] = None):
 8def frequency_to_wavelength(
 9    frequencies: np.ndarray,
10    spectrum: np.ndarray,
11    wavelengths: Optional[np.ndarray] = None,
12):
13    """Convert a frequency spectrum in W/Hz into a wavelength spectrum in W/m.
14    SI units.
15    Args:
16        frequencies (np.ndarray): the frequencies included in the data, in Hz
17        spectrum (np.ndarray): the spectrum corresponding to the input frequency scale
18        wavelengths: (optional) wavelength vector for the output. If not specified, a the output data will be on the same grid, but scaled
19    Returns:
20        wavelengths: wavelengths in m
21        scaled_spectrum: the wavelength-domain spectrum
22    """
23    if wavelengths is None:
24        wavelengths = constants.speed_of_light / frequencies[frequencies > 0.0]
25        spectrum = spectrum[frequencies > 0.0]
26    else:
27        wavelengths = wavelengths[wavelengths > 0.0]
28
29    return wavelengths, spectrum / (wavelengths**2)

Convert a frequency spectrum in W/Hz into a wavelength spectrum in W/m. SI units.

Arguments:
  • frequencies (np.ndarray): the frequencies included in the data, in Hz
  • spectrum (np.ndarray): the spectrum corresponding to the input frequency scale
  • wavelengths: (optional) wavelength vector for the output. If not specified, a the output data will be on the same grid, but scaled
Returns:

wavelengths: wavelengths in m scaled_spectrum: the wavelength-domain spectrum

def transform_limited_pulse_from_spectrometer( wavelengths_nm: numpy.ndarray, spectrum: numpy.ndarray, gate_level: Optional[float] = None):
71def transform_limited_pulse_from_spectrometer(
72    wavelengths_nm: np.ndarray, spectrum: np.ndarray, gate_level: Optional[float] = None
73):
74    """Calculates the transform-limited pulse corresponding to a spectrum
75
76    Args:
77        wavelength_nm: the wavelengths included in the data, in nanometers
78        spectrum: the spectrum corresponding to the input wavelength scale
79        gate_level: (optional) level, relative to the maximum at which to apply a gate to the spectrum. For example, with gate_level=0.01, values less than 1% of the maximum signal will be set to zero
80
81    Returns:
82        t: time vector (s)
83        pulse: the pulse intensity vs. time
84    """
85    f, spec = wavelength_to_frequency(wavelengths_nm, spectrum)
86    df = f[1] - f[0]
87    t = np.fft.fftshift(np.fft.fftfreq(f.shape[0], d=df))
88    gated_spectrum = np.array(spec)
89    if gate_level is not None:
90        gated_spectrum[spec < (gate_level * np.max(spec))] = 0.0
91
92    pulse = np.fft.fftshift(np.abs(np.fft.ifft(np.sqrt(gated_spectrum)))) ** 2
93
94    return t, pulse

Calculates the transform-limited pulse corresponding to a spectrum

Arguments:
  • wavelength_nm: the wavelengths included in the data, in nanometers
  • spectrum: the spectrum corresponding to the input wavelength scale
  • gate_level: (optional) level, relative to the maximum at which to apply a gate to the spectrum. For example, with gate_level=0.01, values less than 1% of the maximum signal will be set to zero
Returns:

t: time vector (s) pulse: the pulse intensity vs. time

def load_calibration_data(calibration_data_filepath):
10def load_calibration_data(calibration_data_filepath):
11    """
12    Load calibration data in a .npz file for the Reso spectrometer
13
14    Args:
15        calibration_data_filepath (StrOrBytesPath): path of the file to load
16    """
17    try:
18        calibration = np.load(calibration_data_filepath)
19        wavelength_calibration = calibration["wavelength"]
20        lamp_spec = calibration["lamp_ref"]
21        lamp_measbyReso = calibration["lamp_measured"]
22        calibration_smoothed = np.abs(calibration["corr_factor_smoothed"])
23    except FileNotFoundError:
24        print(
25            "Error: calibration data for the UV spectrometer not found.\n"
26            "Please copy the folder Attoworld/src/attoworld/spectrum/calibration_data into your current working directory\n"
27            "or alternatively create a calibration file with relative path ./calibration_data/Reso_Spectrometer_CalibrationCorrection.npz\n"
28        )
29        raise FileNotFoundError("calibration data not found")
30    return wavelength_calibration, lamp_spec, lamp_measbyReso, calibration_smoothed

Load calibration data in a .npz file for the Reso spectrometer

Arguments:
  • calibration_data_filepath (StrOrBytesPath): path of the file to load
def read_spectrometer_excel(filename):
165def read_spectrometer_excel(filename):
166    """Reads xls file (passed as the string filename without extension!) produced by the UV-VIS spectrometer RESONANCE VS-7550-VUV.
167
168    This function currently only works in Linux (Ubuntu), since it uses the OS system command to brutally copy the content of the xls file into a txt;
169    such copying is necessary because the xls file is not readable by pandas (it's a non-standard xls, I couldn't find any other workaround).
170    For windows there is a similar command, please replace the line
171    os.system("cat " + filename + ".xls > " + filename + ".txt")
172
173    44 (skipped) rows at the beginning of the file (all headers)
174
175    Args:
176        filename: name of the file (without extension)
177
178    Returns:
179        numpy array with the spectral data (see original excel for more details)"""
180    if "." in filename and "./" not in filename:
181        raise ValueError(
182            "in function read_spectrometer_excel, filename must be passed without extension (filename should not contain a dot)"
183        )
184    os.system("cat " + filename + ".xls > " + filename + ".txt")
185    dataF = pandas.read_table(
186        filename + ".txt", sep="\t", keep_default_na=False, skiprows=44
187    )  # keep_default_na=False,
188    data = []
189    for row in dataF.values:
190        data.append([])
191        for x in row:
192            if x == "":
193                data[-1].append(float("nan"))
194            else:
195                data[-1].append(float(x))
196    return np.array(data)

Reads xls file (passed as the string filename without extension!) produced by the UV-VIS spectrometer RESONANCE VS-7550-VUV.

This function currently only works in Linux (Ubuntu), since it uses the OS system command to brutally copy the content of the xls file into a txt; such copying is necessary because the xls file is not readable by pandas (it's a non-standard xls, I couldn't find any other workaround). For windows there is a similar command, please replace the line os.system("cat " + filename + ".xls > " + filename + ".txt")

44 (skipped) rows at the beginning of the file (all headers)

Arguments:
  • filename: name of the file (without extension)
Returns:

numpy array with the spectral data (see original excel for more details)

def read_spectrum_ocean_optics(filename):
 92def read_spectrum_ocean_optics(filename):
 93    """Reads a spectrum file from the Ocean Optics spectrometer and returns the wavelength and spectrum arrays.
 94    The file is expected to have one column of wavelength data and one column of spectrum data.
 95    wavelengths are in nm.
 96    14 lines of header are skipped."""
 97    data = pandas.read_table(filename, sep="\t", keep_default_na=True, skiprows=14)
 98    wvl = np.array(data.iloc[:, 0])
 99    spectrum = np.array(data.iloc[:, 1])
100    return wvl, spectrum

Reads a spectrum file from the Ocean Optics spectrometer and returns the wavelength and spectrum arrays. The file is expected to have one column of wavelength data and one column of spectrum data. wavelengths are in nm. 14 lines of header are skipped.

def calibrate_reso( data, column: int, calibration_file_path='./calibration_data/Reso_Spectrometer_CalibrationCorrection.npz', dark=None, dark_c=None, stitch: bool = False, smooth_points: int = 10, null_calibration: bool = False, wavelength_calibration_intercept: float = 3.538, wavelength_calibration_slope: float = 1.003427):
199def calibrate(
200    data,
201    column: int,
202    calibration_file_path="./calibration_data/Reso_Spectrometer_CalibrationCorrection.npz",
203    dark=None,
204    dark_c=None,
205    stitch: bool = False,
206    smooth_points: int = 10,
207    null_calibration: bool = False,
208    wavelength_calibration_intercept: float = 3.538,
209    wavelength_calibration_slope: float = 1.003427,
210):
211    """For the UV spectrometer. Calibrate the spectrum number 'column' in the the 'data' array. Intensity calibration factor is loaded in the first lines of UVSpectrumAnalysis.py.
212        Notice: each saved spectrum has 7 columns in the excel file; however, the argument column refers to the index of the spectrum, not the actual column in the file.
213
214    Args:
215        data: data [numpy] array (see read_spectrometer_excel)
216        column: index of the spectrum to be calibrated
217        calibration_file_path: path of the calibration file
218        dark: dark spectrum (optional)
219        dark_c: column of the dark spectrum (optional)
220        stitch: Boolean, if True, the spectra are stitched together (optional, default False)
221                if True, the spectrum will be calculated by stitching the 5 partial spectra in the previous columns.
222                the stitching only works if the spectrum number 'column' is a FULL-SPECTRUM in the xls file.
223                Stitching was implemented because the internal software of the spectrometer is not good enough at it (the saved full spectrum typically has discontinuities).
224        smooth_points: number of points for smoothing (optional, default 10). if 0, no smoothing is applied
225        null_calibration: Boolean, if True, the intensity calibration is not applied (optional, default False)
226        wavelength_calibration_intercept: intercept of the wavelength calibration (optional, default 3.538). If None no wavelength calibration is applied
227        wavelength_calibration_slope: slope of the wavelength calibration (optional, default 1.003427). If None no wavelength calibration is applied
228                Wavelength calibration is in the form λ_true = λ_measured * slope + intercept
229                Notice: wavelength calibration has an error of +/- 1  to 2 nm due to the stitching process.
230                    for a better calibration compute and apply the correction to each of the 5 partial spectra
231
232    Returns:
233        wavelength: wavelength array
234        spectrum: calibrated spectrum
235    """
236    wavelength_calibration, lamp_spec, lamp_measbyReso, calibration_smoothed = (
237        load_calibration_data(calibration_file_path)
238    )
239
240    if wavelength_calibration_slope is None or wavelength_calibration_intercept is None:
241        cal_slope = 1.0
242        cal_intercept = 0.0
243    else:
244        cal_slope = wavelength_calibration_slope
245        cal_intercept = wavelength_calibration_intercept
246
247    if stitch:
248        # check that the chosen spectrum (in column) is actually a full one
249        theoretically_complete_wvl = (
250            data[:, 7 * (column) + 1] * cal_slope + cal_intercept
251        )
252        theoretically_complete_wvl = theoretically_complete_wvl[
253            ~np.isnan(theoretically_complete_wvl)
254        ]
255        if theoretically_complete_wvl[-1] - theoretically_complete_wvl[0] < 900:
256            raise ValueError(
257                "in function calibrate(), the spectrum to be stitched is not a full spectrum, please check the column index"
258            )
259
260        extrema = []
261        spectra = []
262        for i in range(5):
263            wvl_single_sp = (
264                data[:, 7 * (column - 5 + i) + 1] * cal_slope + cal_intercept
265            )
266            single_sp = data[:, 7 * (column - 5 + i) + 2]
267            if dark is not None and dark_c is not None:
268                single_sp -= dark[:, 7 * (dark_c - 5 + i) + 2]
269            wvl_single_sp = wvl_single_sp[~np.isnan(wvl_single_sp)]
270            single_sp = single_sp[~np.isnan(single_sp)]
271            extrema.append([wvl_single_sp[0], wvl_single_sp[-1]])
272            spectra.append([wvl_single_sp, single_sp])
273
274        overlaps = []
275        centers_of_overlap = [extrema[0][0]]
276        for i in range(4):
277            overlaps.append(extrema[i][1] - extrema[i + 1][0])
278            centers_of_overlap.append(extrema[i][1] / 2 + extrema[i + 1][0] / 2)
279
280        w = np.min(overlaps)
281        centers_of_overlap.append(extrema[4][1] + w / 2)
282        centers_of_overlap[0] = centers_of_overlap[0] - w / 2
283
284        wavelength = data[:, 7 * column + 1] * cal_slope + cal_intercept
285        spectrum = wavelength * 0.0
286
287        for i in range(5):
288            center = centers_of_overlap[i] / 2 + centers_of_overlap[i + 1] / 2
289            FWHM = centers_of_overlap[i + 1] - centers_of_overlap[i]
290            spectrum += np.interp(
291                wavelength,
292                spectra[i][0],
293                spectra[i][1] * tukey_window(spectra[i][0], center, FWHM, w),
294            )
295            plt.plot(
296                spectra[i][0],
297                spectra[i][1] * tukey_window(spectra[i][0], center, FWHM, w),
298            )
299        calibr_interpolated = np.interp(
300            wavelength, wavelength_calibration, calibration_smoothed
301        )
302        if not null_calibration:
303            spectrum *= calibr_interpolated
304
305        plt.plot(
306            data[:, 7 * column + 1] * cal_slope + cal_intercept, data[:, 7 * column + 2]
307        )
308        plt.plot(wavelength, spectrum)
309
310        if w < 0:
311            raise ValueError(
312                "in function calibrate(), the spectra to be stitched do not overlap"
313            )
314
315    else:
316        wavelength = data[:, 7 * column + 1] * cal_slope + cal_intercept
317        if dark is None or dark_c is None:
318            calibr_interpolated = np.interp(
319                wavelength, wavelength_calibration, calibration_smoothed
320            )
321            if null_calibration:
322                calibr_interpolated = calibr_interpolated * 0.0 + 1
323            spectrum = (
324                data[:, 7 * column + 2] - np.min(data[:, 7 * column + 2])
325            ) * calibr_interpolated
326        else:
327            calibr_interpolated = np.interp(
328                wavelength, wavelength_calibration, calibration_smoothed
329            )
330            if null_calibration:
331                calibr_interpolated = calibr_interpolated * 0.0 + 1
332            spectrum = (
333                data[:, 7 * column + 2] - dark[:, 7 * dark_c + 2]
334            ) * calibr_interpolated
335
336            if len(data[:, 7 * column + 2][~np.isnan(data[:, 7 * column + 2])]) != len(
337                dark[:, 7 * dark_c + 2][~np.isnan(dark[:, 7 * dark_c + 2])]
338            ):
339                raise ValueError(
340                    "In function calibrate(), the dark spectrum is not the same length as the spectrum to be calibrated.\n"
341                    "Please check the column indices of the dark spectrum and the main spectrum"
342                )
343            if (
344                data[:, 7 * column + 1][~np.isnan(data[:, 7 * column + 1])][0]
345                != dark[:, 7 * dark_c + 2][~np.isnan(dark[:, 7 * dark_c + 1])][0]
346            ):
347                print(
348                    "WARNING: In function calibrate(), the dark spectrum does not have the same wavelength as the spectrum to be calibrated.\n"
349                    "Please check the column indices of the dark spectrum and the main spectrum"
350                )
351
352    if smooth_points is not None and smooth_points > 0:
353        spectrum = smooth(spectrum, smooth_points)
354    return wavelength, spectrum

For the UV spectrometer. Calibrate the spectrum number 'column' in the the 'data' array. Intensity calibration factor is loaded in the first lines of UVSpectrumAnalysis.py. Notice: each saved spectrum has 7 columns in the excel file; however, the argument column refers to the index of the spectrum, not the actual column in the file.

Arguments:
  • data: data [numpy] array (see read_spectrometer_excel)
  • column: index of the spectrum to be calibrated
  • calibration_file_path: path of the calibration file
  • dark: dark spectrum (optional)
  • dark_c: column of the dark spectrum (optional)
  • stitch: Boolean, if True, the spectra are stitched together (optional, default False) if True, the spectrum will be calculated by stitching the 5 partial spectra in the previous columns. the stitching only works if the spectrum number 'column' is a FULL-SPECTRUM in the xls file. Stitching was implemented because the internal software of the spectrometer is not good enough at it (the saved full spectrum typically has discontinuities).
  • smooth_points: number of points for smoothing (optional, default 10). if 0, no smoothing is applied
  • null_calibration: Boolean, if True, the intensity calibration is not applied (optional, default False)
  • wavelength_calibration_intercept: intercept of the wavelength calibration (optional, default 3.538). If None no wavelength calibration is applied
  • wavelength_calibration_slope: slope of the wavelength calibration (optional, default 1.003427). If None no wavelength calibration is applied Wavelength calibration is in the form λ_true = λ_measured * slope + intercept Notice: wavelength calibration has an error of +/- 1 to 2 nm due to the stitching process. for a better calibration compute and apply the correction to each of the 5 partial spectra
Returns:

wavelength: wavelength array spectrum: calibrated spectrum