From 70b1eb46e421739244db294218f6e02e9589fa55 Mon Sep 17 00:00:00 2001 From: Guenter Quast <guenter.quast@kit.edu> Date: Sun, 7 Jul 2024 18:09:01 +0200 Subject: [PATCH] removed mimoCoRB interface from this release - now in repo redpitaya-daq --- README.md | 35 - config/spectrum_config.yaml | 90 --- modules/filters.py | 94 --- modules/plot_histograms.py | 27 - modules/plot_waveform.py | 33 - modules/redPitaya_source.py | 54 -- modules/save_files.py | 26 - modules/spectrum_filter.py | 107 --- read_npy.py | 23 - redP_mimoCoRB.py | 99 --- redPdaq.py | 1332 ----------------------------------- 11 files changed, 1920 deletions(-) delete mode 100644 config/spectrum_config.yaml delete mode 100644 modules/filters.py delete mode 100644 modules/plot_histograms.py delete mode 100644 modules/plot_waveform.py delete mode 100644 modules/redPitaya_source.py delete mode 100644 modules/save_files.py delete mode 100644 modules/spectrum_filter.py delete mode 100755 read_npy.py delete mode 100755 redP_mimoCoRB.py delete mode 100755 redPdaq.py diff --git a/README.md b/README.md index 89a646d..8cf26a3 100644 --- a/README.md +++ b/README.md @@ -36,16 +36,9 @@ courses at the Faculty of Physics at Karlsruhe Institute of Technology. - *README.md* this documentation - *mchpa.py* client program - - *redPdaq.py* client for data-acquisition using the mcpha server - *examples/* recorded spectra - *examples/peakFitter.py* code to find and fit peaks in spectum data - - *read_npy.py* a simple example to read waveforms saved in *.npy* format - - *redP_mimocorb.py* runs *redPdaq* as a client of the buffer manager *mimoCoRB* - - *setup.yaml* coniguration script defining the *mimoCoRB* application - - *modules/* and *config/* contain code and configuration files for the *redP_mimoCoRB* application - *mcpha.ui* qt5 graphical user interface for *mcpha* application - - *rpControl.ui* qt5 tab for *redPdaq* application - - *mcpha_daq.ui* qt5 tab for oscilloscope with daq mode - *mcpha_gen.ui* qt5 tab for generator - *mcpha_osc.ui* qt5 tab for oscilloscope - *mcpha_hst.ui* qt5 tab for histogram display @@ -121,31 +114,6 @@ Note that spectra and waveforms are plotted with a very large number of channels the resolution of a computer display. It is therefore possible to use the looking-glass button of the *matplotlib*window to mark regions to zoom in for a detailed inspection of the data. -## Oscilloscope and data recorder - -The script *redPdaq.py* relies on the same server and FPGA image as the pulse-height analyzer, -providing the same functionality as *mcpha.py*. In addition, however, there is a button -"*Start DAQ*" in the oscilloscope tab to run the oscilloscope in data acquisition mode, -i.e. continuously. A subset of the data is shown in the oscilloscope display, together with -information on the trigger rate and the transferred data volume. A configurable user-defined -function may also be called to analyse and store the recorded waveforms. -It is possible to transfer data over a one-Gbit network from the RedPitaya with a rate of 50 MB/s -or about 500 waveforms/s. - -An examples of call-back functions callable from within redPdaq is provided with the package - - - redP_consumer() - calculates and displays statistics on trigger rate and data volume - - -*redP_mimocorb.py* is a script containing code to be started from the command line and -a function defined in the script, *redP_to_rb*, is called as sub-process within the -*mimiCoRB* buffer manager frame-work for more advanced data analysis tasks requiring -multiple processes running in parallel. A *mimoCoRB* setup-file is also provided and can -be started by running typing `redP_mimoCoRB.py setup.yaml` on the command line. Modules -and configuration files for a pulse-height analysis of recorded signals are contained -as exampless in the sub-directories *modules/* and *config/*, respectively. - ## Installation @@ -206,9 +174,6 @@ Then, on the client side: the *oscilloscpe* tab; - now click the tab *spectrum histogram 1*; adjust the amplitude threshold and time of exposure, then click the *Start* button and watch the spectrum building up; - - if running `redPdaq.py`, threre is a buttton "StartDQQ"; click it to cintinuously transfer - waveform data to the client computer. If a filename was specified, data is recorded to disk - in *.npy* format; note that any active spectrum rab is put in paused mode if DAQ is active. - when finished, use the *Save* button to save the spectrum to a file with a meaningful name. diff --git a/config/spectrum_config.yaml b/config/spectrum_config.yaml deleted file mode 100644 index 2a6cfc1..0000000 --- a/config/spectrum_config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Dict with uid's as key and a nested dict with configuration variables -general: - runtime: 600 # desired runtime in seconds - runevents: &number_of_events 100000 - number_of_samples: &number_of_samples 1000 - analogue_offset: &analogue_offset 0 - # special settings for RedPitaya - decimation_index: &decimation 4 # decimation 0:1, i:2^(i+1) # data decimation factor, 0 is 8 ns/sample - sample_time_ns: &sample_time_ns 256 # for decimation 4 - invert_channel1: &invert1 0 - invert_channel2: &invert2 0 - - trigger_level: &trigger_level 50 - trigger_channel: &trigger_channel '1' - trigger_direction: &trigger_direction "rising" # or "falling" - pre_trigger_samples: &pre_trigger_samples 103 # 5% - -# dict for spectrum_filter, function find_peaks -find_peaks: - # signal_characteristics: 256ns/sample, duration 50µs ( ~200 samples) - sample_time_ns: *sample_time_ns - analogue_offset: *analogue_offset - number_of_samples: *number_of_samples - pre_trigger_samples: *pre_trigger_samples - peak_minimal_prominence: 200 # has to be positive and higher than avg. noise peaks to not cause havoc! - trigger_channel: *trigger_channel - peak_minimal_distance: 400 # minimal distance between two peaks in number of samples - peak_minimal_width: 100 # in number of samples - trigger_channel: *trigger_channel - trigger_position_tolerance: 20 # in number of samples - -# dict for RedPitaya redPoscdaq -redP_to_rb: - ip_address: '192.168.0.103' - eventcount: *number_of_events - sample_time_ns: *sample_time_ns - number_of_samples: *number_of_samples - pre_trigger_samples: *pre_trigger_samples - trigger_channel: *trigger_channel - trigger_level: *trigger_level - trigger_mode: "norm" # or "auto" - # special settings for RedPitaya - decimation_index: *decimation - invert_channel1: *invert1 - invert_channel2: *invert2 - -# Dict for simul_source.py -simul_source: - sample_time_ns: *sample_time_ns - number_of_samples: *number_of_samples - pre_trigger_samples: *pre_trigger_samples - analogue_offset: *analogue_offset - eventcount: *number_of_events - sleeptime: 0.03 - random: true - -# Dict for push_simul -push_simul: - sample_time_ns: *sample_time_ns - number_of_samples: *number_of_samples - pre_trigger_samples: *pre_trigger_samples - analogue_offset: *analogue_offset - eventcount: *number_of_events - sleeptime: 0.03 - random: true - -save_to_txt: - filename: "spectrum" - -save_parquet: - filename: "spectrum" - -plot_waveform: - title: "Muon waveform" - min_sleeptime: 0.5 # time to wait between graphics updates - number_of_samples: *number_of_samples - sample_time_ns: *sample_time_ns - analogue_offset: *analogue_offset # analogue offset in V - pre_trigger_samples: *pre_trigger_samples - channel_range: 4096 # channel range in mV - trigger_channel: *trigger_channel # Channel name in the PicoScope. Valid values are 'A', 'B', 'C' or 'D' - trigger_level: *trigger_level # value in mV, take account of analogue_offset, which is added to input voltage ! - -plot_histograms: - title: "on-line histograms" - # define histograms - histograms: - # name min max nbins ymax name lin/log - ch1_height: [50., 3000., 250, 20., "ph 1A", 0] - ch2_height: [50., 3000., 250, 20., "ph 1B", 0] diff --git a/modules/filters.py b/modules/filters.py deleted file mode 100644 index 7158cc6..0000000 --- a/modules/filters.py +++ /dev/null @@ -1,94 +0,0 @@ -from mimocorb import mimo_buffer as bm -from scipy import signal -import numpy as np -from numpy.lib import recfunctions as rfn -import sys -import os - - -def normed_pulse(ch_input, position, prominence, analogue_offset): - # > Compensate for analogue offset - ch_data = ch_input - analogue_offset - # > Find pulse area - # rel_height is not good because of the quantized nature of the picoscope data - # so we have to "hack" a little bit to always cut 10mV above the analogue offset - width_data = signal.peak_widths(ch_data, [int(position)], rel_height=(ch_data[int(position)] - 10) / prominence) - left_ips, right_ips = width_data[2], width_data[3] - # Crop pulse area and normalize - pulse_data = ch_data[int(np.floor(left_ips)) : int(np.ceil(right_ips))] - pulse_int = sum(pulse_data) - pulse_data *= 1 / pulse_int - return pulse_data, int(np.floor(left_ips)), pulse_int - - -def correlate_pulses(data_pulse, reference_pulse): - correlation = signal.correlate(data_pulse, reference_pulse, mode="same") - shift_array = signal.correlation_lags(data_pulse.size, reference_pulse.size, mode="same") - shift = shift_array[np.argmax(correlation)] - return shift - - -def tag_peaks(input_data, prominence, distance, width): - peaks = {} - peaks_prop = {} - for key in input_data.dtype.names: - peaks[key], peaks_prop[key] = signal.find_peaks( - input_data[key], prominence=prominence, distance=distance, width=width - ) - return peaks, peaks_prop - - -def correlate_peaks(peaks, tolerance): - m_dtype = [] - for key in peaks.keys(): - m_dtype.append((key, np.int32)) - next_peak = {} - for key, data in peaks.items(): - if len(data) > 0: - next_peak[key] = data[0] - correlation_list = [] - while len(next_peak) > 0: - minimum = min(next_peak.values()) - line = [] - for key, data in peaks.items(): - if key in next_peak: - if abs(next_peak[key] - minimum) < tolerance: - idx = data.tolist().index(next_peak[key]) - line.append(idx) - if len(data) > idx + 1: - next_peak[key] = data[idx + 1] - else: - del next_peak[key] - else: - line.append(-1) - else: - line.append(-1) - correlation_list.append(line) - array = np.zeros(len(correlation_list), dtype=m_dtype) - for idx, line in enumerate(correlation_list): - array[idx] = tuple(line) - return array - - -def match_signature(peak_matrix, signature): - if len(signature) > len(peak_matrix): - return False - # Boolean array with found peaks - input_peaks = rfn.structured_to_unstructured(peak_matrix) >= 0 - must_have_peak = np.array(signature, dtype=np.str0) == "+" - must_not_have_peak = np.array(signature, dtype=np.str0) == "-" - match = True - # Check the signature for each peak (1st peak with 1st signature, 2nd peak with 2nd signature, ...) - for idx in range(len(signature)): - # Is everywhere a peak, where the signature expects one -> Material_conditial(A, B): (not A) OR B - first = (~must_have_peak[idx]) | input_peaks[idx] - # Is everywhere no peak, where the signature expects no peak -> NAND(A, B): not (A and B) - second = ~(must_not_have_peak[idx] & input_peaks[idx]) - match = match & (np.all(first) & np.all(second)) - return match - - -if __name__ == "__main__": - print("Script: " + os.path.basename(sys.argv[0])) - print("Python: ", sys.version, "\n".ljust(22, "-")) - print("THIS IS A MODULE AND NOT MEANT FOR STANDALONE EXECUTION") diff --git a/modules/plot_histograms.py b/modules/plot_histograms.py deleted file mode 100644 index fce2408..0000000 --- a/modules/plot_histograms.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -**plot_histograms**: histogram variable(s) from buffer using mimoCoRB.histogram_buffer -""" -import sys -import os -from mimocorb.histogram_buffer import histogram_buffer -import matplotlib - -# select matplotlib frontend if needed -matplotlib.use("TkAgg") - - -def plot_histograms(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - """ - Online display of histogram(s) of variable(s) from mimiCoRB buffer - - :param input: configuration dictionary - - """ - histbuf = histogram_buffer(source_list, sink_list, observe_list, config_dict, **rb_info) - histbuf() - - -if __name__ == "__main__": - print("Script: " + os.path.basename(sys.argv[0])) - print("Python: ", sys.version, "\n".ljust(22, "-")) - print("THIS IS A MODULE AND NOT MEANT FOR STANDALONE EXECUTION") diff --git a/modules/plot_waveform.py b/modules/plot_waveform.py deleted file mode 100644 index 66d0c12..0000000 --- a/modules/plot_waveform.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -**plot**: plotting waveforms from buffer using mimoCoRB.buffer_control.OberserverData -""" - -import sys -import os -from mimocorb.plot_buffer import plot_buffer -import matplotlib - -# select matplotlib frontend if needed -matplotlib.use("TkAgg") - - -def plot_waveform(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - """ - Plot waveform data from mimiCoRB buffer - - :param input: configuration dictionary - - - plot_title: graphics title to be shown on graph - - min_sleeptime: time between updates - - sample_time_ns, channel_range, pretrigger_samples and analogue_offset - describe the waveform data as for oscilloscope setup - """ - - pltbuf = plot_buffer(source_list, sink_list, observe_list, config_dict, **rb_info) - pltbuf() - - -if __name__ == "__main__": - print("Script: " + os.path.basename(sys.argv[0])) - print("Python: ", sys.version, "\n".ljust(22, "-")) - print("THIS IS A MODULE AND NOT MEANT FOR STANDALONE EXECUTION") diff --git a/modules/redPitaya_source.py b/modules/redPitaya_source.py deleted file mode 100644 index adabc04..0000000 --- a/modules/redPitaya_source.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -**simul_source**: a simple template for a mimoCoRB source to -enter simulated wave form data in a mimoCoRB buffer. - -Input data is provided as numpy-arry of shape (number_of_channels, number_of_samples). -""" - -from mimocorb.buffer_control import rbImport -import numpy as np -import sys, time -from pulseSimulator import pulseSimulator - -def simul_source(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - """ - Generate simulated data and pass data to buffer - - The class mimocorb.buffer_control/rbImport is used to interface to the - newBuffer and Writer classes of the package mimoCoRB.mimo_buffer - - This example may serve as a template for other data sources - - :param config_dict: configuration dictionary - - - events_required: number of events to be simulated or 0 for infinite - - sleeptime: (mean) time between events - - random: random time between events according to a Poission process - - number_of_samples, sample_time_ns, pretrigger_samples and analogue_offset - describe the waveform data to be generated (as for oscilloscope setup) - - Internal parameters of the simulated physics process (the decay of a muon) - are (presently) not exposed to user. - """ - - events_required = 1000 if "eventcount" not in config_dict else config_dict["eventcount"] - - def yield_data(): - """generate simulated data, called by instance of class mimoCoRB.rbImport""" - - event_count = 0 - while events_required == 0 or event_count < events_required: - pulse = dataSource(number_of_channels) - # deliver pulse data and no metadata - yield (pulse, None) - event_count += 1 - - dataSource = pulseSimulator(config_dict) - simulsource = rbImport(config_dict=config_dict, sink_list=sink_list, ufunc=yield_data, **rb_info) - number_of_channels = len(simulsource.sink.dtype) - # possibly check consistency of provided dtype with simulation ! - - # TODO: Change to logger! - # print("** simul_source ** started, config_dict: \n", config_dict) - # print("?> sample interval: {:02.1f}ns".format(osci.time_interval_ns.value)) - simulsource() diff --git a/modules/save_files.py b/modules/save_files.py deleted file mode 100644 index 0007447..0000000 --- a/modules/save_files.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Module save_files to handle file I/O for data in txt and parquet format - - This module relies on classes in mimocorb.buffer_control -""" - -import sys -import os -from mimocorb.buffer_control import rb_toTxtfile, rb_toParquetfile - - -# def save_to_txt(source_dict): -def save_to_txt(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - sv = rb_toTxtfile(source_list=source_list, config_dict=config_dict, **rb_info) - sv() - # print("\n ** save_to_txt: end seen") - - -def save_parquet(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - sv = rb_toParquetfile(source_list=source_list, config_dict=config_dict, **rb_info) - sv() - - -if __name__ == "__main__": - print("Script: " + os.path.basename(sys.argv[0])) - print("Python: ", sys.version, "\n".ljust(22, "-")) - print("THIS IS A MODULE AND NOT MEANT FOR STANDALONE EXECUTION") diff --git a/modules/spectrum_filter.py b/modules/spectrum_filter.py deleted file mode 100644 index aaa9962..0000000 --- a/modules/spectrum_filter.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Module **pulse_filter** - -This (rather complex) module filters waveform data to search for valid signal pulses. -The code first validates the trigger pulse, identifies coincidences of signals in -different layers (indiating the passage of a cosmic ray particle, a muon) and finally -searches for double-pulse signatures indicating that a muon was stopped in or near -a detection layer where the resulting decay-electron produced a delayed pulse. -The time difference between the initial and the delayed pulses is the individual -lifetime of the muon. - -The decay time and the properties of the signal pulses (height, integral and -postition in time) are written to a buffer; the raw wave forms are optionally -also written to another buffer. - -The callable functions *find_peaks()* and *calulate_decay_time()* depend on the -buffer manager *mimoCoRB* and provide the filter functionality described above. -These functions support multiple sinks to be configured for output. - -The relevant configuration parameters can be found in the section *find_peaks:* -and *calculate_decay_time:* in the configuration file. - -""" - -from mimocorb.buffer_control import rbTransfer -import numpy as np -import pandas as pd -import os, sys - -from filters import * - -def find_peaks(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - """filter client for mimoCoRB: Find valid signal pulses in waveform data - - Input: - - - wave form data from source buffer defined in source_list - - Returns: - - - None if filter not passed - - list of list(s) of pulse parameters, written to sinks defined in sink_list - - """ - - if config_dict is None: - raise ValueError("ERROR! Wrong configuration passed (in lifetime_modules: calculate_decay_time)!!") - - # Load configuration - sample_time_ns = config_dict["sample_time_ns"] - analogue_offset = config_dict["analogue_offset"]*1000 - peak_minimal_prominence = config_dict["peak_minimal_prominence"] - peak_minimal_distance = config_dict["peak_minimal_distance"] - peak_minimal_width = config_dict["peak_minimal_width"] - pre_trigger_samples = config_dict["pre_trigger_samples"] - trigger_channel = config_dict["trigger_channel"] -# if trigger_channel not in ['A','B','C','D']: - if trigger_channel not in ['1','2']: - trigger_channel = None - trigger_position_tolerance = config_dict["trigger_position_tolerance"] - - pulse_par_dtype = sink_list[-1]['dtype'] - - - def tag_pulses(input_data): - """find all valid pulses - - This function to be called by instance of class mimoCoRB.rbTransfer - - Args: input data as structured ndarray - - Returns: list of parameterized pulses - """ - - # Find all the peaks and store them in a dictionary - peaks, peaks_prop = tag_peaks(input_data, peak_minimal_prominence, peak_minimal_distance, peak_minimal_width) - - # identify trigger channel, validate trigger pulse and get time of trigger pulse - if trigger_channel is not None: - trigger_peaks = peaks['ch' + trigger_channel] - if len(trigger_peaks) == 0: - return None - reference_position = trigger_peaks[np.argmin(np.abs(trigger_peaks - pre_trigger_samples))] - else: # external or no trigger: set to nominal position - reference_position = pre_trigger_samples - - peak_data= np.zeros( (1,), dtype=pulse_par_dtype) - for key in peaks.keys(): - for position, height, left_ips, right_ips in zip( - peaks[key], peaks_prop[key]['prominences'], - peaks_prop[key]['left_ips'], peaks_prop[key]['right_ips']): - if np.abs(reference_position - position) < trigger_position_tolerance: - peak_data[0][key+'_position'] = position - peak_data[0][key+'_height'] = input_data[key][position] - analogue_offset #height - left = int(np.floor(left_ips)) - right = int(np.ceil(right_ips)) - peak_data[0][key+'_integral'] = \ - sum(input_data[key][left:right] - analogue_offset) * sample_time_ns * 1e-9/50/5 - return [peak_data] - - p_filter = rbTransfer(source_list=source_list, sink_list=sink_list, config_dict=config_dict, - ufunc=tag_pulses, **rb_info) - p_filter() - -if __name__ == "__main__": - print("Script: " + os.path.basename(sys.argv[0])) - print("Python: ", sys.version, "\n".ljust(22, '-')) - print("THIS IS A MODULE AND NOT MEANT FOR STANDALONE EXECUTION") diff --git a/read_npy.py b/read_npy.py deleted file mode 100755 index 74fdc45..0000000 --- a/read_npy.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -"""read file from redPoscdaq (in npy format) and display data -""" - -from npy_append_array import NpyAppendArray -import numpy as np -import sys -import matplotlib.pyplot as plt - -data = np.load(sys.argv[1], mmap_mode='r') -print("data read sucessfully, shape = ", data.shape) - -n_samples = len(data[0,0]) -xplt = 0.5 + np.linspace(0, n_samples, num=n_samples, endpoint=True) -fig = plt.figure("Oscillogram", figsize=(8,6)) - -for d in data: - plt.plot(xplt, d[0], '-') - plt.plot(xplt, d[1], '-') - plt.xlabel("time bin") - plt.ylabel("Voltage") - plt.show() - diff --git a/redP_mimoCoRB.py b/redP_mimoCoRB.py deleted file mode 100755 index c165d73..0000000 --- a/redP_mimoCoRB.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -""" -**redP_mimoCoRB**: use mimoCoRB with the RedPitaya and redPoscdaq.py - -Input data is provided as numpy-arry of shape (number_of_channels, number_of_samples) -via callback of the __call__() function in class redP_mimoCoRB. - -This script depends on redPdaq.py and is started as a sup-process within the mimoCoRB -framework. The detailed set-up of ring buffers and the associated funtions is specified -in a configuration file, *setup.yaml*. The process suite is started by running this -script from the command line, possibly specifying the name of the configutation file -as an argument. - -As a demonstration, a configuration *setup.yaml* is contained in this package to -import waveforms from the RedPitaya, display a sub-set of the waveforms and perform -a pulse-height analysis with updating results shown as histograms. -To run this example, connect the out1 of the RedPitaya to one or both of the inputs, -type "./redP_mimoCoRB.py* to run the example and use the graphical interface to connect -the RedPitaya to the network, start the pulse generator, and finally press the *StartDAQ" -button in the oscilloscope tag to start data transfer to the *mimiCoRB* input buffer. - -Stop data taking with the button "End run" in the *mimoCoRB* conotrol window to -cleanly shut down all processes. - -Note that this script depends on additional code for the *mimoCoRB* peak finder -in the sub-directory *modules/* and a corresponding configuration file in -subdirectory *config/*. -""" - -import time -import sys -import redPdaq as rp -from mimocorb.buffer_control import rbPut - -class redP_mimocorb(): - """ Interface for redPoscdaq.py to the daq rinbuffer mimoCoRB - """ - def __init__(self, source_list=None, sink_list=None, observe_list=None, config_dict=None, - **rb_info): - # initialize mimoCoRB interface - self.rb_exporter = rbPut(config_dict=config_dict, sink_list=sink_list, **rb_info) - self.number_of_channels = len(self.rb_exporter.sink.dtype) - self.events_required = 1000 if "eventcount" not in config_dict else config_dict["eventcount"] - - self.event_count = 0 - self.active=True - def __call__(self, data): - """function called by redPoscdaq - """ - if (self.events_required == 0 or self.event_count < self.events_required) and self.active: - # deliver pulse data and no metadata - active = self.rb_exporter(data, None) # send data - self.event_count += 1 - else: - active = self.rb_exporter(None, None) # send None when done - print("redPoscdaq exiting") - sys.exit() - -def redP_to_rb(source_list=None, sink_list=None, observe_list=None, config_dict=None, **rb_info): - """Main function, - executed as a multiprocessing Process, to pass data from the RedPitaya to a mimoCoRB buffer - - :param config_dict: configuration dictionary - - events_required: number of events to be taken - - number_of_samples, sample_time_ns, pretrigger_samples and analogue_offset - - decimation index, invert flags, trigger mode and trigger direction for RedPitaya - """ - - # initialize mimocorb class - rb_source= redP_mimocorb(config_dict=config_dict, sink_list=sink_list, **rb_info) - #print("data source initialized") - - # start oscilloscope in callback mode - #print("starting osci") - rp.run_rpControl(callback=rb_source, conf_dict=config_dict) - -if __name__ == "__main__": # -------------------------------------- -#run mimoCoRB data acquisition suite - # the code below is idenical to the mimoCoRB script run_daq.py - import argparse - import sys, time - from mimocorb.buffer_control import run_mimoDAQ - - # define command line arguments ... - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('filename', nargs='?', default = "setup.yaml", - help = "configuration file") - parser.add_argument('-v','--verbose', type=int, default=2, - help="verbosity level (2)") - parser.add_argument('-d','--debug', action='store_true', - help="switch on debug mode (False)") - # ... and parse command line input - args = parser.parse_args() - - print("\n*==* script " + sys.argv[0] + " running \n") - daq = run_mimoDAQ(args.filename, verbose=args.verbose, debug=args.debug) - daq.setup() - daq.run() - print("\n*==* script " + sys.argv[0] + " finished " + time.asctime() + "\n") diff --git a/redPdaq.py b/redPdaq.py deleted file mode 100755 index 06a20dc..0000000 --- a/redPdaq.py +++ /dev/null @@ -1,1332 +0,0 @@ -#!/usr/bin/env python3 -"""redPoscdaq: fast data acquistion using the oscilloscope client of the MCPHA - application running on a RedPitaya FPGA board - - Contains a button to run the oscilloscope in daq mode, i.e. it is restarted - continously. If defined, data is exported via calling a callback function. - Optionally, triggered waveforms can be stored a numpy binary file - (.npy format). - - Code derived from mcpha.py by Pavel Demin - - This code is compatible with release 20240204 of the alpine linux image - https://github.com/pavel-demin/red-pitaya-notes/releases/tag/20240204 -""" - -script_name = 'redPdaq.py' - -# Communication with server process is achieved via command codes: -# command(code, number, data) # number typically is channel number -# -# code values -# code 2: reset/initialze oscilloscope -# code 4: set decimation factor -# code 11: read status -# code 13: set trigger source chan 1/2 -# code 14: set trigger slope rise/fall -# code 15: set trigger mode norm/autoUi_HstDisplay, QWidget = loadUiType("mcpha_hst.ui") -# code 16: set trigger level in ADC counts -# code 17: set number of pre-trigger samples -# code 18: set number of total samples -# code 19: start oscilloscope -# code 20: read oscilloscope data (two channels at once) -# code 21: set pulse fall-time for generator in µs -# code 22: set pulse rise-time in ns -# code 25: set pulse rate in kHz -# code 26: fixed frequency or poisson -# code 27_ reset generator -# code 28: set bin for pulse distribution, as a histogram with 4096 channels, 0-500 mV -# code 29: start generator -# code 30: stop generator - -import argparse -import os -import sys -import time -import struct - -from functools import partial -import numpy as np -import matplotlib -from matplotlib.figure import Figure -from multiprocessing import Event - -# !!! for conditional import from npy_append_array !!! -def import_npy_append_array(): - global NpyAppendArray - from npy_append_array import NpyAppendArray - -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar - -if "PyQt5" in sys.modules: - from PyQt5.uic import loadUiType - from PyQt5.QtCore import Qt, QTimer, QEventLoop, QRegExp - from PyQt5.QtGui import QPalette, QColor, QRegExpValidator - from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog, QFileDialog - from PyQt5.QtWidgets import QWidget, QLabel, QCheckBox, QComboBox - from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket -else: - from PySide2.QtUiTools import loadUiType - from PySide2.QtCore import Qt, QTimer, QEventLoop, QRegExp - from PySide2.QtGui import QPalette, QColor, QRegExpValidator - from PySide2.QtWidgets import QApplication, QMainWindow, QDialog, QFileDialog - from PySide2.QtWidgets import QWidget, QLabel, QCheckBox, QComboBox - from PySide2.QtNetwork import QAbstractSocket, QTcpSocket - -# define global graphics style -import matplotlib.pyplot as plt -pref_style = "default" -_style = pref_style if pref_style in plt.style.available else "default" -plt.style.use(_style) - -Ui_MCPHA, QMainWindow = loadUiType("rpControl.ui") -Ui_LogDisplay, QWidget = loadUiType("mcpha_log.ui") -Ui_HstDisplay, QWidget = loadUiType("mcpha_hst.ui") -Ui_OscDisplay, QWidget = loadUiType("mcpha_daq.ui") -Ui_GenDisplay, QWidget = loadUiType("mcpha_gen.ui") - -if sys.platform != "win32": - path = "." -else: - path = os.path.expanduser("~") - - -class rpControl(QMainWindow, Ui_MCPHA): - """ control Pavel Demin's MCHPA application on RedPitaya - - Modes of operation: - - in oscilloscope mode to just display data - - - in daq mode, i.e. read data at maximum rate, - optionally recording data to disk or calling an external function - """ - - rates = {0:1, 1: 4, 2: 8, 3: 16, 4: 32, 5: 64, 6: 128, 7: 256} - def __init__(self, callback=None, conf_dict=None): - """ - Args: - - callback: function receiving recorded waveforms - conf_dict: a configuration dictionary - """ - super(rpControl, self).__init__() - - plt.style.use("default") # set graphics style - - self.callback_function = callback - # initialize parameters - self.confd = {} if conf_dict is None else conf_dict - self.parse_confd() - # get configuration from command line - if os.path.split(sys.argv[0])[1] == script_name: - self.interactive = True - self.parse_args() - else: - self.interactive = False - self.callback = True if callback is not None else False - - # set physical units (for axis labels) - self.get_physical_units() - self.setupUi(self) - # initialize variables - self.idle = True - self.hst_waiting = [False for i in range(2)] - self.osc_ready = False - self.osc_waiting = False - self.hst_reset = 0 - self.state = 0 - self.status = np.zeros(9, np.uint32) - self.timers = self.status[:4].view(np.uint64) - # create tabs - self.log = LogDisplay() - if self.interactive: - self.hst1 = HstDisplay(self, self.log, 0) - self.hst2 = HstDisplay(self, self.log, 1) - else: - self.hst1 = None - self.hst2 = None - self.osc_daq = OscDAQ(self, self.log) - self.gen = GenDisplay(self, self.log) - self.tabindex_log = self.tabWidget.addTab(self.log, "Messages") - self.tabWidget.addTab(self.hst1, "Spectrum Channel 1") - self.tabWidget.addTab(self.hst2, "Spectrum Channel 2") - self.tabindex_osc = self.tabWidget.addTab(self.osc_daq, "Oscilloscope") - self.tabindex_gen = self.tabWidget.addTab(self.gen, "Pulse generator") - # configure controls - self.connectButton.clicked.connect(self.start) - self.neg1Check.toggled.connect(partial(self.set_negator, 0)) - self.neg2Check.toggled.connect(partial(self.set_negator, 1)) - self.rateValue.addItems(map(str, rpControl.rates.values())) - self.rateValue.setEditable(True) - self.rateValue.lineEdit().setReadOnly(True) - self.rateValue.lineEdit().setAlignment(Qt.AlignRight) - for i in range(self.rateValue.count()): - self.rateValue.setItemData(i, Qt.AlignRight, Qt.TextAlignmentRole) - self.rateValue.setCurrentIndex(1) - self.rateValue.currentIndexChanged.connect(self.set_rate) - rx = QRegExp(r"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|rp-[0-9A-Fa-f]{6}\.local$") - self.addrValue.setValidator(QRegExpValidator(rx, self.addrValue)) - # create TCP socket - self.socket = QTcpSocket(self) - self.socket.connected.connect(self.connected) - self.socket.error.connect(self.display_error) - # create event loop - self.loop = QEventLoop() - self.socket.readyRead.connect(self.loop.quit) - self.socket.error.connect(self.loop.quit) - # create timers - self.startTimer = QTimer(self) # timer for network connectin - self.startTimer.timeout.connect(self.start_timeout) - self.readTimer = QTimer(self) # for readout - # self.readTimer.timeout.connect(self.update_oscDisplay) - self.readTimer.timeout.connect(self.read_timeout) - - # transfer command-line arguments to gui - self.osc_daq.levelValue.setValue(self.trigger_level) - self.osc_daq.autoButton.setChecked(self.trigger_mode) - self.osc_daq.ch2Button.setChecked(self.trigger_source) - self.osc_daq.fallingButton.setChecked(self.trigger_slope) - self.rateValue.setCurrentIndex(self.decimation_index) - self.neg1Check.setChecked(self.invert1) - self.neg2Check.setChecked(self.invert2) - - # automatically connect - if self.ip_address is not None: - self.addrValue.setText(self.ip_address) - self.start() - else: - self.addrValue.setStyleSheet("color: darkorange") - - def parse_confd(self): - # all relevant parameters are here - self.daq_mode = False # True - self.ip_address ='192.168.1.100' if "ip_address" not in self.confd \ - else self.confd["ip_address"] - self.sample_size = 2048 if "number_of_samples" not in self.confd \ - else self.confd["number_of_samples"] - self.pretrigger_fraction = 0.05 if "pre_trigger_samples" not in self.confd \ - else self.confd["pre_trigger_samples"]/self.sample_size - self.trigger_mode = 0 if "trigger_mode" not in self.confd else \ - 0 if (self.confd["trigger_mode"] == "norm" or self.confd["trigger_mode"] == 0) else 1 - self.trigger_source = 0 if "trigger_channel" not in self.confd \ - else int(self.confd["trigger_channel"])-1 - self.trigger_level = 500 if "trigger_level" not in self.confd else self.confd["trigger_level"] - self.trigger_slope = 0 if "trigger_direction" not in self.confd else \ - 0 if (self.confd["trigger_direction"]=="rising" or self.confd["trigger_direction"]=="0") else 1 - self.decimation_index = 1 if "decimation_index" not in self.confd else \ - self.confd["decimation_index"] - self.invert1 = 0 if "invert_channel1" not in self.confd else self.confd["invert_channel1"] - self.invert2 = 0 if "invert_channel2" not in self.confd else self.confd["invert_channel2"] - self.readInterval = 1000 # used to control update of oscilloscope display - # other parameters - self.filename = '' # file name for data export, '' means disable - - def parse_args(self): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('-c', '--connect_ip', type=str, default=self.ip_address, - help='connect IP address of RedPitaya') - # for oscilloscope display - parser.add_argument('-i', '--interval', type=int, default=self.readInterval, - help='interval for readout (in ms)') - # trigger mode - parser.add_argument('-t', '--trigger_level', type=int, default=self.trigger_level, - help='trigger level in ADC counts') - parser.add_argument('--trigger_slope', type=int, choices={0, 1}, - default=self.trigger_slope, help='trigger slope') - parser.add_argument('--trigger_source', type=int, choices={1, 2}, - default=self.trigger_source+1, help='trigger channel') - parser.add_argument('--trigger_mode', type=int, choices={0 , 1}, - default=self.trigger_mode, help='trigger mode') - parser.add_argument('-s', '--sample_size', type=int, default=self.sample_size, - help='size of waveform sample') - parser.add_argument('-p', '--pretrigger_fraction', type=float, - default=self.pretrigger_fraction, help='pretrigger fraction') - parser.add_argument('-f', '--file', type=str, default='', - help='file name') - args = parser.parse_args() - # all relevant parameters are here - self.ip_address = args.connect_ip - self.sample_size = args.sample_size - self.pretrigger_fraction = args.pretrigger_fraction - self.readInterval = args.interval # used to control oscilloscope display - # - self.trigger_level = args.trigger_level - self.trigger_source = args.trigger_source - 1 - self.trigger_mode = args.trigger_mode - self.trigger_slope = args.trigger_slope - # other parameters - self.filename = args.file - if self.filename: - # data recording with npy_append_array() - import_npy_append_array() - - def get_physical_units(self): - """get physical units corresponding to ADC units and channel numbers - """ - # Voltages: 4096 channels/Volt - self.adc_unit = 1000/4096 - # time per sample at sampling rate of 125 MHz / decimation_factor - self.time_bin = 0.008 - # !gq end - - def start(self): - self.socket.connectToHost(self.addrValue.text(), 1001) # connect to port 1001 (mcpha_server on RP) - self.startTimer.start(1000) # shorter time-out - self.connectButton.setText("Disconnect") - self.connectButton.clicked.disconnect() - self.connectButton.clicked.connect(self.stop) - - def stop(self): - self.osc_daq.stop() - self.gen.stop() - self.readTimer.stop() - self.startTimer.stop() - self.loop.quit() - self.socket.abort() - self.connectButton.setText("Connect") - self.connectButton.clicked.disconnect() - self.connectButton.clicked.connect(self.start) - self.addrValue.setStyleSheet("color: red") - self.log.print("IO stopped") - self.idle = True - - def closeEvent(self, event): - self.stop() - - def start_timeout(self): - self.log.print("error: connection timeout") - self.stop() - - def display_error(self): - self.log.print("error: %s" % self.socket.errorString()) - self.stop() - - def connected(self): - # coming here when connection is established - self.startTimer.stop() - self.log.print("IO started") - self.addrValue.setStyleSheet("color: green") - self.tabWidget.setCurrentIndex(self.tabindex_osc) - # initialize variables for readout - self.idle = False - self.osc_waiting = False - self.daq_waiting = False - self.osc_reset = False - self.osc_start = False - self.state = 0 - self.hst_reset = 0 - self.hst_waiting = [False for i in range(2)] - self.set_rate(self.rateValue.currentIndex()) - self.set_negator(0, self.neg1Check.isChecked()) - self.set_negator(1, self.neg2Check.isChecked()) - # - # finally, start readout timer calling update_oscData() read_timeout() - self.readTimer.start(self.readInterval) - - #### start oscilloscope in DAQ mode -#! if self.interactive and self.daq_mode: - if self.daq_mode: - self.osc_daq() # __call__ method of osc_daq class - - def command(self, code, number, value): - self.socket.write(struct.pack("<Q", code << 56 | number << 52 | (int(value) & 0xFFFFFFFFFFFFF))) - - def read_data(self, data): - view = data.view(np.uint8) - size = view.size - while self.socket.state() == QAbstractSocket.ConnectedState and self.socket.bytesAvailable() < size: - self.loop.exec_() - if self.socket.bytesAvailable() < size: - return False - else: - view[:] = np.frombuffer(self.socket.read(size), np.uint8) - return True - - def read_timeout(self): - """data transfer from RP, triggered by QTimer readTimer - """ - # send reset commands for histograms - if self.hst_reset & 1: - self.command(0, 0, 0) # hst 1 - if self.hst_reset & 2: - self.command(0, 1, 0) # hst 2 - if self.hst_reset & 4: - self.command(1, 0, 0) # reset timer 1 - if self.hst_reset & 8: - self.command(1, 1, 0) # reset timer 2 - self.hst_reset = 0 - if self.osc_reset: - self.reset_osc() - if self.osc_start: - self.start_osc() - # read histogram and oscilloscope data and update displays if not in daq mode - if not self.daq_waiting: - self.command(11, 0, 0) # read status - if not self.read_data(self.status): - return - # spectrum ch1 - if self.hst_waiting[0]: - self.command(12, 0, 0) # code 12: read histogram - if self.read_data(self.hst1.buffer): - self.hst1.update(self.timers[0], False) - else: - return - # spectrum ch2 - if self.hst_waiting[1]: - self.command(12, 1, 0) - if self.read_data(self.hst2.buffer): - self.hst2.update(self.timers[1], False) - else: - return - # oscilloscope - if self.osc_waiting and not self.status[8] & 1: - self.command(20, 0, 0) # read oscilloscope data - if self.read_data(self.osc_daq.buffer): - self.osc_daq.update_osci_display() - self.mark_reset_osc() - self.mark_start_osc() - else: - self.log.print("failed to read oscilloscope data") - return 1 - - - def run_oscDaq(self): - """continuous fast data transfer from RedPitaya via mcpha oscilloscope - """ - # depends on - # self.start_daq() - # osc.process_data() - - # presently, not compatible with historgram mode, so pause - if self.hst_waiting[0]: - self.hst1.pause() - if self.hst_waiting[1]: - self.hst2.pause() - # initialize trigger mode etc. - self.osc_daq.start_daq() - # - self.reset_osc() - self.start_osc() - while self.daq_waiting: - # check status - self.read_status() - if not self.read_data(self.status): - self.log.print("failed to read status") - return - if not self.status[8] & 1: - self.command(20, 0, 0) # read oscilloscope data - if self.read_data(self.osc_daq.buffer): - self.timestamp = time.time() - self.reset_osc() - self.osc_daq.process_data() - if self.callback: - self.callback_function(self.osc_daq.data) - self.start_osc() - self.osc_daq.deadT += time.time() - self.timestamp - else: - self.log.print("failed to read oscilloscope data") - return - - def reset_hst(self, number): - self.hst_reset |= 1 << number - - def reset_timer(self, number): - self.hst_reset |= 4 << number - - def set_pha_delay(self, number, value): - self.command(6, number, value) - - def set_pha_thresholds(self, number, min, max): - self.command(7, number, min) - self.command(8, number, max) - - def set_timer(self, number, value): - self.command(9, number, value) - - def set_timer_mode(self, number, value): - self.command(10, number, value) - self.hst_waiting[number] = value - - def mark_start_osc(self): - self.osc_start = True - # self.osc_waiting = True - - def mark_reset_osc(self): - self.osc_reset = True - - def reset_osc(self): - self.command(2, 0, 0) - self.osc_reset = False - - def start_osc(self): - self.command(19, 0, 0) - self.osc_start = False - - def read_status(self): - self.command(11, 0, 0) - - def stop_osc(self): - self.reset_osc() - self.osc_waiting = False - self.daq_waiting = False - - def set_rate(self, index): - # set RP decimation factor - self.command(4, 0, rpControl.rates[index]) - - def set_negator(self, number, value): - self.command(5, number, value) - - def set_trg_source(self, number): - self.command(13, number, 0) - - def set_trg_slope(self, value): - self.command(14, 0, value) - - def set_trg_mode(self, value): - self.command(15, 0, value) - - def set_trg_level(self, value): - self.command(16, 0, value) - - def set_osc_pre(self, value): - self.command(17, 0, value) - - def set_osc_tot(self, value): - self.command(18, 0, value) - - def set_gen_fall(self, value): - self.command(21, 0, value) - - def set_gen_rise(self, value): - self.command(22, 0, value) - - def set_gen_rate(self, value): - self.command(25, 0, value) - - def set_gen_dist(self, value): - self.command(26, 0, value) - - def reset_gen(self): - self.command(27, 0) - - def set_gen_bin(self, value): - self.command(28, 0, value) - - def start_gen(self): - self.command(29, 0, 1) - - def stop_gen(self): - self.command(30, 0, 0) - - -class LogDisplay(QWidget, Ui_LogDisplay): - def __init__(self): - super(LogDisplay, self).__init__() - self.setupUi(self) - - def print(self, text): - self.logViewer.appendPlainText(text) - -class HstDisplay(QWidget, Ui_HstDisplay): - def __init__(self, rpControl, log, number): - super(HstDisplay, self).__init__() - self.setupUi(self) - # initialize variables - self.rpControl = rpControl - self.log = log - self.number = number - self.sum = 0 - self.time = np.uint64([75e8, 0]) - self.factor = 1 - self.bins = 4096 - self.min = 0 - self.max = self.bins - 1 - self.buffer = np.zeros(self.bins, np.uint32) - if number == 0: - self.color = "#FFAA00" - else: - self.color = "#00CCCC" - # create figure - self.figure = Figure() - if sys.platform != "win32": - self.figure.set_facecolor("none") - # gq self.figure.subplots_adjust(left=0.18, bottom=0.08, right=0.98, top=0.95) - self.figure.subplots_adjust(left=0.10, bottom=0.08, right=0.98, top=0.92) - self.canvas = FigureCanvas(self.figure) - self.plotLayout.addWidget(self.canvas) - self.ax = self.figure.add_subplot(111) - self.ax.grid() - self.ax.set_ylabel("counts") - self.xunit = "[{:.3f} mV / channel]".format(1000/4096 * self.factor) - self.ax.set_xlabel("channel number " + self.xunit) -# self.ax.set_xlabel("channel number") - # !gq - self.ax_x2=self.ax.secondary_xaxis('top', - functions=(self.adc2mV, self.mV2adc)) - self.ax_x2.set_xlabel("Voltage [mV]", color='grey') - # !gq end - x = np.arange(self.bins) - (self.curve,) = self.ax.plot(x, self.buffer, drawstyle="steps-mid", color=self.color) - self.roi = [0, self.max] - self.line = [None, None] - self.active = [False, False] - self.releaser = [None, None] - # marker lines for roi - self.line[0] = self.ax.axvline(self.min, picker=True, pickradius=5) - self.line[1] = self.ax.axvline(self.max, picker=True, pickradius=5) - # create navigation toolbar - self.toolbar = NavigationToolbar(self.canvas, None, False) - self.toolbar.layout().setSpacing(6) - # remove subplots action - actions = self.toolbar.actions() - self.toolbar.removeAction(actions[6]) - self.toolbar.removeAction(actions[7]) - self.logCheck = QCheckBox("log scale") - self.logCheck.setChecked(False) - self.binsLabel = QLabel("rebin factor") - self.binsValue = QComboBox() - self.binsValue.addItems(["1", "2", "4", "8"]) - self.binsValue.setEditable(True) - self.binsValue.lineEdit().setReadOnly(True) - self.binsValue.lineEdit().setAlignment(Qt.AlignRight) - for i in range(self.binsValue.count()): - self.binsValue.setItemData(i, Qt.AlignRight, Qt.TextAlignmentRole) - self.toolbar.addSeparator() - self.toolbar.addWidget(self.logCheck) - self.toolbar.addSeparator() - self.toolbar.addWidget(self.binsLabel) - self.toolbar.addWidget(self.binsValue) - self.plotLayout.addWidget(self.toolbar) - # configure controls - actions[0].triggered.disconnect() - actions[0].triggered.connect(self.home) - self.logCheck.toggled.connect(self.set_scale) - self.binsValue.currentIndexChanged.connect(self.set_bins) - self.thrsCheck.toggled.connect(self.set_thresholds) - self.startButton.clicked.connect(self.start) - self.resetButton.clicked.connect(self.reset) - self.saveButton.clicked.connect(self.save) - self.loadButton.clicked.connect(self.load) - self.canvas.mpl_connect("motion_notify_event", self.on_motion) - self.canvas.mpl_connect("pick_event", self.on_pick) - # update controls - self.set_thresholds(self.thrsCheck.isChecked()) - self.set_time(self.time[0]) - self.set_scale(self.logCheck.isChecked()) - - # !gq helper functions to convert adc counts to physical units - def adc2mV(self, c): - """convert adc-count to Voltage in mV - """ - return c * self.rpControl.adc_unit * self.factor - - def mV2adc(self, v): - """convert voltage in mV to adc-count - """ - return v / (self.rpControl.adc_unit * self.factor) - # !gq end helper - - def start(self): - if self.rpControl.idle: - return - self.set_thresholds(self.thrsCheck.isChecked()) - self.set_enabled(False) - h = self.hoursValue.value() - m = self.minutesValue.value() - s = self.secondsValue.value() - value = (h * 3600000 + m * 60000 + s * 1000) * 125000 - self.sum = 0 - self.time[:] = [value, 0] - self.rpControl.reset_timer(self.number) - self.rpControl.set_pha_delay(self.number, 100) - self.rpControl.set_pha_thresholds(self.number, self.min, self.max) - self.rpControl.set_timer(self.number, value) -# - self.resume() - - def pause(self): - self.rpControl.set_timer_mode(self.number, 0) - self.startButton.setText("Resume") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.resume) - self.log.print("timer %d stopped" % (self.number + 1)) - - def resume(self): - self.rpControl.set_timer_mode(self.number, 1) - self.startButton.setText("Pause") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.pause) - self.log.print("timer %d started" % (self.number + 1)) - - def stop(self): - self.rpControl.set_timer_mode(self.number, 0) - self.set_enabled(True) - self.set_time(self.time[0]) - self.startButton.setText("Start") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.start) - self.log.print("timer %d stopped" % (self.number + 1)) - - def reset(self): - if self.rpControl.idle: - return - self.stop() - self.rpControl.reset_hst(self.number) - self.rpControl.reset_timer(self.number) - self.totalValue.setText("%.2e" % 0) - self.instValue.setText("%.2e" % 0) - self.avgValue.setText("%.2e" % 0) - self.buffer[:] = np.zeros(self.bins, np.uint32) - self.update_plot() - self.update_roi() - - def set_enabled(self, value): - if value: - self.set_thresholds(self.thrsCheck.isChecked()) - else: - self.minValue.setEnabled(False) - self.maxValue.setEnabled(False) - self.thrsCheck.setEnabled(value) - self.hoursValue.setEnabled(value) - self.minutesValue.setEnabled(value) - self.secondsValue.setEnabled(value) - - def home(self): - self.set_scale(self.logCheck.isChecked()) - - def set_xplot_range(self): - """set x-range for histogram plot - """ - mn = self.min // self.factor - mx = self.max // self.factor - xsize = mx - mn - self.ax.set_xlim(mn - 0.02*xsize, mx*1.02) - self.ax.relim() - self.ax.autoscale_view(scalex=True, scaley=True) - - def set_scale(self, checked): - self.toolbar.home() - self.toolbar.update() - if checked: - self.ax.set_ylim(1, 1e10) - self.ax.set_yscale("log") - else: - self.ax.set_ylim(auto=True) - self.ax.set_yscale("linear") - # !gq size = self.bins // self.factor - # self.ax.set_xlim(-0.05 * size, size * 1.05) - self.set_xplot_range() - self.canvas.draw() - - def set_bins(self, value): - factor = 1 << value - self.factor = factor - bins = self.bins // self.factor - self.xunit = "[{:.3f} mV / channel]".format(1000/4096 * self.factor) - self.ax.set_xlabel("channel number " + self.xunit) - x = np.arange(bins) - y = self.buffer.reshape(-1, self.factor).sum(-1) - self.curve.set_xdata(x) - self.curve.set_ydata(y) - self.set_scale(self.logCheck.isChecked()) - # gq update roi - self.roi[0] = self.min - self.roi[1] = self.max - self.update_roi() - - def set_thresholds(self, checked): - self.minValue.setEnabled(checked) - self.maxValue.setEnabled(checked) - if checked: - self.min = self.minValue.value() - self.max = self.maxValue.value() - else: - self.min = 0 - self.max = self.bins - 1 - - # !gq update xplot range and roi - self.set_xplot_range() - self.roi[0] = self.min - self.roi[1] = self.max - self.update_roi() - - def set_time(self, value): - value = value // 125000 - h, mod = divmod(value, 3600000) - m, mod = divmod(mod, 60000) - s = mod / 1000 - self.hoursValue.setValue(int(h)) - self.minutesValue.setValue(int(m)) - self.secondsValue.setValue(s) - - def update(self, value, sync): - self.update_rate(value) - if not sync: - self.update_time(value) - self.update_plot() - self.update_roi() - - def update_rate(self, value): - sum = self.buffer.sum() - self.totalValue.setText("%.2e" % sum) - if value > self.time[1]: - rate = (sum - self.sum) / (value - self.time[1]) * 125e6 - self.instValue.setText("%.2e" % rate) - if value > 0: - rate = sum / value * 125e6 - self.avgValue.setText("%.2e" % rate) - self.sum = sum - self.time[1] = value - - def update_time(self, value): - if value < self.time[0]: - self.set_time(self.time[0] - value) - else: - self.stop() - - def update_plot(self): - y = self.buffer.reshape(-1, self.factor).sum(-1) - self.curve.set_ydata(y) - self.ax.relim() - self.ax.autoscale_view(scalex=False, scaley=True) - self.canvas.draw() - - def update_roi(self): - y = self.buffer.reshape(-1, self.factor).sum(-1) - x0 = self.roi[0] // self.factor - x1 = self.roi[1] // self.factor - roi = y[x0 : x1 + 1] - y0 = roi[0] - y1 = roi[-1] - tot = roi.sum() - bkg = (x1 + 1 - x0) * (y0 + y1) / 2.0 - self.roistartValue.setText("%d" % x0) - self.roiendValue.setText("%d" % x1) - self.roitotValue.setText("%.2e" % tot) - self.roibkgValue.setText("%.2e" % bkg) - self.line[0].set_xdata([x0, x0]) - self.line[1].set_xdata([x1, x1]) - self.canvas.draw_idle() - - def on_motion(self, event): - if event.inaxes != self.ax: - return - x = int(event.xdata + 0.5) - if x < 0: - x = 0 - if x >= self.bins // self.factor: - x = self.bins // self.factor - 1 - y = self.curve.get_ydata(True)[x] - self.numberValue.setText("%d" % x) - self.entriesValue.setText("%d" % y) - delta = 40 - if self.active[0]: - x0 = x * self.factor - if x0 > self.roi[1] - delta: - self.roi[0] = self.roi[1] - delta - else: - self.roi[0] = x0 - self.update_roi() - if self.active[1]: - x1 = x * self.factor - if x1 < self.roi[0] + delta: - self.roi[1] = self.roi[0] + delta - else: - self.roi[1] = x1 - self.update_roi() - - def on_pick(self, event): - for i in range(2): - if event.artist == self.line[i]: - self.active[i] = True - self.releaser[i] = self.canvas.mpl_connect("button_release_event", partial(self.on_release, i)) - - def on_release(self, i, event): - self.active[i] = False - self.canvas.mpl_disconnect(self.releaser[i]) - - def save(self): - try: - dialog = QFileDialog(self, "Save hst file", path, "*.hst") - dialog.setDefaultSuffix("hst") - timestamp = time.strftime("%Y%m%d-%H%M%S") - name = "histogram-%s.hst" % timestamp - dialog.selectFile(name) - dialog.setAcceptMode(QFileDialog.AcceptSave) - if dialog.exec() == QDialog.Accepted: - name = dialog.selectedFiles() - np.savetxt(name[0], - self.buffer, - fmt="%u", - header ='mcpha spectrum ' + timestamp + '\n counts per channel', - newline=os.linesep) - self.log.print("histogram %d saved to file %s" % ((self.number + 1), name[0])) - except: - self.log.print("error: %s" % sys.exc_info()[1]) - - def load(self): - try: - dialog = QFileDialog(self, "Load hst file", path, "*.hst") - dialog.setDefaultSuffix("hst") - dialog.setAcceptMode(QFileDialog.AcceptOpen) - if dialog.exec() == QDialog.Accepted: - name = dialog.selectedFiles() - self.buffer[:] = np.loadtxt(name[0], np.uint32) - self.update_plot() - except: - self.log.print("error: %s" % sys.exc_info()[1]) - - -class OscDAQ(QWidget, Ui_OscDisplay): - """Oscilloscope display with daq mode: - in daq_mode: - - data is read from the RedPitaya at maximum rate - - statistics is accumulated and displayed - - optinally, data is output to a file or sent to an external function - """ - def __init__(self, rpControl, log): - super(OscDAQ, self).__init__() - self.setupUi(self) - # initialize variables - self.rpControl = rpControl - self.log = log - self.l_tot = self.rpControl.sample_size - self.pre = self.rpControl.pretrigger_fraction * self.l_tot - self.buffer = np.zeros(self.l_tot * 2, np.int16) - # same memory with different view of shape(2, samples_per_channel) - self.data = self.buffer.reshape(2, self.l_tot, order='F') - self.filename = rpControl.filename if self.rpControl.filename != '' else None - # create figure - self.figure = Figure() - if sys.platform != "win32": - self.figure.set_facecolor("none") - # gq self.figure.subplots_adjust(left=0.18, bottom=0.08, right=0.98, top=0.95) - self.figure.subplots_adjust(left=0.10, bottom=0.08, right=0.90, top=0.92) - self.canvas = FigureCanvas(self.figure) - self.plotLayout.addWidget(self.canvas) - self.ax = self.figure.add_subplot(111) - self.ax.grid() - self.ax.set_xlim(-0.02*self.l_tot, 1.02*self.l_tot) - self.ax.set_ylim(-4500, 4500) - self.xunit = "[{:d} ns / sample]".format(4*8) - self.ax.set_xlabel("sample number " + self.xunit) - self.yunit = "[{:.3f} mV]".format(1./4.096) - self.ax.set_ylabel("ADC units " + self.yunit) - # self.ax.set_xlabel("sample number") - # self.ax.set_ylabel("ADC units") - # gq - self.ax_x2=self.ax.secondary_xaxis('top', functions=(self.tbin2t, self.t2tbin)) - self.ax_x2.set_xlabel("time [µs]", color='grey', size='x-large') - self.ax_y2=self.ax.secondary_yaxis('right', - functions=(self.adc2mV, self.mV2adc)) - self.ax_y2.set_ylabel("Voltage [mV]", color='grey', size='x-large') - # gq end - self.osctxt= self.ax.text(0.1, 0.96, ' ', transform=self.ax.transAxes, - color="darkblue", alpha=0.7) - - x = np.arange(self.l_tot) - (self.curve2,) = self.ax.plot(x, self.buffer[1::2], - color="#00CCCC", label="chan 2") - (self.curve1,) = self.ax.plot(x, self.buffer[0::2], - color="#FFAA00", label="chan 1") - self.ax.legend(handles=[self.curve1, self.curve2]) - self.line = [None, None] - self.line[0] = self.ax.axvline(self.pre, linestyle="dotted") - self.line[1] = self.ax.axhline(self.levelValue.value(), linestyle="dotted") - self.canvas.draw() - # create navigation toolbar - self.toolbar = NavigationToolbar(self.canvas, None, False) - self.toolbar.layout().setSpacing(6) - # remove subplots action - actions = self.toolbar.actions() - self.toolbar.removeAction(actions[6]) - self.toolbar.removeAction(actions[7]) - # configure colors - self.plotLayout.addWidget(self.toolbar) - palette = QPalette(self.ch1Label.palette()) - palette.setColor(QPalette.Window, QColor("#FFAA00")) - palette.setColor(QPalette.WindowText, QColor("black")) - self.ch1Label.setAutoFillBackground(True) - self.ch1Label.setPalette(palette) - self.ch1Value.setAutoFillBackground(True) - self.ch1Value.setPalette(palette) - palette.setColor(QPalette.Window, QColor("#00CCCC")) - palette.setColor(QPalette.WindowText, QColor("black")) - self.ch2Label.setAutoFillBackground(True) - self.ch2Label.setPalette(palette) - self.ch2Value.setAutoFillBackground(True) - self.ch2Value.setPalette(palette) - # configure controls - self.autoButton.toggled.connect(self.rpControl.set_trg_mode) - self.ch2Button.toggled.connect(self.rpControl.set_trg_source) - self.fallingButton.toggled.connect(self.rpControl.set_trg_slope) - self.levelValue.valueChanged.connect(self.set_trg_level) - self.startButton.clicked.connect(self.start) - self.startDAQButton.clicked.connect(self.rpControl.run_oscDaq) - self.saveButton.clicked.connect(self.save) - self.loadButton.clicked.connect(self.load) - self.canvas.mpl_connect("motion_notify_event", self.on_motion) - - self.rpControl.osc_ready = True - - def __call__(self): - # run in data acquisition mode - self.rpControl.run_oscDaq() - - # gq helper functions to convert acd channels and sampling time to physical units - def tbin2t(self, tbin): - """convert time bin to time in µs - """ - dfi = self.rpControl.rateValue.currentIndex() # current decimation factor index - df = 4 if dfi == -1 else rpControl.rates[dfi] # decimation factor - return (tbin-self.pre)*self.rpControl.time_bin * df - - def t2tbin(self, t): - """convert time in µs to time bin - """ - dfi = self.rpControl.rateValue.currentIndex() # current decimation factor index - df = 4 if dfi == -1 else rpControl.rates[dfi] # decimation factor - return t/(self.rpControl.time_bin * df)+self.pre - - def adc2mV(self, c): - """convert adc-count to Voltage in mV - """ - return c * self.rpControl.adc_unit - - def mV2adc(self, v): - """convert voltage in mV to adc-count - """ - return v / self.rpControl.adc_unit - # gq end - - def setup_trigger(self): - # extract trigger parameters from gui and send to server - self.trg_mode = int(self.autoButton.isChecked()) - self.trg_source = int(self.ch2Button.isChecked()) - self.trg_slope = int(self.fallingButton.isChecked()) - self.trg_level = self.levelValue.value() - self.rpControl.set_trg_mode(self.trg_mode) - self.rpControl.set_trg_source(self.trg_source) - self.rpControl.set_trg_slope(self.trg_slope) - self.rpControl.set_trg_level(self.trg_level) - self.rpControl.set_osc_pre(self.pre) - self.rpControl.set_osc_tot(self.l_tot) - - def set_gui4start(self): - self.startButton.setText("Start monitor") - self.startButton.setStyleSheet("") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.start) - self.startDAQButton.setEnabled(True) - if self.rpControl.hst1 is not None: - self.rpControl.hst1.startButton.setEnabled(True) - if self.rpControl.hst2 is not None: - self.rpControl.hst2.startButton.setEnabled(True) - - def set_gui4stop(self): - # set start and stop buttons - self.startButton.setText("Stop") - self.startButton.setStyleSheet("color: red") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.stop) - self.startDAQButton.setEnabled(False) - - def start(self): - """start oscilloscope display - """ - if self.rpControl.idle: - return - # - self.setup_trigger() - self.set_gui4stop() - # - self.rpControl.mark_reset_osc() - self.rpControl.mark_start_osc() - self.rpControl.osc_waiting = True - self.osctxt.set_text('') - self.log.print("oscilloscope started") - - def start_daq(self): - """start oscilloscope in daq mode - """ - if self.rpControl.idle: - return - # initialize daq statisics - self.NTrig = 0 - self.dN = 0 - self.Nprev = self.NTrig - self.T0 = time.time() - self.deadT = 0. - self.Tprev = self.T0 - self.dT = 0. - # set-up trigger and GUI Buttons - self.setup_trigger() - self.set_gui4stop() - # disable spectrum update - if self.rpControl.hst1 is not None: - self.rpControl.hst1.startButton.setEnabled(False) - if self.rpControl.hst2 is not None: - self.rpControl.hst2.startButton.setEnabled(False) - self.rpControl.daq_waiting = True - self.log.print("daq started") - - def stop(self): - self.rpControl.stop_osc() - self.set_gui4start() - self.log.print("oscilloscope stopped") - - def process_data(self): - """ statistics recorded data: - - - count number of Triggers and keep account of timing; - - graphical display of a subset of the data - """ - self.NTrig += 1 - t = time.time() - dt = t - self.Tprev - self.Tprev = t - self.dT += dt - dead_time_fraction = self.deadT/dt - self.deadT = 0. - # - # do something with data (e.g. pass to sub-process via mpQueue) - # write to file: - if self.filename is not None: - with NpyAppendArray(self.filename) as npa: - npa.append( np.array([self.data]) ) - # - # output status and update scope display once per readInterval - if 1000*self.dT >= self.rpControl.readInterval: - dN = self.NTrig - self.Nprev - r = dN/self.dT - self.Nprev = self.NTrig - T_active = t - self.T0 - self.dT = 0. - status_txt = "active: {:.1f}s trigger rate: {:.2f} Hz, data rate: {:.4g} MB/s".format(T_active, r, r*self.l_tot*4e-6) - # print(status_txt, end='\r') - self.osctxt.set_text(status_txt) - # update graph on display - self.curve1.set_ydata(self.data[0]) - self.curve2.set_ydata(self.data[1]) - self.xunit = "[{:d} ns / sample]".format(8*rpControl.rates[self.rpControl.rateValue.currentIndex()]) - self.ax.set_xlabel("sample number " + self.xunit) - self.canvas.draw() - - def update_osci_display(self): - self.curve1.set_ydata(self.buffer[0::2]) - self.curve2.set_ydata(self.buffer[1::2]) - self.xunit = "[{:d} ns / sample]".format(8*rpControl.rates[self.rpControl.rateValue.currentIndex()]) - self.ax.set_xlabel("sample number " + self.xunit) - self.canvas.draw() - - def set_trg_level(self, value): - self.line[1].set_ydata([value, value]) - self.canvas.draw() - self.rpControl.set_trg_level(value) - - def on_motion(self, event): - if event.inaxes != self.ax: - return - x = int(event.xdata + 0.5) - if x < 0: - x = 0 - if x >= self.l_tot: - x = self.l_tot - 1 - y1 = self.curve1.get_ydata(True)[x] - y2 = self.curve2.get_ydata(True)[x] - self.timeValue.setText("%d" % x) - self.ch1Value.setText("%d" % y1) - self.ch2Value.setText("%d" % y2) - - def save(self): - try: - dialog = QFileDialog(self, "Save osc file", path, "*.osc") - dialog.setDefaultSuffix("osc") - name = "oscillogram-%s.osc" % time.strftime("%Y%m%d-%H%M%S") - dialog.selectFile(name) - dialog.setAcceptMode(QFileDialog.AcceptSave) - if dialog.exec() == QDialog.Accepted: - name = dialog.selectedFiles() - self.buffer.tofile(name[0]) - self.log.print("histogram %d saved to file %s" % ((self.number + 1), name[0])) - except: - self.log.print("error: %s" % sys.exc_info()[1]) - - def load(self): - try: - dialog = QFileDialog(self, "Load osc file", path, "*.osc") - dialog.setDefaultSuffix("osc") - dialog.setAcceptMode(QFileDialog.AcceptOpen) - if dialog.exec() == QDialog.Accepted: - name = dialog.selectedFiles() - self.buffer[:] = np.fromfile(name[0], np.int16) - self.update() - except: - self.log.print("error: %s" % sys.exc_info()[1]) - - -class GenDisplay(QWidget, Ui_GenDisplay): - def __init__(self, rpControl, log): - super(GenDisplay, self).__init__() - self.setupUi(self) - # initialize variables - self.rpControl = rpControl - self.log = log - self.bins = 4096 - self.buffer = np.zeros(self.bins, np.uint32) - for i in range(16): # initialize with delta-functions - self.buffer[(i+1)*256-1] = 1 - # create figure - self.figure = Figure() - if sys.platform != "win32": - self.figure.set_facecolor("none") - # !gq self.figure.subplots_adjust(left=0.18, bottom=0.08, right=0.98, top=0.95) - self.figure.subplots_adjust(left=0.08, bottom=0.08, right=0.98, top=0.92) - self.canvas = FigureCanvas(self.figure) - self.plotLayout.addWidget(self.canvas) - self.ax = self.figure.add_subplot(111) - self.ax.grid() - self.ax.set_ylabel("counts") - self.xunit = "[0.122 mV /channel]" - self.ax.set_xlabel("channel number " + self.xunit) - # self.ax.set_xlabel("channel number") - # !gq - self.ax_x2=self.ax.secondary_xaxis('top', - functions=(self.adc2mV, self.mV2adc)) - self.ax_x2.set_xlabel("Voltage [mV]", color='grey') - x = np.arange(self.bins) - (self.curve,) = self.ax.plot(x, self.buffer, drawstyle="steps-mid", color="#FFAA00") - # create navigation toolbar - self.toolbar = NavigationToolbar(self.canvas, None, False) - self.toolbar.layout().setSpacing(6) - # remove subplots action - actions = self.toolbar.actions() - self.toolbar.removeAction(actions[6]) - self.toolbar.removeAction(actions[7]) - self.logCheck = QCheckBox("log scale") - self.logCheck.setChecked(False) - self.toolbar.addSeparator() - self.toolbar.addWidget(self.logCheck) - self.plotLayout.addWidget(self.toolbar) - # configure controls - actions[0].triggered.disconnect() - actions[0].triggered.connect(self.home) - self.logCheck.toggled.connect(self.set_scale) - self.startButton.clicked.connect(self.start) - self.loadButton.clicked.connect(self.load) - - # gq - def adc2mV(self, c): - """convert adc-count to Voltage in mV - !!! there is a factor of two betwenn channel definition here and in hstDisplay ! - """ - return c * self.rpControl.adc_unit/2 - - def mV2adc(self, v): - """convert voltage in mV to adc-count - """ - return v * self.rpControl.adc_unit*2 - # gq end - - def start(self): - if self.rpControl.idle: - return - self.rpControl.set_gen_fall(self.fallValue.value()) - self.rpControl.set_gen_rise(self.riseValue.value()) - self.rpControl.set_gen_rate(self.rateValue.value() * 1000) - self.rpControl.set_gen_dist(self.poissonButton.isChecked()) - for value in np.arange(self.bins, dtype=np.uint64) << 32 | self.buffer: - self.rpControl.set_gen_bin(value) - self.rpControl.start_gen() - self.startButton.setText("Stop") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.stop) - self.log.print("generator started") - - def stop(self): - self.rpControl.stop_gen() - self.startButton.setText("Start") - self.startButton.clicked.disconnect() - self.startButton.clicked.connect(self.start) - self.log.print("generator stopped") - - def home(self): - self.set_scale(self.logCheck.isChecked()) - - def set_scale(self, checked): - self.toolbar.home() - self.toolbar.update() - if checked: - self.ax.set_ylim(1, 1e10) - self.ax.set_yscale("log") - else: - self.ax.set_ylim(auto=True) - self.ax.set_yscale("linear") - self.ax.relim() - self.ax.autoscale_view(scalex=True, scaley=True) - self.canvas.draw() - - def load(self): - try: - dialog = QFileDialog(self, "Load gen file", path, "*.gen") - dialog.setDefaultSuffix("gen") - dialog.setAcceptMode(QFileDialog.AcceptOpen) - if dialog.exec() == QDialog.Accepted: - name = dialog.selectedFiles() - self.buffer[:] = np.loadtxt(name[0], np.uint32) - self.curve.set_ydata(self.buffer) - self.ax.relim() - self.ax.autoscale_view(scalex=False, scaley=True) - self.canvas.draw() - except: - self.log.print("error: %s" % sys.exc_info()[1]) - -class redP_consumer(): - def __init__(self): - self.NTrig = 0 - self.dN = 0 - self.Nprev = self.NTrig - self.T0 = time.time() - self.Tprev = self.T0 - self.dT = 0. - - def data_sink(self, data): - """function called by redPoscdaq - this simple version calculates statistics only - """ - self.databuffer = data - # analyze data - self.NTrig += 1 - t = time.time() - dt = t - self.Tprev - self.Tprev = t - self.dT += dt - l_tot = len(self.databuffer[0]) - - # output status and update scope display once per second - if self.dT >= 1.: - dN = self.NTrig - self.Nprev - r = dN/self.dT - self.Nprev = self.NTrig - T_active = t - self.T0 - self.dT = 0. - status_txt = "active: {:.1f}s trigger rate: {:.2f} Hz, data rate: {:.4g} MB/s".format(T_active, r, r*l_tot*4e-6) - print(status_txt, end='\r') - -def run_rpControl(callback=None, conf_dict=None): - # start redPidaya GUI under Qt5 - app = QApplication(sys.argv) - dpi = app.primaryScreen().logicalDotsPerInch() - matplotlib.rcParams["figure.dpi"] = dpi - application = rpControl(callback=callback, conf_dict=conf_dict) - application.show() - sys.exit(app.exec_()) - - -if __name__ == '__main__': # -------------------------------------------- -# run_rpControl() - - data_processor = redP_consumer() - run_rpControl(callback=data_processor.data_sink) -- GitLab