Source code for experiments.bb_viper_wobbler

"""
Providing the "Broadband VIPER (Wobbler)" experiment. In this
experiment, a broad IR pump pulse is used to excite the sample into the
first vibrational state. A second UV/VIS pump pulse is used to further
excite the sample into an electronically excited state. An IR probe
pulse is used to scan the samples' response. The IR delay stage is used
to move to different delays of the IR pump pulse, the UV/VIS stage is
used to move to different delays of the UV/VIS pump pulse. The broadband
pulse contains all the pump frequencies generated by the OPA at once (in
contrast to the Fabry Perot which only pumps at a single distinct
frequency). For this experiment, the UV/VIS and IR chopper must be
running. 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.

VIPER stands for Vibrationally Promoted Electronic Resonance. Frequency
domain VIPER experiments use 2 choppers. One chopper chops the IR Pump,
the second one chops the UV/VIS Pump. The IR pump excites the molecules
to a higher vibrational state. The UV/VIS pump pulse then excites the
molecules from that higher vibrational state to an excited electronic
state that is also vibrationally excited. An IR probe pulse is used to
measure the changes induced in the molecule.

VIPER measurements typically contain 4 different types of signals. These
are listed according to their chopper states:

    * (IR off, UV off): Background
    * (IR off, UV on): TRIR + Background
    * (IR on, UV off): IR pump/IR probe + Background
    * (IR on, UV on): VIPER + TRIR + IR pump/IR probe + Background
    
TRIR stands for transient IR signal.

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.

    **Choppers:**

        See show_viper_wobbler.

    **Wobbler:**
    
        How to set up the wobbler is described in the 
        show_signal_wobbler experiment.

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

**Acquisition:**

    1.  Preallocate dictionary (data container) which will contain data
        and information about scan index, delay index etc.
    2.  Start moving the micrometer screw continuously
    3.  Calculate the resulting weights from the UV/VIS and IR delay
        weights
    4.  Set the number of samples to acquire to according to this weight
    5.  Close UV/VIS Shutter
    6.  Move IR and UV/VIS stage simultaneously to next respective delay
    7.  Open UV/VIS Shutter
    8.  Read data for from the ADC
    9.  Place data into dictionary and hand it over to
        primary processing

**Primary Processing:**

    1.  Preallocate arrays for data, counts, weights, chopper, 
        phase cycled data etc.
        Here there are 2 states for each chopper. On (chopper high)
        and off (chopper low). Also there are (generally) 4 different
        wobbler states (left, center, right, center position of wobbler)
    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.  Identify the chopper states for all laser shots using the 
        corresponding channel(s) in the ADCs' data for both choppers
    6.  Identify the different wobbler states for all laser shots
    7.  Sort the data (transmissions) for each state and calculate 
        statistics
    8.  Phase cycle by averaging the wobbler states in the transmission 
        space. Calculate the resulting phase cycled counts and weights
    9.  Average the phase cycled data by weighting equally 
    10. Put this information into data container and hand it over to
        secondary processing
    11. Save data (and raw data) including counts and weights if 
        respective checkboxes on GUI were checked.

**Secondary Processing:**

    1.  Preallocate arrays for VIPER signal
    2.  Calculate absorption of phase cycled transmission for all states
    3.  Calculate all difference signals:

            * (IR off, UV off): Background
            * (IR off, UV on): TRIR + Background
            * (IR on, UV off): IR pump/IR probe + Background
            * (IR on, UV on): VIPER + TRIR + IR pump/IR probe + Background
            
        subtract them in the proper way to obtain:

            * TRIR: (IR off, UV on) - (IR off, UV off)
            * Pseudo TRIR: (IR on, UV on) - (IR on, UV off)
            * IR Pump: (IR on, UV off) - (IR off, UV off)
            * Pseudo IR Pump:(IR on, UV on) - (IR off, UV on)
            * VIPER signal: (IR on, UV on) - (IR on, UV off)
              - (IR off, UV on) + (IR off, UV off)

    4.  Calculate the non phase cycled VIPER signal
        (VIPER signal for each wobbler state)
    5.  Calculate the average intensities and standard deviation of 
        the intensities for each pixel
    6.  Put this information into data container and hand it over to
        Pyqtplotting thread
    7.  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:

        * Phase cycled and non phase cycled VIPER signal for current
          delay
        * Time-signal 2D heatmap (single scan and scan averaged)
        * Intensities and their standard deviation (multiplied by 5)
        * The additional four signals 
        
    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_probe_pixels, n_delays, n_ir_chopper_states, n_vis_chopper_states)]
        
        saving dimension:
        [(n_probe_pixels, n_ir_chopper_states, n_vis_chopper_states)]
        
        raw data dimension:
        [(n_channels, samples_to_acquire)]

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

In **scans/dXXX** 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 (VIPER 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.

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 data_processing import ChopperStateFinder as CSF

from analog_digital_converter import AnalogDigitalConverter as ADC
from triax import Triax as Spectrometer
from pi_control import PiStage
from newport_control import NewportControl as MuScrew
from shutter import Shutter

from save_data import SaveData, Background


[docs]class BbViperWobbler: """ 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. 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. ir_chopper_info (dict): Contains the information that is necessary to identify the different chopper states of the chopper that chops the IR 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. 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, prl: PRL, vis_chopper_info: dict, ir_chopper_info: dict, 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") self.acquisition = Acquisition( vis_delays, ir_delays, adc, vis_delay_stage, ir_delay_stage, 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, prl, ir_chopper_info, vis_chopper_info, wobbler_freq, adc.laser_frequency, background_handler, saver, ) self.secondary_processing = SecondaryProcessing( self.processing_queue, self.plot_queue, info_queue, vis_delays, ir_delays, adc.probe_pixel_idx, ) self.plotting = PyqtPlotting( widget_pyqtgraph, adc, self.plot_queue, vis_delays, ir_delays )
[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): """ Acquisition class (python multithreaded). This is where the actual experiment is conducted. This class is used to control the hardware devices required for the experiment. The sole purpose of it is to collect the data according to the parameters specified for the experiment by moving the delay stages, opening and closing shutters etc. A dictionary which will contain data and other information (scan index etc.) is instantiated here. The acquisition class passes the collected information (raw data, scan index etc.) to the primary processing class. Note: **No data processing beyond what is required to conduct the experiment should be implemented in this class.** The rationale behind this is to minimize down time/ maximize laser time. Data processing costs computation time and will, generally speaking, slow down the measurement process because the computer is busy while the rest of the hardware is idle. If implemented correctly the data processing could be carried out while the data acquisition is waiting for all data to become available. But even in this scenario the problem that the data processing takes longer than the acquisition time can occur and is thus best avoided through parallelisation. The reason why this class is a child of the threading module instead of the multiprocessing module is that to use multiprocessing all objects passed to the function must be picklable. This is not the case for some of the objects interfacing with the hardware (e.g. ADC). In an ideal scenario the acquisition too, would run in its own process seperated from the GUI thread but this would only be possible with major restructuring of the software. 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. 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, 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 # Hardware devices self.adc = adc self.spectrometer = spectrometer self.ir_delay_stage = ir_delay_stage self.vis_delay_stage = vis_delay_stage self.mu_screw = mu_screw self.vis_shutter = vis_shutter # 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 # Multiprocessing Queue self.acq_queue = acq_queue # Initialize scan index self.scan_idx = 0 # Create dictionary that will hold data that is passed # to other queues self.data_container = {} # Create dictionary that holds information # in which scan we are (etc.) self.info = {} # Multiprocessing event to stop experiment self.exit = threading.Event() # 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()
[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)): # 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 this delay samples_to_acquire = int(round(self.base_samples * weight)) self.adc.set_samples_to_acquire(samples_to_acquire) # Add delay index to data container self.data_container["delay index"] = d_idx # Move to next delay(s) # First close UV/Vis shutter before moving stage # to avoid unnecessary exposition to # UV/VIS pump light self.vis_shutter.close() # 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["probe axis"] = self.spectrometer.wn_axis.copy() # Give data of prior acquisition to queue self.acq_queue.put(self.data_container.copy()) # Update scan idx self.scan_idx += 1 self.acq_queue.put("stop") # Close UV/Vis shutter so sample # does not get burnt more than necessary self.vis_shutter.close()
[docs] def shutdown(self): # Setting exit will stop the loop # within run self.exit.set()
[docs]class PrimaryProcessing(multiprocessing.Process): """ Primary processing class (python multiprocess). This class' purpose is to process the raw data to a state where it can be saved onto the hard drive as npy (binary) files. This step generally includes linearization, normalisation, sorting and averaging. Besides the actual data additional information that is required for post processing purposes is calculated and saved. I.e. counts and weights. The last step of primary processing should always be to pass the data to secondary processing and save it to the hard disk. Note: The actual signal(s) are generally not intended to be calculated here. Signals and other information that is supposed to be displayed on the GUI should be calculated in secondary processing. The main reason for this is minimizing the risk of an error leading to a crash of the software which then in turn ruins the measurement. The more code that has to run the more likely a crash becomes. Others reasons mostly imply open questions regarding averaging. Generally, shot to shot normalized intensities that are sorted and averaged by their state are saved (*m2 method*). From this - for a simple experiment at least - the signal can be easily calculated while offering different choices of averaging in post processing. For more complex experiments e.g. VIPER or time domain experiments the argument of saving sorted transmissions instead of signals is even more compelling. In VIPER experiments more than one signal of interest is present in the different states. Saving each signal separately would actually increase the amount of data that has to be saved. For time domain experiments we want to save the data in the time domain for post processing reasons like zeropadding and apodization. The goal of primary processing is to make the data as compact as possible while keeping as much information and flexibility as possible. Even if the processes later on crash, the data is secured and can be analysed in post processing. 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. 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. ir_chopper_info (dict): Contains the information that is necessary to identify the different chopper states of the chopper that chops the IR 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. wobbler_freq (float): Frequency in Hz with which the wobbler oscillates. laser_freq (float): Frequency (repetition rate) of the laser in Hz. 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, prl: PRL, ir_chopper_info: dict, vis_chopper_info: dict, wobbler_freq: float, laser_freq: float, 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 # Load background data from file self.background = background_handler.load_background() # 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 # -IR chopper- self.ir_chopper_voltage_level = [ir_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.ir_chopper_name = ir_chopper_info["name"][0] # - 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] # Calculate the number of wobbler states we are going to observe self.wobbler_states = int(laser_freq // wobbler_freq) # We need an array, which describes the number of # all possible states for the sort data function # for each device in this example # the ir chopper as well as the vis chopper # both have two possible states # We have two different chopper states # for each different chopper because we decided # not to use the convolution electronics box built by # victor and connect both choppers to separate inputs # Additionally we have a number of wobbler states (generally 4) self.n_possible_states = np.array([2, 2, self.wobbler_states]) # 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, *self.n_possible_states, ) # all ir chopper states, all vis chopper 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. # Initialize array that holds both # temporary and averaged data # In this case the data is average # relative intensity # (transmission (probe/ref)) # for each pixel pair. # 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_ir_chopper_states, n_vis_chopper_states) self.phase_cycled_data = np.zeros( ( 2, ir_delays.shape[ 0 ], # We only need one delay dimension because we always move both stages simultaneously self.probe_pixel_idx.size, *self.n_possible_states[:-1], ) # all ir and all vis chopper states (excluding the wobbler states) ) 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) transmission = ( intensities[self.probe_pixel_idx] / intensities[self.ref_pixel_idx] ) # Get the corresponding chopper state for each shot # -IR Chopper- ir_chopper_states = np.digitize( raw_data[self.index_dict[self.ir_chopper_name]], self.ir_chopper_voltage_level, ) # -UV/VIS Chopper- vis_chopper_states = np.digitize( raw_data[self.index_dict[self.vis_chopper_name]], self.vis_chopper_voltage_level, ) # Get Wobbler States wobbler_states = dp.get_wobbler_states( raw_data[self.index_dict["wobbler"]], self.laser_freq, wobbler_freq=self.wobbler_freq, ) # Stack chopper and wobbler states into one array # for sort data function # This implicitly decides upon the # order of dimensionality of the data array: # The last axis of data is the wobbler # distinction while the second to # last axis corresponds to the # vis chopper and the third to last axis # corresponds to the ir chopper # (Always make sure that the n_possible_states # array lists its values in the same order!) # ? There is proably a faster way to do this than stacking by preallocating arrays? Nvm for now states = np.vstack((ir_chopper_states, vis_chopper_states, wobbler_states)) # Sort and average data self.data[:], self.weights[:], self.counts[:], statistics = dp.sort_data( transmission, states, self.n_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. self.phase_cycled_data[1] = 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 chopper state # for all wobbler states self.phase_cycled_counts[1] = self.counts.sum(axis=-1) # We also need to update the weights (inverse variance of each state) self.phase_cycled_weights[1] = self.weights.sum(axis=-1) # ----- Average data ---- # Create / Average "averaged" data by using weighted average # between the temp data (weight = 1) and the already # existing average data (weight = scan_idx) (technically # it is the number of total samples that were acquired in a # given state) self.phase_cycled_data[0, delay_idx] = np.average( self.phase_cycled_data[:, delay_idx], axis=0, weights=self.phase_cycled_data[:, delay_idx], ) # 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["intensities"] = intensities.copy() data_container["transmission"] = transmission.copy() data_container["statistics"] = statistics data_container["states"] = states self.processing_queue.put(data_container.copy()) # ---------------------------------------- # Save data (if specified) if self.saver: # Use saver class to save data self.saver.save_scan( self.phase_cycled_data[1], scan_idx, delay_idx=delay_idx ) self.saver.save_avg(self.phase_cycled_data[0], delay_idx=delay_idx) self.saver.save_counts( self.phase_cycled_counts[1], scan_idx, delay_idx=delay_idx ) self.saver.save_weights( self.phase_cycled_weights[1], 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): """ Secondary processing class (python multiprocess). This class is used to process the data from the primary processing class such that it can be displayed on the GUI within plots and lineEdits. This generally implies (if applicable): * calculation of signals * calculation of statistics like standard deviation of intensities and shot to shot standard deviation of signal * interpolation for 2D / heatmap plots (see comments in code why this is necessary) * Fourier transform and phasing for time domain data This data is handed over to the plotting thread. Note: The feature of saving figures/plots to the hard drive should be implemented here if it is needed. 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) """ def __init__( self, processing_queue: Queue, plot_queue: Queue, info_queue: Queue, vis_delays: ndarray, ir_delays: ndarray, probe_pixel_idx: ndarray, ): 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 # 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 # 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. # In this case: signal = VIPER Signal self.signal = np.zeros( ( 2, ir_delays.shape[ 0 ], # We only need one delay dimension because we always move both stages simultaneously self.probe_pixel_idx.size, ) ) # Needed for 2D heatmap /contour plot self.intp_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"] # Calculate the signals from the sorted and phase cycled data # --- Note: We refrain from calculating the # different signals (TRIR, IR-Pump–IR-Probe etc.) # for each wobbler state (non phase cycled data), # as this would "clutter" the plots too much # We only calculate the different signals for the phase # cycled data. Except for the VIPER signal # for this one we also calculate the signal # for each wobbler state. --- # Calculate the signals from the sorted data sorted_data = data_container["phase cycled data"] # Calculate absorption of non phase cycled transmission for all states absorption = -np.log10(sorted_data[:, delay_idx, :, :, :]) # Now calculate all the different difference signals # Last axis UV/VIS pump on-off # Second to last axis IR pump on-off # Beginners notes: # (IR off, UV off): Background = [:, 0, 0] # (IR off, UV on): TRIR + Background = [:, 0, 1] # (IR on, UV off): IR pump/IR probe + Background = [:, 1, 0] # (IR on, UV on): VIPER + TRIR + IR pump/IR probe + Background = [:, 1, 1] # (We did not preallocate arrays for the different # non VIPER signals because they will not be 2d/heatmap # plotted - we also calculate this only for the "temp"/ # single scan data. That is why the 0th axis is indexed with 1) # To calculate the transient IR signal # we calculate difference between UV pump on # and UV pump off (while IR pump is off) trir_signal = absorption[1, :, 0, 1] - absorption[1, :, 0, 0] # Analogously we can calculate this difference # when the IR pump is on - which is not # technically the transient IR signal but # TRIR + VIPER. Because VIPER is an order of # magnitude smaller than TRIR it should yield # more or less the same signal. This can be # used to check if the phases of the choppers are correct. pseudo_trir_signal = absorption[1, :, 1, 1] - absorption[1, :, 1, 0] # To calculate the IR pump IR probe signal # we calculate difference between IR pump on # and IR pump off (while UV/VIS pump is off) irpump_signal = absorption[1, :, 1, 0] - absorption[1, :, 0, 0] # Analogously to pseudo TRIR we calculate # a pseudo IR Pump - IR Probe signal pseudo_irpump_signal = absorption[1, :, 1, 1] - absorption[1, :, 0, 1] # We calculate the VIPER signal by subtracting the # TRIR Signal (UV on, IR off) and IR Pump - IR Probe # Signal (UV off, IR on) from (UV off) # This implies that we removed the background twice # so we additionally add the background # This time we calculate both averaged and single scan self.signal[:, delay_idx, :] = ( absorption[:, :, 1, 1] - absorption[:, :, 1, 0] - absorption[:, :, 0, 1] + absorption[:, :, 0, 0] ) # Compute interpolated phase cycled data for 2d/heatmap plots # Computing this outside the if statement is slow and # unnecessary but at least the plotting works in all cases self.intp_signal[1] = dp.generate_img_data( self.ir_delays[:, 0], data_container["probe axis"], self.signal[1, :].T ) self.intp_signal[0] = dp.generate_img_data( self.ir_delays[:, 0], data_container["probe axis"], self.signal[0, :].T ) # Calculate the non phase cycled VIPER signal # (VIPER signal for each wobbler state) # --- 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_absorption = -np.log10(data_container["sorted data"]) npc_viper_signal = ( npc_absorption[:, 1, 1] - npc_absorption[:, 1, 0] - npc_absorption[:, 0, 1] + npc_absorption[:, 0, 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 data_container["TRIR signal"] = trir_signal data_container["pseudo trir signal"] = pseudo_trir_signal data_container["IR pump signal"] = irpump_signal data_container["pseudo IR pump signal"] = pseudo_irpump_signal data_container["VIPER signal"] = self.signal data_container["VIPER signal (interpol)"] = self.intp_signal data_container["non phase cycled VIPER signal"] = npc_viper_signal 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 # Hand over the mean state information for every pixel 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) """
[docs] class Signals(QObject): new_data = pyqtSignal(dict)
def __init__( self, widget_pyqtgraph, adc: ADC, plot_queue, vis_delays: ndarray, ir_delays: ndarray, ): # Assign attributes self.adc = adc self.widget_pyqtgraph = widget_pyqtgraph self.graphics_layout = widget_pyqtgraph.graphics_layout self.plot_queue = plot_queue self.vis_delays = vis_delays self.ir_delays = ir_delays # 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 first 2 plots in the first row self.widget_pyqtgraph.plots["upper layout"] = self.graphics_layout.addLayout( colspan=4 ) # Setup plot that holds plot for single scan signal for current delay self.widget_pyqtgraph.plots[ "signal wobbler current delay" ] = self.widget_pyqtgraph.plots["upper layout"].addPlot(colspan=2) self.widget_pyqtgraph.plots["signal wobbler current delay"].setTitle( "VIPER signal - IR delay {} fs" ) self.widget_pyqtgraph.plots["signal wobbler current delay"].setLabel( "bottom", "wavenumber [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["signal wobbler current delay"].setLabel( "left", "signal [OD]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["signal wobbler current delay"] ) # Setup plot that holds plot for average signal for current delay self.widget_pyqtgraph.plots[ "phase cycled signal current delay" ] = self.widget_pyqtgraph.plots["upper layout"].addPlot(colspan=2) self.widget_pyqtgraph.plots["phase cycled signal current delay"].setTitle( "Scan averaged VIPER signal - UV/VIS delay {} fs" ) self.widget_pyqtgraph.plots["phase cycled signal current delay"].setLabel( "bottom", "wavenumber [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["phase cycled signal current delay"].setLabel( "left", "signal [OD]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["phase cycled signal current delay"] ) # Skip to next row self.graphics_layout.nextRow() # Add a sub-layout to hold the 2D plots and the histograms in the second row self.widget_pyqtgraph.plots["middle layout"] = self.graphics_layout.addLayout( colspan=4 ) # Setup plot that holds heatmap plot y-axis: probe wavenumber, x-axis: delay self.widget_pyqtgraph.plots[ "single time-signal heatmap" ] = self.widget_pyqtgraph.plots["middle layout"].addPlot() self.widget_pyqtgraph.plots["single time-signal heatmap"].setTitle( "Signal with respect to delay time" ) self.widget_pyqtgraph.plots["single time-signal heatmap"].setLabel( "bottom", "IR delay [fs]" ) self.widget_pyqtgraph.plots["single time-signal heatmap"].setLabel( "left", "wavenumber [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["single time-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 time-signal heatmap histogram"] = pg.HistogramLUTItem() self.widget_pyqtgraph.plots["middle layout"].addItem( self.plot_ref["single time-signal heatmap histogram"] ) self.widget_pyqtgraph.plots[ "avg time-signal heatmap" ] = self.widget_pyqtgraph.plots["middle layout"].addPlot() self.widget_pyqtgraph.plots["avg time-signal heatmap"].setTitle( "Scan averaged signal with respect to delay time" ) self.widget_pyqtgraph.plots["avg time-signal heatmap"].setLabel( "bottom", "IR delay [fs]" ) self.widget_pyqtgraph.plots["avg time-signal heatmap"].setLabel( "left", "wavenumber [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.set_style( self.widget_pyqtgraph.plots["avg time-signal heatmap"] ) self.plot_ref["avg time-signal heatmap histogram"] = pg.HistogramLUTItem() self.widget_pyqtgraph.plots["middle layout"].addItem( self.plot_ref["avg time-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 # Add next row for the last row of plots. here there are 2 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", "wavenumber [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 different signals self.widget_pyqtgraph.plots["signal"] = self.graphics_layout.addPlot( row=2, col=1 ) self.widget_pyqtgraph.plots["signal"].setTitle("Average signals") self.widget_pyqtgraph.plots["signal"].setLabel( "bottom", "wavenumber [cm<sup>-1</sup>]" ) self.widget_pyqtgraph.plots["signal"].setLabel("left", "difference signal[OD]") self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["signal"]) # 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"] trir_signal = data_container["TRIR signal"] pseudo_trir_signal = data_container["pseudo trir signal"] irpump_signal = data_container["IR pump signal"] pseudo_irpump_signal = data_container["pseudo IR pump signal"] viper_signal = data_container["VIPER signal"] intp_signal = data_container["VIPER signal (interpol)"] npc_viper_signal = data_container["non phase cycled VIPER signal"] probe_axis = data_container["probe axis"] avg_intensities = data_container["average intensity"] std_intensities = data_container["std intensity"] # Update title with current delay self.widget_pyqtgraph.plots["signal wobbler current delay"].setTitle( "VIPER - IR delay {} fs".format(self.ir_delays[delay_idx][0]) ) self.widget_pyqtgraph.plots["phase cycled signal current delay"].setTitle( "Averaged VIPER - UV/VIS delay {} fs".format(self.vis_delays[delay_idx][0]) ) # Needs to be bigger than two because # we define two colorbar/histogram items # in init which are already in plot ref if len(self.plot_ref) > 2: # Update signal plots # Single scan + wobbler states self.plot_ref["signal wobbler current delay"].setData( x=probe_axis, y=viper_signal[1, delay_idx] ) # Update non phase cycled VIPER signal / VIPER signal for each wobbler state for i in range(npc_viper_signal.shape[-1]): self.plot_ref["VIPER wobbler state {}".format(i)].setData( x=probe_axis, y=npc_viper_signal[:, i], ) # Phase cycled signals # Scan averaged self.plot_ref["avg phase cycled signal current delay"].setData( x=probe_axis, y=viper_signal[0, delay_idx] ) # Single scan self.plot_ref["single phase cycled signal current delay"].setData( x=probe_axis, y=viper_signal[1, delay_idx] ) # -----------Start 2D Plots--------------- # Update 2d image: time vs. signal(wavenumber) # Single Scan # Get old levels so it does not rescale the colors everytime (see below) levels = self.plot_ref["single time-signal heatmap histogram"].getLevels() self.plot_ref["single time-signal heatmap"].setImage(intp_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 time-signal heatmap histogram"].setLevels(*levels) # Update contour lines dp.update_contour_lines( intp_signal[1], self.plot_ref["single time-signal heatmap contours"] ) # ---Scan averaged--- # Get old levels so it does not rescale the colors everytime (see below) levels = self.plot_ref["avg time-signal heatmap histogram"].getLevels() self.plot_ref["avg time-signal heatmap"].setImage(intp_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 time-signal heatmap histogram"].setLevels(*levels) # Update contour lines dp.update_contour_lines( intp_signal[0], self.plot_ref["avg time-signal heatmap contours"] ) # ----------End 2D Plots---------------- # 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 different signal plots self.plot_ref["trir"].setData(x=probe_axis, y=trir_signal) self.plot_ref["pseudo trir"].setData(x=probe_axis, y=pseudo_trir_signal) self.plot_ref["ir pump"].setData(x=probe_axis, y=irpump_signal) self.plot_ref["pseudo ir pump"].setData( x=probe_axis, y=pseudo_irpump_signal ) else: # First time plotting # VIPER Pens signal_pen = pg.mkPen(color="#d62728", width=2.5, style=QtCore.Qt.SolidLine) avg_signal_pen = pg.mkPen( color="#17becf", width=2.5, style=QtCore.Qt.SolidLine ) npc_viper_pen = pg.mkPen( color="#A9A9A9", width=1.2, style=QtCore.Qt.SolidLine ) std_signal_pen = pg.mkPen( color="#2ca02c", width=2.5, 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? trir_pen = pg.mkPen(color="#1f77b4", width=2.5, style=QtCore.Qt.DashLine) pseudo_trir_pen = pg.mkPen( color="#ff7f0e", width=2.5, style=QtCore.Qt.DotLine ) irpump_pen = pg.mkPen( color="#2ca02c", width=2.5, style=QtCore.Qt.DashDotLine ) pseudo_irpump_pen = pg.mkPen( color="#7f7f7f", width=2.5, style=QtCore.Qt.DashDotDotLine ) # Histogram colormap seismic = pg.graphicsItems.GradientEditorItem.Gradients["seismic"] # Plot signal # Single scan self.plot_ref["signal wobbler current delay"] = self.widget_pyqtgraph.plots[ "signal wobbler current delay" ].plot( x=probe_axis, y=viper_signal[1, delay_idx], name="single scan VIPER signal", pen=signal_pen, ) # Plot non phase cycled VIPER signal / VIPER signal for each wobbler state for i in range(npc_viper_signal.shape[-1]): self.plot_ref[ "VIPER wobbler state {}".format(i) ] = self.widget_pyqtgraph.plots["signal wobbler current delay"].plot( x=probe_axis, y=npc_viper_signal[:, i], name="VIPER signal wobbler state {}".format(i), pen=npc_viper_pen, ) # Phase cycled signals # Scan averaged self.plot_ref[ "avg phase cycled signal current delay" ] = self.widget_pyqtgraph.plots["phase cycled signal current delay"].plot( x=probe_axis, y=viper_signal[0, delay_idx], name="scan averaged VIPER signal", pen=avg_signal_pen, ) # Plot single scan signal additionally in average scan plot (now called phase cycled signal plot) # Single scan self.plot_ref[ "single phase cycled signal current delay" ] = self.widget_pyqtgraph.plots["phase cycled signal current delay"].plot( x=probe_axis, y=viper_signal[1, delay_idx], name="single scan VIPER signal", pen=signal_pen, ) # Plot 2d image: time vs. signal (wavenumber) # Single scan self.plot_ref["single time-signal heatmap"] = pg.ImageItem(intp_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 time-signal heatmap histogram"].setImageItem( self.plot_ref["single time-signal heatmap"] ) # Set the color levels of the histogram self.plot_ref["single time-signal heatmap histogram"].gradient.restoreState( seismic ) # Scale image to match axes dp.scale_img( self.ir_delays[:, 0], probe_axis, intp_signal[1], self.plot_ref["single time-signal heatmap"], ) self.widget_pyqtgraph.plots["single time-signal heatmap"].addItem( self.plot_ref["single time-signal heatmap"] ) self.plot_ref["single time-signal heatmap"].setLookupTable(self.lut) # Generate contour lines self.plot_ref[ "single time-signal heatmap contours" ] = dp.generate_contour_lines( intp_signal[1], self.plot_ref["single time-signal heatmap"] ) # Plot 2d image: time vs. signal (wavenumber) # We only display the non averaged data # Scan averaged self.plot_ref["avg time-signal heatmap"] = pg.ImageItem(intp_signal[0]) self.plot_ref["avg time-signal heatmap histogram"].setImageItem( self.plot_ref["avg time-signal heatmap"] ) # Set the color levels of the histogram self.plot_ref["avg time-signal heatmap histogram"].gradient.restoreState( seismic ) dp.scale_img( self.ir_delays[:, 0], probe_axis, intp_signal[0], self.plot_ref["avg time-signal heatmap"], ) self.widget_pyqtgraph.plots["avg time-signal heatmap"].addItem( self.plot_ref["avg time-signal heatmap"] ) self.plot_ref["avg time-signal heatmap"].setLookupTable(self.lut) # Generate contour lines self.plot_ref[ "avg time-signal heatmap contours" ] = dp.generate_contour_lines( intp_signal[1], self.plot_ref["avg time-signal heatmap"] ) # 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 signal self.plot_ref["trir"] = self.widget_pyqtgraph.plots["signal"].plot( x=probe_axis, y=trir_signal, name="TR-IR", pen=trir_pen ) self.plot_ref["pseudo trir"] = self.widget_pyqtgraph.plots["signal"].plot( x=probe_axis, y=pseudo_trir_signal, name="TR-IR + VIPER", pen=pseudo_trir_pen, ) self.plot_ref["ir pump"] = self.widget_pyqtgraph.plots["signal"].plot( x=probe_axis, y=irpump_signal, name="IR pump/IR probe", pen=irpump_pen ) self.plot_ref["pseudo ir pump"] = self.widget_pyqtgraph.plots[ "signal" ].plot( x=probe_axis, y=pseudo_irpump_signal, name="IR pump/IR probe + VIPER", pen=pseudo_irpump_pen, )
# self.widget_pyqtgraph.disable_autoscale()