Source code for experiments.ft_viper

"""
Providing the "FT VIPER" experiment. This experiment is an IR pump time
domain experiment. The pump pulse is split into two with a Michelson
interferometer. These pulses spatially overlap while the temporal
overlap is "scanned" with the movable path of the interferometer. During
the measurement, a motorized stage "scans" the temporal overlap with a
user defined frequency and amplitude (see pi_control.py). The IR pump
pulse pair is used to excite the sample into the first vibrational
state. A second, chopped UV/VIS pump pulse then excites the molecules
from that higher vibrational state to an electronically excited state
that is also vibrationally excited. A probe pulse is then used to
determine the molecular response. The IR pump pulse must temporally come
before the UV/VIS pump pulse. Therefore the stages need to be set
accordingly. To reduce IR pump light scattering on the detector, a
wobbler is used to phase cycle the pump pulse. The UV/VIS chopper, and
only the UV/VIS chopper needs to be running for this experiment. To
prevent the sample from being burnt a micrometer screw is used to
continuously move the sample up and down.

VIPER stands for Vibrationally Promoted Electronic Resonance. Time
domain VIPER experiments use 1 chopper. The IR Pump comes from the
Michelson interferometer and its molecular response is measured in the
time domain, thus no chopper is required. The UV/VIS pump pulse is
chopped. Frequency domain VIPER measurements typically contain 4
different types of signals. However, time domain VIPER measurements are
calculated differently.

All different *time domain* difference signals:

    * (IR on, UV off): IR pump/IR probe
    * (IR on, UV on): VIPER + IR pump/IR probe
    
The Background and the TRIR signal do not cause an oscillating signal in
the time domain - only an offset which is removed upon fourier
transform.

##########################################
Procedure to Start a FT VIPER Measurement:
##########################################

1.  Wait until laser is warmed up and measure output

2.  Optimize OPA (Optical parametric amplifier) 1 (IR probe) and OPA 2
    (IR pump) on output power

3.  Align beam pointing on iris for the delayline. For that, use the
    "Ophir" powermeter and turn off the dry air because of the
    sensitivity of the powermeter. It observes the fluctuations and
    cannot return the laser power reliably

4.  Optimize OPA 3 (UV pump) on output power at the sample 

5.  Set the overlap of the pump OPAs with the probe OPA using a pinhole
    or a sample. For this, reduce the UV/VIS power (with filter) below
    0.5 microJ for pinhole or GaAs (Gallium arsenide) 

6.  Optimize the chopper phases, delays and Wobbler (see
    show_signal_wobbler for detailed procedure). This step is difficult
    and crucial. FT Viper: UV/VIS 1/8, Wobbler 1/4. Note that it may be
    necessary to setup both the IR and UV/VIS chopper to adjust the
    choppers and observe a VIPER signal in show_viper_wobbler experiment
    even though the FT-Viper only needs the UV/VIS chopper. For steps 7
    and 8 show_signal_wobbler (UV and IR), show_viper_wobbler are
    required

7.  Check chopper phases using photo diodes (if available in setup). The
    pyroelectric detector on the oscilloscope can also give insight into
    the chopper phase

8.  Optimize the chopper output phases by optimizing signal size

9.  Measure time t zero (temporal overlap) for both OPAs with scan tzero
    experiment. For this, block the movable path of the interferometer

10. Measure the temporal overlap using scan t zero experiment of the
    movable path of the interferometer. For that, block the static path

11. Optimize the spatial overlap in the sample for OPA 3 and for the
    movable path of the interferometer of OPA 2 using the overlap
    mirrors. For the static path of the interferometer use the second
    mirror of the static path

12. Optimize the chopper phases using the show_viper_wobbler experiment
    (colorful experiment). Also check the background at -20,000 fs.
    It should be zero

13. Optimize the delay between the pump pulses

14. **Cry** and probably repeat from step 6

15. If everything up until now works (wow you are a wizard!) turn
    off the IR Chopper. If the IR chopper is still on, the interferogram
    is going to be unsightly

16. Set the wavegeneration parameters appropriately for the molecule
    that you want to measure (i.e. coherence time 2ps, frequency 0.4 Hz
    speedupdown 150, overshoot factor 0.1). This step requires a lot 
    of testing, especially if this was never done before

17. Refill the detector, check your file name, check if the data 
    will be saved (for important measurements raw data should always be
    saved)

18. Start the FT-Viper experiment

19. **Go home crying**

References:

    For understanding the general procedure and setup:

        * ft_2d_ir.py (for wavegeneration and alignment of the 
          interferometer. Additional information on the pyroelectric
          detector and the photodiodes as well as the R-2R network can
          be found there)
        * show_signal_wobbler.py
        * show_wobbler_states.py
        * show_viper_wobbler.py

Note:

    **Delay Files:**

        If more than one delay file is required for an experiment (e.g.
        VIPER) the weights of all files are multiplied to yield a
        resulting weight. Generally, the delay files have to increase
        monotonously. For VIPER experiments the UV/VIS delay file does
        not have to increase monotonously. The IR delay file has to. For
        VIPER the delays files need to have a user given offset (time
        delay between the pump pulses). This is due to the fact that the
        IR pump pulse should temporally "arrive" before the UV/VIS pump
        pulse such that the VIPER signal is maximal. This time delay can
        be found by conducting a broadband VIPER experiment where the
        UV/VIS Stage is "scanned" while the IR delay remains static and
        then observing for which relative delay between the stages the
        VIPER signal is maximal. We observed that it can happen that the
        stages have to be set in the opposite way (as if the UV/VIS pump
        pulse arrives before the IR pump pulse. For examples stage
        positions such as IR delay stage 20,500 fs UV/VIS delay stage
        20,000 fs might yield a good VIPER signal). This is can be
        attributed to the imprecision in determining the t zero position
        for each of the stages separately. 

#######################
Step by Step Algorithm:
#######################

**Acquisition:**

    1.  Preallocate dictionary (data container) which will contain data
        and information about scan index, delay index etc.
    2.  Close the UV/VIS Shutter to avoid unnecessary exposition
        to UV/VIS pump light
    3.  Start moving the micrometer screw continuously
    4.  Move the interferometer stage to the position where the 
        interferometer counter needs to be reset
        (this is a position a few hundred femto seconds after the 
        t zero, but not as far as the starting position of the 
        wavegeneration)
    5.  Reset the interferometer counter value to zero
    6.  Move the interferometer stage to the starting position of
        the wavegeneration
    7.  Start wavegeneration
    8.  Close UV/VIS shutter
    9.  Move IR and UV/VIS stage simultaneously to next respective delay
    10. Open UV/VIS Shutter
    11. Read the data from the ADC
    12. Place data into dictionary and hand it over to
        primary processing

**Primary Processing:**

    1.  Preallocate arrays for data, counts, weights, chopper.
        Here there are 2 states for each chopper. Obtain the Chopper
        voltage levels and calculate the Wobbler states from the
        laser frequency and wobbler frequency.
    2.  Subtract background from raw data (dark noise)
    3.  Linearize response of pixels
    4.  Calculate transmission, or more precisely, relative intensity
        (probe intensity / reference intensity) for each laser shot for
        each pixel pair
    5.  Calculate interferometer positions from R-2R data for each laser
        shot. Check if minimum and maximum interferometer positions were
        observed.
    6.  Identify the chopper states for all laser shots using the 
        corresponding channel(s) in the ADCs' data
    7.  Identify the different wobbler states for all laser shots
    8.  Sort the data (transmissions) for each state and calculate 
        statistics
    9.  Phase cycle by averaging the wobbler states in the transmission 
        space. Calculate the resulting phase cycled counts and weights
    10. Average the phase cycled data by weighting equally. If averaging
        fails because not all states were observed ignore exception so
        that the graphs still will be displayed later
    11. Put this information into data container and hand it over to
        secondary processing
    12. Save data (and raw data) including counts and weights
        if respective checkboxes on GUI were checked


**Secondary Processing:**

    1.  Preallocate arrays and variables to hold time domain viper 
        spectrum, time domain 2d ir spectrum, freq domain viper
        spectrum, freq domain 2d ir spectrum, zerobin, pump frequency
        axis, opa range, central pump wavenumber, opa spectrum,
        interpolated phase cycled viper signal, interpolated phase
        cycled 2d ir signal
    2.  Obtain the interferogram. For this we need the phase cycled
        counts because we want to average the interferograms of the
        UV/VIS chopper being turned open and closed (using the phase
        cycle counts as weights can fail, therefore use try except
        statements to make sure that the interferogram etc. are still
        displayed)
    3.  Calculate all the different *time domain* difference signals
        Last axis UV/VIS pump on-off
        Beginners notes (*IR Pump in time domain*):
            
            * (IR on, UV off): IR pump/IR probe
            * (IR on, UV on): VIPER + IR pump/IR probe
        
        The Background and the TRIR signal do not cause an oscillating
        signal in the time domain - only an offset which is removed upon
        fourier transform

    4.  Process the single scan data:

            * Calculate frequency domain VIPER absorption spectrum
            * Calculate frequency domain 2D-IR absorption spectrum
            * Extract the relevant information regarding the spectrum of the OPA
            * Obtain pump frequency axis

    5.  Process averaged data:

            * Calculate frequency domain VIPER absorption spectrum
            * Calculate frequency domain 2D-IR absorption spectrum
            * Extract the relevant information regarding the spectrum of the OPA
            * Obtain pump frequency axis

    6.  Use try except statements to make sure that the information
        about the interferometer can still be displayed even if an 
        error occurs (e.g. if the wavegeneration parameters are chosen
        poorly)
    7.  Calculate VIPER time domain spectrum for each wobbler state
        for central pixel to double check whether phase cycling 
        is working
    8.  Calculate the average intensity for each pixel
    9.  Calculate the average intensities and standard deviation of 
        the intensities for each pixel
    10. Put this information into data container and hand it over to
        Pyqtplotting thread
    11. Calculate the numbers which are displayed in the 
        "statistics box" on the GUI

**Pyqt Plotting:**

    1.  Remove old plots
    2.  Setup the plot that displays:
            * Single 2d ir signal heatmap with histogram
            * Single viper signal heatmap with histogram
            * Average viper signal heatmap with histogram
            * Interferogram
            * Opa spectrum
            * Time domain spectrum of central pixel for all wobbler
              states
            * Intensities and their standard deviation (multiplied by 5)
            * Counts (How often a position was measured)
            * Stage position
    3.  Plot the plots for the first time
    4.  Update plots.

**Saving:**

Note that the phase cycled transmission is saved. Therefore there exists
no dimension for the Wobbler states.

    .. code-block::
        
        Programming data dimension: 
        [(2 ([0] is current average scan, [1] is last single scan), n_delays, n_probe_pixels, n_interferometer_states, n_vis_chopper_states, n_wobbler_states)]
        
        saving dimension:
        [(n_probe_pixels, n_interferometer_states, n_vis_chopper_states, n_wobbler_states)]
        
        raw data dimension:
        [(n_channels, samples_to_acquire)]

################
Folder Structure
################

In **scans/** the first file of 3 contains the data in the saving
dimensions. The counts file contains the number of times a given state
was observed. This should be used as weights when averaging equally
weighted. The weights file contains the inverse variances of a given
state for all pixels, etc. The dimensions of these files are as the
saving dimension suggests. These files can be used to average in
different ways in order to obtain the complete resulting spectrum. Check
the actual code of the corresponding experiment to understand how to
calculate the desired end result (difference spectrum).

The data in **/averaged_data** is averaged equally weighted (using
counts). Likewise to the scan data the difference spectrum still needs
to be calculated from the transmissions for every state. Because the
array contains the transmissions averaged with counts it is only
intended to be used as a first indicator for the measurement. Here time
domain data was averaged. Please note that better quality can be
achieved when Fourier transforming first and averaging in the frequency
domain. This has to with the phasing that varies from scan to scan.

The **/figure** folder is currently empty. It is possible to implement
plotting of figures which are saved here while the experiment is
running. This is however not implemented yet.

The **/hardware config** folder holds every file which contains hardware
configuration parameters. This folder is a compressed copy of the
hardware configuration folder in the software's directory. Here it is
possible to look up the ADC configuration to obtain what channel was
connected to which hardware element (e.g. chopper - ai78). It is also
possible to obtain the R-2R values, the FPAS configuration, the
linearization parameters, etc.

The **/raw data** folder is in the experiments directory only if the
"save raw data" checkbox on the GUI was checked. It contains the raw
(unedited... as raw as it can get) data. Basically, the voltages which
the ADC measured for each channel. The dimensions are as "raw data
dimensions" suggests.

The **background.npy** file is a copy of the most recently collected
dark noise background of the MCT detector array. It corrects for dark
noise. If no new background was collected before the experiment was
started the code will use the latest available background and display a
warning in the log.

**setupinfo.txt** is a ReadMe file that contains the most relevant
experimental parameters at first glance. Additionally, the user can
decide to write a comment in the readme editor of the GUI. The content
of this is written to the **notes.txt** file.

**probe_wn_axis.npy** contains the wavenumber axis which is generated by
the spectrometer triax.py class.

**delays.npy** contains the delays including weights.

.. code-block::

    username/
    ├── date1_experimentname1_000/
    │   ├── averaged_data
    │   │   ├──  d000_date1_experimentname1_000.npy
    │   │   ├──  ...
    │   │   └──  d999_date1_experimentname1_000.npy
    │   ├── figures
    │   ├── hardware config
    │   ├── raw data
    │   │   ├──  delay000
    │   │   │   ├──  s000000_d000_date1_experimentname1_000_raw.npy
    │   │   │   ├──  ...
    │   │   │   └──  s000099_d000_date1_experimentname1_000_raw.npy
    │   │   ├──  ...
    │   │   └──  delay999
    │   ├── scans
    │   │   ├──  delay000
    │   │   │   ├──  s000000_d000_date1_experimentname1_000.npy
    │   │   │   ├──  s000000_d000_counts_date1_experimentname1_000.npy
    │   │   │   ├──  s000000_d000_weights_date1_experimentname1_000.npy
    │   │   │   ├──  ...
    │   │   │   └──  s000099_d000_date1_experimentname1_000.npy
    │   │   ├──  ...
    │   │   └──  delay999
    │   ├── probe_wn_axis_date1_experimentname1_000.npy
    │   ├── delays_date1_experimentname1_000.npy
    │   ├── setupinfo_date1_experimentname1_000.txt
    │   ├── notes_date1_experimentname1_000.txt
    │   └── background_date1_experimentname1_000.npy
    ├── date1_experimentname1_001/
    ├── date2_experimentname1_000/
    └── date2_experimentname2_000/
"""
if __name__ == "__main__":
    # Add directories to path for imports
    import os, sys, inspect

    currentdir = os.path.dirname(
        os.path.abspath(inspect.getfile(inspect.currentframe()))
    )
    parentdir = os.path.dirname(currentdir)

    sys.path.insert(0, parentdir)
    sys.path.insert(0, os.path.join(parentdir, "hardware_interfaces"))
    sys.path.insert(0, os.path.join(parentdir, "gui"))

