"""
Warning:
This experiment is deprecated. Use Show Signal (Wobbler) for a more
convenient way of adjusting the Wobbler. It shows the resulting
phase cycled signal in addition to the signal of the different
wobbler states. This makes it substantially easier to minimize
scattering. The only addition this experiment has in comparison to
Show Signal (Wobbler) is that it plots the ADC sampling the Wobbler
reference channel. But the reference channel is not needed for how
the software processes data. ** In the I-Lab the Wobbler reference
channel is not even connected to the ADC. ** (And if one were to
connect it, one would not see the sine-wave that is displayed on the
oscilloscope without using a voltage follower.)
Providing the "Show Wobbler States" experiment.
In this experiment four different Wobbler states are
displayed. One for each Wobbler window position.
The positions are left, middle, right, middle.
The experiment can be used to adjust the Wobblers
phase, delay and amplitude to phase cycle the signal
properly. The correct way to phase cycle is to phase
cycle the intensities (voltages on detector). If the
four signals are out of phase, phase cycling should work.
The chopper and wobbler should be turned on for this
experiment to work properly.
Note:
**General:**
Sometimes when turning the chopper on an off, the
wobbler phase switches from in phase to out of
phase or vice versa. This might cause the user to
think that he has adjusted the phase correctly, while
it is not adjusted correctly. This probably results from
incorrectly setting the chopper phase. It might come
from the fact that the chopper reference is taken
at a different point of the chopper blade than the
laser pulse travels through. Setting the phase of
the chopper correctly might solve this problem.
There is also the possibility to change duty cycle,
phase and amplitude of the wobbler via external
electronics. The duty cycle should in general be set
to 50 %. Adjust the amplitude first since it also
changes the phase relationship between laser and
wobbler. Then adjust the phase. The resulting four
dots (if wobbler frequency is 1/4 of laser frequency)
should have two outer dots (wobbler left and right
position) and two middle dots which should be overlapping.
Adjust amplitude and phase to overlap the middel dots.
The amplitude sets the phase cycle of the IR light and
thus needs to be adjusted while looking at the software
so that the desired part/interval of the signal is phase
cycled.
**Wobbler Adjustment with Multiple Choppers:**
**General procedure:**
First turn off wobbler. Turn the chopper phase such that the four
displayed lines are laying on top of each other. Turn on the wobbler
and adjust it in a way that two pairs of line are opposite to each
other (cancel out). This should reduce the scattering.
**Detailed procedure:**
For adjusting the wobbler the moveable path of the interferometer
needs to be blocked. A pulse delay circuit which can also set the
duty cycle is required for the choppers. The duty cycle of the
outgoing chopper pulse (delayed pulse) should be tuned to 50 %
with the delay electronics. Next step is to set the chopper phase
correctly. For that it is necessary to tune the delay of the outgoing
pulse. If only three lines are displayed (wobbler off) the chopper
phase is not correct. If everything was set correctly (especially
the phase of the chopper via the delay) and the wobbler is off, four
overlapping lines can be observed in the show wobbler states/ show
signal wobbler experiment. This may be because part of the pulse is chopped
incorrectly and is "sliced" off (this is just a guess). Avoid that
there is an offset and try to set it up such that the four lines
are overlapping perfectly.
In show wobbler states experiment the colors of the lines swap with
each other since there is no reference for the wobbler collected by
the adc to pinpoint it's position. Nevertheless it is still possible
to tune the wobbler since the four different lines still represent the
four different positions of the wobbler. Finally turning the wobbler
on results in two pairs of overlapping lines. Which should be tuned
se to enable phase cycling.
#######################
Step by Step Algorithm:
#######################
**Acquisition:**
1. Preallocate dictionary (data container) which will contain data
and information about scan index, delay index etc.
2. Start the ADC task (in the later experiments this was
done differently. See the source code for further details)
3. Read the data from the ADC
4. Place data into dictionary and hand it over to
primary processing
**Primary Processing:**
1. Preallocate arrays for data, counts, weights, chopper
and wobbler states. Here there are 2 states.
On (chopper high) and off (chopper low). Each position
of the wobbler is also a different state
2. Subtract background from raw data (dark noise)
3. Linearize response of pixels
4. Calculate average intensity for each pixel
5. Calculate transmission, or more precisely, relative intensity
(probe intensity / reference intensity) for each laser shot for
each pixel pair
6. Identify the chopper states for all laser shots using the
corresponding channel(s) in the ADCs' data
7. Obtain the different wobbler states with the dataprocessing
class' "get_wobbler_states" method
8. Sort the data (transmissions) for each state and calculate
statistics
9. Put this information into data container
10. Calculate average by weighting with counts.
11. Save data (and raw data) including counts, weights and s2s_std
if respective checkboxes on GUI were checked.
Save counts and weights
12. Hand over the data container to secondary processing
process
**Secondary Processing:**
1. Calculate the phase cycled transmission by averaging (here it
is done in the way that we think is the more correct way to
phase cycle. Phase cycling away the scattering which occurs on
the detector directly makes more sense than to phase cycle the
difference spectra. Although a comparison yields differences on
the 10^-5 precision, we still try to implement the correct method
as shown in this experiment)
2. Calculate the phase cycled absorption (-log10)
3. Calculate the non phase cycled absorption (-log10)
of the sorted data
4. Calculate the pump probe difference signal (chopper high - chopper low)
5. Calculate the first and second derivative of the signal
6. Find maximum of first derivative of averaged data
because this should be the t zero value
7. Calculate the average intensities and standard deviation of
the intensities for each pixel
8. Put this information into data container
9. Calculate the numbers which are displayed in the
"statistics box" on the GUI
10. Hand over data to Pyqtplotting thread
**Pyqt Plotting:**
1. Remove old plots
2. Setup the plot that displays:
* Time-signal heatmap with histogram
* Signal
* Intensities and standard deviation of signal
* Time-signal
* Time-signal 1st derivative
* Time-signal 2nd derivative
3. Plot the plots for the first time
4. Update plots
**Saving:**
.. code-block::
programming data dimension:
[(2 ([0] is current average scan, [1] is last single scan), n_probe_pixels, n_wobbler_states, n_chopper_states)]
saving dimension:
[(n_probe_pixels, n_wobbler_states, n_chopper_states)]
raw data dimension:
[(n_channels, samples_to_acquire)]
################
Folder Structure
################
In **scans/** the first file of 3 contains the data in the
saving dimensions. The counts file contains weights to average equally
weighted. The weights file contains the inverse variance for each saving dimension
as weights. The dimensions of these files are as saving dimension suggests.
These files can be averaged with different weights to obtain the complete
resulting spectrum. It might be worth to check the actual code of the
corresponding experiment to understand how to calculate the desired
end result difference spectrum.
The data in **/averaged_data** is averaged equally weighted (using
counts). Likewise to the scan data the difference spectrum still needs
to be calculated from the transmissions for every state. Because the
array contains the transmissions averaged with counts it is only
intended to be used as a first indicator for the measurement.
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.
.. code-block::
username/
├── date1_experimentname1_000/
│ ├── averaged_data
│ │ └── date1_experimentname1_000.npy
│ ├── figures
│ ├── hardware config
│ ├── raw data
│ │ ├── s000000_date1_experimentname1_000_raw.npy
│ │ ├── ...
│ │ └── s000099_date1_experimentname1_000_raw.npy
│ ├── scans
│ │ ├── s000000_date1_experimentname1_000.npy
│ │ ├── s000000_counts_date1_experimentname1_000.npy
│ │ ├── s000000_weights_date1_experimentname1_000.npy
│ │ ├── ...
│ │ └── s000099_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.backends.backend_qt5agg import (
FigureCanvasQTAgg,
NavigationToolbar2QT as NavigationToolbar,
)
from matplotlib.figure import Figure
# 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 analog_digital_converter import AnalogDigitalConverter as ADC
from triax import Triax as Spectrometer
from thorlabs_mc2000 import ChopperController as Chopper
from save_data import SaveData, Background
[docs]class ShowWobblerStates:
"""
Args:
wobbler_freq (float): Frequency in Hz with which the wobbler
oscillates.
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.
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).
chopper_info (dict): Contains the information that is necessary
to identify the different chopper states of the chopper that
chops the 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.
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,
wobbler_freq: float,
widget_pyqtgraph,
adc: ADC,
prl: PRL,
chopper_info: dict,
background_handler: Background,
spectrometer: Spectrometer,
info_queue: Queue,
saver: SaveData = None,
):
self.wobbler_freq = wobbler_freq
# Initialise Queues
self.acq_queue = Queue()
self.processing_queue = Queue()
self.plot_queue = Queue()
if saver:
saver.save_other(background_handler.load_background(), "background")
self.acquisition = Acquisition(adc, spectrometer, self.acq_queue)
self.primary_processing = PrimaryProcessing(
self.acq_queue,
self.processing_queue,
adc.laser_frequency,
self.wobbler_freq,
adc.pixel_idx,
adc.probe_pixel_idx,
adc.reference_pixel_idx,
adc.index_dict,
prl,
chopper_info,
background_handler,
saver,
)
self.secondary_processing = SecondaryProcessing(
self.processing_queue, self.plot_queue, info_queue
)
# self.plotting = Plotting(mpl_widget, adc, self.plot_queue)
self.plotting = PyqtPlotting(widget_pyqtgraph, adc, self.plot_queue)
[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:
adc (ADC): Analog to digital converter hardware object which is
used to communicate with and read data from the ADC.
spectrometer (Spectrometer): Spectrometer/Triax hardware class
which grants functionality to control the triax
spectrometer. Needed to obtain e.g. wavenumber axis etc.
acq_queue (Queue): Multiprocessing queue object that the
acquisition thread uses to pass data to the primary
processing process.
"""
def __init__(self, adc: ADC, spectrometer: Spectrometer, acq_queue):
threading.Thread.__init__(self)
self.adc = adc
self.spectrometer = spectrometer
# 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 = {}
# Multiprocessing event to stop experiment
self.exit = threading.Event()
[docs] def run(self):
# Read first set of data outside of loop
self.adc.read()
while not self.exit.is_set():
# Start acquisition without waiting
# for data to become available
self.adc.start()
# Put relevant information into data container
self.data_container["data"] = self.adc.data
self.data_container["scan index"] = self.scan_idx
self.data_container["probe axis"] = self.spectrometer.wn_axis
# Give data of prior acquisition to queue
# Hand over raw data because standard
# deviations needs to be calculated in
# the other process
self.acq_queue.put(self.data_container.copy())
# Update scan idx
self.scan_idx += 1
# Read data with given parameters
# (i.e. samples to acquire usually from GUI)
self.adc.read()
self.acq_queue.put("stop")
[docs] def shutdown(self):
# Setting exit will stop the loop
# within run
self.exit.set()
[docs]class PrimaryProcessing(multiprocessing.Process):
"""
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.
laser_freq (float): Frequency (repetition rate) of the laser in
Hz.
wobbler_freq (float): Frequency in Hz with which the wobbler
oscillates.
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).
chopper_info (dict): Contains the information that is necessary
to identify the different chopper states of the chopper that
chops the 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,
laser_freq: int,
wobbler_freq: float,
pixel_idx: ndarray,
probe_pixel_idx: ndarray,
reference_pixel_idx: ndarray,
index_dict: dict,
prl: PRL,
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
self.prl = prl
self.saver = saver
self.laser_freq = laser_freq
self.wobbler_freq = wobbler_freq
# 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()
# In this experiment we have two different chopper states
self.n_chopper_states = 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
self.chopper_voltage_level = [chopper_info["high voltage level"][0] / 2]
self.chopper_name = chopper_info["name"][0]
# Initialize the wobbler states array.
self.n_wobbler_states = int(self.laser_freq / self.wobbler_freq)
# We need an array, which describes all possible states
# for the sort data function
self.n_possible_states = np.array(
(self.n_wobbler_states, self.n_chopper_states)
)
# Initialize array that holds both
# temporary and averaged data
# In this case the data is average
# intensity 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_probe_pixels, n_wobbler_states, n_chopper_states)
self.data = np.zeros(
(
2,
self.probe_pixel_idx.size,
int(self.n_wobbler_states),
self.n_chopper_states,
)
)
self.counts = np.zeros(self.data.shape)
self.weights = np.zeros(self.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"]
# 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
chopper_states = np.digitize(
raw_data[self.index_dict[self.chopper_name]], self.chopper_voltage_level
)
# Get the correctly ordered wobbler states
# The channel which contains the wobbler data is specified in a json file
# and can be accessed here via the index_dict.
wobbler_states = dp.get_wobbler_states(
raw_data[self.index_dict["wobbler"]], self.laser_freq, self.wobbler_freq
)
# Create an array containing chopper and wobbler states.
states_array = np.vstack((wobbler_states, chopper_states))
# Sort and average data
self.data[1], self.weights[1], self.counts[1], statistics = dp.sort_data(
transmission, states_array, self.n_possible_states
)
# Add everything to data container
# and give it to processing queue
data_container["sorted data"] = self.data[1]
data_container["intensities"] = intensities
data_container["transmission"] = transmission
data_container["statistics"] = statistics
data_container["chopper states"] = chopper_states
data_container["wobbler states"] = wobbler_states
# Get the data from the adc-channel which the wobbler is connected to
data_container["wobbler signal"] = raw_data[self.index_dict["wobbler"]]
self.processing_queue.put(data_container.copy())
# ----------------------------------------
# Save data (if specified)
if self.saver:
# Create / Average "averaged" data by using weighted average
# between the temp data (weight = 1) and the already
# existing average data (weight = scan_idx)
self.data[0] = np.average(self.data, axis=0, weights=self.counts)
# Use saver class to save data
self.saver.save_scan(self.data[1], scan_idx)
self.saver.save_avg(self.data[0])
self.saver.save_counts(self.counts[1], scan_idx)
self.saver.save_weights(self.weights[1], scan_idx)
if self.saver.raw_data:
self.saver.save_raw_data(raw_data, scan_idx)
# Update counts
self.counts[0] += self.counts[1]
# 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.
"""
def __init__(
self, processing_queue: Queue, plot_queue: Queue, info_queue: Queue,
):
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
[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
# Calculate the pump probe signal from the sorted data
sorted_data = data_container["sorted data"]
absorption = -np.log10(sorted_data)
signal = absorption[:, :, 1] - absorption[:, :, 0]
# signal[:,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["signal"] = 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.
"""
[docs] class Signals(QObject):
new_data = pyqtSignal(dict)
def __init__(self, widget_pyqtgraph, adc: ADC, plot_queue):
# Assign attributes
self.adc = adc
self.widget_pyqtgraph = widget_pyqtgraph
self.graphics_layout = widget_pyqtgraph.graphics_layout
self.plot_queue = plot_queue
# Setup signals and threadpool
self.threadpool = QThreadPool()
self.signals = self.Signals()
# Clear old plots
self.widget_pyqtgraph.remove_plots()
# Setup plot that displays pump probe signal
self.widget_pyqtgraph.plots["signal"] = self.graphics_layout.addPlot(
row=0, col=0, colspan=2
)
self.widget_pyqtgraph.plots["signal"].setTitle(
"Signal at Different Phase Cycles of Wobbler"
)
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"])
# Setup plot that displays intensities
self.widget_pyqtgraph.plots["intensities"] = self.graphics_layout.addPlot(
row=1, 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 wobbler signal collected by adc
self.widget_pyqtgraph.plots["wobbler"] = self.graphics_layout.addPlot(
row=1, col=1
)
self.widget_pyqtgraph.plots["wobbler"].setTitle(
"Wobbler signal collected by ADC"
)
self.widget_pyqtgraph.plots["wobbler"].setLabel("bottom", "time [s]")
self.widget_pyqtgraph.plots["wobbler"].setLabel("left", "voltage [V]")
self.widget_pyqtgraph.set_style(self.widget_pyqtgraph.plots["wobbler"])
# Calculate x-Axis for wobbler signal
self.wobbler_x_axis = np.linspace(
0, adc.acquisition_time, adc.samples_to_acquire, endpoint=False
)
# Create dictionary that holds reference to lines etc.
self.plot_ref = {}
# 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):
# Plot for the first time to get line references
signal = data_container["signal"]
probe_axis = data_container["probe axis"]
avg_intensities = data_container["average intensity"]
std_intensities = data_container["std intensity"]
wobbler_signal = data_container["wobbler signal"]
if self.plot_ref:
# Update signal plot with wobbler phase
# state 0
self.plot_ref["signal0"].setData(
x=probe_axis, y=signal[:, 0] # [all pixels, wobblerstate 0]
)
# Update signal plot with wobbler phase
# state 1
self.plot_ref["signal1"].setData(x=probe_axis, y=signal[:, 1])
# Update signal plot with wobbler phase
# state 2
self.plot_ref["signal2"].setData(x=probe_axis, y=signal[:, 2])
# Update signal plot with wobbler phase
# state 3
self.plot_ref["signal3"].setData(x=probe_axis, y=signal[:, 3])
# 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]
)
self.plot_ref["wobbler signal"].setData(
x=self.wobbler_x_axis, y=wobbler_signal
)
else:
# First time plotting
signal_pen0 = pg.mkPen(
color="#d62728", width=2.5, style=QtCore.Qt.SolidLine
)
signal_pen1 = pg.mkPen(
color="#9467bd", width=2.5, style=QtCore.Qt.SolidLine
)
signal_pen2 = pg.mkPen(
color="#8c564b", width=2.5, style=QtCore.Qt.SolidLine
)
signal_pen3 = pg.mkPen(
color="#e377c2", 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?
wobbler_signal_pen = pg.mkPen(
color="#2ca02c", width=1, style=QtCore.Qt.SolidLine
)
# Plot signal of wobbler phase state 0
self.plot_ref["signal0"] = self.widget_pyqtgraph.plots["signal"].plot(
x=probe_axis,
y=signal[:, 0],
name="Average pump-probe signal of Wobbler phase state 0",
pen=signal_pen0,
)
# Plot signal of wobbler phase state 1
self.plot_ref["signal1"] = self.widget_pyqtgraph.plots["signal"].plot(
x=probe_axis,
y=signal[:, 1],
name="Average pump-probe signal of Wobbler phase state 1",
pen=signal_pen1,
)
# Plot signal of wobbler phase state 2
self.plot_ref["signal2"] = self.widget_pyqtgraph.plots["signal"].plot(
x=probe_axis,
y=signal[:, 2],
name="Average pump-probe signal of Wobbler phase state 2",
pen=signal_pen2,
)
# Plot signal of wobbler phase state 3
self.plot_ref["signal3"] = self.widget_pyqtgraph.plots["signal"].plot(
x=probe_axis,
y=signal[:, 3],
name="Average pump-probe signal of Wobbler phase state 3",
pen=signal_pen3,
)
# 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,
)
self.plot_ref["wobbler signal"] = self.widget_pyqtgraph.plots[
"wobbler"
].plot(
x=self.wobbler_x_axis,
y=wobbler_signal,
name="Wobbler signal collected by ADC",
pen=wobbler_signal_pen,
)
self.widget_pyqtgraph.disable_autoscale()