"""
Providing the "Fabry Perot Viper with Interleaves" experiment. In this
experiment, a narrow IR pump pulse, created with the Fabry Perot, is
used to excite the sample. 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 Fabry
Perot is tuned to different pump pixels (frequencies) specified in the
pump pixel delay file. 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, the IR delay stage is
used to phase cycle the pump pulse. This is called "interleaves" and can
be achieved because the delay stage can move distances that are
fractions of the pump wavelength. This effectively only changes the
phase of the pump light incident on the sample while the effect of the
slightly altered delay is negligible.
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 the physically the IR pump pulse
should "arrive" before the UV/VIS pump pulse (in time).
The time delay can be found out by setting the delay stages
in the corresponding way (i.e. IR delay stage 20,000 ps,
UV/VIS delay stage 20,500 ps). We observed that it can
happen that the stages are set in the opposite way (as if
the UV/VIS pump pulse arrives before the IR pump pulse for
small time differences e.g. 500 ps. For examples stage positions
such as IR delay stage 20,500 ps UV/VIS delay stage 20,000 ps
yield a good Viper signal). This is due to the
imprecision of the stages. Set it in a way that a proper
viper signal can be observed with show viper (of course
the time difference should not be ridiculous i.e.
IR delay stage 20,000 ps UV/VIS delay stage -20,000 ps)
**Chopper:**
See show_viper.
**Fabry Perot Algorithm:**
The algorithm to tune the wavelength of the Fabry Perot is not
particularly stable. Especially when tuning the Fabry Perot to
the borders of the detector it might crash. I.e.: The algorithm
wants to tune to a negative voltage or tunes to a "satellite"
peak. It is advisable to check your pump pixel "delays" by hand
before starting a measurement.
In the case where the tuning algorithm fails it might be
necessary to reset its voltage via the voltage lineEdit. I.e.:
If the central wavelength of the spectrometer corresponds to a
voltage of 3.65 V on the Fabry Perot, then it needs to be reset
to 3.65 V.
When setting up the Fabry Perot the controller needs to be
offset corrected to avoid the algorithm moving out of the
voltage range (0-10 V) under otherwise normal circumstances.
E.g.: If the central wavelength of the spectrometer is at 0.5 V
the lower frequencies might only be accessible with negative
voltages. We noticed that setting the central wavelength to
about 3-4 V is a good starting point for further adjustments.
#######################
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. Close UV/VIS Shutter
4. Tune the Fabry Perot to pump pixel specified in the pump
pixel "delay" file
5. Set the number samples to acquire to account for pump pixel
weights
6. Open IR Shutter
7. Collect pump spectrum (read data from ADC)
8. Close IR Shutter
9. Calculate interleave positions
10. Calculate the resulting weights from the UV/VIS and IR delay
weights
11. Set the number of samples to acquire to according to this weight
12. Close UV/VIS Shutter
13. Move IR and UV/VIS stage simultaneously to next respective delay
14. Open UV/VIS Shutter
15. Move to the interleaves (the 0th interleave is the actual delay)
16. Read data for from the ADC
17. Place data into dictionary and hand it over to
primary processing
**Primary Processing:**
1. Preallocate arrays for data, counts, weights, chopper,
shot to shot data etc. Here there are 2 states for each chopper.
Each chopper has its own axis in the arrays. Additionally an
axis is added for the interleaves. Note that the interleaves are
not "sorted" into separate states because we already know
beforehand which interleave was measured (analogously to how it
is done for the delays)
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 IR and UV/VIS the chopper states for all laser
shots using the corresponding channel(s) in the ADCs' data for
both choppers
6. Sort the data (transmissions) for each state and calculate
statistics
7. Calculate the s2s (shot to shot) VIPER signal and its
statistical information
8. Average the data by weighting equally
9. Calculate average pump spectrum and standard deviation of it
10. Put this information into data container and hand it over to
secondary processing
11. Save data (and raw data) including counts, weights and s2s_std
if respective checkboxes on GUI were checked. Also
save pump spectrum of Fabry Perot.
**Secondary Processing:**
1. Preallocate arrays for VIPER signal and phase cycled
VIPER signal
2. Phase cycle the transmission over all interleaves (here it is
done the way that we think is the correct way to phase cycle)
3. Calculate absorption (-log10) of phase cycled transmissions
4. Calculate the phase cycled VIPER signal
5. Calculate absorption (-log10) of non phase cycled transmission
or all states
6. Calculate all difference signals (without phase cycling to
display the current interleave):
* (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)
7. Calculate the average intensities and standard deviation of
the intensities for each pixel
8. Put this information into data container and hand it over to
Pyqtplotting thread
9. 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 VIPER signal and interleaves for current delay
* 2D-signal heatmap (single scan and scan averaged)
* Intensities and their standard deviation (multiplied by 5)
* Standard deviation of shot to shot VIPER signal
* All four additional signals (non phase cycled)
* Pump spectrum
3. Plot the plots for the first time
4. Update plots
**Saving:**
Note that in the programming data dimension only the IR delays are
necessary, because we always move both stages at the same time.
.. code-block::
programming data dimension:
[(2 ([0] is current average scan, [1] is last single scan),
n_pump_pixel, n_delays, n_interleaves, n_probe_pixels, n_ir_chopper_states, n_vis_chopper_states)]
saving dimension:
[(n_interleaves, 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 4 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 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 s2s_std file
contains the standard deviation of the shot to shot VIPER spectrum (this
was used in the old software as weights to average different scans.
However, this method of averaging has ambiguity and it has been proven
difficult to reason why it should work when averaging transmissions. It
is saved so that the option to revert to this averaging method exists).
Note that the dimensionality of the s2s_std file is not as "saving
dimension" suggests since it results from difference spectra. This means
that except for the probe pixel dimension every other dimension
collapses.
Additionally, the **pump spectrum** of the Fabry Perot for every
pump pixel for every scan is saved in the scans directory.
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.
**pump_pixel.npy** contains the pump pixels including weights.
.. code-block::
username/
├── date1_experimentname1_000/
│ ├── averaged_data
│ │ ├── p000_d000_date1_experimentname1_000.npy
│ │ ├── ...
│ │ ├── p000_d999_date1_experimentname1_000.npy
│ │ ├── p001_d000_date1_experimentname1_000.npy
│ │ ├── ...
│ │ └── p001_d999_date1_experimentname1_000.npy
│ ├── figures
│ ├── hardware config
│ ├── raw data
│ │ ├── pump_pixel000
│ │ │ ├── delay000
│ │ │ │ ├── s000000_p000_d000_intl000_date1_experimentname1_000_raw.npy
│ │ │ │ ├── s000000_p000_d000_intl001_date1_experimentname1_000_raw.npy
│ │ │ │ ├── ...
│ │ │ │ └── s000099_p000_d000_intl016_date1_experimentname1_000_raw.npy
│ │ │ ├── ...
│ │ │ └── pump_pixel999
│ ├── scans
│ │ ├── pump_pixel000
│ │ │ ├── delay000
│ │ │ │ ├── s000000_p000_d000_date1_experimentname1_000.npy
│ │ │ │ ├── s000000_p000_d000_counts_date1_experimentname1_000.npy
│ │ │ │ ├── s000000_p000_d000_s2s_std_date1_experimentname1_000.npy
│ │ │ │ ├── s000000_p000_d000_weights_date1_experimentname1_000.npy
│ │ │ │ ├── ...
│ │ │ │ └── s000099_p000_d000_date1_experimentname1_000_raw.npy
│ │ │ ├── ...
│ │ │ ├── delay999
│ │ │ ├── s000000_p000_pump_spectrum_date1_experimentname1_000.npy
│ │ │ ├── ...
│ │ │ └── s000099_p000_pump_spectrum_date1_experimentname1_000.npy
│ │ ├── ...
│ │ └── pump_pixel999
│ ├── probe_wn_axis_date1_experimentname1_000.npy
│ ├── pump_pixel_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 fabry_perot import FabryPerot
from newport_control import NewportControl as MuScrew
from shutter import Shutter
from save_data import SaveData, Background
[docs]class FabryPerotViperInterleaves:
"""
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)
pump_pixels (ndarray): Array containing the pump pixels to which
the Fabry Perot is supposed be tuned in the 0th column and
their corresponding weights in the 1st column.
* shape: 2D
* E.g.: (number of pump pixels, 2)
interleaves (int): Number of interleaves that should be scanned.
Number should be even and a power of 2. An interleave is a
small step of the delay stage (in addition to the normal
delay) used for phase cycling and removing scattering.
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.
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.
fabry_perot (FabryPerot): Object that provides the functionality
of tuning the Fabry Perot to a pixel on the MCT array.
mu_screw (MuScrew): NewportControl object that can be used to
move the micrometer screw.
ir_shutter (Shutter): Shutter object that can be used to open
and close the IR pump shutter.
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,
pump_pixels: ndarray,
interleaves: int,
adc: ADC,
vis_delay_stage: PiStage,
ir_delay_stage: PiStage,
prl: PRL,
vis_chopper_info: dict,
ir_chopper_info: dict,
background_handler: Background,
spectrometer: Spectrometer,
fabry_perot: FabryPerot,
ir_shutter: Shutter,
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(pump_pixels, "pump_pixels")
saver.save_other(background_handler.load_background(), "background")
self.acquisition = Acquisition(
vis_delays,
ir_delays,
pump_pixels,
interleaves,
adc,
vis_delay_stage,
ir_delay_stage,
spectrometer,
fabry_perot,
ir_shutter,
mu_screw,
vis_shutter,
self.acq_queue,
)
self.primary_processing = PrimaryProcessing(
self.acq_queue,
self.processing_queue,
vis_delays,
ir_delays,
pump_pixels,
interleaves,
adc.pixel_idx,
adc.probe_pixel_idx,
adc.reference_pixel_idx,
adc.index_dict,
prl,
ir_chopper_info,
vis_chopper_info,
background_handler,
saver,
)
self.secondary_processing = SecondaryProcessing(
self.processing_queue,
self.plot_queue,
info_queue,
vis_delays,
ir_delays,
pump_pixels,
interleaves,
adc.probe_pixel_idx,
)
self.plotting = PyqtPlotting(
widget_pyqtgraph,
adc,
self.plot_queue,
vis_delays,
ir_delays,
pump_pixels,
interleaves,
)
[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)
pump_pixels (ndarray): Array containing the pump pixels to which
the Fabry Perot is supposed be tuned in the 0th column and
their corresponding weights in the 1st column.
* shape: 2D
* E.g.: (number of pump pixels, 2)
interleaves (int): Number of interleaves that should be scanned.
Number should be even and a power of 2. An interleave is a
small step of the delay stage (in addition to the normal
delay) used for phase cycling and removing scattering.
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.
fabry_perot (FabryPerot): Object that provides the functionality
of tuning the Fabry Perot to a pixel on the MCT array.
mu_screw (MuScrew): NewportControl object that can be used to
move the micrometer screw.
ir_shutter (Shutter): Shutter object that can be used to open
and close the IR pump shutter.
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,
pump_pixels: ndarray,
interleaves: int,
adc: ADC,
vis_delay_stage: PiStage,
ir_delay_stage: PiStage,
spectrometer: Spectrometer,
fabry_perot: FabryPerot,
ir_shutter: Shutter,
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.pump_pixels = pump_pixels
self.interleaves = interleaves
# Hardware devices
self.adc = adc
self.spectrometer = spectrometer
self.ir_delay_stage = ir_delay_stage
self.vis_delay_stage = vis_delay_stage
self.fabry_perot = fabry_perot
self.ir_shutter = ir_shutter
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 pump frequencies
for p_idx, pump_pixel in enumerate(self.pump_pixels):
# First close UV/Vis shutter before tuning Fabry Perot
# to avoid unnecessary exposition to
# UV/VIS pump light
self.vis_shutter.close()
# Tune fabry perot to the specified pump frequency (pixel)
self.fabry_perot.set_pixel(int(pump_pixel[0]))
# Set samples to acquire according to weight
# for this pump pixel
samples_to_acquire = int(round(self.base_samples * pump_pixel[1]))
self.adc.set_samples_to_acquire(samples_to_acquire)
# Measure Pump Spectrum
self.ir_shutter.open()
self.adc.read()
pump_spectrum = self.adc.data[self.adc.probe_pixel_idx].copy()
self.ir_shutter.close()
# Do not open vis shutter just yet
# because the delay stages still have
# to be moved.
# Add pump (frequency) index to data container
self.data_container["pump index"] = p_idx
# Add pump spectrum to data container
self.data_container["pump spectrum"] = pump_spectrum
# 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
# Generate interleave positions of
# IR delay stage
interleave_pos = dp.calculate_interleave_array(
self.interleaves, self.spectrometer.wavelength, ir_delay[0]
)
# 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
# We do not multiply the weight by base samples
# because we also need to take the weight
# of the fabry perot into consideration
samples_to_acquire = int(
round(self.adc.samples_to_acquire * 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()
# Iterate over all interleaves
for i_idx, interleave in enumerate(interleave_pos):
# Move to next delay/interleave
# Because these are extremely
# short distances we do not close
# the vis shutter
# The 0th interleave position
# is equal to the IR delay
# specified in the file
# so the stage only needs to be moved
# when the interleave index is greater
# 0, because in the other case
# it already is where it is supposed to be
if i_idx > 0:
self.ir_delay_stage.move(interleave)
# 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["interleave index"] = i_idx
self.data_container[
"probe axis"
] = self.spectrometer.wn_axis.copy()
# Add pump axis (this is inefficient and should be moved to init
# (like some other stuff too))
self.data_container["pump axis"] = self.spectrometer.wn_axis[
self.pump_pixels[:, 0].astype(int)
]
# 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)
pump_pixels (ndarray): Array containing the pump pixels to which
the Fabry Perot is supposed be tuned in the 0th column and
their corresponding weights in the 1st column.
* shape: 2D
* E.g.: (number of pump pixels, 2)
interleaves (int): Number of interleaves that should be scanned.
Number should be even and a power of 2. An interleave is a
small step of the delay stage (in addition to the normal
delay) used for phase cycling and removing scattering.
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.
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,
pump_pixels: ndarray,
interleaves: int,
pixel_idx: ndarray,
probe_pixel_idx: ndarray,
reference_pixel_idx: ndarray,
index_dict: dict,
prl: PRL,
ir_chopper_info: dict,
vis_chopper_info: dict,
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
# Number of interleaves
self.interleaves = interleaves
# Pixel response linearisation
self.prl = prl
# 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()
# 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 seperate inputs
self.n_possible_states = np.array([2, 2])
# 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]
# Initialize array that holds both
# temporary and averaged data
# 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_pump_pixel, n_delays, n_interleaves, n_probe_pixels, n_ir_chopper_states, n_vis_chopper_states)
self.data = np.zeros(
(
2,
pump_pixels.shape[0],
ir_delays.shape[
0
], # We only need one delay dimension because we always move both stages simultaneously
self.interleaves,
self.probe_pixel_idx.size,
*self.n_possible_states,
)
)
self.counts = np.zeros(self.data.shape)
self.weights = np.zeros(self.data.shape)
# * The following code should be moved to
# * secondary processing once it is clear
# * whether or not it is actually needed
# * for the averaging of the scans in post
# * processing
#! Insert array that holds data to s2s
#! signal variance for each interleave.
self.s2s_std_signal = np.zeros((self.interleaves, self.probe_pixel_idx.size))
self.s2s_avg_std_signal = np.zeros(self.interleaves)
self.s2s_amp_signal = np.zeros(self.interleaves)
[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"]
pump_idx = data_container["pump index"]
scan_idx = data_container["scan index"]
delay_idx = data_container["delay index"]
intl_idx = data_container["interleave 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,
)
# Stack chopper 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 vis chopper
# distinction while the second to
# last axis corresponds to the
# ir chopper
# (Always make sure that the n_possible_states
# array lists its values in the same order!)
states = np.vstack((ir_chopper_states, vis_chopper_states))
# Sort and average data
idx = (1, pump_idx, delay_idx, intl_idx)
(
self.data[idx],
self.weights[idx],
self.counts[idx],
statistics,
) = dp.sort_data(transmission, states, self.n_possible_states)
# * The following code should be moved to
# * secondary processing once it is clear
# * whether or not it is actually needed
# * for the averaging of the scans in post
# * processing
# Calculate the s2s (shot to shot) viper signal
# and its statistical information
# We are not interested in the 0th element of the tuple
# which represents the shot to shot averaged difference
# signal - we calculated it correctly with the transmissions
# above
(
_,
self.s2s_amp_signal[intl_idx],
self.s2s_std_signal[intl_idx],
self.s2s_avg_std_signal[intl_idx],
) = dp.shot_to_shot_viper(
transmission,
states[1, 0], # 0th sample of UV/VIS Chopper
states[0, :2], # 0th and 1st sample of IR Chopper
)
# 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)
# * Data that was averaged this way should
# * probably yield worse results than
# * phase cycling for each scan
# * this has probably 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.
# * For this type of weighting
# * (essentially equal weights) it should
# * not make a difference as the two
# * sums (one for averaging the interleaves
# * within one scan and one for averaging scans)
# * can be swapped.
self.data[0, pump_idx, delay_idx, intl_idx] = np.average(
self.data[:, pump_idx, delay_idx, intl_idx],
axis=0,
weights=self.counts[:, pump_idx, delay_idx, intl_idx],
)
# Calculate average pump spectrum and corresponding standard deviation
#! (placing it here is inefficient, because the pump spectrum information
#! is only updated once all delays were measured, but is effectively calculated
#! for every delay - but nvm for now)
avg_pump_spectrum = np.average(data_container["pump spectrum"], axis=1)
std_pump_spectrum = np.std(data_container["pump spectrum"], axis=1, ddof=1)
# Add everything to data container
# and give it to processing queue
data_container["sorted data"] = self.data.copy()
data_container["intensities"] = intensities.copy()
data_container["transmission"] = transmission.copy()
data_container["statistics"] = statistics
data_container["states"] = states
data_container["std s2s signal"] = self.s2s_std_signal[intl_idx]
data_container["s2s signal amplitude"] = self.s2s_amp_signal[intl_idx]
data_container["s2s signal average std"] = self.s2s_avg_std_signal[intl_idx]
data_container["average pump spectrum"] = avg_pump_spectrum
data_container["std pump spectrum"] = std_pump_spectrum
self.processing_queue.put(data_container.copy())
# ----------------------------------------
# Save data (if specified)
if self.saver:
# Use saver class to save data
# Save data once last interleave was measured
# (this does not apply to raw data)
if intl_idx == self.interleaves - 1:
self.saver.save_scan(
self.data[1, pump_idx, delay_idx],
scan_idx,
delay_idx=delay_idx,
pump_idx=pump_idx,
)
self.saver.save_avg(
self.data[0, pump_idx, delay_idx],
delay_idx=delay_idx,
pump_idx=pump_idx,
)
self.saver.save_counts(
self.counts[1, pump_idx, delay_idx],
scan_idx,
delay_idx=delay_idx,
pump_idx=pump_idx,
)
self.saver.save_weights(
self.weights[1, pump_idx, delay_idx],
scan_idx,
delay_idx=delay_idx,
pump_idx=pump_idx,
)
self.saver.save_pump_spectrum(
avg_pump_spectrum, scan_idx, pump_idx
) #! This inefficient because pump spectrum will be overwritten for each delay with the same data
# *-------------
self.saver.save_s2s_std(
self.s2s_std_signal,
scan_idx,
delay_idx=delay_idx,
pump_idx=pump_idx,
)
# *-------------
if self.saver.raw_data:
self.saver.save_raw_data(
raw_data,
scan_idx,
delay_idx=delay_idx,
pump_idx=pump_idx,
intlv=intl_idx,
)
# Update counts
self.counts[0, pump_idx, delay_idx, intl_idx] += self.counts[
1, pump_idx, delay_idx, intl_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)
pump_pixels (ndarray): Array containing the pump pixels to which
the Fabry Perot is supposed be tuned in the 0th column and
their corresponding weights in the 1st column.
* shape: 2D
* E.g.: (number of pump pixels, 2)
interleaves (int): Number of interleaves that should be scanned.
Number should be even and a power of 2. An interleave is a
small step of the delay stage (in addition to the normal
delay) used for phase cycling and removing scattering.
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,
pump_pixels: ndarray,
interleaves: int,
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
# Number of interleaves
self.interleaves = interleaves
# 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
# "programming data dimension":
# ["(2 ([0] is current average scan, [1] is last single scan),
# n_pump_pixel, n_delays, n_interleaves, n_probe_pixels,
# n_ir_chopper_states, n_vis_chopper_states)"],
self.signal = np.zeros(
(
2,
pump_pixels.shape[0],
ir_delays.shape[
0
], # We only need one delay dimension because we always move both stages simultaneously
self.interleaves,
self.probe_pixel_idx.size,
)
)
# Phase cycled VIPER signal
# phase cycled = scatter free
self.phase_cycled_signal = np.zeros(
(
2,
pump_pixels.shape[0],
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
# intp: interpolated
self.intp_phase_cycled_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 pump index
pump_idx = data_container["pump index"]
# Get delay index
delay_idx = data_container["delay index"]
# Get interleave index
intl_idx = data_container["interleave index"]
# Calculate the signals from the sorted data
sorted_data = data_container["sorted data"]
# Phase cycle transmissions if last interleave was
# reached
# * 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
# ** We (Jens and us) agree that it conceptually
# ** makes more sense to average the relative
# ** intensities.
# phase cycled = scatter free
if intl_idx == self.interleaves - 1:
phase_cycled_transmission = np.average(
sorted_data[:, pump_idx, delay_idx], axis=1
)
# Calculate absorption of phase cycled transmissions
# pc_abs = phase cycled absorption
pc_abs = -np.log10(phase_cycled_transmission)
# Calculate phase cycled VIPER signal
# See beginngers notes
self.phase_cycled_signal[:, pump_idx, delay_idx] = (
pc_abs[:, :, 1, 1]
- pc_abs[:, :, 1, 0]
- pc_abs[:, :, 0, 1]
+ pc_abs[:, :, 0, 0]
)
# Calculate absorption of non phase cycled transmission for all states
absorption = -np.log10(
sorted_data[:, pump_idx, delay_idx, intl_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[:, pump_idx, delay_idx, intl_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_phase_cycled_signal[1] = dp.generate_img_data(
data_container["probe axis"],
data_container["pump axis"],
self.phase_cycled_signal[1, :, delay_idx],
)
self.intp_phase_cycled_signal[0] = dp.generate_img_data(
data_container["probe axis"],
data_container["pump axis"],
self.phase_cycled_signal[0, :, delay_idx],
)
# 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[
"phase cycled VIPER signal"
] = self.phase_cycled_signal.copy()
data_container[
"phase cycled VIPER signal (interpol)"
] = self.intp_phase_cycled_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)
pump_pixels (ndarray): Array containing the pump pixels to which
the Fabry Perot is supposed be tuned in the 0th column and
their corresponding weights in the 1st column.
* shape: 2D
* E.g.: (number of pump pixels, 2)
interleaves (int): Number of interleaves that should be scanned.
Number should be even and a power of 2. An interleave is a
small step of the delay stage (in addition to the normal
delay) used for phase cycling and removing scattering.
"""
[docs] class Signals(QObject):
new_data = pyqtSignal(dict)
def __init__(
self,
widget_pyqtgraph,
adc: ADC,
plot_queue,
vis_delays: ndarray,
ir_delays: ndarray,
pump_pixels: ndarray,
interleaves: int,
):
# 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
self.pump_pixels = pump_pixels
self.interleaves = interleaves
# 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=3
)
# Setup plot that holds plot for single scan signal for current delay
self.widget_pyqtgraph.plots[
"signal interleaves current delay"
] = self.widget_pyqtgraph.plots["upper layout"].addPlot(colspan=2)
self.widget_pyqtgraph.plots["signal interleaves current delay"].setTitle(
"VIPER: IR {} fs, ω<sub>3</sub> {:.0f} cm<sup>-1</sup>"
)
self.widget_pyqtgraph.plots["signal interleaves current delay"].setLabel(
"bottom", "probe frequency ω<sub>1</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["signal interleaves current delay"].setLabel(
"left", "difference signal [OD]"
)
self.widget_pyqtgraph.set_style(
self.widget_pyqtgraph.plots["signal interleaves 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(
"Averaged VIPER: IR {} fs, ω<sub>3</sub> {:.0f} cm<sup>-1</sup>"
)
self.widget_pyqtgraph.plots["phase cycled signal current delay"].setLabel(
"bottom", "probe frequency ω<sub>1</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["phase cycled signal current delay"].setLabel(
"left", "difference 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=3
)
# Setup plot that holds heatmap plot y-axis: probe wavenumber, x-axis: delay
self.widget_pyqtgraph.plots[
"single 2d-signal heatmap"
] = self.widget_pyqtgraph.plots["middle layout"].addPlot()
self.widget_pyqtgraph.plots["single 2d-signal heatmap"].setTitle(
"VIPER-signal - IR delay {} fs"
)
self.widget_pyqtgraph.plots["single 2d-signal heatmap"].setLabel(
"bottom", "probe frequency ω<sub>1</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["single 2d-signal heatmap"].setLabel(
"left", "pump frequency ω<sub>3</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.set_style(
self.widget_pyqtgraph.plots["single 2d-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-signal heatmap histogram"] = pg.HistogramLUTItem()
self.widget_pyqtgraph.plots["middle layout"].addItem(
self.plot_ref["single 2d-signal heatmap histogram"]
)
self.widget_pyqtgraph.plots[
"avg 2d-signal heatmap"
] = self.widget_pyqtgraph.plots["middle layout"].addPlot()
self.widget_pyqtgraph.plots["avg 2d-signal heatmap"].setTitle(
"Avg VIPER-signal UV/VIS delay {} fs"
)
self.widget_pyqtgraph.plots["avg 2d-signal heatmap"].setLabel(
"bottom", "probe frequency ω<sub>1</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["avg 2d-signal heatmap"].setLabel(
"left", "pump frequency ω<sub>3</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.set_style(
self.widget_pyqtgraph.plots["avg 2d-signal heatmap"]
)
self.plot_ref["avg 2d-signal heatmap histogram"] = pg.HistogramLUTItem()
self.widget_pyqtgraph.plots["middle layout"].addItem(
self.plot_ref["avg 2d-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()
# Add a sub-layout to hold the 2D plots and the histograms in the second row
self.widget_pyqtgraph.plots["lower layout"] = self.graphics_layout.addLayout(
colspan=3
)
# Setup plot that displays intensities
self.widget_pyqtgraph.plots["intensities"] = self.widget_pyqtgraph.plots[
"lower layout"
].addPlot()
self.widget_pyqtgraph.plots["intensities"].setTitle("Average intensities")
self.widget_pyqtgraph.plots["intensities"].setLabel(
"bottom", "probe frequency ω<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 standard deviation of signal
# Make the title of std signal so that the Average standard deviation
# of signal over all wavenumbers is displayed. HTML is
# used because setTitle works with it.
self.widget_pyqtgraph.plots["std signal"] = self.widget_pyqtgraph.plots[
"lower layout"
].addPlot()
self.widget_pyqtgraph.plots["std signal"].setTitle("Standard deviation VIPER")
self.widget_pyqtgraph.plots["std signal"].setLabel(
"bottom", "probe frequency ω<sub>1</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["std signal"].setLabel(
"left", "difference signal [OD]"
)
self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["std signal"])
# Setup plot that displays different signals
self.widget_pyqtgraph.plots["signal"] = self.widget_pyqtgraph.plots[
"lower layout"
].addPlot()
self.widget_pyqtgraph.plots["signal"].setTitle("Average signals")
self.widget_pyqtgraph.plots["signal"].setLabel(
"bottom", "probe frequency ω<sub>1</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["signal"].setLabel("left", "difference signal[OD]")
self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["signal"])
# Setup plot that displays pump spectrum for current fabry perot settings
self.widget_pyqtgraph.plots["pump spectrum"] = self.widget_pyqtgraph.plots[
"lower layout"
].addPlot()
self.widget_pyqtgraph.plots["pump spectrum"].setTitle(
"Pump spectrum Fabry Perot"
)
self.widget_pyqtgraph.plots["pump spectrum"].setLabel(
"bottom", "pump frequency ω<sub>3</sub> [cm<sup>-1</sup>]"
)
self.widget_pyqtgraph.plots["pump spectrum"].setLabel(
"left", "intensity [a.u.]"
)
self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["pump spectrum"])
# 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"]
intl_idx = data_container["interleave index"]
pump_idx = data_container["pump 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"]
# we name the VIPER signal = signal because that will save
# time replacing variable names
# the same applies for the phase cycled signals etc.
signal = data_container["VIPER signal"]
phase_cycled_signal = data_container["phase cycled VIPER signal"]
intp_phase_cycled_signal = data_container[
"phase cycled VIPER signal (interpol)"
]
pump_axis = data_container["pump axis"]
probe_axis = data_container["probe axis"]
avg_intensities = data_container["average intensity"]
std_intensities = data_container["std intensity"]
std_signal = data_container["std s2s signal"]
avg_pump_spectrum = data_container["average pump spectrum"]
std_pump_spectrum = data_container["std pump spectrum"]
# Update titles with current delay and pump frequency
self.widget_pyqtgraph.plots["signal interleaves current delay"].setTitle(
"VIPER: IR {} fs, ω<sub>3</sub> {:.0f} cm<sup>-1</sup>".format(
self.ir_delays[delay_idx][0], pump_axis[pump_idx]
)
)
self.widget_pyqtgraph.plots["phase cycled signal current delay"].setTitle(
"Averaged VIPER: IR {} fs, ω<sub>3</sub> {:.0f} cm<sup>-1</sup>".format(
self.ir_delays[delay_idx][0], pump_axis[pump_idx]
)
)
self.widget_pyqtgraph.plots["single 2d-signal heatmap"].setTitle(
"VIPER-signal - IR delay {} fs".format(self.ir_delays[delay_idx][0])
)
self.widget_pyqtgraph.plots["avg 2d-signal heatmap"].setTitle(
"Avg VIPER-signal - 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 + Interleaves
self.plot_ref["signal interleaves current delay"].setData(
x=probe_axis, y=phase_cycled_signal[1, pump_idx, delay_idx]
)
for i in range(self.interleaves):
self.plot_ref["single interleave {} current delay".format(i)].setData(
x=probe_axis, y=signal[1, pump_idx, delay_idx, i],
)
# Phase cycled signals
# Scan averaged
self.plot_ref["avg phase cycled signal current delay"].setData(
x=probe_axis, y=phase_cycled_signal[0, pump_idx, delay_idx]
)
# Single scan
self.plot_ref["single phase cycled signal current delay"].setData(
x=probe_axis, y=phase_cycled_signal[1, pump_idx, delay_idx]
)
# -----------Start 2D Plots---------------
# 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 2d-signal heatmap histogram"].getLevels()
self.plot_ref["single 2d-signal heatmap"].setImage(
intp_phase_cycled_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-signal heatmap histogram"].setLevels(*levels)
# Update contour lines
dp.update_contour_lines(
intp_phase_cycled_signal[1],
self.plot_ref["single 2d-signal heatmap contours"],
)
# Scan averaged
# Get old levels so it does not rescale the colors everytime (see below)
levels = self.plot_ref["avg 2d-signal heatmap histogram"].getLevels()
self.plot_ref["avg 2d-signal heatmap"].setImage(intp_phase_cycled_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 2d-signal heatmap histogram"].setLevels(*levels)
# Update contour lines
dp.update_contour_lines(
intp_phase_cycled_signal[0],
self.plot_ref["avg 2d-signal heatmap contours"],
)
# ----------End 2D Plots----------------
# Plot standard deviation of shot to shot VIPER signal
self.plot_ref["std signal"].setData(x=probe_axis, y=std_signal)
# 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
)
# Update pump spectrum of fabry perot
self.plot_ref["pump spectrum"].setData(x=probe_axis, y=avg_pump_spectrum)
# Update intensity error bars for pump spectrum
self.plot_ref["pump spectrum error bars"].setData(
x=probe_axis, y=avg_pump_spectrum, height=5 * std_pump_spectrum
)
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
)
intl_signal_pen = pg.mkPen(
color="#A9A9A9", width=1.2, style=QtCore.Qt.DotLine
)
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
)
pump_spectrum_pen = pg.mkPen(
color="#8c564b", width=2.5, style=QtCore.Qt.SolidLine
)
bisector_pen = pg.mkPen(
color="#7f7f7f", width=1.5, style=QtCore.Qt.SolidLine
)
# Histogram colormap
seismic = pg.graphicsItems.GradientEditorItem.Gradients["seismic"]
# Plot signal
# Single scan
self.plot_ref[
"signal interleaves current delay"
] = self.widget_pyqtgraph.plots["signal interleaves current delay"].plot(
x=probe_axis,
y=phase_cycled_signal[1, pump_idx, delay_idx],
name="single scan VIPER signal",
pen=signal_pen,
)
for i in range(self.interleaves):
self.plot_ref[
"single interleave {} current delay".format(i)
] = self.widget_pyqtgraph.plots[
"signal interleaves current delay"
].plot(
x=probe_axis,
y=signal[1, pump_idx, delay_idx, i],
name="interleave {}".format(i),
pen=intl_signal_pen,
)
# Don't plot interleaves for scan averaged data
# 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=phase_cycled_signal[0, pump_idx, 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=phase_cycled_signal[1, pump_idx, delay_idx],
name="single scan VIPER signal",
pen=signal_pen,
)
# Plot 2d image: time vs. signal (wavenumber)
# Single scan
self.plot_ref["single 2d-signal heatmap"] = pg.ImageItem(
intp_phase_cycled_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-signal heatmap histogram"].setImageItem(
self.plot_ref["single 2d-signal heatmap"]
)
# Set the color levels of the histogram
self.plot_ref["single 2d-signal heatmap histogram"].gradient.restoreState(
seismic
)
# Scale image to match axes
dp.scale_img(
probe_axis,
pump_axis,
intp_phase_cycled_signal[1],
self.plot_ref["single 2d-signal heatmap"],
)
self.widget_pyqtgraph.plots["single 2d-signal heatmap"].addItem(
self.plot_ref["single 2d-signal heatmap"]
)
self.plot_ref["single 2d-signal heatmap"].setLookupTable(self.lut)
# Generate contour lines
self.plot_ref[
"single 2d-signal heatmap contours"
] = dp.generate_contour_lines(
intp_phase_cycled_signal[1], self.plot_ref["single 2d-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-signal heatmap"].addItem(bisector)
# Plot 2d image: time vs. signal (wavenumber)
# We only display the non averaged data
# Scan averaged
self.plot_ref["avg 2d-signal heatmap"] = pg.ImageItem(
intp_phase_cycled_signal[0]
)
self.plot_ref["avg 2d-signal heatmap histogram"].setImageItem(
self.plot_ref["avg 2d-signal heatmap"]
)
# Set the color levels of the histogram
self.plot_ref["avg 2d-signal heatmap histogram"].gradient.restoreState(
seismic
)
dp.scale_img(
probe_axis,
pump_axis,
intp_phase_cycled_signal[0],
self.plot_ref["avg 2d-signal heatmap"],
)
self.widget_pyqtgraph.plots["avg 2d-signal heatmap"].addItem(
self.plot_ref["avg 2d-signal heatmap"]
)
self.plot_ref["avg 2d-signal heatmap"].setLookupTable(self.lut)
# Generate contour lines
self.plot_ref["avg 2d-signal heatmap contours"] = dp.generate_contour_lines(
intp_phase_cycled_signal[1], self.plot_ref["avg 2d-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 2d-signal heatmap"].addItem(bisector)
# Plot standard deviation of shot to shot VIPER signal
self.plot_ref["std signal"] = self.widget_pyqtgraph.plots[
"std signal"
].plot(
x=probe_axis,
y=std_signal,
name="Standard deviation of VIPER signal",
pen=std_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 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,
)
# Plot pump spectrum of fabry perot
self.plot_ref["pump spectrum"] = self.widget_pyqtgraph.plots[
"pump spectrum"
].plot(
x=probe_axis,
y=avg_pump_spectrum,
name="Pump spectrum resulting from Fabry Perot",
pen=pump_spectrum_pen,
)
# Create intensity error bars for pump spectrum
self.plot_ref["pump spectrum error bars"] = pg.ErrorBarItem(
x=probe_axis,
y=avg_pump_spectrum,
height=5 * std_pump_spectrum,
beam=0.3,
pen=std_intensity_pen,
)
self.widget_pyqtgraph.plots["pump spectrum"].addItem(
self.plot_ref["pump spectrum error bars"]
)
# self.widget_pyqtgraph.disable_autoscale()