import multiprocessing
from multiprocessing import Process, Queue
import sys

import threading

import numpy as np
from numpy import ndarray

# Matplotlib
import matplotlib
from matplotlib import pyplot as plt
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg,
    NavigationToolbar2QT as NavigationToolbar,
)
from matplotlib.figure import Figure
from matplotlib import cm

# PyQTGraph
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg

from PyQt5 import QtWidgets, QtCore  # , QtGui
from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject
from qt_multithreading_wrapper import Worker

# Data processing
import data_processing as dp
from data_processing import PixelResponseLinearization as PRL

from save_data import SaveData, Background

# Hardware modules
from analog_digital_converter import AnalogDigitalConverter as ADC
from triax import Triax as Spectrometer
from pi_control import PiStage
from interferometer_counter import InterferometerCounter
from newport_control import NewportControl as MuScrew
from shutter import Shutter

# Set up logger
import logging

logger = logging.getLogger(__name__)


[docs]class FtViper: """ Args: widget_pyqtgraph (QWidget): WidgetPyqtgraph object on which the plots are going to be displayed. Has methods for plot manipulation (i.e. removal of plots, autoscale). vis_delays (ndarray): Array containing the delays in fs for the UV/VIS stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) ir_delays (ndarray): Array containing the delays in fs for the IR stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) adc (ADC): Analog to digital converter hardware object which is used to communicate with and read data from the ADC. vis_delay_stage (PiStage): PiStage hardware control object which provides the interface to the UV/VIS delay stage. ir_delay_stage (PiStage): PiStage hardware control object which provides the interface to the IR delay stage. interferometer_stage (PiStage): PiStage hardware control object which provides the interface to the interferometer stage. interferometer_counter (InterferometerCounter): InterferometerCounter object which provides the functionalities necessary to use the interferometer counter electronics. prl (PRL): Pixel response linearization object which grants the functionality to linearize raw data according to the linearization parameters specified in the corresponding *pixel_linearization_fit_parameters.json* file (for each lab). vis_chopper_info (dict): Contains the information that is necessary to identify the different chopper states of the chopper that chops the UV/VIS pump pulse. It contains the keys "high voltage level" and "name". "high voltage level" is the voltage read by the ADC when the chopper reference output is high. It is needed as a reference for the digitization function that is used. The "name" key is required to determine to which channel of the adc the chopper is connected and its value needs to match the corresponding key in index_dict. he_ne_wl (float): Wavelength of the light source (generally a Helium-Neon laser) which is used for determining the position of the interferometer stage via the photodiodes and the counter electronics. wobbler_freq (float): Frequency in Hz with which the wobbler oscillates. background_handler (Background): Instance of Background class which can access the most recently collected background. This background is later subtracted from the raw data as dark noise correction. spectrometer (Spectrometer): Spectrometer/Triax hardware class which grants functionality to control the triax spectrometer. Needed to obtain e.g. wavenumber axis etc. mu_screw (MuScrew): NewportControl object that can be used to move the micrometer screw. vis_shutter (Shutter): Shutter object that can be used to open and close the UV/VIS (pump) shutter. info_queue (Queue): Multiprocessing queue object which is used to transfer/hand over information to lineEdits on GUI. Contains (if applicable) scan index, delay index, interleave index, values for statistics groupBox etc. saver (SaveData): Object that manages saving of data (including counts, weights, probe wavenumber axis etc.) into their respective directories. If None is passed, no data is going to be saved. If the raw data checkbox was checked on the GUI the savers raw data attribute is set to True and the raw data is saved automatically. Defaults to None. """ def __init__( self, widget_pyqtgraph, vis_delays: ndarray, ir_delays: ndarray, adc: ADC, vis_delay_stage: PiStage, ir_delay_stage: PiStage, interferometer_stage: PiStage, interferometer_counter: InterferometerCounter, prl: PRL, vis_chopper_info: dict, he_ne_wl: float, wobbler_freq: float, background_handler: Background, spectrometer: Spectrometer, mu_screw: MuScrew, vis_shutter: Shutter, info_queue: Queue, saver: SaveData = None, ): # Initialise Queues self.acq_queue = Queue() self.processing_queue = Queue() self.plot_queue = Queue() # Save wavenumber axis, delay files etc. to file system if saver: saver.save_other(spectrometer.wn_axis, "probe_wn_axis") saver.save_other(vis_delays, "vis_delay_file") saver.save_other(ir_delays, "ir_delay_file") saver.save_other(background_handler.load_background(), "background") # Calculate number of interferometer states # we expect to observe because both processing # processes needs this information # Calculate the number of bins within this interferometer # amplitude removing the overshooting distance at both ends. # This automatically corresponds to the (interferometer) # number_of_states used in dp.sort_data(). We also use this to # preallocate our arrays. # We need to multiply by two because of the mirrors the light travels # twice the distance (or for other path factors an even greater multiple) interferometer_states = ( int( np.ceil( ( interferometer_stage.amplitude - 2 * interferometer_stage.overshoot_mm ) // he_ne_wl ) ) * interferometer_stage.path_factor ) # Get index of central pixel in MCT array. # This is necessary to display the time domain # spectrum on the GUI. # ? Not sure if this works for all analog input # ? configurations self.central_pixel = adc.probe_pixel_idx.size // 2 self.acquisition = Acquisition( vis_delays, ir_delays, adc, vis_delay_stage, ir_delay_stage, interferometer_stage, interferometer_counter, spectrometer, mu_screw, vis_shutter, self.acq_queue, ) self.primary_processing = PrimaryProcessing( self.acq_queue, self.processing_queue, vis_delays, ir_delays, adc.pixel_idx, adc.probe_pixel_idx, adc.reference_pixel_idx, adc.index_dict, interferometer_counter.r2r_indices, prl, vis_chopper_info, wobbler_freq, adc.laser_frequency, interferometer_states, interferometer_counter.bin_reference_values, background_handler, saver, ) self.secondary_processing = SecondaryProcessing( self.processing_queue, self.plot_queue, info_queue, vis_delays, ir_delays, adc.probe_pixel_idx, interferometer_states, self.central_pixel, saver, ) self.plotting = PyqtPlotting( widget_pyqtgraph, adc, self.plot_queue, vis_delays, ir_delays, self.central_pixel, )
[docs] def start(self): self.plotting.threadpool.start(self.plotting.work) self.secondary_processing.start() self.primary_processing.start() self.acquisition.start()
[docs]class Acquisition(threading.Thread): """ Args: vis_delays (ndarray): Array containing the delays in fs for the UV/VIS stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) ir_delays (ndarray): Array containing the delays in fs for the IR stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) adc (ADC): Analog to digital converter hardware object which is used to communicate with and read data from the ADC. vis_delay_stage (PiStage): PiStage hardware control object which provides the interface to the UV/VIS delay stage. ir_delay_stage (PiStage): PiStage hardware control object which provides the interface to the IR delay stage. interferometer_stage (PiStage): PiStage hardware control object which provides the interface to the interferometer stage. interferometer_counter (InterferometerCounter): InterferometerCounter object which provides the functionalities necessary to use the interferometer counter electronics. spectrometer (Spectrometer): Spectrometer/Triax hardware class which grants functionality to control the triax spectrometer. Needed to obtain e.g. wavenumber axis etc. mu_screw (MuScrew): NewportControl object that can be used to move the micrometer screw. vis_shutter (Shutter): Shutter object that can be used to open and close the UV/VIS (pump) shutter. acq_queue (Queue): Multiprocessing queue object that the acquisition thread uses to pass data to the primary processing process. """ def __init__( self, vis_delays: ndarray, ir_delays: ndarray, adc: ADC, vis_delay_stage: PiStage, ir_delay_stage: PiStage, interferometer_stage: PiStage, interferometer_counter: InterferometerCounter, spectrometer: Spectrometer, mu_screw: MuScrew, vis_shutter: Shutter, acq_queue: Queue, ): threading.Thread.__init__(self) # Assign attributes self.vis_delays = vis_delays self.ir_delays = ir_delays self.adc = adc # Hardware devices self.vis_delay_stage = vis_delay_stage self.ir_delay_stage = ir_delay_stage self.interferometer_stage = interferometer_stage self.interferometer_counter = interferometer_counter self.spectrometer = spectrometer self.mu_screw = mu_screw self.vis_shutter = vis_shutter # Multiprocessing Queue self.acq_queue = acq_queue # Save the base amount of samples # to acquire # This is needed to reset the weighting # for each delay self.base_samples = self.adc.samples_to_acquire # Initialize scan index self.scan_idx = 0 # Create dictionary that will hold data that is passed # to other queues self.data_container = {} # Multithreading event to stop experiment self.exit = threading.Event() # ----------- Prepare Acquisition ------- # ------- Close UV/VIS Shutter # to avoid unnecessary exposition to # UV/VIS pump light. The next time it is # needed is during the actual measurement # process. (It is not needed for the # setup process.) self.vis_shutter.close() # ------- Start moving the mircometer screw # back and forth continuously # if it was not doing that already if not self.mu_screw.continuously_moving: self.mu_screw.toggle_continuous_movement() # -------- Start moving interferometer and counting # Move interferometer to position where counter is supposed # to be reset. This is the actual minimum position of the # wavegeneration if overshooting would not be necessary. # ---Note: We actually want the stage to pass the reset position # during wavegeneration. If this happens, an underflow occurs # and the counter starts counting backwards starting at 0xFFFF/2 # This is wanted behavior! For optimal data processing we need # to fill every interferometer position (and also every other state) # with data (laser shots). So reaching beyond the 0 ensures # measuring the 0th bin. Please see the comments for the # sort_data function in Primary Processing for # further information. --- self.interferometer_stage.move_mm(self.interferometer_stage.reset_position) # Reset interferometer counter to value 0 self.interferometer_counter.reset_counter() # Move to starting position of wavegeneration. self.interferometer_stage.move_mm(self.interferometer_stage.start_pos) # Start moving interferometer stage back and forth # Note: For this to be possible a wavetable # already needs to be setup beforehand self.interferometer_stage.start_wavegen()
[docs] def run(self): # * You might wonder why this acquisition # * is structured differently from the acquisition # * of the other (older/simpler) experiments # * [Or you might not wonder - # * Lets hope that someone fixed this discrepancy # * already] # * The way it is implemented here (the straight # * forward way) was chosen because it simplifies # * the indices counting a lot, and within multiprocessing # * should not lead to any loss of laser time. # * Before the 0th data set was acquired outside of # * the loop and was processed while the second # * acquisition was running while not self.exit.is_set(): # Iterate over UV/VIS and IR # delays at the same time for d_idx, delays in enumerate(zip(self.vis_delays, self.ir_delays)): # First close UV/VIS shutter before moving stages # to avoid unnecessary exposition to # UV/VIS pump light self.vis_shutter.close() # Unpack delays for better clarity vis_delay, ir_delay = delays # At this point the way we are using delays # files has become ambiguous because we # can specify weights in both the UV/Vis # and the IR delay file # Here we decide to multiply the weights # to get a resulting total weight weight = vis_delay[1] * ir_delay[1] # Set samples to acquire according to weight # for these delay samples_to_acquire = int(round(self.base_samples * weight)) self.adc.set_samples_to_acquire(samples_to_acquire) # Move both stages simultaneously # (thats why wait = False) self.vis_delay_stage.move(vis_delay[0], wait=False) self.ir_delay_stage.move(ir_delay[0], wait=False) #! If micrometer screw is supposed to move step wise #! insert here # Now wait for both stages to reach their positions self.vis_delay_stage.wait() self.ir_delay_stage.wait() # To measure data open shutter self.vis_shutter.open() # Read data with given parameters self.adc.read() # Put data in acquisition queue self.data_container["data"] = self.adc.data.copy() self.data_container["scan index"] = self.scan_idx self.data_container["delay index"] = d_idx self.data_container["probe axis"] = self.spectrometer.wn_axis.copy() # Give data to queue self.acq_queue.put(self.data_container.copy()) # Update scan idx self.scan_idx += 1 # Stop wavegeneration for interferometer self.interferometer_stage.stop_wavegen() # Close UV/Vis shutter so sample # does not get burnt more than necessary self.vis_shutter.close() # Tell processes to stop after last data self.acq_queue.put("stop")
[docs] def shutdown(self): # Setting exit will stop the loop # within run self.exit.set()
[docs]class PrimaryProcessing(multiprocessing.Process): """ Args: acq_queue (Queue): Multiprocessing queue object that the acquisition thread uses to pass data to the primary processing process. processing_queue (Queue): Multiprocessing queue object that the primary processing process uses to pass data to the secondary processing process. vis_delays (ndarray): Array containing the delays in fs for the UV/VIS stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) ir_delays (ndarray): Array containing the delays in fs for the IR stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) pixel_idx (ndarray): Array that contains the indices of the rows in the ADCs' data that correspond to pixel input channels. These are specified in the "analog input configuration.json" for each laboratory and can be easily accessed with the attribute "pixel_idx" of the ADC. * shape: 1D * E.g.: (64) or (128) probe_pixel_idx (ndarray): Array that contains the indices of the rows in the ADCs' data that correspond to *probe* pixel input channels. These are specified in the "analog input configuration.json" for each laboratory and can be easily accessed with the attribute "probe_pixel_idx" of the ADC. It is highly relevant that the order the pixels are listed in this array match the order of the array in the reference_pixel_idx argument. This means that if reference pixel 3 is listed first in the other array here probe pixel 3 needs to be listed first as well and so on. Otherwise the intensities on the probe array are not going to be normalized correctly. For the plotting to work correctly the pixels also need to be listed in the order of the wavenumber axis array of the spectrometer. * shape: 1D * E.g.: (32) or (64) reference_pixel_idx (ndarray): Array that contains the indices of the rows in the ADCs' data that correspond to *reference* pixel input channels. These are specified in the "analog input configuration.json" for each laboratory and can be easily accessed with the attribute "reference_pixel_idx" of the ADC. It is highly relevant that the order the pixels are listed in this array match the order of the array in the probe_pixel_idx argument. This means that if probe pixel 10 is listed first in the other array here reference pixel 10 needs to be listed first as well and so on. Otherwise the intensities on the probe array are not going to be normalized correctly. For the plotting to work correctly the pixels also need to be listed in the order of the wavenumber axis array of the spectrometer. * shape: 1D * E.g.: (32) or (64) index_dict (dict): Dictionary that maps the names of the input channels to their corresponding row in the ADCs' data as they are specified in the "analog input configuration.json" for each laboratory. I.e.: It contains the information which entries of the ADCs' data array belong choppers, wobblers etc. This dictionary can be easily accessed with the attribute "index_dict" of the ADC. r2r_indices (ndarray): Array that contains the indices of the rows in the ADCs' data that correspond to R-2R input channels in ascending order of significance (LSB channel first, etc.) * shape: 1D * E.g.: (4) prl (PRL): Pixel response linearization object which grants the functionality to linearize raw data according to the linearization parameters specified in the corresponding *pixel_linearization_fit_parameters.json* file (for each lab). vis_chopper_info (dict): Contains the information that is necessary to identify the different chopper states of the chopper that chops the UV/VIS pump pulse. It contains the keys "high voltage level" and "name". "high voltage level" is the voltage read by the ADC when the chopper reference output is high. It is needed as a reference for the digitization function that is used. The "name" key is required to determine to which channel of the adc the chopper is connected and its value needs to match the corresponding key in index_dict. interferometer_states (int): Number of interferometer states that are expected to be observed given a coherence time. wobbler_freq (float): Frequency in Hz with which the wobbler oscillates. laser_freq (float): Frequency (repetition rate) of the laser in Hz. bin_reference_values (ndarray): Reference values for each of the R-2R networks. * shape: 2D (4, 15) 4 rows for each of the R-2R Networks, 15 values for the 16 levels of the R-2R. background_handler (Background): Instance of Background class which can access the most recently collected background. This background is later subtracted from the raw data as dark noise correction. saver (SaveData): Object that manages saving of data (including counts, weights, probe wavenumber axis etc.) into their respective directories. If None is passed, no data is going to be saved. If the raw data checkbox was checked on the GUI the savers raw data attribute is set to True and the raw data is saved automatically. Defaults to None. """ def __init__( self, acq_queue: Queue, processing_queue: Queue, vis_delays: ndarray, ir_delays: ndarray, pixel_idx: ndarray, probe_pixel_idx: ndarray, reference_pixel_idx: ndarray, index_dict: dict, r2r_indices: ndarray, prl: PRL, vis_chopper_info: dict, wobbler_freq: float, laser_freq: float, interferometer_states: int, bin_reference_values: ndarray, background_handler: Background, saver: SaveData = None, ): super(multiprocessing.Process, self).__init__() # Multiprocessing Queue # Gets data from acquisition class self.acq_queue = acq_queue # Gives data to secondary processing class self.processing_queue = processing_queue # Pixel response linearisation self.prl = prl # Laser frequency in Hz self.laser_freq = laser_freq # Wobbler frequency in Hz self.wobbler_freq = wobbler_freq # Data saving instance self.saver = saver # Pixel index is an array which tells us which # entries in our adc data are pixels # Append probe and reference pixel indices self.pixel_idx = pixel_idx self.probe_pixel_idx = probe_pixel_idx self.ref_pixel_idx = reference_pixel_idx self.index_dict = index_dict self.r2r_indices = r2r_indices # Bin reference values for R-2R Network self.bin_reference_values = bin_reference_values # Load background data from file self.background = background_handler.load_background() # ---- Calculate the number of states that # are going to be observed for each device # This is required for the sort_data function # The number of interferometer states we expect to # observe have already been calculated self.interferometer_states = interferometer_states # In this experiment we have two different (UV/VIS) chopper states self.n_chopper_states = 2 # Calculate the number of wobbler states we are going to observe self.wobbler_states = int(laser_freq // wobbler_freq) # Stack the different number of states into one array. self.number_of_possible_states = np.array( [self.interferometer_states, self.n_chopper_states, self.wobbler_states] ) # ---- Save chopper high voltage level divided by 2 as list # We need this for the digitize function that identifies # the High and Low Chopper # We select half of the high voltage as the limit # at which the distinction between states is done # - UV/VIS chopper - self.vis_chopper_voltage_level = [vis_chopper_info["high voltage level"][0] / 2] # The chopper name is needed to retrieve the index (row) # within the adc raw data that corresponds to this # chopper self.vis_chopper_name = vis_chopper_info["name"][0] # --- Initialize array that # sorted data is written into. # This is different from other # experiments (without wobbler) # because the delay axis as well # as the average and single scan # dimension have been removed. # Here we need to phase cycle directly: # 1. The get_wobbler_states algorithm # is not "safe" in fringe cases. # This could lead to averaging # different wobbler states together # when averaging different scans. # If we directly average wobbler states # for each scan this problem does not # arise. # 2. It makes the most sense to directly # phase cycle the data of one scan: # this has to do with the # imprecision of the delay stage # when measuring two time the same delay # it will move to slightly different # locations yielding slightly different # phases. Averaging these my yield # artifacts. # 3. This reduces the size of the data # set that is going to be saved by # a factor of 4. self.data = np.zeros( ( self.probe_pixel_idx.size + 1, # +1 because of interferogram / pyro detector channel self.interferometer_states, # all interferometer positions self.n_chopper_states, # UV/VIS chopper open/closed self.wobbler_states, ) # all wobbler states ) self.counts = np.zeros(self.data.shape) self.weights = np.zeros(self.data.shape) # Because we use a Wobbler in this experiment # we need a second set of arrays where # the phase cycled/ scatter free transmissions # are written into. # In this case the data is average # relative intensity # (transmission (probe/ref)) # for each pixel. # By convention the averaged is the # 0 th entry of the 0th axis of the array # and the temp data is the 1st entry of the 0th # axis of the array. # dimensions: 2 = (current average scan, last single scan) # dimensions: (2, n_delays, n_probe_pixels, n_interferometer_states, n_chopper_states) # Phase cycled = scatter free self.phase_cycled_data = np.zeros( ( 2, # averaged and non averaged (scan) ir_delays.shape[ 0 ], # We only need one delay dimension because we always move both stages simultaneously self.probe_pixel_idx.size + 1, # +1 because of interferogram / pyro detector channel self.interferometer_states, # all interferometer positions self.n_chopper_states, ) # UV/VIS chopper open/closed ) self.phase_cycled_counts = np.zeros(self.phase_cycled_data.shape) self.phase_cycled_weights = np.zeros(self.phase_cycled_data.shape)
[docs] def run(self): while True: # Get data / information from acquisition process data_container = self.acq_queue.get() if type(data_container) == str: if data_container == "stop": break # Write data in dictionary into variable raw_data = data_container["data"] scan_idx = data_container["scan index"] delay_idx = data_container["delay index"] # Subtract background from raw data # of pixels and pixels only background_corrected_data = ( raw_data[self.pixel_idx] - self.background[self.pixel_idx, np.newaxis] ) # Linearize response of Pixels (and pixels only) intensities = self.prl.linearize(background_corrected_data) # Calculate transmission/ relative intensity # (probe intensity / reference intensity) # In LabView the transmission was not calculated # shot-to-shot. We believe it makes more sense # to do calculate shot to shot normalized data: # When comparing the sorting of non-normalized intensities # to sorting normalized intensities/transmission for time-domain # pump and frequency-domain probe experiments (namely FT-2D-IR) # for the same raw data set there was no significant difference. # The difference between the two was at least two orders of magnitude # smaller than the resulting frequency-domain difference absorption # spectrum. There seems to be no reason against sorting # shot-to-shot normalized intensities (transmission). We believe # this should also generally lead to better data quality. # Caveat: The raw data set this was tested on only contained # negative delays. We recommend investigating this in # more depth. transmission = ( intensities[self.probe_pixel_idx] / intensities[self.ref_pixel_idx] ) # Calculate/ generate interferometer counter positions counter_data = dp.calculate_counter_values( raw_data, self.r2r_indices, self.bin_reference_values ) # Check if lower and upper interferometer positions were observed if not (counter_data == 0).any(): logger.fatal("------THE 0TH BIN WAS NOT OBSERVED----------") if not (counter_data == self.interferometer_states).any(): logger.fatal( "------THE LAST ({}) BIN WAS NOT OBSERVED----------".format( self.interferometer_states ) ) # Get the corresponding chopper state for each shot # -UV/VIS Chopper- vis_chopper_states = np.digitize( raw_data[self.index_dict[self.vis_chopper_name]], self.vis_chopper_voltage_level, ) print(vis_chopper_states[:30]) # Get Wobbler States wobbler_states = dp.get_wobbler_states( raw_data[self.index_dict["wobbler"]], self.laser_freq, wobbler_freq=self.wobbler_freq, ) # Generate states array by stacking counter information, chopper_states # and wobbler states # ? There is proably a faster way to do this than stacking by preallocating arrays? Nvm for now states = np.vstack((counter_data, vis_chopper_states, wobbler_states)) # We need to sort our interferogram too. # Thats why "append" it to the unsorted transmissions. unsorted_data = np.vstack( (transmission, raw_data[self.index_dict["pyro detector"]]) ) # Sort and average data # --- Explanation for sorting interferometer states: # The sort_data function sorts samples (laser shots) # into an array, according to the counter position # for each shot, and then averages them. # Each entry corresponds to one interferometer # position, starting at the 0th bin and ending # with the self.interferometer_states bin. # What happens if the counter position is outside this interval? # The information is then added # to the bin at the closer end of the array. For counter data # greater than self.interferometer_states # the information is added to the last entry. # For counter_data < 0 it would be added to the # 0th entry of the array (but this cannot occur # in the case of counter data). # This "clipping" is not an issue. # At the end of the coherence time there should # be no signal, or else other procedures like zeropadding # would not work either. And thus clipping data to # the end of the time domain data is no problem. # (Technically we are ignoring underflowed counter # values here - but even in this case the discrepancy # should average out.) self.data[:], self.weights[:], self.counts[:], statistics = dp.sort_data( unsorted_data, states, self.number_of_possible_states ) # --------- Phase cycling ---------- # phase cycled = scatter free # -- Phase cycle transmissions -- # Remove scattering by averaging the different wobbler states # * In VB6 the interleaves are averaged # * once the difference spectra are calculated # * this (below) first phase cycles and then # * offers data for signal calculating # ** Update: We tested/compared the difference # ** for one data set and the absolute difference # ** was consistently smaller 10e-3 smaller # ** than the signal (this was done with interleaves # ** but the same logic applies to the wobbler) # ** We (Jens and us) agree that it conceptually # ** makes more sense to average the relative # ** intensities. #! It should be tested whether it makes sense to first average #! scans and then phase cycle (we are first phase cycling then averaging here) # * In this scenario we don't actually need to compute the wobbler states # * which wobbler position came first etc. # * It would suffice to separate the data in 4 different states like # * this: 0,1,2,3,0,1,2,3,0,1.... # * When averaging the way it is done here the actual position of the # * does not matter because they are averaged out before we average # * two different scans. idx = (1, delay_idx) self.phase_cycled_data[idx] = np.average( self.data, # Do not use weights to phase cycle because each wobbler position needs to be weighted equally axis=-1, # The last axis of the array is the wobbler axis ) # Now we need to calculate the total counts # that were observed in each interferometer position # for all wobbler states self.phase_cycled_counts[idx] = self.counts.sum(axis=-1) # We also need to update the weights (inverse variance of each state) self.phase_cycled_weights[idx] = self.weights.sum(axis=-1) # ----- Average data ---- # Create / Average "averaged" phase cycled data by using # weighted average between the phase cycled temp data # (weight = counts for each state in "temp" data set) # and the already existing average data # (weight = counts for each state in average data set) # --- Note: In LabView it was observed that averaging # the data in the frequency domain instead of the # time domain is beneficial. This was attributed # to the phase drifting between scans. # This is not done here because this would imply # computing the frequency domain only for display purposes. # If this would crash, the whole measurement would be stopped. # It is always the time domain data that should be saved, # because of postprocessing reasons(different windows, more # zeropadding etc.). --- # There are several scenarios in which the following # weighted average can fail (most likely in the 0th scan): # 1. The interferometer counter is not counting (correctly) # 2. The acquisition time is too small - thus not all states # have been observed within one scan. # 3. Everything should be working as expected but # some states have not been observed. This might # be the stage that did not completely move to its # corresponding defined end. # 4. The wavegeneration does not reach all interferometer # positions. # 5. ... # In all these cases we do not want the whole measurement to # crash. Thats why we put the averaging into a try statement: try: self.phase_cycled_data[0, delay_idx] = np.average( self.phase_cycled_data[:, delay_idx], axis=0, weights=self.phase_cycled_counts[:, delay_idx], ) except ZeroDivisionError: logger.fatal( """Averaging failed. Setting phase cycled counts and weights to 0. Not all states were observed.""" ) # Set phase cycled counts to 0 self.phase_cycled_counts[idx] = 0 # Set phase cycled weights to 0 self.phase_cycled_weights[idx] = 0 # Add everything to data container # and give it to processing queue data_container["sorted data"] = self.data.copy() data_container["phase cycled data"] = self.phase_cycled_data.copy() data_container["counts"] = self.counts.copy() data_container["phase cycled counts"] = self.phase_cycled_counts.copy() data_container["intensities"] = intensities.copy() data_container["transmission"] = transmission.copy() data_container["interferometer positions"] = counter_data data_container["statistics"] = statistics self.processing_queue.put(data_container.copy()) # ---------------------------------------- # Save data (if specified) # In this case save phase cycled data to save space if self.saver: # Use saver class to save data self.saver.save_scan( self.phase_cycled_data[1, delay_idx], scan_idx, delay_idx=delay_idx ) self.saver.save_avg( self.phase_cycled_data[0, delay_idx], delay_idx=delay_idx ) self.saver.save_counts( self.phase_cycled_counts[1, delay_idx], scan_idx, delay_idx=delay_idx, ) self.saver.save_weights( self.phase_cycled_weights[1, delay_idx], scan_idx, delay_idx=delay_idx, ) if self.saver.raw_data: self.saver.save_raw_data(raw_data, scan_idx, delay_idx=delay_idx) # Update counts self.phase_cycled_counts[0, delay_idx] += self.phase_cycled_counts[ 1, delay_idx ] # Once stop signal was received the function breaks out of the loop. # Now we need to tell the secondary processing to stop. self.processing_queue.put("stop")
[docs]class SecondaryProcessing(multiprocessing.Process): """ Args: processing_queue (Queue): Multiprocessing queue object that the primary processing process uses to pass data to the secondary processing process. plot_queue (Queue): Multiprocessing queue object that the secondary processing process uses to pass data to the plot thread. info_queue (Queue): Multiprocessing queue object which is used to transfer/hand over information to lineEdits on GUI. Contains (if applicable) scan index, delay index, interleave index, values for statistics groupBox etc. vis_delays (ndarray): Array containing the delays in fs for the UV/VIS stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) ir_delays (ndarray): Array containing the delays in fs for the IR stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) probe_pixel_idx (ndarray): Array that contains the indices of the rows in the ADCs' data that correspond to *probe* pixel input channels. These are specified in the "analog input configuration.json" for each laboratory and can be easily accessed with the attribute "probe_pixel_idx" of the ADC. It is highly relevant that the order the pixels are listed in this array match the order of the array in the reference_pixel_idx argument. This means that if reference pixel 3 is listed first in the other array here probe pixel 3 needs to be listed first as well and so on. Otherwise the intensities on the probe array are not going to be normalized correctly. For the plotting to work correctly the pixels also need to be listed in the order of the wavenumber axis array of the spectrometer. * shape: 1D * E.g.: (32) or (64) interferometer_states (int): Number of interferometer states that are expected to be observed given a coherence time. central_pixel (int): Index of the central pixel of the detector. Used to display the signal on the central pixel. saver (SaveData): Object that manages saving of data (including counts, weights, probe wavenumber axis etc.) into their respective directories. If None is passed, no data is going to be saved. If the raw data checkbox was checked on the GUI the savers raw data attribute is set to True and the raw data is saved automatically. Defaults to None. """ def __init__( self, processing_queue: Queue, plot_queue: Queue, info_queue: Queue, vis_delays: ndarray, ir_delays: ndarray, probe_pixel_idx: ndarray, interferometer_states: int, central_pixel, saver: SaveData = None, ): super(multiprocessing.Process, self).__init__() # Multiprocessing Queue # Gets data from acquisition class self.processing_queue = processing_queue # Gives data to secondary processing class self.plot_queue = plot_queue # Give experimental status and statistics to # GUI self.info_queue = info_queue # Delays self.ir_delays = ir_delays self.vis_delays = vis_delays # The file saver is required the save the resulting plot self.saver = saver # Pixel index is an array which tells us which # entries in our adc data are pixels # In this case we only need this to # preallocate our signal array self.probe_pixel_idx = probe_pixel_idx # Number of interferometer states we expect to observe self.interferometer_states = interferometer_states # Central pixel of MCT array self.central_pixel = central_pixel # Preallocate array that will hold signal information # By convention the averaged is the # 0 th entry of the 0th axis of the array # and the temp data is the 1st entry of the 0th # axis of the array. self.time_domain_viper_spectrum = np.zeros( ( 2, ir_delays.shape[0], self.probe_pixel_idx.size, self.interferometer_states, ) ) self.time_domain_2d_ir_spectrum = np.zeros( ( 2, ir_delays.shape[0], self.probe_pixel_idx.size, self.interferometer_states, ) ) # We cannot preallocate an array that holds the combined # frequency data set because we cannot exactly determine # its size. The reason for this is, that the time domain # data is cut off at the zerobin and then zeropadded. # Instead we use a list to hold the single scan # and averaged 2D frequency domain spectrum self.freq_domain_viper_spectrum = [None, None] self.freq_domain_2d_ir_spectrum = [None, None] # Preallocate some more lists holding relevant information self.zerobin = [None, None] self.pump_frequency_axis = [None, None] self.opa_range = [None, None] self.central_pump_wn = [None, None] self.opa_spectrum = [None, None] # Needed for 2D heatmap /contour plot # intp: interpolated self.intp_viper_signal = [None, None] self.intp_2d_ir_signal = [None, None]
[docs] def run(self): while True: # Get data / information from acquisition process data_container = self.processing_queue.get() if type(data_container) == str: if data_container == "stop": break # ---------------------------------------- # Calculate information for plotting and GUI # We first calculate everything that is required # for plotting and pass it to the plot queue because # plotting can also cost time. Only then we # calculate the values that we want to display on # line edits on the GUI # Get delay index delay_idx = data_container["delay index"] # Get the sorted and phase cycled data sorted_data = data_container["phase cycled data"] # Get interferogram # For this we need the phase cycled counts because # we want to average interferograms for vis chopper # on and off because they should not be different. # (The UV/VIS chopper does not influence the # IR interferometer) phase_cycled_counts = data_container["phase cycled data"][:, -1, delay_idx] # Using the phase_cycled_counts as weights # can fail if the averaging in primary processing failed # for this reason we put it into a try statement try: # The last entry on the 2nd axis of sorted_data holds the # interferograms (averaged and single scan) for both chopper # states interferogram = np.average( sorted_data[:, delay_idx, -1], axis=2, # This is the axis of the chopper states weights=phase_cycled_counts, ) except ZeroDivisionError: interferogram = np.average( sorted_data[:, delay_idx, -1], axis=2, # This is the axis of the chopper states # Do not use any weights if weighted average failed. ) # Calculate time domain absorption for vis chopper on and off self.time_domain_absorption = -np.log10( sorted_data[:, delay_idx, :-1] ) # The last entry on the 2nd axis is the interferogram # Now calculate all the different *time domain* difference signals # Last axis UV/VIS pump on-off # Beginners notes (*IR Pump in time domain*): # (IR on, UV off): IR pump/IR probe # (IR on, UV on): VIPER + IR pump/IR probe # The Background and the TRIR signal do not cause # an oscillating signal in the time domain - only # an offset which is removed upon fourier transform self.time_domain_2d_ir_spectrum[:, delay_idx] = self.time_domain_absorption[ :, :, :, 0 ] self.time_domain_viper_spectrum[:, delay_idx] = ( self.time_domain_absorption[:, :, :, 1] - self.time_domain_absorption[:, :, :, 0] ) # In some cases the processing of the time domain spectra might not work # thats why we put it in a try statement try: # --- Process single scan data --- # Calculate frequency domain VIPER absorption spectrum # * At the moment we use no window function for fft self.freq_domain_viper_spectrum[1], ifgr_info = dp.process_ft2dir_data( self.time_domain_viper_spectrum[1, delay_idx], interferogram[1], # this entry holds the interferogram ) # Calculate frequency domain 2D-IR absorption spectrum # * At the moment we use no window function for fft self.freq_domain_2d_ir_spectrum[1], _ = dp.process_ft2dir_data( self.time_domain_2d_ir_spectrum[1, delay_idx], interferogram[1], # this entry holds the interferogram ) # The ifgr_info (interferogram info) is the same in both cases # thats why we disregard the second one. # Extract the relevant information regarding the spectrum of the OPA self.zerobin[1] = ifgr_info[0] self.pump_frequency_axis[1] = ifgr_info[3] self.opa_range[1] = ifgr_info[4][1] self.central_pump_wn[1] = ifgr_info[3][ifgr_info[4][0]] self.opa_spectrum[1] = np.abs( ifgr_info[2] ) # The fft of the interferogram yields complex values - the OPA spectrum is the amplitude # Interpolate frequency domain 2d spectrum so that it can be # displayed using pyqtgraph # while doing this also transpose array s.t. pump axis # is the 0th axis and probe axis is the 1st axis. # Get the range of the pump axis in which the OPA # emits light pump_axis = self.pump_frequency_axis[1][ self.opa_range[1] ] # Pump axis is the same for both VIPER and 2D-IR # VIPER self.intp_viper_signal[1] = dp.generate_img_data( data_container["probe axis"], pump_axis, self.freq_domain_viper_spectrum[1] .take(self.opa_range[1], axis=-1) .T, ) # 2D-IR self.intp_2d_ir_signal[1] = dp.generate_img_data( data_container["probe axis"], pump_axis, self.freq_domain_2d_ir_spectrum[1] .take(self.opa_range[1], axis=-1) .T, ) # --- Process averaged data --- # Calculate frequency domain VIPER absorption spectrum # * At the moment we use no window function for fft self.freq_domain_viper_spectrum[0], ifgr_info = dp.process_ft2dir_data( self.time_domain_viper_spectrum[0, delay_idx], interferogram[0], # this entry holds the interferogram ) # Calculate frequency domain 2D-IR absorption spectrum # * At the moment we use no window function for fft self.freq_domain_2d_ir_spectrum[0], _ = dp.process_ft2dir_data( self.time_domain_2d_ir_spectrum[0, delay_idx], interferogram[0], # this entry holds the interferogram ) # The ifgr_info (interferogram info) is the same in both cases # thats why we disregard the second one. # Extract the relevant information regarding the spectrum of the OPA self.zerobin[0] = ifgr_info[0] self.pump_frequency_axis[0] = ifgr_info[3] self.opa_range[0] = ifgr_info[4][1] self.central_pump_wn[0] = ifgr_info[3][ifgr_info[4][0]] self.opa_spectrum[0] = np.abs( ifgr_info[2] ) # The fft of the interferogram yields complex values - the OPA spectrum is the amplitude # Interpolate frequency domain 2d spectrum so that it can be # displayed using pyqtgraph # while doing this also transpose array s.t. pump axis # is the 0th axis and probe axis is the 1st axis. # Get the range of the pump axis in which the OPA # emits light pump_axis = self.pump_frequency_axis[0][ self.opa_range[0] ] # Pump axis is the same for both VIPER and 2D-IR # VIPER self.intp_viper_signal[0] = dp.generate_img_data( data_container["probe axis"], pump_axis, self.freq_domain_viper_spectrum[0] .take(self.opa_range[0], axis=-1) .T, ) # 2D-IR self.intp_2d_ir_signal[0] = dp.generate_img_data( data_container["probe axis"], pump_axis, self.freq_domain_2d_ir_spectrum[0] .take(self.opa_range[0], axis=-1) .T, ) except: logger.fatal( "Processing of time domain data to frequency domain failed. Setting all affected variables to 0." ) # Write 0 in all variables # This is done so that the plotting does not crash self.freq_domain_viper_spectrum[0] = np.zeros( self.time_domain_viper_spectrum[0, delay_idx].shape ) self.freq_domain_2d_ir_spectrum[0] = np.zeros( self.time_domain_2d_ir_spectrum[0, delay_idx].shape ) self.zerobin[0] = 0 self.pump_frequency_axis[0] = np.arange(interferogram[0].size) self.opa_range[0] = np.arange(interferogram[0].size) self.central_pump_wn[0] = 0 self.opa_spectrum[0] = np.zeros(interferogram[0].size) self.intp_viper_signal[0] = np.zeros( self.time_domain_viper_spectrum[0, delay_idx].shape ) self.intp_2d_ir_signal[0] = np.zeros( self.time_domain_2d_ir_spectrum[0, delay_idx].shape ) self.freq_domain_viper_spectrum[1] = np.zeros( self.time_domain_viper_spectrum[1, delay_idx].shape ) self.freq_domain_2d_ir_spectrum[1] = np.zeros( self.time_domain_2d_ir_spectrum[1, delay_idx].shape ) self.zerobin[1] = 0 self.pump_frequency_axis[1] = np.arange(interferogram[1].size) self.opa_range[1] = np.arange(interferogram[1].size) self.central_pump_wn[1] = 0 self.opa_spectrum[1] = np.zeros(interferogram[1].size) self.intp_viper_signal[1] = np.zeros( self.time_domain_viper_spectrum[1, delay_idx].shape ) self.intp_2d_ir_signal[1] = np.zeros( self.time_domain_2d_ir_spectrum[1, delay_idx].shape ) # Calculate VIPER time domain spectrum for each wobbler state # for central pixel to double check whether phase cycling # is working. # --- Note considering naming: # For experiments with interleaves we choose # to call the non phase cycled signals="signal" # and the phase cycled signal="phase_cycled_signal" # here we reverse this order. So non phase cycled # signals = "npc_signal" and phase cycled signal # = "signal". # We do this because with interleaves it takes # time to measure all delay stage positions. # Here all different phase states are collected # within one acquisition. --- # npc = non phase cycled # abs = absorption npc_time_domain_abs_central_pixel = -np.log10( data_container["sorted data"][self.central_pixel] ) npc_time_domain_viper_central_pixel = ( npc_time_domain_abs_central_pixel[:, 1] - npc_time_domain_abs_central_pixel[:, 0] ) # Calculate the average intensity for each pixel avg_intensities = np.average(data_container["intensities"], axis=1) # Calculate the standard deviation of the # intensities std_intensities = np.std(data_container["intensities"], axis=1, ddof=1) # Add everything to data container # and give to plot queue # VIPER data_container[ "time domain viper spectrum" ] = self.time_domain_viper_spectrum[:, delay_idx] data_container[ "non phase cycled time domain viper central pixel" ] = npc_time_domain_viper_central_pixel data_container[ "frequency domain viper spectrum" ] = self.freq_domain_viper_spectrum data_container[ "interpolated frequency domain viper spectrum" ] = self.intp_viper_signal # 2D IR data_container[ "time domain 2d-ir spectrum" ] = self.time_domain_2d_ir_spectrum[:, delay_idx] data_container[ "frequency domain 2d-ir spectrum" ] = self.freq_domain_2d_ir_spectrum data_container[ "interpolated frequency domain 2d-ir spectrum" ] = self.intp_2d_ir_signal # Interferometer data_container["zerobin"] = self.zerobin data_container["interferogram"] = interferogram data_container["pump frequency axis"] = self.pump_frequency_axis data_container["opa range"] = self.opa_range data_container["central pump wn"] = self.central_pump_wn data_container["opa spectrum"] = self.opa_spectrum # MCT data_container["average intensity"] = avg_intensities data_container["std intensity"] = std_intensities self.plot_queue.put(data_container.copy()) # ---------------------------------------- # Calculate everything that is needed for statistical information # on GUI and add it to data container and give it to info queue # Calculate the average intensities over all pixels data_container["mean state intensity"] = np.average( data_container["transmission"], axis=1 ) data_container["mean state std"] = data_container["statistics"][1] self.info_queue.put(data_container.copy()) # Once stop signal was received the function breaks out of the loop. # Now we need to tell the plotting and updating of info on GUI to stop. self.plot_queue.put("stop") self.info_queue.put("stop")
[docs]class PyqtPlotting: """ Pyqt Plotting class (Qt multithreaded). This class is necessary for displaying plots on the GUI. PyQtGraph is used as the plotting engine. Generally the plots are set up first (type of plot, layout, title etc.). On the first run, the plots are drawn for the first time. Then the plots are updated every iteration. We update the same plot references every time to make it more efficient. Args: widget_pyqtgraph (QWidget): WidgetPyqtgraph object on which the plots are going to be displayed. Has methods for plot manipulation (i.e. removal of plots, autoscale). adc (ADC): Analog to digital converter hardware object which is used to communicate with and read data from the ADC. plot_queue (Queue): Multiprocessing queue object that the secondary processing process uses to pass data to the plot thread. vis_delays (ndarray): Array containing the delays in fs for the UV/VIS stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) ir_delays (ndarray): Array containing the delays in fs for the IR stage in the 0th column and their corresponding weights in the 1st column. I.e.: loaded from a delay file which can be generated via the delay file editor. These delays need to be temporally offset relative to the IR delays. * shape: 2D * E.g.: (number of delays, 2) central_pixel (int): Index of the central pixel of the detector. Used to display the signal on the central pixel. """
[docs] class Signals(QObject): new_data = pyqtSignal(dict)
def __init__( self, widget_pyqtgraph, adc: ADC, plot_queue: Queue, vis_delays: ndarray, ir_delays: ndarray, central_pixel: int, ): # Assign attributes self.adc = adc self.vis_delays = vis_delays self.ir_delays = ir_delays self.central_pixel = central_pixel self.widget_pyqtgraph = widget_pyqtgraph self.graphics_layout = widget_pyqtgraph.graphics_layout # Multiprocessing Queue self.plot_queue = plot_queue # Setup signals and threadpool self.threadpool = QThreadPool() self.signals = self.Signals() # # Clear old plots self.widget_pyqtgraph.remove_plots() # Create dictionary that holds reference to lines etc. self.plot_ref = {} # Add a sub-layout to hold the 2D plots and the histograms in the first row self.widget_pyqtgraph.plots["upper layout"] = self.graphics_layout.addLayout( colspan=6 ) # Setup plots that hold heatmap 2D plot y-axis: pump axis, x-axis: probe axis self.widget_pyqtgraph.plots[ "single 2d-ir-signal heatmap" ] = self.widget_pyqtgraph.plots["upper layout"].addPlot() self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"].setTitle( "2D-IR-signal - IR delay {} fs" ) self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"].setLabel( "bottom", "probe frequency &omega;<sub>1</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"].setLabel( "left", "pump frequency &omega;<sub>3</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"] ) # Add the HistogramLUTItem to the plotlayout directly after 2D plot # Also add the item. With that it will be directly drawn at the correct position self.plot_ref["single 2d-ir-signal heatmap histogram"] = pg.HistogramLUTItem() self.widget_pyqtgraph.plots["upper layout"].addItem( self.plot_ref["single 2d-ir-signal heatmap histogram"] ) self.widget_pyqtgraph.plots[ "single viper-signal heatmap" ] = self.widget_pyqtgraph.plots["upper layout"].addPlot() self.widget_pyqtgraph.plots["single viper-signal heatmap"].setTitle( "VIPER-signal - UV/VIS delay {} fs" ) self.widget_pyqtgraph.plots["single viper-signal heatmap"].setLabel( "bottom", "probe frequency &omega;<sub>1</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["single viper-signal heatmap"].setLabel( "left", "pump frequency &omega;<sub>3</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["single viper-signal heatmap"] ) # Add the HistogramLUTItem to the plotlayout directly after 2D plot # Also add the item. With that it will be directly drawn at the correct position self.plot_ref["single viper-signal heatmap histogram"] = pg.HistogramLUTItem() self.widget_pyqtgraph.plots["upper layout"].addItem( self.plot_ref["single viper-signal heatmap histogram"] ) self.widget_pyqtgraph.plots[ "avg viper-signal heatmap" ] = self.widget_pyqtgraph.plots["upper layout"].addPlot() self.widget_pyqtgraph.plots["avg viper-signal heatmap"].setTitle( "Scan averaged VIPER" ) self.widget_pyqtgraph.plots["avg viper-signal heatmap"].setLabel( "bottom", "probe frequency &omega;<sub>1</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["avg viper-signal heatmap"].setLabel( "left", "pump frequency &omega;<sub>3</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["avg viper-signal heatmap"] ) self.plot_ref["avg viper-signal heatmap histogram"] = pg.HistogramLUTItem() self.widget_pyqtgraph.plots["upper layout"].addItem( self.plot_ref["avg viper-signal heatmap histogram"] ) # Setup colormap for heatmaps # Credit: https://github.com/pyqtgraph/pyqtgraph/issues/561 colormap = cm.get_cmap("seismic") # cm.get_cmap("CMRmap") colormap._init() # [:-3,:] because the last values of the colormap are fringe # cases which are matplotlib specific and do not define our # colormap self.lut = (colormap._lut * 255).view(np.ndarray)[ :-3, : ] # Convert matplotlib colormap from 0-1 to 0 -255 for Qt # Skip to next row self.graphics_layout.nextRow() # Add a sub-layout to hold the 2 plots in the second row self.widget_pyqtgraph.plots["middle layout"] = self.graphics_layout.addLayout( colspan=6 ) # Setup plot that holds plot for interferogram self.widget_pyqtgraph.plots["interferogram"] = self.widget_pyqtgraph.plots[ "middle layout" ].addPlot(colspan=2) self.widget_pyqtgraph.plots["interferogram"].setTitle( "Interferogram (zerobin: {})" ) self.widget_pyqtgraph.plots["interferogram"].setLabel("bottom", "position") self.widget_pyqtgraph.plots["interferogram"].setLabel( "left", "intensity [a.u.]" ) self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["interferogram"]) # Setup plot that holds plot for OPA pump spectrum self.widget_pyqtgraph.plots["opa spectrum"] = self.widget_pyqtgraph.plots[ "middle layout" ].addPlot(colspan=2) self.widget_pyqtgraph.plots["opa spectrum"].setTitle("OPA pump spectrum") self.widget_pyqtgraph.plots["opa spectrum"].setLabel( "bottom", "pump frequency &omega;<sub>3</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["opa spectrum"].setLabel("left", "intensity [a.u.]") self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["opa spectrum"]) # Setup plot that holds time domain data for central pixel for each wobbler state self.widget_pyqtgraph.plots[ "time-domain central pixel" ] = self.widget_pyqtgraph.plots["middle layout"].addPlot(colspan=2) self.widget_pyqtgraph.plots["time-domain central pixel"].setTitle( "Time domain absorption for central pixel" ) self.widget_pyqtgraph.plots["time-domain central pixel"].setLabel( "bottom", "interferometer position" ) self.widget_pyqtgraph.plots["time-domain central pixel"].setLabel( "left", "intensity [a.u.]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["time-domain central pixel"] ) # Add next row for the last row of plots. self.graphics_layout.nextRow() # Setup plot that displays intensities self.widget_pyqtgraph.plots["intensities"] = self.graphics_layout.addPlot( row=2, col=0 ) self.widget_pyqtgraph.plots["intensities"].setTitle("Average intensities") self.widget_pyqtgraph.plots["intensities"].setLabel( "bottom", "probe frequency &omega;<sub>1</sub> [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["intensities"].setLabel("left", "intensity [a.u.]") self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["intensities"]) # Setup plot that displays counts for different wobbler states self.widget_pyqtgraph.plots["counts"] = self.graphics_layout.addPlot( row=2, col=1 ) self.widget_pyqtgraph.plots["counts"].setTitle( "How often a position was measured" ) self.widget_pyqtgraph.plots["counts"].setLabel( "bottom", "interferometer position" ) self.widget_pyqtgraph.plots["counts"].setLabel("left", "counts") self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["counts"]) # Setup plot that displays interferometer positions over time self.widget_pyqtgraph.plots["stage position"] = self.graphics_layout.addPlot( row=2, col=2 ) self.widget_pyqtgraph.plots["stage position"].setTitle( "Position of interferometer stage" ) self.widget_pyqtgraph.plots["stage position"].setLabel("bottom", "time") self.widget_pyqtgraph.plots["stage position"].setLabel( "left", "interferometer counter value" ) self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["stage position"]) # Connect signal that data has arrived to update the plot self.signals.new_data.connect( lambda data_container: self.update_plot(data_container) ) # Start loop that gets data from queue in Qt Thread self.work = Worker(self.run)
[docs] def run(self): while True: data_container = self.plot_queue.get() if type(data_container) == str: if data_container == "stop": break self.signals.new_data.emit(data_container)
[docs] def update_plot(self, data_container): delay_idx = data_container["delay index"] intp_viper_signal = data_container[ "interpolated frequency domain viper spectrum" ] intp_2d_ir_signal = data_container[ "interpolated frequency domain 2d-ir spectrum" ] interferogram = data_container["interferogram"] pump_axes = data_container["pump frequency axis"] opa_range = data_container["opa range"] opa_spectrum = data_container["opa spectrum"] zerobin = data_container["zerobin"] time_domain_central_pixel = data_container["time domain viper spectrum"][ 1, self.central_pixel ] npc_time_domain_central_pixel = data_container[ "non phase cycled time domain viper central pixel" ] probe_axis = data_container["probe axis"] avg_intensities = data_container["average intensity"] std_intensities = data_container["std intensity"] # Update titles with current delay self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"].setTitle( "2D-IR-signal - IR delay {} fs".format(self.ir_delays[delay_idx][0]) ) self.widget_pyqtgraph.plots["single viper-signal heatmap"].setTitle( "VIPER-signal - UV/VIS delay {} fs".format(self.vis_delays[delay_idx][0]) ) # Update interferogram title with zerobin that the algorithm found self.widget_pyqtgraph.plots["interferogram"].setTitle( "Interferogram (zerobin: {})".format(zerobin[1]) ) #! Add corresponding phase to zerobin too. # Needs to be bigger than three because # we define three colorbar/histogram items # in init which are already in plot ref if len(self.plot_ref) > 3: # -----------Start 2D Plots--------------- # Update 2D-IR image: probe frequency vs. pump frequency # Single Scan # Get old levels so it does not rescale the colors everytime (see below) levels = self.plot_ref["single 2d-ir-signal heatmap histogram"].getLevels() self.plot_ref["single 2d-ir-signal heatmap"].setImage(intp_2d_ir_signal[1]) # "Disable" autoscale after 0th scan has run. if data_container["scan index"] > 0: # Set to old levels of the histogram self.plot_ref["single 2d-ir-signal heatmap histogram"].setLevels( *levels ) # Update contour lines dp.update_contour_lines( intp_2d_ir_signal[1], self.plot_ref["single 2d-ir-signal heatmap contours"], ) # Update 2d image: probe frequency vs. pump frequency # Single Scan # Get old levels so it does not rescale the colors everytime (see below) levels = self.plot_ref["single viper-signal heatmap histogram"].getLevels() self.plot_ref["single viper-signal heatmap"].setImage(intp_viper_signal[1]) # "Disable" autoscale after 0th scan has run. if data_container["scan index"] > 0: # Set to old levels of the histogram self.plot_ref["single viper-signal heatmap histogram"].setLevels( *levels ) # Update contour lines dp.update_contour_lines( intp_viper_signal[1], self.plot_ref["single viper-signal heatmap contours"], ) # Scan averaged # Get old levels so it does not rescale the colors everytime (see below) levels = self.plot_ref["avg viper-signal heatmap histogram"].getLevels() self.plot_ref["avg viper-signal heatmap"].setImage(intp_viper_signal[0]) # "Disable" autoscale after 0th scan has run. if data_container["scan index"] > 0: # Set to old levels of the histogram self.plot_ref["avg viper-signal heatmap histogram"].setLevels(*levels) # Update contour lines dp.update_contour_lines( intp_viper_signal[0], self.plot_ref["avg viper-signal heatmap contours"] ) # ----------End 2D Plots---------------- # Update interferogram # Scan averaged self.plot_ref["single interferogram"].setData( # x = probe_axis, #! Add mm or fs scale y=interferogram[1] ) # Avg scan self.plot_ref["avg interferogram"].setData( # x = probe_axis, #! Add mm or fs scale y=interferogram[0] ) # Update OPA spectrum # Single scan self.plot_ref["single opa spectrum"].setData( x=pump_axes[1][: opa_spectrum[1].size], y=opa_spectrum[1] ) # Scan averaged self.plot_ref["avg opa spectrum"].setData( x=pump_axes[0][: opa_spectrum[0].size], y=opa_spectrum[0] ) # Update time domain spectrum for central pixel for all wobbler states for i in range(npc_time_domain_central_pixel.shape[-1]): self.plot_ref["time-domain signal wobbler state {}".format(i)].setData( y=npc_time_domain_central_pixel[:, i] ) # Update phase cycled time domain spectrum in same plot self.plot_ref["time-domain signal"].setData(y=time_domain_central_pixel) # Update intensity error bars for probe array self.plot_ref["probe error bars"].setData( x=probe_axis, y=avg_intensities[self.adc.probe_pixel_idx], height=5 * std_intensities[self.adc.probe_pixel_idx], ) # Update intensity error bars for reference array self.plot_ref["ref error bars"].setData( x=probe_axis, y=avg_intensities[self.adc.reference_pixel_idx], height=5 * std_intensities[self.adc.reference_pixel_idx], ) # Update intensities self.plot_ref["probe intensities"].setData( x=probe_axis, y=avg_intensities[self.adc.probe_pixel_idx] ) self.plot_ref["ref intensities"].setData( x=probe_axis, y=avg_intensities[self.adc.reference_pixel_idx] ) # Update counts for a given stage position - (every wobbler state and every chopper one line) for chopper_state in range(data_container["counts"].shape[-2]): for wobbler_state in range(data_container["counts"].shape[-1]): self.plot_ref[ "stage counts c{} w{}".format(chopper_state, wobbler_state) ].setData( data_container["counts"][ 0, :, chopper_state, wobbler_state ], # Choose pixel 0 - all pixels have the same counts ) # Plot interferometer position self.plot_ref["stage position"].setData( # x = pump_axes[1][opa_range[1]], #! add proper time scale y=data_container["interferometer positions"], #! rescale to fs or mm ) else: # First time plotting signal_pen = pg.mkPen(color="#d62728", width=2.5, style=QtCore.Qt.SolidLine) npc_signal_pen = pg.mkPen( color="#A9A9A9", width=1.2, style=QtCore.Qt.SolidLine ) avg_signal_pen = pg.mkPen( color="#17becf", width=2.5, style=QtCore.Qt.SolidLine ) position_pen = pg.mkPen( color="#9467bd", width=1.5, style=QtCore.Qt.SolidLine ) count_pen = [ pg.mkPen(color="#2ca02c", width=1, style=QtCore.Qt.SolidLine), pg.mkPen(color="#17becf", width=1, style=QtCore.Qt.SolidLine), ] probe_pen = pg.mkPen(color="#1f77b4", width=2.5, style=QtCore.Qt.SolidLine) reference_pen = pg.mkPen( color="#ff7f0e", width=2.5, style=QtCore.Qt.SolidLine ) std_intensity_pen = pg.mkPen( color="#7f7f7f", width=1.5, style=QtCore.Qt.SolidLine ) # ? dashed lines? bisector_pen = pg.mkPen( color="#7f7f7f", width=1.5, style=QtCore.Qt.SolidLine ) # Histogram colormap seismic = pg.graphicsItems.GradientEditorItem.Gradients["seismic"] # Plot 2D-IR image: probe vs pump frequency (wavenumber) # Single scan self.plot_ref["single 2d-ir-signal heatmap"] = pg.ImageItem( intp_2d_ir_signal[1] ) # Generate a scrollable colorbar # This generates a histogram with # which it is possible to scale the # Data which will be displayed in # the heatmaps # First create a reference for the histogram # which containes the image item "single time-signal heatmap" # This basically means, that HistogramLUTItem contains the data # from the heatmap self.plot_ref["single 2d-ir-signal heatmap histogram"].setImageItem( self.plot_ref["single 2d-ir-signal heatmap"] ) # Set the color levels of the histogram self.plot_ref[ "single 2d-ir-signal heatmap histogram" ].gradient.restoreState(seismic) # Get the range of the pump axis in which the OPA # emits light pump_axis = pump_axes[1][opa_range[1]] # Scale image to match axes dp.scale_img( probe_axis, pump_axis, intp_2d_ir_signal[1], self.plot_ref["single 2d-ir-signal heatmap"], ) self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"].addItem( self.plot_ref["single 2d-ir-signal heatmap"] ) self.plot_ref["single 2d-ir-signal heatmap"].setLookupTable(self.lut) # Generate contour lines self.plot_ref[ "single 2d-ir-signal heatmap contours" ] = dp.generate_contour_lines( intp_2d_ir_signal[1], self.plot_ref["single 2d-ir-signal heatmap"] ) # Add bisector to plot (we do not add it to plot_ref because we are not going to change it, ever) bisector = pg.InfiniteLine(angle=45, pen=bisector_pen) bisector.setZValue(10) self.widget_pyqtgraph.plots["single 2d-ir-signal heatmap"].addItem(bisector) # Plot VIPER image: probe vs pump frequency (wavenumber) # Single scan self.plot_ref["single viper-signal heatmap"] = pg.ImageItem( intp_viper_signal[1] ) # Generate a scrollable colorbar # This generates a histogram with # which it is possible to scale the # Data which will be displayed in # the heatmaps # First create a reference for the histogram # which containes the image item "single time-signal heatmap" # This basically means, that HistogramLUTItem contains the data # from the heatmap self.plot_ref["single viper-signal heatmap histogram"].setImageItem( self.plot_ref["single viper-signal heatmap"] ) # Set the color levels of the histogram self.plot_ref[ "single viper-signal heatmap histogram" ].gradient.restoreState(seismic) # Get the range of the pump axis in which the OPA # emits light pump_axis = pump_axes[1][opa_range[1]] # Scale image to match axes dp.scale_img( probe_axis, pump_axis, intp_viper_signal[1], self.plot_ref["single viper-signal heatmap"], ) self.widget_pyqtgraph.plots["single viper-signal heatmap"].addItem( self.plot_ref["single viper-signal heatmap"] ) self.plot_ref["single viper-signal heatmap"].setLookupTable(self.lut) # Generate contour lines self.plot_ref[ "single viper-signal heatmap contours" ] = dp.generate_contour_lines( intp_viper_signal[1], self.plot_ref["single viper-signal heatmap"] ) # Add bisector to plot (we do not add it to plot_ref because we are not going to change it, ever) bisector = pg.InfiniteLine(angle=45, pen=bisector_pen) bisector.setZValue(10) self.widget_pyqtgraph.plots["single viper-signal heatmap"].addItem(bisector) # Plot 2d image: probe vs pump frequency (wavenumber) # Scan averaged self.plot_ref["avg viper-signal heatmap"] = pg.ImageItem( intp_viper_signal[0] ) self.plot_ref["avg viper-signal heatmap histogram"].setImageItem( self.plot_ref["avg viper-signal heatmap"] ) # Set the color levels of the histogram self.plot_ref["avg viper-signal heatmap histogram"].gradient.restoreState( seismic ) # Get the range of the pump axis in which the OPA # emits light pump_axis = pump_axes[1][opa_range[1]] # Scale image to match axes dp.scale_img( probe_axis, pump_axis, intp_viper_signal[0], self.plot_ref["avg viper-signal heatmap"], ) self.widget_pyqtgraph.plots["avg viper-signal heatmap"].addItem( self.plot_ref["avg viper-signal heatmap"] ) self.plot_ref["avg viper-signal heatmap"].setLookupTable(self.lut) # Generate contour lines self.plot_ref[ "avg viper-signal heatmap contours" ] = dp.generate_contour_lines( intp_viper_signal[1], self.plot_ref["avg viper-signal heatmap"] ) # Add bisector to plot (we do not add it to plot_ref because we are not going to change it, ever) bisector = pg.InfiniteLine(angle=45, pen=bisector_pen) bisector.setZValue(10) self.widget_pyqtgraph.plots["avg viper-signal heatmap"].addItem(bisector) # Plot interferogram # Single scan self.plot_ref["single interferogram"] = self.widget_pyqtgraph.plots[ "interferogram" ].plot( # x = probe_axis, #! Add mm or fs scale y=interferogram[1], name="interferogram of this scan for current delay", pen=signal_pen, ) # Scan averaged self.plot_ref["avg interferogram"] = self.widget_pyqtgraph.plots[ "interferogram" ].plot( # x = probe_axis, #! Add mm or fs scale y=interferogram[0], name="average interferogram for current delay", pen=avg_signal_pen, ) # Plot OPA spectrum # Single scan self.plot_ref["single opa spectrum"] = self.widget_pyqtgraph.plots[ "opa spectrum" ].plot( x=pump_axes[1][: opa_spectrum[1].size], y=opa_spectrum[1], name="Pump OPA spectrum of this scan for current delay", pen=signal_pen, ) # Scan averaged self.plot_ref["avg opa spectrum"] = self.widget_pyqtgraph.plots[ "opa spectrum" ].plot( x=pump_axes[0][: opa_spectrum[0].size], y=opa_spectrum[0], name="average pump OPA spectrum for current delay", pen=avg_signal_pen, ) # Set x-axis range on pump OPA peak self.widget_pyqtgraph.plots["opa spectrum"].setRange( xRange=pump_axes[0][opa_range[0][[0, -1]]], padding=0 ) # Plot time domain spectrum for central pixel for all wobbler states for i in range(npc_time_domain_central_pixel.shape[-1]): self.plot_ref[ "time-domain signal wobbler state {}".format(i) ] = self.widget_pyqtgraph.plots["time-domain central pixel"].plot( y=npc_time_domain_central_pixel[:, i], name="wobbler state {}".format(i), pen=npc_signal_pen, ) # Plot phase cycled time domain spectrum in same plot self.plot_ref["time-domain signal"] = self.widget_pyqtgraph.plots[ "time-domain central pixel" ].plot( y=time_domain_central_pixel, name="phase cycled time domain signal", pen=signal_pen, ) # Create intensity error bars for probe array self.plot_ref["probe error bars"] = pg.ErrorBarItem( x=probe_axis, y=avg_intensities[self.adc.probe_pixel_idx], height=5 * std_intensities[self.adc.probe_pixel_idx], beam=0.3, pen=std_intensity_pen, ) self.widget_pyqtgraph.plots["intensities"].addItem( self.plot_ref["probe error bars"] ) # Create intensity error bars for reference array self.plot_ref["ref error bars"] = pg.ErrorBarItem( x=probe_axis, y=avg_intensities[self.adc.reference_pixel_idx], height=5 * std_intensities[self.adc.reference_pixel_idx], beam=0.3, pen=std_intensity_pen, ) self.widget_pyqtgraph.plots["intensities"].addItem( self.plot_ref["ref error bars"] ) # Plot intensities self.plot_ref["probe intensities"] = self.widget_pyqtgraph.plots[ "intensities" ].plot( x=probe_axis, y=avg_intensities[self.adc.probe_pixel_idx], name="Average intensities on probe array", pen=probe_pen, ) self.plot_ref["ref intensities"] = self.widget_pyqtgraph.plots[ "intensities" ].plot( x=probe_axis, y=avg_intensities[self.adc.reference_pixel_idx], name="Average intensities on reference array", pen=reference_pen, ) # Plot counts for a given stage position - (every wobbler state and every chopper one line) for chopper_state in range(data_container["counts"].shape[-2]): for wobbler_state in range(data_container["counts"].shape[-1]): self.plot_ref[ "stage counts c{} w{}".format(chopper_state, wobbler_state) ] = self.widget_pyqtgraph.plots["counts"].plot( y=data_container["counts"][ 0, :, chopper_state, wobbler_state ], # Choose pixel 0 - all pixels have the same counts name="stage counts for chopper state {} wobbler state {}".format( chopper_state, wobbler_state ), pen=count_pen[chopper_state], ) # Set y-axis range of count plots to "ignore"/overlook border cases self.widget_pyqtgraph.plots["counts"].setRange( yRange=[0, data_container["counts"][0, 1, 0, 0] + 5], padding=0 ) # +5 to not cutoff the line # Plot interferometer position self.plot_ref["stage position"] = self.widget_pyqtgraph.plots[ "stage position" ].plot( # x = pump_axes[1][opa_range[1]], #! add proper time scale y=data_container["interferometer positions"], #! rescale to fs or mm pen=position_pen, )